diff --git a/.cargo/config.toml b/.cargo/config.toml index ae52362ef602..667c044b517a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -9,7 +9,6 @@ run-wasm = "run --release --package run_wasm --" [unstable] codegen-backend = true -config-include = true [build] # Incremental compilation blows up the size of the target folder, and is also buggy. @@ -43,10 +42,33 @@ AR = "llvm-ar" # Various Wasm targets fail on mac without llvm-ar # # `getrandom_backend="wasm_js"` is required to enable the `getrandom` crate to use the `wasm_js` backend. # https://docs.rs/getrandom/latest/getrandom/#webassembly-support +# +# `-Ctarget-feature=+simd128,…` enables LLVM auto-vectorization and a handful +# of other Wasm proposals for wasm32. The SIMD baseline is +# Chrome 91+, Firefox 89+, Safari 16.4+ — the JS loader feature-detects SIMD +# and shows a friendly error on older browsers. Every other feature listed +# here shipped strictly earlier in all three browsers, so the SIMD check is +# sufficient to gate them all: +# +# simd128 Chrome 91 / FF 89 / Safari 16.4 (auto-vectorization) +# bulk-memory Chrome 75 / FF 79 / Safari 15 (single-instr memcpy/memset) +# nontrapping-fptoint Chrome 75 / FF 64 / Safari 15 (saturating float→int) +# multivalue Chrome 85 / FF 78 / Safari 15 (multi-value returns) +# +# Note `bulk-memory` is already on by default in rustc 1.82+; listing it is +# redundant but explicit. `mutable-globals`, `sign-ext`, and `reference-types` +# are also enabled by default and not listed. [target.wasm32-unknown-unknown] -rustflags = ['--cfg=web_sys_unstable_apis', '--cfg=getrandom_backend="wasm_js"'] +rustflags = [ + '--cfg=web_sys_unstable_apis', + '--cfg=getrandom_backend="wasm_js"', + '-Ctarget-feature=+simd128,+bulk-memory,+nontrapping-fptoint,+multivalue', +] -# Use rust-lld on Windows due problems with large debug symbols. -# See https://github.com/rust-lang/rust/issues/141626 -[target.x86_64-pc-windows-msvc] -linker = "rust-lld" +# `rust-lld` produces non-deterministic output, so we use `link.exe /Brepro` instead. +# `/Brepro` is an undocumented flag that replaces the timestamp in the dll's PE header with a fixed value. +# See (e.g.) https://nikhilism.com/post/2020/windows-deterministic-builds/ +# Note that some sources claim that this is incompatible with incremental builds. +[target.'cfg(all(target_os = "windows", target_env = "msvc"))'] +linker = "link.exe" +rustflags = ["-C", "link-arg=/Brepro"] diff --git a/.config/nextest.toml b/.config/nextest.toml index 5960293e71cb..af79515e583b 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -3,3 +3,9 @@ [profile.default] # warn if a test takes longer than 30 seconds and terminate it after 2 minutes slow-timeout = { period = "30s", terminate-after = 4 } + +# CI profile: retry flaky tests once. Set via `--profile ci` in CI scripts. +# Local runs stay on `default` (no retries) so flakes surface. +[profile.ci] +slow-timeout = { period = "30s", terminate-after = 4 } +retries = 1 diff --git a/.gitattributes b/.gitattributes index 83abb61c2de1..f6c2cf58c647 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,14 +6,15 @@ pixi.lock linguist-generated=true **/*.urdf filter=lfs diff=lfs merge=lfs -text **/*.stl filter=lfs diff=lfs merge=lfs -text **/*.STL filter=lfs diff=lfs merge=lfs -text +**/*.dae filter=lfs diff=lfs merge=lfs -text +**/*.DAE filter=lfs diff=lfs merge=lfs -text **/*.mcap filter=lfs diff=lfs merge=lfs -text **/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text +**/droid-loader/**/*.png filter=lfs diff=lfs merge=lfs -text +**/droid/resources/*.png filter=lfs diff=lfs merge=lfs -text +**/viewer/apple-touch-icon.png filter=lfs diff=lfs merge=lfs -text **/*.h264 filter=lfs diff=lfs merge=lfs -text +**/*.h5 filter=lfs diff=lfs merge=lfs -text **/*.mp4 filter=lfs diff=lfs merge=lfs -text -landing/**/*.jpg filter=lfs diff=lfs merge=lfs -text -landing/**/*.jpeg filter=lfs diff=lfs merge=lfs -text -landing/**/*.png filter=lfs diff=lfs merge=lfs -text -landing/**/*.gif filter=lfs diff=lfs merge=lfs -text -landing/**/*.webp filter=lfs diff=lfs merge=lfs -text examples/assets/example.rrd filter=lfs diff=lfs merge=lfs -text tests/assets/image/*.bin filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/annoyance_report.md b/.github/ISSUE_TEMPLATE/annoyance_report.md index 24f130d174e0..86f9efbe31fe 100644 --- a/.github/ISSUE_TEMPLATE/annoyance_report.md +++ b/.github/ISSUE_TEMPLATE/annoyance_report.md @@ -41,3 +41,13 @@ Steps to reproduce the behavior: **Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index af0101704638..fe86a805853b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -38,3 +38,13 @@ Steps to reproduce the behavior: **Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 97e0419affdb..036126bf9854 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -23,3 +23,13 @@ First look if there is already a similar feature request. If there is, upvote th **Additional context** + + diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md index 3b7d1f6c978a..cef9c207b790 100644 --- a/.github/ISSUE_TEMPLATE/other.md +++ b/.github/ISSUE_TEMPLATE/other.md @@ -8,3 +8,13 @@ assignees: '' --- If you are asking a question, use [the Rerun Discord server](https://discord.gg/PXtCgFBSmH) instead. + + diff --git a/.github/ISSUE_TEMPLATE/project.md b/.github/ISSUE_TEMPLATE/project.md index cae40afa051e..e9ec71515499 100644 --- a/.github/ISSUE_TEMPLATE/project.md +++ b/.github/ISSUE_TEMPLATE/project.md @@ -52,3 +52,13 @@ NOTE: take care to not put any proprietary user-information here or in any publi ## Non-goals and won't do + + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 4a570fd4aa14..7b0b1ff9ca8c 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -8,3 +8,13 @@ assignees: '' --- If you are asking a question, use [the Rerun Discord server](https://discord.gg/PXtCgFBSmH) instead. + + diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 45245aa11de0..7022f8e4d6dc 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -2,7 +2,7 @@ # - The correct version of Rust based on the `rust-toolchain` file # - All components + targets specified in `rust-toolchain` # - Caching of individual compilation requests via `sccache` and GCS -# - Uses our own `rerun-io/sccache-action` which supports GCS +# - Downloads sccache from the mirror at build.rerun.io # - `cargo nextest` # # Note that due to the use of GCS as an sccache storage backend, @@ -54,7 +54,7 @@ runs: shell: bash --noprofile --norc -euo pipefail {0} - name: Set up GCP credentials - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ inputs.workload_identity_provider }} service_account: ${{ inputs.service_account }} @@ -79,24 +79,9 @@ runs: done - name: Set up sccache - uses: rerun-io/sccache-action@v0.8.0 - with: - version: "v0.10.0" - use_gcs: true - gcs_bucket: rerun-sccache - gcs_read_only: false - - - name: Configure sccache environment - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "SCCACHE_GHA_ENABLED=false" >> $GITHUB_ENV - echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV - echo "CARGO_INCREMENTAL=0" >> $GITHUB_ENV - - - name: Verify sccache shell: bash --noprofile --norc -euo pipefail {0} run: | - sccache --show-stats + "$GITHUB_ACTION_PATH/../../scripts/setup_sccache.sh" --version v0.14.0 --backend gcs # Recommended way to install nextest on CI. - name: Install latest nextest release diff --git a/.github/actions/vercel/action.yml b/.github/actions/vercel/action.yml index 57617d4f71c1..7b01e1a8d0e9 100644 --- a/.github/actions/vercel/action.yml +++ b/.github/actions/vercel/action.yml @@ -47,5 +47,5 @@ inputs: required: false runs: - using: "node20" + using: "node24" main: "index.mjs" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 72f60699c39a..5af11016b1f1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -30,3 +30,13 @@ For maintainers: For more details check the PR section on . --> + + diff --git a/.github/scripts/setup_sccache.sh b/.github/scripts/setup_sccache.sh new file mode 100755 index 000000000000..6b63a12e5229 --- /dev/null +++ b/.github/scripts/setup_sccache.sh @@ -0,0 +1,277 @@ +#!/usr/bin/env bash +set -euo pipefail + +MIRROR_BASE_URL="https://build.rerun.io/mirror/mozilla/sccache" + +usage() { + cat <<'EOF' +Install and configure sccache for CI. + +Usage: setup_sccache.sh --version VERSION [--backend auto|s3|gcs] [--gcs-bucket BUCKET] [--gcs-read-only [true|false]] +EOF +} + +version="" +backend="auto" +gcs_bucket="rerun-sccache" +gcs_read_only="false" + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) + version="${2:?--version requires a value}" + shift 2 + ;; + --backend) + backend="${2:?--backend requires a value}" + shift 2 + ;; + --gcs-bucket) + gcs_bucket="${2:?--gcs-bucket requires a value}" + shift 2 + ;; + --gcs-read-only) + if [[ "${2:-}" == "true" || "${2:-}" == "false" ]]; then + gcs_read_only="$2" + shift 2 + else + gcs_read_only="true" + shift + fi + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "$version" ]]; then + echo "--version is required" >&2 + usage >&2 + exit 2 +fi + +append_line() { + local path="$1" + local line="$2" + printf '%s\n' "$line" >> "$path" +} + +export_env() { + local name="$1" + local value="$2" + export "$name=$value" + append_line "${GITHUB_ENV:?GITHUB_ENV is required}" "$name=$value" +} + +runner_arch() { + local raw="${RUNNER_ARCH:-$(uname -m)}" + case "$raw" in + X64|x64|x86_64|AMD64|amd64) echo "x86_64" ;; + ARM64|arm64|aarch64) echo "aarch64" ;; + ARM|arm|armv7|armv7l) echo "armv7" ;; + *) echo "Unsupported architecture: $raw" >&2; exit 1 ;; + esac +} + +runner_platform() { + local raw="${RUNNER_OS:-$(uname -s)}" + case "$raw" in + Linux|linux) echo "unknown-linux-musl tar.gz sccache" ;; + macOS|Darwin|darwin) echo "apple-darwin tar.gz sccache" ;; + Windows|windows|MINGW*|MSYS*|CYGWIN*) echo "pc-windows-msvc zip sccache.exe" ;; + *) echo "Unsupported OS: $raw" >&2; exit 1 ;; + esac +} + +download() { + local url="$1" + local destination="$2" + local curl_args=(--fail --location --retry 4 --connect-timeout 30 --max-time 120) + if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then + curl_args+=(--retry-all-errors) + fi + echo "Downloading $url" >&2 + curl "${curl_args[@]}" --user-agent "rerun-setup-sccache" --output "$destination" "$url" +} + +sha1_file() { + local path="$1" + if command -v sha1sum >/dev/null 2>&1; then + sha1sum "$path" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 1 "$path" | awk '{print $1}' + elif command -v openssl >/dev/null 2>&1; then + openssl sha1 "$path" | awk '{print $NF}' + else + echo "No SHA1 tool found" >&2 + exit 1 + fi +} + +verify_sha1() { + local archive="$1" + local sha1_path="$2" + local expected actual + expected="$(awk '{print $1}' "$sha1_path")" + # shellcheck disable=SC2218 # False positive: sha1_file is defined before verify_sha1 is called. + actual="$(sha1_file "$archive")" + actual="$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')" + expected="$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')" + # GNU checksum tools prefix hashes with `\` when paths contain backslashes. + # See also: https://unix.stackexchange.com/a/424629 + actual="${actual#\\}" + expected="${expected#\\}" + if [[ "$actual" != "$expected" ]]; then + echo "SHA1 mismatch for $(basename "$archive"): expected $expected, got $actual" >&2 + exit 1 + fi +} + +extract_archive() { + local archive="$1" + local destination="$2" + local extension="$3" + if [[ "$extension" == "zip" ]]; then + if command -v unzip >/dev/null 2>&1; then + unzip -q "$archive" -d "$destination" + elif command -v powershell.exe >/dev/null 2>&1; then + powershell.exe -NoProfile -Command "Expand-Archive -LiteralPath '$archive' -DestinationPath '$destination' -Force" + else + echo "No zip extraction tool found" >&2 + exit 1 + fi + else + tar -xzf "$archive" -C "$destination" + fi +} + +resolve_backend() { + if [[ "$backend" != "auto" ]]; then + echo "$backend" + elif [[ "${RUNNER_OS:-}" == "Linux" && -n "${RUNS_ON_S3_BUCKET_CACHE:-}" ]]; then + echo "s3" + else + echo "gcs" + fi +} + +write_config() { + local path="$1" + local resolved_backend="$2" + local rw_mode="READ_WRITE" + + if [[ "$gcs_read_only" == "true" ]]; then + rw_mode="READ_ONLY" + fi + + { + echo "server_startup_timeout_ms = 60000" + if [[ "$resolved_backend" == "gcs" ]]; then + cat < "$path" +} + +configure_backend() { + local resolved_backend="$1" + case "$resolved_backend" in + s3) + if [[ -z "${RUNS_ON_S3_BUCKET_CACHE:-}" ]]; then + echo "RUNS_ON_S3_BUCKET_CACHE is required for s3 backend" >&2 + exit 1 + fi + if [[ -z "${RUNS_ON_AWS_REGION:-}" ]]; then + echo "RUNS_ON_AWS_REGION is required for s3 backend" >&2 + exit 1 + fi + export_env "SCCACHE_BUCKET" "$RUNS_ON_S3_BUCKET_CACHE" + export_env "SCCACHE_REGION" "$RUNS_ON_AWS_REGION" + export_env "SCCACHE_S3_KEY_PREFIX" "cache/sccache" + ;; + gcs) + ;; + *) + echo "Unsupported backend: $resolved_backend" >&2 + exit 1 + ;; + esac + + write_config "${SCCACHE_CONF:?SCCACHE_CONF is required}" "$resolved_backend" +} + +start_server() { + local sccache_bin="$1" + for attempt in 1 2 3; do + echo "Starting sccache server (attempt $attempt/3)…" + if "$sccache_bin" --start-server; then + "$sccache_bin" --show-stats + return + fi + if [[ "$attempt" != "3" ]]; then + sleep $((attempt * 5)) + fi + done + echo "sccache --start-server failed after 3 attempts" >&2 + exit 1 +} + +install_sccache() { + local arch target_platform extension exe filename install_root archive sha1_path extract_dir bin_dir sccache_bin base_url + # shellcheck disable=SC2218 # False positive: runner_arch is defined before install_sccache is called. + arch="$(runner_arch)" + read -r target_platform extension exe <<< "$(runner_platform)" + filename="sccache-${version}-${arch}-${target_platform}.${extension}" + install_root="${RUNNER_TEMP:?RUNNER_TEMP is required}/sccache-${version}-${arch}-${target_platform}" + archive="$install_root/$filename" + sha1_path="$archive.sha1" + extract_dir="$install_root/extract" + bin_dir="$extract_dir/sccache-${version}-${arch}-${target_platform}" + sccache_bin="$bin_dir/$exe" + + mkdir -p "$install_root" "$extract_dir" + base_url="$MIRROR_BASE_URL/$version" + download "$base_url/$filename" "$archive" + download "$base_url/$filename.sha1" "$sha1_path" + verify_sha1 "$archive" "$sha1_path" + extract_archive "$archive" "$extract_dir" "$extension" + + if [[ ! -f "$sccache_bin" ]]; then + echo "Missing extracted sccache binary at $sccache_bin" >&2 + echo "Extracted files:" >&2 + find "$extract_dir" -print >&2 + exit 1 + fi + + append_line "${GITHUB_PATH:?GITHUB_PATH is required}" "$bin_dir" + echo "$sccache_bin" +} + +case "$backend" in + auto|s3|gcs) ;; + *) echo "Unsupported backend: $backend" >&2; exit 2 ;; +esac + +# shellcheck disable=SC2218 # False positive: resolve_backend is defined above. +resolved_backend="$(resolve_backend)" +sccache_bin="$(install_sccache)" + +export_env "SCCACHE_PATH" "$sccache_bin" +export_env "SCCACHE_GHA_ENABLED" "false" +export_env "RUSTC_WRAPPER" "sccache" +export_env "CARGO_INCREMENTAL" "0" +export_env "SCCACHE_CONF" "${RUNNER_TEMP:?RUNNER_TEMP is required}/sccache-config.toml" +configure_backend "$resolved_backend" +start_server "$sccache_bin" diff --git a/.github/workflows/auto_approve.yml b/.github/workflows/auto_approve.yml index 42523c1f8f54..670cd74cdd8b 100644 --- a/.github/workflows/auto_approve.yml +++ b/.github/workflows/auto_approve.yml @@ -22,11 +22,11 @@ jobs: (github.event_name == 'pull_request_target' || github.event.issue.pull_request) steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Wait a few seconds run: | diff --git a/.github/workflows/auto_docs.yml b/.github/workflows/auto_docs.yml index b29e92d3e230..65538aa7afca 100644 --- a/.github/workflows/auto_docs.yml +++ b/.github/workflows/auto_docs.yml @@ -22,7 +22,7 @@ jobs: outputs: result: ${{ steps.find-pr.outputs.result }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: # ref - not set, because we want to end up on the merge commit fetch-depth: 0 # don't perform a shallow clone @@ -52,7 +52,7 @@ jobs: if: needs.has-label.outputs.result == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RERUN_BOT_TOKEN }} diff --git a/.github/workflows/auto_docs_check.yml b/.github/workflows/auto_docs_check.yml index 88897d3377f6..3814a3bd76dc 100644 --- a/.github/workflows/auto_docs_check.yml +++ b/.github/workflows/auto_docs_check.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest if: github.event.pull_request.head.repo.owner.login == 'rerun-io' && contains(github.event.pull_request.labels.*.name, 'deploy docs') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: # ref - not set, because we want to end up on the merge commit fetch-depth: 0 # don't perform a shallow clone @@ -59,7 +59,9 @@ jobs: - name: Add success comment # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 + uses: mshick/add-pr-comment@v3.9.1 + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment on success. + continue-on-error: true if: success() with: message-id: "cherry-pick-check" @@ -70,7 +72,7 @@ jobs: - name: Add failure comment # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 + uses: mshick/add-pr-comment@v3.9.1 if: failure() with: message-id: "cherry-pick-check" diff --git a/.github/workflows/build-viewer.yml b/.github/workflows/build-viewer.yml index 45f83ab1e5be..39a428845ec6 100644 --- a/.github/workflows/build-viewer.yml +++ b/.github/workflows/build-viewer.yml @@ -63,7 +63,7 @@ jobs: sudo apt-get install -y \ libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev \ libxcb-xfixes0-dev libxkbcommon-dev libvulkan-dev \ - mesa-vulkan-drivers libxkbcommon-x11-0 + mesa-vulkan-drivers libxkbcommon-x11-0 libudev-dev - uses: actions/cache@v4 with: @@ -180,6 +180,7 @@ jobs: xcb-util-devel \ xcb-util-wm-devel \ libxkbcommon-devel \ + systemd-devel \ python3-pip dnf clean all diff --git a/.github/workflows/cargo_shear.yml b/.github/workflows/cargo_shear.yml index 8b0f5d51db04..f0ad5da4e08f 100644 --- a/.github/workflows/cargo_shear.yml +++ b/.github/workflows/cargo_shear.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/checkboxes.yml b/.github/workflows/checkboxes.yml index e17b6c4a01de..2d5c7238845a 100644 --- a/.github/workflows/checkboxes.yml +++ b/.github/workflows/checkboxes.yml @@ -28,11 +28,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check PR checkboxes run: | diff --git a/.github/workflows/clear_cache.yml b/.github/workflows/clear_cache.yml index fa081e73609b..10cf1b20553c 100644 --- a/.github/workflows/clear_cache.yml +++ b/.github/workflows/clear_cache.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clear cache - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: | const opts = { diff --git a/.github/workflows/contrib_checks.yml b/.github/workflows/contrib_checks.yml index 86ec666c9a7b..fee5b4e43e23 100644 --- a/.github/workflows/contrib_checks.yml +++ b/.github/workflows/contrib_checks.yml @@ -47,11 +47,11 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Python format check run: pixi run py-fmt-check @@ -67,11 +67,11 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Build via mkdocs run: | @@ -80,17 +80,16 @@ jobs: no-codegen-changes: name: Check if running codegen would produce any changes timeout-minutes: 60 - # TODO(andreas): setup-vulkan doesn't work on 24.4 right now due to missing .so - runs-on: ubuntu-22.04-large + runs-on: ubuntu-latest-16-cores steps: # Note: We explicitly don't override `ref` here. We need to see if changes would be made # in a context where we have merged with main. Otherwise we might miss changes such as one # PR introduces a new type and another PR changes the codegen. - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Codegen check run: pixi run codegen --force --check --warnings-as-errors @@ -101,16 +100,15 @@ jobs: rs-lints: name: Rust lints (fmt, check, clippy, tests, doc) timeout-minutes: 60 - # TODO(andreas): setup-vulkan doesn't work on 24.4 right now due to missing .so - runs-on: ubuntu-22.04-large + runs-on: ubuntu-latest-16-cores steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: lfs: true - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 # Install the Vulkan SDK, so we can use the software rasterizer. # TODO(andreas): It would be nice if `setup_software_rasterizer.py` could do that for us as well (note though that this action here is very fast when cached!) @@ -121,6 +119,7 @@ jobs: install_runtime: true cache: true stripdown: true + destination: ${{ github.workspace }}/vulkan_sdk - name: Setup software rasterizer run: pixi run python ./scripts/ci/setup_software_rasterizer.py @@ -135,7 +134,7 @@ jobs: run: pixi run rs-check --skip individual_crates docs_slow - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: test-results-linux @@ -148,14 +147,14 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" @@ -167,11 +166,11 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Toml format check run: pixi run toml-fmt-check @@ -181,11 +180,11 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check for too large files run: pixi run check-large-files @@ -195,22 +194,50 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check example thumbnails run: pixi run uvpy ./scripts/ci/thumbnails.py check + check-doc-order: + name: Check docs order + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: prefix-dev/setup-pixi@v0.10.0 + with: + pixi-version: v0.71.3 + + - name: Check docs order + run: pixi run uvpy ./scripts/ci/check_doc_order.py + + check-no-d2-code-blocks: + name: Check for D2 code blocks + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: prefix-dev/setup-pixi@v0.10.0 + with: + pixi-version: v0.71.3 + + - name: Check for D2 code blocks + run: pixi run uvpy ./scripts/ci/check_d2_diagrams.py + spell-check: name: Spell Check timeout-minutes: 60 runs-on: ubuntu-latest steps: - name: Checkout Actions Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Check spelling of entire workspace uses: crate-ci/typos@v1.45.1 @@ -220,11 +247,11 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 environments: cpp - name: Run clang format on all relevant files @@ -235,11 +262,11 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest-16-cores steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 environments: cpp # TODO(emilk): make this work somehow. Right now this just results in diff --git a/.github/workflows/contrib_rerun_py.yml b/.github/workflows/contrib_rerun_py.yml index aa9f9f19ea18..87ca06dc0f97 100644 --- a/.github/workflows/contrib_rerun_py.yml +++ b/.github/workflows/contrib_rerun_py.yml @@ -42,18 +42,18 @@ jobs: runs-on: ubuntu-latest-16-cores timeout-minutes: 60 container: - image: ghcr.io/rerun-io/ci_docker:0.17.0 + image: ghcr.io/rerun-io/ci_docker:0.18.0 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: lfs: true - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Build rerun-cli run: | diff --git a/.github/workflows/cpp_matrix_full.json b/.github/workflows/cpp_matrix_full.json index 7128cf71a29e..b8bdb2e98bd3 100644 --- a/.github/workflows/cpp_matrix_full.json +++ b/.github/workflows/cpp_matrix_full.json @@ -33,8 +33,8 @@ "name": "Mac aarch64", "runs_on": "macos-26-xlarge", "cache_key": "cpp-macos-arm64", - "extra_env_vars": "RERUN_USE_ASAN=1 LSAN_OPTIONS=suppressions=.github/workflows/lsan_suppressions.supp", + "extra_env_vars": "", "pr_ci": true } ] -} +} \ No newline at end of file diff --git a/.github/workflows/enforce_branch_name.yml b/.github/workflows/enforce_branch_name.yml index c9a021834aea..36f5f9cbddc2 100644 --- a/.github/workflows/enforce_branch_name.yml +++ b/.github/workflows/enforce_branch_name.yml @@ -28,7 +28,9 @@ jobs: - name: Leave comment if PR is from master/main branch of fork d if: ${{ failure() }} - uses: actions/github-script@v6 + uses: actions/github-script@v8 + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 29951bd3086a..16769c239af3 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -49,13 +49,13 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Get current wasm-bindgen version id: current-version diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 47a1e5d440a9..d71206aa902b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -62,10 +62,10 @@ jobs: [ubuntu-latest-16-cores, macos-26-xlarge, windows-latest-8-cores] runs-on: ${{ matrix.runs_on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.92.0 + toolchain: 1.95.0 - run: cargo build -p rerun # Intentionally NOT using pixi, because we wanna check that we can build outside of pixi diff --git a/.github/workflows/notify-reality-sync.yml b/.github/workflows/notify-reality-sync.yml index 3d02d823b439..06e2a17a1287 100644 --- a/.github/workflows/notify-reality-sync.yml +++ b/.github/workflows/notify-reality-sync.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Create GitHub App token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.SYNC_APP_ID }} private-key: ${{ secrets.SYNC_APP_PRIVATE_KEY }} diff --git a/.github/workflows/on_gh_release.yml b/.github/workflows/on_gh_release.yml index 8f4ad6d15bd5..32a32d4e4b7b 100644 --- a/.github/workflows/on_gh_release.yml +++ b/.github/workflows/on_gh_release.yml @@ -34,3 +34,23 @@ jobs: CONCURRENCY: "${{ github.event.release.tag_name || inputs.tag_name }}" RELEASE_VERSION: "${{ github.event.release.tag_name || inputs.tag_name }}" secrets: inherit + + dispatch-gradio-rerun-viewer: + name: "Trigger gradio-rerun-viewer release" + # Only on real GH release events (not workflow_dispatch), and only for finals. + if: github.event_name == 'release' && !github.event.release.prerelease + runs-on: ubuntu-latest + steps: + - name: Send repository_dispatch + env: + GH_TOKEN: ${{ secrets.RERUN_BOT_TOKEN }} + run: | + version="${{ github.event.release.tag_name }}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Tag $version is not a final X.Y.Z release; skipping." + exit 0 + fi + gh api repos/rerun-io/gradio-rerun-viewer/dispatches \ + -f event_type=rerun-released \ + -F client_payload[version]="$version" \ + -F client_payload[source_release_url]="${{ github.event.release.html_url }}" diff --git a/.github/workflows/on_pr_comment.yml b/.github/workflows/on_pr_comment.yml index a33c97fe1b95..52edec97e7b8 100644 --- a/.github/workflows/on_pr_comment.yml +++ b/.github/workflows/on_pr_comment.yml @@ -33,7 +33,7 @@ jobs: command: ${{ steps.parse.outputs.command }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Parse comment id: parse @@ -55,7 +55,7 @@ jobs: -f content=eyes - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Dispatch main workflow id: dispatch @@ -120,7 +120,7 @@ jobs: - name: Create PR comment # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 + uses: mshick/add-pr-comment@v3.9.1 with: # We use `GITHUB_TOKEN` here so there is no chance that we'll trigger another run of this workflow. # https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow diff --git a/.github/workflows/on_pull_request.yml b/.github/workflows/on_pull_request.yml index 6f82e867996f..624db1ad2925 100644 --- a/.github/workflows/on_pull_request.yml +++ b/.github/workflows/on_pull_request.yml @@ -36,7 +36,7 @@ jobs: protobuf_changes: ${{ steps.filter.outputs.protobuf_changes }} web_changes: ${{ steps.filter.outputs.web_changes }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - uses: dorny/paths-filter@v3 @@ -203,6 +203,15 @@ jobs: CHANNEL: main secrets: inherit + web-test: + name: "Web tests" + needs: [paths-filter] + if: github.event.pull_request.head.repo.owner.login == 'rerun-io' && needs.paths-filter.outputs.web_changes == 'true' + uses: ./.github/workflows/reusable_web_test.yml + with: + CONCURRENCY: pr-${{ github.event.pull_request.number }} + secrets: inherit + upload-web: name: "Upload Web" needs: [build-web] diff --git a/.github/workflows/on_push_docs.yml b/.github/workflows/on_push_docs.yml index 2dc8251c6562..9291beb8c6fb 100644 --- a/.github/workflows/on_push_docs.yml +++ b/.github/workflows/on_push_docs.yml @@ -28,11 +28,11 @@ jobs: outputs: version: ${{ steps.versioning.outputs.crate_version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Get version id: versioning @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-latest-16-cores needs: [get-version] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Rust uses: ./.github/actions/setup-rust @@ -54,9 +54,9 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Build search index run: | @@ -66,19 +66,33 @@ jobs: --master-key "${{ secrets.MEILISEARCH_TOKEN }}" \ --release-version "${{ needs.get-version.outputs.version }}" - redeploy-rerun-io: + # The website reads prose from GCS at SSR time, so a `docs-latest` + # push takes effect via a GCS re-upload + ISR revalidate. + upload-prose: + name: Upload prose to GCS runs-on: ubuntu-latest needs: [get-version] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Re-deploy rerun.io - uses: ./.github/actions/vercel + - uses: prefix-dev/setup-pixi@v0.10.0 with: - command: "deploy" - vercel_token: "${{ secrets.VERCEL_TOKEN }}" - vercel_team_name: "${{ vars.VERCEL_TEAM_NAME }}" - vercel_project_name: "${{ vars.VERCEL_PROJECT_NAME }}" - release_commit: "docs-latest" - release_version: "${{ needs.get-version.outputs.version }}" - target: "production" + pixi-version: v0.71.3 + + - id: "auth" + uses: google-github-actions/auth@v3 + with: + workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} + + - name: "Upload markdown docs & examples to GCS" + env: + ISR_BYPASS_TOKEN: ${{ secrets.ISR_BYPASS_TOKEN }} + VERSION_LABEL: ${{ needs.get-version.outputs.version }} + RERUN_COMMIT: ${{ github.sha }} + run: | + pixi run uv run scripts/ci/upload_docs.py upload \ + --version "$VERSION_LABEL" \ + --rerun-commit "$RERUN_COMMIT" \ + --mark-latest \ + --purge-token "$ISR_BYPASS_TOKEN" diff --git a/.github/workflows/on_push_main.yml b/.github/workflows/on_push_main.yml index f504e780086c..944e123f6dc5 100644 --- a/.github/workflows/on_push_main.yml +++ b/.github/workflows/on_push_main.yml @@ -56,6 +56,7 @@ jobs: PY_DOCS_VERSION_NAME: "main" CPP_DOCS_VERSION_NAME: "main" JS_DOCS_VERSION_NAME: "main" + MARKDOWN_DOCS_VERSION_NAME: "dev" UPDATE_LATEST: false secrets: inherit @@ -67,6 +68,13 @@ jobs: CHANNEL: main secrets: inherit + web-test: + name: "Web tests" + uses: ./.github/workflows/reusable_web_test.yml + with: + CONCURRENCY: push-${{ github.ref_name }}-${{ inputs.CONCURRENCY }} + secrets: inherit + upload-web: name: "Upload Web" needs: [build-web] diff --git a/.github/workflows/pr-trigger-reality-sync.yml b/.github/workflows/pr-trigger-reality-sync.yml index 72a66a31092f..f40f12e6f8e5 100644 --- a/.github/workflows/pr-trigger-reality-sync.yml +++ b/.github/workflows/pr-trigger-reality-sync.yml @@ -12,7 +12,10 @@ jobs: if: | github.event_name == 'issue_comment' && github.event.issue.pull_request && - contains(github.event.comment.body, '@rerun-bot reality-sync') && + ( + contains(github.event.comment.body, '@rerun-bot reality-sync') || + contains(github.event.comment.body, '@rerun-bot sync-reality') + ) && github.event.comment.user.type != 'Bot' && github.event.comment.author_association != 'OWNER' && github.event.comment.author_association != 'MEMBER' @@ -20,7 +23,7 @@ jobs: steps: - name: Create GitHub App token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.SYNC_APP_ID }} private-key: ${{ secrets.SYNC_APP_PRIVATE_KEY }} @@ -36,13 +39,16 @@ jobs: --body "Sorry, only organization members can trigger reality-sync." trigger-import: - # For issue_comment: org members (OWNER/MEMBER) can trigger with '@rerun-bot reality-sync' + # For issue_comment: org members (OWNER/MEMBER) can trigger with '@rerun-bot reality-sync' or '@rerun-bot sync-reality' # For pull_request_target: only auto-sync non-fork PRs (fork PRs require explicit comment trigger) if: | ( github.event_name == 'issue_comment' && github.event.issue.pull_request && - contains(github.event.comment.body, '@rerun-bot reality-sync') && + ( + contains(github.event.comment.body, '@rerun-bot reality-sync') || + contains(github.event.comment.body, '@rerun-bot sync-reality') + ) && github.event.comment.user.type != 'Bot' && ( github.event.comment.author_association == 'OWNER' || @@ -59,7 +65,7 @@ jobs: steps: - name: Create GitHub App token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.SYNC_APP_ID }} private-key: ${{ secrets.SYNC_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99fafa9c597a..5f309af3276f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,10 +54,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.11 @@ -88,18 +88,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RERUN_BOT_TOKEN }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: - node-version: "22.x" + node-version: "24.x" - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Update crate versions id: versioning @@ -108,7 +108,7 @@ jobs: pixi run python scripts/ci/crates.py check-git-branch-name echo Parse the release version from the branch name… - # `prepare-release-0.8.1-meta.N` -> `0.8.1` + # `prepare-release-0.8.1-alpha.N` -> `0.8.1` release_version=$(pixi run python scripts/ci/crates.py get-version --from git --finalize) echo "release_version: $release_version" @@ -207,7 +207,7 @@ jobs: ### Next steps - Test the release - For alpha releases: - - [ ] If a GH release should be published for this alpha (give extra love to alphas deployed to Rerun Cloud or when external testing is required): + - [ ] If a GH release should be published for this alpha (give extra love to alphas deployed to Rerun Hub or when external testing is required): - [ ] Create the GitHub release manually from the UI - [ ] Stretch goal: generate a raw changelog and add it to the GH release with this disclaimer: **DISCLAIMER**: This is an unreviewed, automatically generated changelog. We only provide fully reviewed changelogs for final releases. @@ -216,12 +216,13 @@ jobs: - Otherwise, close the PR without merging. Cherrypick any interesting commit to `main`, but *not* the version bump one (it would introduce bad links). - For non-alpha releases: - For any added commits, run the release workflow in 'rc' mode again - - After testing, _ensure that this PR is mergeable to `main`_, then run the release workflow in 'release' mode + - After testing, _ensure that this PR is mergeable to `main`_, then run the release workflow in 'final' mode - Once the final release workflow finishes it will create a GitHub release for you. Then: - [ ] Sanity check the build artifacts: - [ ] pip install: does it install and run? - [ ] cargo install of cli tool: does it install and run? - [ ] C++ SDK zip: does it contain rerun_c for all platforms? + - [ ] Web Viewer: does it run and connect to Rerun Hub servers? - [ ] Edit and publish the GitHub release: - Do NOT create a GitHub release draft yourself! Let the release job do it. - Populate the release with the changelog and a nice header video/picture @@ -229,9 +230,9 @@ jobs: - Click `Publish release` - Once published, the release assets will sync to it automatically. - [ ] Update the [google colab notebooks](https://colab.research.google.com/drive/1R9I7s4o6wydQC_zkybqaSRFTtlEaked_) to install this version and re-execute the notebook. - - [ ] Release a new version of gradio (@oxkitsune, @jprochazk) + - [ ] Verify the auto-triggered [gradio-rerun-viewer release workflow](https://github.com/rerun-io/gradio-rerun-viewer/actions/workflows/auto_release_on_rerun.yml) succeeded (dispatched from `on_gh_release.yml` for final releases) - [ ] Create a new branch from the prepare-release branch, rebase that one on main and sync it to reality so it can be merged. - - [ ] (A few hours later) Check on the [conda feedstock PR](https://github.com/conda-forge/rerun-sdk-feedstock/pulls) (ping Antoine, Nick and/or Jeremy in necessary) + - [ ] (A few hours later) Check on the [conda feedstock PR](https://github.com/conda-forge/rerun-sdk-feedstock/pulls) (ping Antoine and/or Nick if necessary) - [ ] Tests - [ ] Windows @@ -254,13 +255,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ needs.version.outputs.release-commit }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check uv.lock is up-to-date run: pixi run uv-lock-check @@ -274,6 +275,7 @@ jobs: PY_DOCS_VERSION_NAME: ${{ inputs.release-type == 'final' && needs.version.outputs.final || 'dev' }} CPP_DOCS_VERSION_NAME: ${{ inputs.release-type == 'final' && 'stable' || 'dev' }} JS_DOCS_VERSION_NAME: ${{ inputs.release-type == 'final' && needs.version.outputs.final || 'dev' }} + MARKDOWN_DOCS_VERSION_NAME: ${{ inputs.release-type == 'final' && needs.version.outputs.final || 'dev' }} RELEASE_COMMIT: ${{ needs.version.outputs.release-commit }} RELEASE_VERSION: ${{ needs.version.outputs.final }} UPDATE_LATEST: ${{ inputs.release-type == 'final' }} @@ -319,9 +321,18 @@ jobs: release-commit: ${{ needs.version.outputs.release-commit }} secrets: inherit + web-test: + name: "Web tests" + needs: [version] + uses: ./.github/workflows/reusable_web_test.yml + with: + CONCURRENCY: ${{ github.ref_name }} + REF: ${{ needs.version.outputs.release-commit }} + secrets: inherit + publish-web: name: "Build and Publish Web" - needs: [version, publish-wheels] + needs: [version, publish-wheels, web-test] uses: ./.github/workflows/reusable_publish_web.yml with: release-version: ${{ needs.version.outputs.current }} @@ -360,7 +371,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RERUN_BOT_TOKEN }} @@ -392,7 +403,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RERUN_BOT_TOKEN }} @@ -425,6 +436,8 @@ jobs: - name: Create comment if: inputs.release-type == 'rc' || inputs.release-type == 'final' + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true env: GH_TOKEN: ${{ secrets.RERUN_BOT_TOKEN }} run: | @@ -459,18 +472,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RERUN_BOT_TOKEN }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: - node-version: "22.x" + node-version: "24.x" - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: git config run: | @@ -525,12 +538,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.RERUN_BOT_TOKEN }} - name: Create comment + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true env: GH_TOKEN: ${{ secrets.RERUN_BOT_TOKEN }} run: | diff --git a/.github/workflows/reusable_bench.yml b/.github/workflows/reusable_bench.yml index e391339ea759..42e0c157da6d 100644 --- a/.github/workflows/reusable_bench.yml +++ b/.github/workflows/reusable_bench.yml @@ -48,7 +48,7 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest-16-cores steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 # we need full history ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} @@ -62,9 +62,9 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Add SHORT_SHA env property with commit short sha run: echo "SHORT_SHA=`echo ${{github.sha}} | cut -c1-7`" >> $GITHUB_ENV @@ -83,7 +83,7 @@ jobs: -- --output-format=bencher | tee /tmp/${{ env.SHORT_SHA }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" diff --git a/.github/workflows/reusable_build_and_upload_rerun_c.yml b/.github/workflows/reusable_build_and_upload_rerun_c.yml index e076eba5913e..9f55410dff38 100644 --- a/.github/workflows/reusable_build_and_upload_rerun_c.yml +++ b/.github/workflows/reusable_build_and_upload_rerun_c.yml @@ -74,13 +74,13 @@ jobs: linux-arm64) runner="ubuntu-arm-16-core" target="aarch64-unknown-linux-gnu" - container="'ghcr.io/rerun-io/ci_docker:0.17.0'" + container="'ghcr.io/rerun-io/ci_docker:0.18.0'" lib_name="librerun_c.a" ;; linux-x64) runner="ubuntu-latest-16-cores" target="x86_64-unknown-linux-gnu" - container="'ghcr.io/rerun-io/ci_docker:0.17.0'" + container="'ghcr.io/rerun-io/ci_docker:0.18.0'" lib_name="librerun_c.a" ;; windows-x64) @@ -128,13 +128,13 @@ jobs: JOB_CONTEXT: ${{ toJson(job) }} INPUTS_CONTEXT: ${{ toJson(inputs) }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || ((github.event_name == 'pull_request' && github.event.pull_request.head.ref) || '') }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Set up Rust and Authenticate to GCS uses: ./.github/actions/setup-rust diff --git a/.github/workflows/reusable_build_and_upload_rerun_cli.yml b/.github/workflows/reusable_build_and_upload_rerun_cli.yml index 25de0801e8c0..3915247d314d 100644 --- a/.github/workflows/reusable_build_and_upload_rerun_cli.yml +++ b/.github/workflows/reusable_build_and_upload_rerun_cli.yml @@ -76,13 +76,13 @@ jobs: linux-arm64) runner="ubuntu-arm-16-core" target="aarch64-unknown-linux-gnu" - container="'ghcr.io/rerun-io/ci_docker:0.17.0'" + container="'ghcr.io/rerun-io/ci_docker:0.18.0'" bin_name="rerun" ;; linux-x64) runner="ubuntu-latest-16-cores" target="x86_64-unknown-linux-gnu" - container="'ghcr.io/rerun-io/ci_docker:0.17.0'" + container="'ghcr.io/rerun-io/ci_docker:0.18.0'" bin_name="rerun" ;; windows-x64) @@ -130,7 +130,7 @@ jobs: JOB_CONTEXT: ${{ toJson(job) }} INPUTS_CONTEXT: ${{ toJson(inputs) }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || ((github.event_name == 'pull_request' && github.event.pull_request.head.ref) || '') }} @@ -143,9 +143,9 @@ jobs: service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} targets: ${{ needs.set-config.outputs.TARGET }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Build web-viewer (release) run: pixi run rerun-build-web-release @@ -174,6 +174,21 @@ jobs: full_commit="${{ inputs.RELEASE_COMMIT || ((github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha) }}" echo "sha=$(echo $full_commit | cut -c1-7)" >> "$GITHUB_OUTPUT" + # Wrap the macOS binary in a Rerun.app bundle so macOS uses "Rerun" as the + # dock label / "About" name instead of falling back to the binary filename. + - name: Bundle Rerun.app (macOS) + if: inputs.PLATFORM == 'macos-arm64' + run: | + version=$(pixi run python scripts/ci/crates.py get-version) + mkdir -p bundle-out + pixi run python scripts/ci/bundle_macos_app.py \ + --binary "./target/${{ needs.set-config.outputs.TARGET }}/release/${{ needs.set-config.outputs.BIN_NAME }}" \ + --icon crates/viewer/re_viewer/data/app_icon_mac.png \ + --info-plist scripts/ci/macos/Info.plist \ + --version "$version" \ + --output-dir bundle-out + tar -czf bundle-out/Rerun.app.tar.gz -C bundle-out Rerun.app + - name: "Upload rerun-cli (commit)" uses: google-github-actions/upload-cloud-storage@v3 with: @@ -182,6 +197,20 @@ jobs: parent: false process_gcloudignore: false + - name: "Upload Rerun.app (commit, macOS)" + if: inputs.PLATFORM == 'macos-arm64' + uses: google-github-actions/upload-cloud-storage@v3 + with: + path: "bundle-out/Rerun.app.tar.gz" + destination: "rerun-builds/commit/${{ steps.get-sha.outputs.sha }}/rerun-cli/${{ inputs.PLATFORM }}" + parent: false + process_gcloudignore: false + # The tarball is already gzip-compressed. Uploading with the action's default + # `gzip: true` sets `Content-Encoding: gzip` on the blob, so the download client + # transparently decompresses one layer while validating the checksum against the + # still-compressed stored hash — a guaranteed checksum mismatch. + gzip: false + - name: "Upload rerun-cli (adhoc)" if: ${{ inputs.ADHOC_NAME != '' }} uses: google-github-actions/upload-cloud-storage@v3 @@ -190,3 +219,14 @@ jobs: destination: "rerun-builds/adhoc/${{inputs.ADHOC_NAME}}/rerun-cli/${{ inputs.PLATFORM }}" parent: false process_gcloudignore: false + + - name: "Upload Rerun.app (adhoc, macOS)" + if: ${{ inputs.ADHOC_NAME != '' && inputs.PLATFORM == 'macos-arm64' }} + uses: google-github-actions/upload-cloud-storage@v3 + with: + path: "bundle-out/Rerun.app.tar.gz" + destination: "rerun-builds/adhoc/${{inputs.ADHOC_NAME}}/rerun-cli/${{ inputs.PLATFORM }}" + parent: false + process_gcloudignore: false + # See the note on the commit upload above: keep the already-gzipped tarball raw. + gzip: false diff --git a/.github/workflows/reusable_build_and_upload_wheels.yml b/.github/workflows/reusable_build_and_upload_wheels.yml index dd74b4c89e2f..ffe3f8883790 100644 --- a/.github/workflows/reusable_build_and_upload_wheels.yml +++ b/.github/workflows/reusable_build_and_upload_wheels.yml @@ -89,7 +89,7 @@ jobs: COMPAT: ${{ steps.set-config.outputs.compat }} steps: - name: Login to GHCR (with retries) - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_seconds: 10 max_attempts: 12 @@ -103,14 +103,14 @@ jobs: linux-arm64) runner="ubuntu-arm-16-core" target="aarch64-unknown-linux-gnu" - container="'ghcr.io/rerun-io/ci_docker:0.17.0'" # Required to be manylinux compatible + container="'ghcr.io/rerun-io/ci_docker:0.18.0'" # Required to be manylinux compatible compat="manylinux_2_28" ;; linux-x64) runner="ubuntu-latest-16-cores" target="x86_64-unknown-linux-gnu" compat="manylinux_2_28" - container="'ghcr.io/rerun-io/ci_docker:0.17.0'" # Required to be manylinux compatible + container="'ghcr.io/rerun-io/ci_docker:0.18.0'" # Required to be manylinux compatible ;; windows-x64) runner="windows-latest-32-cores" @@ -159,7 +159,7 @@ jobs: JOB_CONTEXT: ${{ toJson(job) }} INPUTS_CONTEXT: ${{ toJson(inputs) }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || ((github.event_name == 'pull_request' && github.event.pull_request.head.ref) || '') }} @@ -173,9 +173,9 @@ jobs: service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} targets: ${{ needs.set-config.outputs.TARGET }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Get sha id: get-sha @@ -185,11 +185,21 @@ jobs: - name: "Download rerun-cli" run: | - pixi run fetch-artifact \ - --commit-sha ${{ steps.get-sha.outputs.sha }} \ - --artifact rerun-cli \ - --platform ${{ inputs.PLATFORM }} \ - --dest rerun_py/rerun_sdk/rerun_cli + if [ "${{ inputs.PLATFORM }}" = "macos-arm64" ]; then + # macOS gets the Rerun.app bundle so the dock label reads "Rerun" + # (see scripts/ci/bundle_macos_app.py). + pixi run fetch-artifact \ + --commit-sha ${{ steps.get-sha.outputs.sha }} \ + --artifact rerun-cli-macos-app \ + --platform ${{ inputs.PLATFORM }} \ + --dest rerun_py/rerun_sdk/rerun_cli + else + pixi run fetch-artifact \ + --commit-sha ${{ steps.get-sha.outputs.sha }} \ + --artifact rerun-cli \ + --platform ${{ inputs.PLATFORM }} \ + --dest rerun_py/rerun_sdk/rerun_cli + fi - name: Build run: | @@ -202,7 +212,7 @@ jobs: - name: Save wheel artifact if: ${{ inputs.WHEEL_ARTIFACT_NAME != '' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{inputs.WHEEL_ARTIFACT_NAME}} path: dist/${{ needs.set-config.outputs.TARGET }} @@ -222,7 +232,7 @@ jobs: - name: Save rerun_notebook wheel artifact if: ${{ (inputs.MODE == 'pypi' || inputs.MODE == 'extra') && inputs.PLATFORM == 'linux-x64' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: rerun_notebook_wheel path: dist diff --git a/.github/workflows/reusable_build_examples.yml b/.github/workflows/reusable_build_examples.yml index a46580e4b9d1..1670c34b9bc7 100644 --- a/.github/workflows/reusable_build_examples.yml +++ b/.github/workflows/reusable_build_examples.yml @@ -35,7 +35,7 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest-16-cores steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.ref) || '' }} lfs: true @@ -48,18 +48,18 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Download Rerun Wheel - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ${{ inputs.WHEEL_ARTIFACT_NAME }} path: wheel - name: Download Rerun Notebook Wheel - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: rerun_notebook_wheel path: wheel @@ -98,7 +98,7 @@ jobs: example_data/snippets - name: Upload assets - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: example_data path: example_data diff --git a/.github/workflows/reusable_build_js.yml b/.github/workflows/reusable_build_js.yml index ecf32940950b..d1c3a9dfee25 100644 --- a/.github/workflows/reusable_build_js.yml +++ b/.github/workflows/reusable_build_js.yml @@ -29,13 +29,13 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest-16-cores steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: - node-version: "22.x" + node-version: "24.x" - name: Install Yarn run: npm install -g yarn @@ -49,9 +49,9 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Install yarn dependencies run: pixi run yarn --cwd rerun_js install @@ -66,7 +66,7 @@ jobs: cp rerun_js/*/*.tar.gz rerun_js_package/ - name: Upload rerun_js - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: rerun_js path: rerun_js_package diff --git a/.github/workflows/reusable_build_web.yml b/.github/workflows/reusable_build_web.yml index 639925573d43..b77b055829c3 100644 --- a/.github/workflows/reusable_build_web.yml +++ b/.github/workflows/reusable_build_web.yml @@ -37,7 +37,7 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest-16-cores steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} @@ -50,7 +50,9 @@ jobs: - name: Status comment if: github.event_name == 'pull_request' # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 + uses: mshick/add-pr-comment@v3.9.1 + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true with: message-id: "web-viewer-build-status" repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -74,9 +76,9 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Build web-viewer (release) run: | @@ -98,7 +100,7 @@ jobs: "crates/viewer/re_web_viewer_server/web_viewer/examples_manifest.json" - name: Upload web viewer - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: web_viewer path: "crates/viewer/re_web_viewer_server/web_viewer" @@ -106,7 +108,9 @@ jobs: - name: Status comment if: failure() && github.event_name == 'pull_request' # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 + uses: mshick/add-pr-comment@v3.9.1 + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true with: message-id: "web-viewer-build-status" repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable_bundle_and_upload_rerun_cpp.yml b/.github/workflows/reusable_bundle_and_upload_rerun_cpp.yml index bedf940bf2f3..07965dd7df76 100644 --- a/.github/workflows/reusable_bundle_and_upload_rerun_cpp.yml +++ b/.github/workflows/reusable_bundle_and_upload_rerun_cpp.yml @@ -32,18 +32,18 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || ((github.event_name == 'pull_request' && github.event.pull_request.head.ref) || '') }} - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" diff --git a/.github/workflows/reusable_checks.yml b/.github/workflows/reusable_checks.yml index 50547e47d934..7570a902fa10 100644 --- a/.github/workflows/reusable_checks.yml +++ b/.github/workflows/reusable_checks.yml @@ -37,7 +37,7 @@ jobs: # Note: We explicitly don't override `ref` here. We need to see if changes would be made # in a context where we have merged with main. Otherwise we might miss changes such as one # PR introduces a new type and another PR changes the codegen. - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Rust uses: ./.github/actions/setup-rust @@ -47,9 +47,9 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Codegen check run: pixi run codegen --force --check --warnings-as-errors @@ -70,16 +70,16 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" @@ -92,17 +92,20 @@ jobs: - name: Check forbidden dependencies run: pixi run python scripts/check_forbidden_dependencies.py + - name: Check skills + run: pixi run python scripts/ci/check_skills.py + toml-format-check: name: Toml format check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Toml format check run: pixi run toml-fmt-check @@ -111,13 +114,13 @@ jobs: name: Check uv.lock is up-to-date runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check uv.lock run: pixi run uv-lock-check @@ -126,13 +129,13 @@ jobs: name: Check for too large files runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check for too large files run: pixi run check-large-files @@ -141,13 +144,13 @@ jobs: name: Check for wrong publish flags runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check for wrong publish flags run: pixi run check-publish-flags @@ -156,13 +159,13 @@ jobs: name: Check example thumbnails runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check example thumbnails # Use uv run --frozen python instead of uvpy. @@ -170,20 +173,53 @@ jobs: # uv-lock-check will independently validate the lockfile status. run: pixi run uv run --frozen python ./scripts/ci/thumbnails.py check + check-doc-order: + name: Check docs order + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} + + - uses: prefix-dev/setup-pixi@v0.10.0 + with: + pixi-version: v0.71.3 + + - name: Check docs order + run: pixi run uv run --frozen python ./scripts/ci/check_doc_order.py + + check-no-d2-code-blocks: + name: Check for D2 code blocks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} + + - uses: prefix-dev/setup-pixi@v0.10.0 + with: + pixi-version: v0.71.3 + + - name: Check for D2 code blocks + # Use uv run --frozen python instead of uvpy. + # It's nice to get valid checks here even if the uv lockfile is out-of-date + # uv-lock-check will independently validate the lockfile status. + run: pixi run uv run --frozen python ./scripts/ci/check_d2_diagrams.py + check-example-manifest-coverage: name: Check example manifest coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" @@ -197,16 +233,16 @@ jobs: name: Lint markdown runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" @@ -225,7 +261,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} @@ -238,13 +274,13 @@ jobs: name: Misc formatting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: prettier --check run: pixi run misc-fmt-check @@ -256,21 +292,25 @@ jobs: runs-on: ubuntu-latest # do not fail entire workflow (e.g. nightly) if this is the only failing check continue-on-error: true + env: + # lychee uses this to avoid GitHub rate limiting when checking github.com links. + # See https://github.com/lycheeverse/lychee#github-token + GITHUB_TOKEN: ${{ github.token }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: # For PRs, we need to fetch the base branch to compare against fetch-depth: 0 - name: Set up Python if: github.event_name == 'pull_request' - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" - name: Restore lychee cache id: restore-cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .lycheecache key: cache-lychee-${{ github.sha }} @@ -278,9 +318,9 @@ jobs: - name: Set up pixi for PR Link Checker if: ${{ inputs.CHANNEL == 'pr' }} - uses: prefix-dev/setup-pixi@v0.9.4 + uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 # For PRs: Check only links in added lines - name: PR Link Checker (added lines only) diff --git a/.github/workflows/reusable_checks_cpp.yml b/.github/workflows/reusable_checks_cpp.yml index 7d253b1a664a..95bb20c7d149 100644 --- a/.github/workflows/reusable_checks_cpp.yml +++ b/.github/workflows/reusable_checks_cpp.yml @@ -38,7 +38,7 @@ jobs: outputs: MATRIX: ${{ steps.set-matrix.outputs.matrix }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - name: Load C++ test matrix @@ -56,15 +56,15 @@ jobs: # Skipping the entire step would apparently require a separate job, not doing that here. # Instead we keep checking for the `matrix.pr_ci` flag. # See https://stackoverflow.com/questions/77186893/how-can-i-skip-the-whole-job-for-a-matrix-match-in-github-action - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 if: ${{ github.event_name != 'pull_request' || matrix.pr_ci != false }} with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 if: ${{ github.event_name != 'pull_request' || matrix.pr_ci != false }} with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 environments: cpp - name: Set up Rust @@ -109,13 +109,13 @@ jobs: name: C++ formatting check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 environments: cpp - name: Run clang format on all relevant files diff --git a/.github/workflows/reusable_checks_doc_redirects.yml b/.github/workflows/reusable_checks_doc_redirects.yml index 64e5f6278623..ff8d21ce84d3 100644 --- a/.github/workflows/reusable_checks_doc_redirects.yml +++ b/.github/workflows/reusable_checks_doc_redirects.yml @@ -23,14 +23,14 @@ jobs: name: Check doc redirects runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} fetch-depth: 0 # Need full history for git diff against main - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Check doc redirects run: pixi run python scripts/ci/check_doc_redirects.py --base origin/main diff --git a/.github/workflows/reusable_checks_protobuf.yml b/.github/workflows/reusable_checks_protobuf.yml index 941fbc7ff2c4..a432ff37ce66 100644 --- a/.github/workflows/reusable_checks_protobuf.yml +++ b/.github/workflows/reusable_checks_protobuf.yml @@ -30,13 +30,13 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Fetch latest main (so we can grab the current schema snapshot) run: time git fetch origin main # yes, we need full --depth for `buf` to work diff --git a/.github/workflows/reusable_checks_python.yml b/.github/workflows/reusable_checks_python.yml index e50daabd9ab6..db2dab14f2cb 100644 --- a/.github/workflows/reusable_checks_python.yml +++ b/.github/workflows/reusable_checks_python.yml @@ -30,13 +30,13 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Python format check run: pixi run py-fmt-check @@ -54,13 +54,13 @@ jobs: timeout-minutes: 10 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Run notebook tests run: pixi run py-test-notebook @@ -72,13 +72,13 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Build via mkdocs run: | diff --git a/.github/workflows/reusable_checks_rust.yml b/.github/workflows/reusable_checks_rust.yml index 0359f2851719..1314fd458caf 100644 --- a/.github/workflows/reusable_checks_rust.yml +++ b/.github/workflows/reusable_checks_rust.yml @@ -42,10 +42,9 @@ jobs: rs-lints: name: Rust lints (fmt, check, clippy, doc) timeout-minutes: 60 - # TODO(andreas): setup-vulkan doesn't work on 24.4 right now due to missing .so - runs-on: ubuntu-22.04-large + runs-on: ubuntu-latest-16-cores steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} lfs: true @@ -58,9 +57,9 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Rust checks (PR subset) if: ${{ inputs.CHANNEL == 'pr' }} @@ -86,12 +85,11 @@ jobs: rs-tests: name: Test on Linux timeout-minutes: 60 - # TODO(andreas): setup-vulkan doesn't work on 24.4 right now due to missing .so - runs-on: ubuntu-22.04-large + runs-on: ubuntu-latest-16-cores env: RUSTDOCFLAGS: "" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} lfs: true @@ -104,9 +102,9 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 # Install the Vulkan SDK, so we can use the software rasterizer. # TODO(andreas): It would be nice if `setup_software_rasterizer.py` could do that for us as well (note though that this action here is very fast when cached!) @@ -117,6 +115,7 @@ jobs: install_runtime: true cache: true stripdown: true + destination: ${{ github.workspace }}/vulkan_sdk - name: Setup software rasterizer run: pixi run python ./scripts/ci/setup_software_rasterizer.py @@ -129,7 +128,7 @@ jobs: run: pixi run rs-check --only tests_without_all_features - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: test-results-linux @@ -152,7 +151,7 @@ jobs: if: ${{ inputs.CHANNEL == 'main' }} runs-on: ${{ matrix.runs_on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: lfs: true @@ -165,9 +164,9 @@ jobs: service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} # Building with `--all-features` requires extra build tools like `nasm`. - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 # Install the Vulkan SDK, so we can use the software rasterizer. # TODO(andreas): It would be nice if `setup_software_rasterizer.py` could do that for us as well (note though that this action here is very fast when cached!) @@ -178,6 +177,7 @@ jobs: install_runtime: true cache: true stripdown: true + destination: ${{ github.workspace }}/vulkan_sdk - name: Setup software rasterizer run: pixi run python ./scripts/ci/setup_software_rasterizer.py @@ -186,7 +186,7 @@ jobs: run: pixi run rs-check --only tests - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: test-results-${{ matrix.name }} @@ -230,7 +230,7 @@ jobs: if: ${{ inputs.CHANNEL == 'nightly' }} runs-on: ${{ matrix.platform.runs_on }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: lfs: true @@ -243,9 +243,9 @@ jobs: service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} # Building with `--all-features` requires extra build tools like `nasm`. - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 # Install the Vulkan SDK, so we can use the software rasterizer. # TODO(andreas): It would be nice if `setup_software_rasterizer.py` could do that for us as well (note though that this action here is very fast when cached!) @@ -256,6 +256,7 @@ jobs: install_runtime: true cache: true stripdown: true + destination: ${{ github.workspace }}/vulkan_sdk - name: Setup software rasterizer run: pixi run python ./scripts/ci/setup_software_rasterizer.py @@ -264,7 +265,7 @@ jobs: run: pixi run rs-check --only ${{ matrix.checks_group.checks }} - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: test-results-${{ matrix.platform.name }}-${{ matrix.checks_group.name }} diff --git a/.github/workflows/reusable_deploy_docs.yml b/.github/workflows/reusable_deploy_docs.yml index d86e89d8f3d5..f5ff404e3631 100644 --- a/.github/workflows/reusable_deploy_docs.yml +++ b/.github/workflows/reusable_deploy_docs.yml @@ -15,6 +15,14 @@ on: CPP_DOCS_VERSION_NAME: required: true type: string + MARKDOWN_DOCS_VERSION_NAME: + required: true + type: string + description: | + Version label to upload markdown docs/examples under on GCS + (`gs://rerun-docs/prose/{label}/`). Use `dev` for non-final + releases / pushes to main, the semver string for final + releases, or `pr-{N}` for PR previews. RELEASE_VERSION: required: false type: string @@ -51,22 +59,22 @@ jobs: name: Python runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || (github.event_name == 'pull_request' && github.event.pull_request.head.ref || '') }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" @@ -125,19 +133,19 @@ jobs: JOB_CONTEXT: ${{ toJson(job) }} INPUTS_CONTEXT: ${{ toJson(inputs) }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || (github.event_name == 'pull_request' && github.event.pull_request.head.ref || '') }} - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Doxygen C++ docs run: pixi run -e cpp cpp-docs @@ -165,22 +173,22 @@ jobs: name: JS runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || (github.event_name == 'pull_request' && github.event.pull_request.head.ref || '') }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" @@ -200,3 +208,39 @@ jobs: if: ${{ inputs.UPDATE_LATEST }} run: | pixi run uv run --group dev scripts/update_docs_url_rewrite.py --version ${{ inputs.JS_DOCS_VERSION_NAME }} --language js + + # --------------------------------------------------------------------------- + + md-deploy-docs: + name: Markdown + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.RELEASE_COMMIT || (github.event_name == 'pull_request' && github.event.pull_request.head.ref || '') }} + + - uses: prefix-dev/setup-pixi@v0.10.0 + with: + pixi-version: v0.71.3 + + - id: "auth" + uses: google-github-actions/auth@v3 + with: + workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} + + - name: "Upload markdown docs & examples to GCS" + env: + ISR_BYPASS_TOKEN: ${{ secrets.ISR_BYPASS_TOKEN }} + VERSION_LABEL: ${{ inputs.MARKDOWN_DOCS_VERSION_NAME }} + MARK_LATEST: ${{ inputs.UPDATE_LATEST }} + RERUN_COMMIT: ${{ inputs.RELEASE_COMMIT }} + run: | + ARGS=(upload --version "$VERSION_LABEL" --purge-token "$ISR_BYPASS_TOKEN") + if [ "$MARK_LATEST" = "true" ]; then + ARGS+=(--mark-latest) + fi + if [ -n "$RERUN_COMMIT" ]; then + ARGS+=(--rerun-commit "$RERUN_COMMIT") + fi + pixi run uv run scripts/ci/upload_docs.py "${ARGS[@]}" diff --git a/.github/workflows/reusable_deploy_landing_preview.yml b/.github/workflows/reusable_deploy_landing_preview.yml index ba334f6b022d..2dd2d6912baa 100644 --- a/.github/workflows/reusable_deploy_landing_preview.yml +++ b/.github/workflows/reusable_deploy_landing_preview.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.ref) || '' }} @@ -40,85 +40,58 @@ jobs: full_commit="${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha }}" echo "sha=$full_commit" >> "$GITHUB_OUTPUT" - - name: Deploy rerun.io preview - id: vercel-initial-deploy - uses: ./.github/actions/vercel + - uses: prefix-dev/setup-pixi@v0.10.0 with: - command: "deploy" - vercel_token: ${{ secrets.VERCEL_TOKEN }} - vercel_team_name: ${{ vars.VERCEL_TEAM_NAME }} - vercel_project_name: ${{ vars.VERCEL_PROJECT_NAME }} - release_commit: ${{ steps.get-sha.outputs.sha }} - target: "preview" - - - name: Create pending comment - # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 - if: success() - with: - message-id: "vercel-preview" - repo-token: ${{ secrets.GITHUB_TOKEN }} - message: | - Latest documentation preview deployment is pending: - ${{ steps.vercel-initial-deploy.outputs.vercel_preview_inspector_url }} + pixi-version: v0.71.3 - | Result | Commit | Link | - | ------ | ------- | ----- | - | ⏳ | ${{ steps.get-sha.outputs.sha }} | unavailable | - - - name: Wait for deployment - id: vercel - uses: ./.github/actions/vercel - if: success() + - id: "auth" + uses: google-github-actions/auth@v3 with: - command: "wait-for-deployment" - vercel_token: ${{ secrets.VERCEL_TOKEN }} - vercel_team_name: ${{ vars.VERCEL_TEAM_NAME }} - vercel_project_name: ${{ vars.VERCEL_PROJECT_NAME }} - vercel_deployment_id: ${{ steps.vercel-initial-deploy.outputs.vercel_preview_deployment_id }} - - - name: Create PR comment - # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 - if: success() && steps.vercel.outputs.vercel_preview_result == 'success' - with: - message-id: "vercel-preview" - repo-token: ${{ secrets.GITHUB_TOKEN }} - message: | - Latest documentation preview deployed successfully. - - | Result | Commit | Link | - | ------ | ------- | ----- | - | ✅ | ${{ steps.get-sha.outputs.sha }} | https://${{ steps.vercel.outputs.vercel_preview_url }}/docs | - - Note: This comment is updated whenever you push a commit. + workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} + + - name: "Upload PR-preview markdown docs & examples to GCS" + env: + ISR_BYPASS_TOKEN: ${{ secrets.ISR_BYPASS_TOKEN }} + PR_NUMBER: ${{ inputs.PR_NUMBER }} + RERUN_COMMIT: ${{ steps.get-sha.outputs.sha }} + run: | + # PR previews live under `gs://rerun-docs/prose/pr-{N}/` and + # are not advertised in versions.json. The production website + # reads them directly via `/docs/pr-{N}/…`, so re-uploads + # must purge that subtree from the edge ISR cache. + pixi run uv run scripts/ci/upload_docs.py upload \ + --version "pr-${PR_NUMBER}" \ + --rerun-commit "$RERUN_COMMIT" \ + --purge-token "$ISR_BYPASS_TOKEN" - name: Create PR comment - # https://github.com/mshick/add-pr-comment uses: mshick/add-pr-comment@v2.8.2 - if: success() && steps.vercel.outputs.vercel_preview_result == 'failure' + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true + if: success() with: message-id: "vercel-preview" repo-token: ${{ secrets.GITHUB_TOKEN }} message: | - Latest documentation preview failed to deploy: - ${{ steps.vercel.outputs.vercel_preview_inspector_url }} + Latest documentation preview uploaded successfully. | Result | Commit | Link | | ------ | ------- | ----- | - | ❌ | ${{ steps.get-sha.outputs.sha }} | unavailable | + | ✅ | ${{ steps.get-sha.outputs.sha }} | https://rerun.io/docs/pr-${{ inputs.PR_NUMBER }} | Note: This comment is updated whenever you push a commit. - name: Create PR comment - # https://github.com/mshick/add-pr-comment uses: mshick/add-pr-comment@v2.8.2 + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true if: failure() with: message-id: "vercel-preview" repo-token: ${{ secrets.GITHUB_TOKEN }} message: | - Latest documentation preview failed to deploy, check the CI for more details: + Latest documentation preview failed to upload, check the CI for more details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}?pr=${{ github.event.pull_request.number }} | Result | Commit | Link | diff --git a/.github/workflows/reusable_pip_index.yml b/.github/workflows/reusable_pip_index.yml index 57bd87413c00..46b86c5f5a6a 100644 --- a/.github/workflows/reusable_pip_index.yml +++ b/.github/workflows/reusable_pip_index.yml @@ -33,23 +33,23 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ inputs.COMMIT || (github.event_name == 'pull_request' && github.event.pull_request.head.ref || '') }} - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.11 - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" diff --git a/.github/workflows/reusable_publish_js.yml b/.github/workflows/reusable_publish_js.yml index a21771cb8a3b..53bdbd40fbe1 100644 --- a/.github/workflows/reusable_publish_js.yml +++ b/.github/workflows/reusable_publish_js.yml @@ -47,13 +47,13 @@ jobs: runs-on: ubuntu-latest-16-cores needs: [get-commit-sha] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.release-commit }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: - node-version: "22.x" + node-version: "24.x" registry-url: "https://registry.npmjs.org" - name: Install Yarn @@ -67,13 +67,11 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Publish packages - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | pixi run node rerun_js/scripts/publish.mjs diff --git a/.github/workflows/reusable_publish_web.yml b/.github/workflows/reusable_publish_web.yml index a58ce173314e..d0cb51d94988 100644 --- a/.github/workflows/reusable_publish_web.yml +++ b/.github/workflows/reusable_publish_web.yml @@ -52,19 +52,19 @@ jobs: runs-on: ubuntu-latest-16-cores needs: [get-commit-sha] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.release-commit }} lfs: true - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" @@ -76,9 +76,9 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Bootstrap examples run: | @@ -86,7 +86,7 @@ jobs: # built by `reusable_build_and_publish_wheels` - name: Download Wheel - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ${{ inputs.wheel-artifact-name }} path: wheel diff --git a/.github/workflows/reusable_publish_wheels.yml b/.github/workflows/reusable_publish_wheels.yml index 74f10561b405..ec611427a75a 100644 --- a/.github/workflows/reusable_publish_wheels.yml +++ b/.github/workflows/reusable_publish_wheels.yml @@ -112,23 +112,23 @@ jobs: runs-on: ubuntu-latest-16-cores steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # Don't do a shallow clone since we need it for finding the full commit hash ref: ${{ inputs.release-commit }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" diff --git a/.github/workflows/reusable_release_crates.yml b/.github/workflows/reusable_release_crates.yml index a420b39e070c..1453b0a0da92 100644 --- a/.github/workflows/reusable_release_crates.yml +++ b/.github/workflows/reusable_release_crates.yml @@ -29,13 +29,13 @@ jobs: runs-on: ubuntu-latest-16-cores steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || (github.event_name == 'pull_request' && github.event.pull_request.head.ref || '') }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Build web-viewer (release) run: pixi run rerun-build-web-release diff --git a/.github/workflows/reusable_run_notebook.yml b/.github/workflows/reusable_run_notebook.yml index e8c8ee319947..fa1f4247effd 100644 --- a/.github/workflows/reusable_run_notebook.yml +++ b/.github/workflows/reusable_run_notebook.yml @@ -30,27 +30,27 @@ jobs: name: Run notebook runs-on: ubuntu-latest-16-cores # Note that as of writing we need the additional storage page (14 gb of the ubuntu-latest runner is not enough). container: - image: ghcr.io/rerun-io/ci_docker:0.17.0 # Required to run the wheel or we get "No matching distribution found for attrs>=23.1.0" during `pip install rerun-sdk` + image: ghcr.io/rerun-io/ci_docker:0.18.0 # Required to run the wheel or we get "No matching distribution found for attrs>=23.1.0" during `pip install rerun-sdk` credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Download Wheel - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ${{ inputs.WHEEL_ARTIFACT_NAME }} path: wheel - name: Download Notebook Wheel - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: rerun_notebook_wheel path: wheel @@ -72,7 +72,7 @@ jobs: run: pixi run uv jupyter nbconvert --to=html --ExecutePreprocessor.enabled=True examples/python/notebook/cube.ipynb --output /tmp/cube.html - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} diff --git a/.github/workflows/reusable_sync_release_assets.yml b/.github/workflows/reusable_sync_release_assets.yml index 6cc1ee6de286..7e7f8251ace2 100644 --- a/.github/workflows/reusable_sync_release_assets.yml +++ b/.github/workflows/reusable_sync_release_assets.yml @@ -33,22 +33,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" diff --git a/.github/workflows/reusable_test_wheels.yml b/.github/workflows/reusable_test_wheels.yml index 996a455095b7..e99d9aea8372 100644 --- a/.github/workflows/reusable_test_wheels.yml +++ b/.github/workflows/reusable_test_wheels.yml @@ -55,7 +55,7 @@ jobs: CONTAINER: ${{ steps.set-config.outputs.container }} steps: - name: Login to GHCR (with retries) - uses: nick-fields/retry@v3 + uses: nick-fields/retry@v4 with: timeout_seconds: 10 max_attempts: 12 @@ -71,12 +71,12 @@ jobs: linux-arm64) runner="ubuntu-arm-16-core" target="aarch64-unknown-linux-gnu" - container="'ghcr.io/rerun-io/ci_docker:0.17.0'" + container="'ghcr.io/rerun-io/ci_docker:0.18.0'" ;; linux-x64) runner="ubuntu-latest-16-cores" target="x86_64-unknown-linux-gnu" - container="'ghcr.io/rerun-io/ci_docker:0.17.0'" + container="'ghcr.io/rerun-io/ci_docker:0.18.0'" ;; windows-x64) runner="windows-latest-16-cores" @@ -122,7 +122,7 @@ jobs: JOB_CONTEXT: ${{ toJson(job) }} INPUTS_CONTEXT: ${{ toJson(inputs) }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.RELEASE_COMMIT || ((github.event_name == 'pull_request' && github.event.pull_request.head.ref) || '') }} lfs: true @@ -136,12 +136,12 @@ jobs: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Download Wheel - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ${{ inputs.WHEEL_ARTIFACT_NAME }} path: wheel diff --git a/.github/workflows/reusable_track_size.yml b/.github/workflows/reusable_track_size.yml index d00788c01d7d..4f5b6404e220 100644 --- a/.github/workflows/reusable_track_size.yml +++ b/.github/workflows/reusable_track_size.yml @@ -28,7 +28,7 @@ jobs: name: "Track Sizes" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 # we need full history ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} @@ -39,25 +39,25 @@ jobs: echo "short_sha=$(echo ${{ github.sha }} | cut -c1-7)" >> "$GITHUB_OUTPUT" - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" - name: Download web_viewer - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: web_viewer path: "crates/viewer/re_web_viewer_server/web_viewer" - name: Download examples if: ${{ inputs.WITH_EXAMPLES }} - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: example_data path: example_data @@ -162,7 +162,9 @@ jobs: - name: Create PR comment if: inputs.PR_NUMBER != '' && steps.measure.outputs.is_comparison_set == 'true' # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 + uses: mshick/add-pr-comment@v3.9.1 + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true with: repo-token: ${{ secrets.GITHUB_TOKEN }} message: | @@ -170,9 +172,9 @@ jobs: ${{ steps.measure.outputs.comparison }} - - uses: prefix-dev/setup-pixi@v0.9.4 + - uses: prefix-dev/setup-pixi@v0.10.0 with: - pixi-version: v0.55.0 + pixi-version: v0.71.3 - name: Render benchmark result if: github.ref == 'refs/heads/main' diff --git a/.github/workflows/reusable_upload_examples.yml b/.github/workflows/reusable_upload_examples.yml index 5eabf9ee2e18..81e9d8f3b6a6 100644 --- a/.github/workflows/reusable_upload_examples.yml +++ b/.github/workflows/reusable_upload_examples.yml @@ -43,19 +43,19 @@ jobs: name: Upload Examples to Google Cloud runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - name: Download assets - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: example_data path: example_data # Upload the wasm, html etc to a Google cloud bucket: - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} diff --git a/.github/workflows/reusable_upload_js.yml b/.github/workflows/reusable_upload_js.yml index a8ee67ce44d3..24c881af3fbd 100644 --- a/.github/workflows/reusable_upload_js.yml +++ b/.github/workflows/reusable_upload_js.yml @@ -45,19 +45,19 @@ jobs: name: Upload rerun_js to google cloud runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - name: Download rerun_js package - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: rerun_js path: rerun_js_package # Upload the wasm, html etc to a Google cloud bucket: - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} diff --git a/.github/workflows/reusable_upload_web.yml b/.github/workflows/reusable_upload_web.yml index 74c7f12a5f14..36e70dbcf1d3 100644 --- a/.github/workflows/reusable_upload_web.yml +++ b/.github/workflows/reusable_upload_web.yml @@ -45,25 +45,25 @@ jobs: name: Upload web build to google cloud (wasm32 + wasm-bindgen) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} - name: Download Web Viewer - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: web_viewer path: "crates/viewer/re_web_viewer_server/web_viewer" # Upload the wasm, html etc to a Google cloud bucket: - id: "auth" - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v2" + uses: "google-github-actions/setup-gcloud@v3" with: version: ">= 363.0.0" @@ -160,7 +160,9 @@ jobs: - name: Status comment if: success() && github.event_name == 'pull_request' # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 + uses: mshick/add-pr-comment@v3.9.1 + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true with: message-id: "web-viewer-build-status" repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -178,7 +180,9 @@ jobs: - name: Status comment if: failure() && github.event_name == 'pull_request' # https://github.com/mshick/add-pr-comment - uses: mshick/add-pr-comment@v2.8.2 + uses: mshick/add-pr-comment@v3.9.1 + # GitHub API is very unreliable. We don't want to fail the entire workflow just because we couldn't post a comment. + continue-on-error: true with: message-id: "web-viewer-build-status" repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable_web_test.yml b/.github/workflows/reusable_web_test.yml new file mode 100644 index 000000000000..5031f3da707e --- /dev/null +++ b/.github/workflows/reusable_web_test.yml @@ -0,0 +1,62 @@ +name: Reusable Web tests + +on: + workflow_call: + inputs: + CONCURRENCY: + required: true + type: string + REF: + required: false + type: string + default: "" + +concurrency: + group: ${{ inputs.CONCURRENCY }}-web-test + cancel-in-progress: true + +env: + RUSTFLAGS: --deny warnings + RUSTDOCFLAGS: --deny warnings + RUST_BACKTRACE: full + CARGO_TERM_COLOR: always + +defaults: + run: + shell: bash --noprofile --norc -euo pipefail {0} + +permissions: + contents: "read" + id-token: "write" + +jobs: + web-test: + name: Browser tests + timeout-minutes: 60 + runs-on: ubuntu-latest-16-cores + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.REF || (github.event_name == 'pull_request' && github.event.pull_request.head.ref) || '' }} + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache_key: "web-test" + save_cache: false + targets: wasm32-unknown-unknown + workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} + + - uses: prefix-dev/setup-pixi@v0.10.0 + with: + pixi-version: v0.71.3 + + - name: Set up Firefox + uses: browser-actions/setup-firefox@v1 + + - name: Set up geckodriver + uses: browser-actions/setup-geckodriver@latest + + - name: Run Wasm browser tests + run: pixi run web-test diff --git a/.github/workflows/update_kittest_snapshots.yml b/.github/workflows/update_kittest_snapshots.yml index 80560fa949b7..c4cdc4ce03f1 100644 --- a/.github/workflows/update_kittest_snapshots.yml +++ b/.github/workflows/update_kittest_snapshots.yml @@ -20,7 +20,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: lfs: true # We can't use the workflow token since that would prevent our commit to cause further workflows. diff --git a/.gitignore b/.gitignore index bb27b8431522..3aaa5c16719f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ _deps /target_ra /target_wasm +# Test coverage output (`pixi run rs-coverage`). HTML lives under /target (already ignored). +/lcov.info + # Python virtual environment: **/venv* **/.venv* diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000000..bb5af0093432 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "rerun": { + "command": "./target/debug/re-viewer-mcp", + "args": [] + } + } +} diff --git a/.typos.toml b/.typos.toml index de621ce087cf..187ee91e6f50 100644 --- a/.typos.toml +++ b/.typos.toml @@ -33,9 +33,15 @@ Yoh = "Yoh" # Part of @YohDeadfall # Use the more common spelling adaptor = "adapter" adaptors = "adapters" +framerate = "frame rate" # For consistency we prefer American English: aeroplane = "airplane" +amortisation = "amortization" +amortise = "amortize" +amortised = "amortized" +amortises = "amortizes" +amortising = "amortizing" analogue = "analog" analyse = "analyze" appetiser = "appetizer" @@ -49,6 +55,12 @@ calibre = "caliber" candour = "candor" capitalise = "capitalize" catalogue = "catalog" +categorisation = "categorization" +categorise = "categorize" +categorised = "categorized" +centralisation = "centralization" +centralise = "centralize" +centralised = "centralized" centre = "center" characterise = "characterize" chequerboard = "checkerboard" @@ -72,44 +84,80 @@ endeavour = "endeavor" enrol = "enroll" epilogue = "epilog" equalise = "equalize" +familiarise = "familiarize" +familiarised = "familiarized" favour = "favor" favourite = "favorite" fibre = "fiber" +finalisation = "finalization" +finalise = "finalize" +finalised = "finalized" +finalises = "finalizes" +finalising = "finalizing" flavour = "flavor" fulfil = "fufill" gaol = "jail" +generalisation = "generalization" +generalise = "generalize" +generalised = "generalized" grey = "gray" greys = "grays" greyscale = "grayscale" harbour = "habor" honour = "honor" humour = "humor" +initialisation = "initialization" +initialise = "initialize" +initialised = "initialized" +initialises = "initializes" +initialising = "initializing" instalment = "installment" instil = "instill" +itemise = "itemize" +itemised = "itemized" jewellery = "jewelry" kerb = "curb" labour = "labor" litre = "liter" lustre = "luster" +maximisation = "maximization" +maximise = "maximize" +maximised = "maximized" meagre = "meager" metre = "meter" +minimisation = "minimization" +minimise = "minimize" +minimised = "minimized" mobilise = "mobilize" monologue = "monolog" naturalise = "naturalize" neighbour = "neighbor" neighbourhood = "neighborhood" +neighbouring = "neighboring" +neighbours = "neighbors" normalise = "normalize" normalised = "normalized" odour = "odor" offence = "offense" +optimisation = "optimization" +optimise = "optimize" +optimised = "optimized" +optimising = "optimizing" organise = "organize" parlour = "parlor" +penalise = "penalize" +penalised = "penalized" plough = "plow" popularise = "popularize" pretence = "pretense" +prioritisation = "prioritization" +prioritise = "prioritize" +prioritised = "prioritized" programme = "program" prologue = "prolog" rancour = "rancor" +randomise = "randomize" +randomised = "randomized" realise = "realize" recognise = "recognize" recognised = "recognized" @@ -131,16 +179,32 @@ specialisation = "specialization" specialise = "specialize" specialised = "specialized" splendour = "splendor" +stabilise = "stabilize" +stabilised = "stabilized" standardise = "standardize" sulphur = "sulfur" +summarisation = "summarization" +summarise = "summarize" +summarised = "summarized" symbolise = "symbolize" +synchronisation = "synchronization" +synchronise = "synchronize" +synchronised = "synchronized" +synchronises = "synchronizes" +synchronising = "synchronizing" theatre = "theater" tonne = "ton" travelogue = "travelog" tumour = "tumor" +utilisation = "utilization" +utilise = "utilize" +utilised = "utilized" valour = "valor" vaporise = "vaporize" vigour = "vigor" +visualisation = "visualization" +visualise = "visualize" +visualised = "visualized" # End of American English section # null-terminated is the name of the wikipedia article! @@ -180,6 +244,8 @@ extend-ignore-re = [ "isse", # Name + "[\".]framerate\\b", # ffmpeg/avfoundation container option name, or PyAV's `CodecContext.framerate` attribute + "@[a-zA-Z]+", # GitHub user names "\\[[a-f0-9]{7}\\]\\(https://github\\.com/rerun-io/rerun/commit/", # Commit SHA links in changelog diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 03e0df123353..b91aba488599 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -22,5 +22,6 @@ "wayou.vscode-todo-highlight", "webfreak.debug", "xaver.clang-format", // C++ formatter + "ryanluker.vscode-coverage-gutters", // Renders line coverage from `pixi run rs-coverage` (lcov.info) as inline gutters. ] } diff --git a/AGENTS.md b/AGENTS.md index 1a0e130bcda6..7b2d28283a75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,11 @@ Add custom functionality to generated types via `_ext` files: - use `…` instead of `...` - Validate conventions via `pixi run lint-rerun ` (no file = check everything) +- Prose style (em vs en dash, sentence endings, casing) — see [`DESIGN.md`](DESIGN.md). In short: spaced em dash ` — `, never unspaced `word—word`, and don't use `–` as a sentence dash (it's for numeric ranges only) +- In error and log messages, put the error first and any file path at the end (e.g. `Failed to import: {err}\nFile path: {path}`), never in the middle. + Paths can be long or sensitive, so trailing placement makes them easy to strip when copy-pasting. +- One sentence per line in markdown files. + Markdown joins consecutive lines into a paragraph, so rendering is unchanged — but diffs become much easier to review. ## Architecture overview @@ -94,6 +99,9 @@ crates/ More details in `ARCHITECTURE.md`. +**When adding, removing, or renaming a crate**, update `ARCHITECTURE.md`: +add the crate to the appropriate crate table, and flag for the author that the crate-organization diagram (FigJam) needs a manual update — see the HTML comment next to the diagram in `ARCHITECTURE.md` for instructions. + ### Type system hierarchy Three levels (generated from .fbs files): @@ -190,7 +198,12 @@ Key points: - [`ARCHITECTURE.md`](ARCHITECTURE.md) - Detailed architecture docs - [`BUILD.md`](BUILD.md) - Full build instructions - [`CODE_STYLE.md`](CODE_STYLE.md) - Code style guidelines -- [`CONTRIBUTING.md`](CONTRIBUTING.md) - Contribution guidelines - [`DESIGN.md`](DESIGN.md) - UI design guidelines (GUI, CLI, docs, log messages) - [`docs/README.md`](docs/README.md) - Documentation system (sites, builds, deployment) - [`rerun_py/README.md`](rerun_py/README.md) - Python SDK instructions + +## Contributing + +Don't open pull requests or issues unless explicitly asked. +When opening or interacting with one, follow the [pull request template](.github/pull_request_template.md) or [issue templates](.github/ISSUE_TEMPLATE/), and disclose that you are an LLM. +Let the user know that you included this disclosure. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 326cba7e2779..6ade8133b911 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -99,11 +99,11 @@ Of course, this will only take us so far. In the future we plan on caching queri Here is an overview of the crates included in the project: - - - - - + + + + + +https://github.com/user-attachments/assets/87fb80da-66dd-4fcd-8b35-ab553696f536 + +#### 🎮 Gamepad support in 3D views + +You can now use a gamepad to navigate 3D views in the native viewer. +This makes it easier to do fine-grained, complex maneuvers with varying speed - e.g. for navigating large scenes or for screen videos. +Analog sticks control the eye position and look target, shoulder triggers move the eye up and down, and shoulder buttons accelerate/decelerate. + +**Note:** The gamepad feature is currently experimental and can be activated through the settings menu. +Switch the 3D view's eye controls to `FirstPerson` for optimal experience. +Under the hood, we use the [`gilrs`](https://crates.io/crates/gilrs) crate that supports a wide range of devices. + +#### Drag & drop components + +You can now drag & drop a component right from the streams panel to visualize it in a Time series view or Status timeline. + + + +https://github.com/user-attachments/assets/d70587a9-2020-4ae8-9cf3-0fef54dcf896 + +#### Transform debugging tool + +We added a new debugging UI for visual introspection of the 3D transform cache. +This allows to view the tree structure of the transform hierarchy, including potentially disconnected trees, and inspect the latest stored values of each frame node or transform edge. +The UI supports horizontal and vertical tree layout and you can filter by transform type (e.g. static or temporal). + +**Note:** this UI is currently a tab in the dev panel (accessible via "Toggle dev panel" in the menu or ctrl/cmd+shift+m). +But we are open to making this a dedicated view in the future - let us know if you have any feedback! + + +https://github.com/user-attachments/assets/b4b1ea6e-bce9-4e88-9ede-262f545e3b47 ### ⚠️ Breaking changes -- `rerun rrd compact` renamed to `rerun rrd optimize` + +- **SDK**: If you relied on the `log_tick` timeline being automatically created, you'll now have to call `set_log_tick_enabled(true)`. +- **Python**: `rr.send_dataframe` is now stricter for more correctness. See the migration guide for more details. +- **Python**: The deprecated python module `rerun.recording` has been removed; Use `rerun.experimental.RrdReader` instead. +- **SDK**: Several deprecated `DatasetEntry` methods have been removed. +- **SDK**: `ParquetReader` column rules have been removed in favor of [lenses](https://rerun.io/docs/concepts/query-and-transform/lenses). + +🧳 Migration guide: https://rerun.io/docs/reference/migration/migration-0-34 + +### 🔎 Details + +#### 🪵 Log API +- Make `log_tick` timeline OPT-IN [f734978](https://github.com/rerun-io/rerun/commit/f734978e091b52ddc04f7781a6d396441f526a7c) +- Allow to change shading of `Points3D` [cd7fa3e](https://github.com/rerun-io/rerun/commit/cd7fa3ebde1979239a59c720f5f81ad26e33fcf5) +- Extract out re_mp4_reader from importer [6eeeede](https://github.com/rerun-io/rerun/commit/6eeeede8fff1e95706fdb14f2070cbe3815d16d9) +- Add stream-mode support and Python `Mp4Reader` LazyChunkStream binding [e87dd89](https://github.com/rerun-io/rerun/commit/e87dd89d96e6942e020f03bb0fa7b5e46ef56340) + +#### 🐍 Python API +- Add headless viewer mode [7e31c42](https://github.com/rerun-io/rerun/commit/7e31c42409b7c3d3f00b7d28c28573c65240597a) +- rerun-sdk[datafusion] and rerun-sdk[dataplatform] extras are now rerun-sdk[catalog] [182cbb0](https://github.com/rerun-io/rerun/commit/182cbb03b8218cf69607cf77c78453960368904a) +- Remove deprecated `Recording` and related APIs [2156155](https://github.com/rerun-io/rerun/commit/2156155687b619944cebebd7bdc91864524a3641) +- Split dataloader queries into windowed and keyframe-anchored [9ccc89e](https://github.com/rerun-io/rerun/commit/9ccc89e4b3f4be2f0c01e673d6f752896f544b90) +- Introduce direct `ChunkStore` querying with `.reader()` [d14018e](https://github.com/rerun-io/rerun/commit/d14018eb59f2ad7c7bb8624670e084b2b318c65d) +- Fix `ViewerClient.close` not closing subprocesses on Windows [2bc53b6](https://github.com/rerun-io/rerun/commit/2bc53b634414ef0f7ce209e4c7fb1ac55a0adacc) +- feature-removal: drop custom indices [4dd9c78](https://github.com/rerun-io/rerun/commit/4dd9c7843f6d5af7b075b84a8eb9ab38f76bfa5c) +- Deprecate `DatasetEntry.manifest()` [fd24de9](https://github.com/rerun-io/rerun/commit/fd24de9ddec4cb9096441b53af49bd2b135eae13) +- Make `Chunk.from_record_batch` more flexible [d1771d4](https://github.com/rerun-io/rerun/commit/d1771d4042e91b95ee0044f8f0491463557f4876) +- Remove `column_rules` from `ParquetReader` API [d18da8e](https://github.com/rerun-io/rerun/commit/d18da8e2b298f3d0cc02d0656dccc514b610a0eb) +- Add casting capability to derive lens [f76a7d5](https://github.com/rerun-io/rerun/commit/f76a7d5b2738db5bbda24f089a24147d753b3c91) +- Introduce `pack` built-in function to lenses [a5ea965](https://github.com/rerun-io/rerun/commit/a5ea96557b4c1f424ff183ea7d0503c63e5c3ca3) +- Introduce `DeriveLens` helpers for common components [a93eb62](https://github.com/rerun-io/rerun/commit/a93eb62232a480f834abf0797d6d8d8706573ae8) +- Add support for duration timelines to `FixedRateSampler` [45bddb7](https://github.com/rerun-io/rerun/commit/45bddb7dc2e8eb3ef7cdec743a657f36b756deaf) + +#### 🦀 Rust API +- Add headless viewer mode [7e31c42](https://github.com/rerun-io/rerun/commit/7e31c42409b7c3d3f00b7d28c28573c65240597a) + +#### 🪳 Bug fixes +- Take grpc server into account when purging viewer memory [f83f167](https://github.com/rerun-io/rerun/commit/f83f167346dd13cac9b0386a7b9ea4bc2e9bdf38) +- Move relative time view range when moving time cursor [e9b22c5](https://github.com/rerun-io/rerun/commit/e9b22c564dae9966c22dfd24d321ab6ea5e21a99) +- Don't show loader for encoded images when playing before their encoding data has been loaded [c2eed0a](https://github.com/rerun-io/rerun/commit/c2eed0afa179e2f3dd4222e1c023d4f99c9ce646) +- GC ever growing fields in `ChunkStore` [f5747cf](https://github.com/rerun-io/rerun/commit/f5747cf69134a43ea51bc3ac7a6fbd68bba75edd) +- Fix clicking in-view links to entities outside of view (usually via transform tree) [b82a1d9](https://github.com/rerun-io/rerun/commit/b82a1d93e17a7cc860cb47ec9a16c7f5a30b7118) +- add missing `App::logic` callbacks to examples [#12810](https://github.com/rerun-io/rerun/pull/12810) (thanks [@adsick](https://github.com/adsick)!) +- Do not delete "duplicate" chunks that contain transform data [11b90ed](https://github.com/rerun-io/rerun/commit/11b90ed9fa6d3a82edff8c3879b6fbaca76ce24c) +- Fix sort order of `null` values in table UI [8a0f7c5](https://github.com/rerun-io/rerun/commit/8a0f7c5df1ffc328309b8f82efdfc8b7ba66f324) +- Navigate back after closing [71ecad0](https://github.com/rerun-io/rerun/commit/71ecad026799879df7e9eaf8b8e9ea371fcc71b8) +- Fix custom visualizer example showing a black viewer [e068ce8](https://github.com/rerun-io/rerun/commit/e068ce834d84ede9dfa420bcfcf2553f693c93ed) +- Fix `face_rendering` on arkit_scenes example [3d04f6f](https://github.com/rerun-io/rerun/commit/3d04f6f954a7a79a12f4198d14cd1cdc7b682063) +- Fix ROS 2 reflection decoding of byte/char, empty specs, and wstring [be0a632](https://github.com/rerun-io/rerun/commit/be0a632076d7775406846ed63341660c33a0ab57) +- Handle codec changing for video-likes [f7466ef](https://github.com/rerun-io/rerun/commit/f7466efea1945e1b9a3dd5db7256d142f8e99de7) +- Retain entry list in recording panel on refresh [e7357ba](https://github.com/rerun-io/rerun/commit/e7357baa3858a7cc9f174c194e40d443ab908697) +- Don't buffer & fetch more of entities based on what's hovered [d9b3008](https://github.com/rerun-io/rerun/commit/d9b3008f8431ec1d8b40a891b6e810fffaa89252) +- `rerun rrd optimize`: continue on error [63e0882](https://github.com/rerun-io/rerun/commit/63e08824e5e7781104008a64e8273ebf387122a7) +- Look at `source_component` and `selector` when assigning colors to plots [71e2cf7](https://github.com/rerun-io/rerun/commit/71e2cf7cb75507d0df347a52b645d5ccab52efeb) +- Fix viewer hang when loading static compressed images [b9acd34](https://github.com/rerun-io/rerun/commit/b9acd34aa2c90631759cbeb426082a68230a2f69) + +#### 🌁 Viewer improvements +- Temporary time pause on scrubbing [bfea333](https://github.com/rerun-io/rerun/commit/bfea333d6fba8766288becf83b10bf5f0b6fa451) +- Experimental gamepad support for 3D spatial views (native only) [da470f7](https://github.com/rerun-io/rerun/commit/da470f7c0330a4bcc68f9da44ac4c5c287545a6a) +- Rename memory panel -> dev panel [c54e7b8](https://github.com/rerun-io/rerun/commit/c54e7b808c807016d53b3e1b5d6d628ab6548a02) +- Show rejection reason on state component drop [b6a764d](https://github.com/rerun-io/rerun/commit/b6a764d8f732cfdd202a97935a5ed1d42c7d2b41) +- Table blueprint registration instead of base64 encoded table blueprints [f2a2805](https://github.com/rerun-io/rerun/commit/f2a2805b3b112c9b0e44a35b802a21d062efc5f3) +- Hold command/control to see and drag all preview timelines [91286ba](https://github.com/rerun-io/rerun/commit/91286ba89e849c88c9d50673aeb9edb6d2e55f75) +- Improve default & visualizer reporting for `GridMap` colormap [e8386c5](https://github.com/rerun-io/rerun/commit/e8386c5f5434c4c0d3f1c7dac7dbfe1a9b1a7bb4) +- Table blueprint registration for segment tables [0c60d04](https://github.com/rerun-io/rerun/commit/0c60d046d4071478bc4f89e8c0c5ba85f60005ef) +- Add sparse VoxelGridMap archetype [fa40ec1](https://github.com/rerun-io/rerun/commit/fa40ec12509b73ec9cce3a808c95f2d9db3c335a) (thanks [@makeecat](https://github.com/makeecat)!) +- Expose `App::current_query()` for external viewer [#12811](https://github.com/rerun-io/rerun/pull/12811) (thanks [@adsick](https://github.com/adsick)!) +- Don't play time forward when video is buffering [c4c6832](https://github.com/rerun-io/rerun/commit/c4c6832bb0b7399f12eebf644b6bbe0027791855) +- Local catalog server [3c6b02e](https://github.com/rerun-io/rerun/commit/3c6b02e91d489ea2ab125ac5c40d1cf82af5c81e) +- Drag & drop scalars into time series view [ae4a157](https://github.com/rerun-io/rerun/commit/ae4a15776f75b385a2a79598254b041e4e68bafd) +- Always buffer time [81384a4](https://github.com/rerun-io/rerun/commit/81384a43489d9273aef52cbc44484426a8e47f6a) +- Visualize transform trees in dev panel [a534ac6](https://github.com/rerun-io/rerun/commit/a534ac61ea388f33699940fb93981060e96fbf92) +- Query only visible parts of the state timeline [9118c03](https://github.com/rerun-io/rerun/commit/9118c03effe1c6d04cae8257de8e3e7fe2948a85) +- Add `rerun viewer-mcp` [aa56c88](https://github.com/rerun-io/rerun/commit/aa56c88190ab5c4337f6acc6708c94a90454a5aa) +- Hide screenshot notification on automated screenshots (scripts, mcp) [8924417](https://github.com/rerun-io/rerun/commit/8924417429de45fe52e9c8f073590b82727bb299) +- State timeline view is now stable [0a26a8b](https://github.com/rerun-io/rerun/commit/0a26a8bfb9b161edee370d34578f415ec3354068) + +#### 🗄️ OSS server +- Fix slow registration calls from OSS Catalog Server [5b74d8f](https://github.com/rerun-io/rerun/commit/5b74d8f078302e01afa0355162f301611d603122) + +#### 🚀 Performance improvements +- Cache string interning calls [a05a4f8](https://github.com/rerun-io/rerun/commit/a05a4f8bb018063e4a63b62036e447df0801fa28) +- Skip empty visualizers [7d6aeef](https://github.com/rerun-io/rerun/commit/7d6aeef7e928365ee92171d9e60edfb25ac5ebf4) +- Don't load blueprint for previews [e861b9e](https://github.com/rerun-io/rerun/commit/e861b9e6edf07b7312c0c2a35565c90828922c43) +- Redap client connection pool [5406d3a](https://github.com/rerun-io/rerun/commit/5406d3a69604bbb18bf89178d8c0ed9a6d095f2b) +- Replace `cdr-encoding` with `re_cdr` [d2bc3b8](https://github.com/rerun-io/rerun/commit/d2bc3b8dd2165639fc1cb0665dfc2ad9fbc8441e) +- Make the .ply parser ~10x faster using a custom PropertyAccess [6f8fb76](https://github.com/rerun-io/rerun/commit/6f8fb7648a0ec4e663a730ef6247ec8b41f46ab6) + +#### 🧑‍🏫 Examples +- Local Vector Search example [0a336d5](https://github.com/rerun-io/rerun/commit/0a336d52015e542b2f075bc704cc6b8e2ae8e591) + +#### 🖼 UI improvements +- Add button to copy server URL [15aeda9](https://github.com/rerun-io/rerun/commit/15aeda909ef500d951e827c5153a79dc9cdac4ee) +- Implement `WatchEvents` in `re_server` [8e07bdb](https://github.com/rerun-io/rerun/commit/8e07bdbe47cdb8cf39bb275e76914edf4545f849) +- Show average bitrate for selected video [9e28016](https://github.com/rerun-io/rerun/commit/9e2801688eae710d283f82552806598b4ee67bff) + +#### 🧢 MCAP +- MCAP: add support for ROS `nav2_msgs/VoxelGrid` [651c140](https://github.com/rerun-io/rerun/commit/651c14069735aa4a3d200ec5578c5f0dca1f7359) +- MCAP: add support for `foxglove.VoxelGrid` [c8580c1](https://github.com/rerun-io/rerun/commit/c8580c11928c2354b905819ef33f029b917a83e1) +- Move sensor_msgs/msg/MagneticField to lens [fe9b74f](https://github.com/rerun-io/rerun/commit/fe9b74f233f9e017f594636799b354b466d0138e) + +#### 🧑‍💻 Dev-experience +- Add Rerun agent skills [6732840](https://github.com/rerun-io/rerun/commit/673284042dda3d6ff218c6c2e954315cbcfc9aa7) +- Show bound and connect URLs when serving web viewer [#12753](https://github.com/rerun-io/rerun/pull/12753) (thanks [@terror](https://github.com/terror)!) +- Skills: mandate the idiomatic reader+lens pipeline (steer away from hand-built chunks) [c436da5](https://github.com/rerun-io/rerun/commit/c436da5a61956bb3645de61993b29997d0695e59) + +#### 📚 Docs +- Document catalog entry renaming and directory delimiter [57da6f3](https://github.com/rerun-io/rerun/commit/57da6f3da88e57a23e6d2d6d46990988df30780d) + +#### 📦 Dependencies +- chore: update lance, datafusion, and arrow [47d29ff](https://github.com/rerun-io/rerun/commit/47d29ffce4c53610471469b56d20d7cab6516b18) + +#### 🤷‍ Other +- Push down selected components to `fuzzy_descriptors` [92f2281](https://github.com/rerun-io/rerun/commit/92f22810ea0789323f72e3d116ed8bba8b99b053) +- Add RRD footers section to `rerun rrd stats` [4ab8388](https://github.com/rerun-io/rerun/commit/4ab838881f2fd4a4996be25b02b9510ca43d52fd) +- Bound server chunk scans with per-segment index-value pushdown [990166c](https://github.com/rerun-io/rerun/commit/990166c2f284a29bd63a6f03f103b70de7a1ac81) +- sdk: retry+backoff on behalf of the customer [2ef0fde](https://github.com/rerun-io/rerun/commit/2ef0fde4e08b54de30a456a6fd435163f3cee2d2) + +## [0.33.1](https://github.com/rerun-io/rerun/compare/0.33.0...0.33.1) - 2026-06-22 + +### 🔎 Details + +#### 🪳 Bug fixes +- Do not delete "duplicate" chunks that contain transform data [b2ac735](https://github.com/rerun-io/rerun/commit/b2ac735c15cba853d9e38b41412d5671ac16e06c) + +#### 🌁 Viewer improvements +- Temporary time pause on scrubbing [c7578b9](https://github.com/rerun-io/rerun/commit/c7578b9406b0f5d19237d13085155144f6fd38e6) + +## [0.33.0](https://github.com/rerun-io/rerun/compare/0.32.2...0.33.0) - 2026-05-29 + +🧳 Migration guide: https://rerun.io/docs/reference/migration/migration-0-33 + +### ✨ Overview & highlights + +After our large [0.32.0](https://github.com/rerun-io/rerun/releases/tag/0.32.0) release, this one is more focused but still has some great new things in store for you! + +#### Headless viewer + +This release comes with a new headless mode for the viewer! +Together with smaller improvements to the screenshot API, this can be an invaluable tool for automation and LLM usage. + +```python +import rerun.blueprint as rrb +from rerun.experimental import ViewerClient + +# Spawn a headless viewer; the client owns its lifetime. +# ⚠️ you need a graphics driver to run this (software rasterizers like lavapipe are fine too!). +with ViewerClient(spawn=True, headless=True) as viewer: + rec = rr.RecordingStream("rerun_example_screenshot") + rec.connect_grpc(url=viewer.url) + + view = rrb.Spatial3DView(name="my blue 3D", background=[100, 149, 237]) + rec.send_blueprint(view) + + # Screenshot only the view we created earlier. + viewer.save_screenshot("my_view.png", view_id=view.id) + + # Disconnect the RecordingStream before the headless viewer shuts down. + rec.disconnect() +``` + +We're planning more features for `ViewerClient` Python object, including an MCP server allowing agents to fully instrument the Viewer. +Stay tuned! + +#### Push-down filtering on chunk processing + +This release brings a significant optimization to pipelines in the shape of: + +```python +from rerun.experimental import RrdReader + +lazy_store = RrdReader(...).store() +stream = lazy_store.stream().filter(...) +# more stream operations +``` + +The filter is now pushed down to `RrdReader`, which will selectively load the matching chunks only. +This massively accelerates targeted data extraction from large RRDs (e.g. extract a joint data from a RRD that also contains multiple video streams). + +#### Improvements on experimental state timeline view + +We're continue to perfect the state timeline view, and this release brings this lot of improvements: + +https://github.com/user-attachments/assets/b7549593-363f-4e13-ab9b-184d6434fc19 + +- Support for numbers and boolean components, not just strings. +- Drag a component right from the streams tree into a state timeline. +- Highlight the time range of a state by hovering it. +- Clear state by logging an empty string or a `Clear` message: the timeline shows a gap until the next state value. + +#### Improvements on experimental dataset review + +Amongst other improvements, we made the play behavior much nicer for our experimental dataset review and table blueprint feature: + + + +https://github.com/user-attachments/assets/4543af53-52ca-4488-90b0-8c365f9fb89b + +#### Nicer native Viewer title bars on Windows & some Linux desktops + +On MacOS we used to have a compact title bar for a very long time. Now the same feature comes finally to Windows +and some Linux desktops. + +Before: + + + bulky title bar before + + + + +✨ After ✨: + + + compact title bar after + + + + + + +If you experience any issues with this you can turn it off in the settings menu. + +### ⚠️ Breaking changes + +The Python optional-dependency extra for catalog/query API tools has been renamed to `catalog`. + +| Before | After | +|------------------------------|-----------------------| +| `pip install rerun-sdk[dataplatform]` | `pip install rerun-sdk[catalog]` | +| `pip install rerun-sdk[datafusion]` | `pip install rerun-sdk[catalog]` | + +🧳 Migration guide: https://rerun.io/docs/reference/migration/migration-0-33 + +### 🔎 Details + +#### 🪵 Log API +- Fix problem of intermixing different store messages in one rrd [bee551f](https://github.com/rerun-io/rerun/commit/bee551f0a8f993007563ddde6a97dfe20e4993c6) + +#### 🐍 Python API +- Add `trim_metadata_keys` argument to `Chunk.format` [2d6cd8d](https://github.com/rerun-io/rerun/commit/2d6cd8db8064106d9b0a40036a3161d79018f83a) +- Allow `#when` anchors without `#time_selection` [1c242c8](https://github.com/rerun-io/rerun/commit/1c242c86c7e5c2435d44461680efb805de1c07e3) +- Optional Hub ingestion of customer SDK traces [5a362a3](https://github.com/rerun-io/rerun/commit/5a362a322131957ce4f874e2f16a053083a08cb7) +- Remove segment id validation with `dataset.reader(..., using_index_value=...)` [7846d38](https://github.com/rerun-io/rerun/commit/7846d3826e6dfbd578ac68d88889a427b003a8cf) +- Dedup video stream samples in video decoder [d341ba4](https://github.com/rerun-io/rerun/commit/d341ba465d45aecda5a2ddcedd6b8ad791adadf3) +- Pushdown `LazyChunkStore.filter()` to `LazyStore` [99a2149](https://github.com/rerun-io/rerun/commit/99a21494776e3ce431e6aa22f397fc989b996b11) +- Add headless viewer mode [b050087](https://github.com/rerun-io/rerun/commit/b05008774816aab3b18f2b3dbc434ac2b871a72d) +- rerun-sdk[datafusion] and rerun-sdk[dataplatform] extras are now rerun-sdk[catalog] [fcb5b13](https://github.com/rerun-io/rerun/commit/fcb5b13a34d4e0862d4a633838ff6f8344257bc4) + +#### 🦀 Rust API +- Increase the re_sdk viewer spawn timeout to 4s [230cde6](https://github.com/rerun-io/rerun/commit/230cde680c96cdfb9529b8f8a36370ae786f3f59) +- Optional Hub ingestion of customer SDK traces [5a362a3](https://github.com/rerun-io/rerun/commit/5a362a322131957ce4f874e2f16a053083a08cb7) +- Add headless viewer mode [b050087](https://github.com/rerun-io/rerun/commit/b05008774816aab3b18f2b3dbc434ac2b871a72d) + +#### 🪳 Bug fixes +- Create spatial topology from schema instead of from chunk data (fixing to sometimes never pull data) [3fffd8b](https://github.com/rerun-io/rerun/commit/3fffd8b91de671adace05000075aecc7861703b1) +- Respect play state from blueprint with Catalog Server [35613c9](https://github.com/rerun-io/rerun/commit/35613c988d83a2fe646392f96cea043f136e2e69) +- Fix orbital zoom clamp panic on tiny scenes [6f1bcde](https://github.com/rerun-io/rerun/commit/6f1bcde8e963c748878c5ad6309f83ec84f3cf43) +- Fix notification id collision [a453f9a](https://github.com/rerun-io/rerun/commit/a453f9af9596a5642f344d2d223385053a29f17a) +- Fix previews repeatedly opening log sources [4e03a27](https://github.com/rerun-io/rerun/commit/4e03a27433bcd5c62e7b2d6331cd3dfadb24bfb1) +- Fix video issues after GC at start of recording [9670f20](https://github.com/rerun-io/rerun/commit/9670f206c05bef8ef3181fbf68fc07ad39fc24cc) +- Dataloader: skip predicate keyframes [c8b963f](https://github.com/rerun-io/rerun/commit/c8b963f42d3e5a7405cac5c57c3b2526d73c71e2) +- Default Safari to WebGL — Safari 26.4 broke 3D under WebGPU [#12789](https://github.com/rerun-io/rerun/pull/12789) +- Fix occasional Viewer hangs on some Wayland systems [1f7680c](https://github.com/rerun-io/rerun/commit/1f7680c46d47c28d606486a297485754537e5852) +- Fix handling of png encoded depth images on the web [bcfbd22](https://github.com/rerun-io/rerun/commit/bcfbd22a846b876c9ab18fa8c1e8e3da8e85fa01) +- MCAP: resolve field type ambiguities in message schema reflection [0ce185b](https://github.com/rerun-io/rerun/commit/0ce185beff0284fe86c939aa5f237618903718b0) +- Implement unclamped `SetTime` and fix `#when` anchors [d33a6ab](https://github.com/rerun-io/rerun/commit/d33a6abbe4229325fb693c11a38d64f21e1622e2) +- Fix encoded rvl images [717b40d](https://github.com/rerun-io/rerun/commit/717b40d5ef5e43e65a5692ba052983bbb1906fe7) +- MCAP: fix `sensor_msgs/PointCloud2` offsets for extra fields [20fe293](https://github.com/rerun-io/rerun/commit/20fe293ea4d741e6d6a75b872e7f7772bd0379d2) +- Fix AV1 OBU walker cursor drift [c61b60b](https://github.com/rerun-io/rerun/commit/c61b60b4bf551f8125e0acab9b596914e90795cd) +- `Clear` log support for state timeline [639c5e6](https://github.com/rerun-io/rerun/commit/639c5e60ec59f0fafeb988f5d572731a6b2f6e8c) +- Take grpc server into account when purging viewer memory [09b0192](https://github.com/rerun-io/rerun/commit/09b0192ce32c2a5f820133bc865d5afb2341fe97) + +#### 🌁 Viewer improvements +- Emit `VideoStream::is_keyframe` in `rrd optimize` [ab74f37](https://github.com/rerun-io/rerun/commit/ab74f37dca39cab50143bfd5579c21eaa2c825a6) +- Opening a url with a timestamp anchor now always pauses the recording [24827ea](https://github.com/rerun-io/rerun/commit/24827ea8a53f97bb2701e41588a9d032f55e6690) +- Add a copy button to image previews [8e5e5ad](https://github.com/rerun-io/rerun/commit/8e5e5adf6defc5ac4bf2db2d69c24651c35e5187) +- Components from timeline panel can be dropped to State timeline [e68e001](https://github.com/rerun-io/rerun/commit/e68e001b3c5e7aa302b557cfcb63b6eaca97daef) +- Show loading indicator for dataset previews [9e811e5](https://github.com/rerun-io/rerun/commit/9e811e5f266495dac3f14b79e5c365cc5e79f569) +- State timeline accepts numbers and booleans [de849aa](https://github.com/rerun-io/rerun/commit/de849aa5759f5bc33412caa41f96cf132274241c) +- Compact title bar on Linux & Windows [475fc45](https://github.com/rerun-io/rerun/commit/475fc450bfa226c05c1d6c1d9026a2523e73c95a) +- Surface errors when loading a URI [e3aabd7](https://github.com/rerun-io/rerun/commit/e3aabd76e899b6f8e345bfd9ba6f20fe4bc3eb84) +- Add "Show/Hide in all views" entity context menu actions [aa49fc4](https://github.com/rerun-io/rerun/commit/aa49fc4a1f2c8a342488add6e378b5b0cbfcbc80) (thanks [@ollema](https://github.com/ollema)!) +- Individual controllable time playing for previews [4b17956](https://github.com/rerun-io/rerun/commit/4b179567296d56d6180413d6856fe5fef18770c1) +- Vertical scroll support for state timelines [6dc3e49](https://github.com/rerun-io/rerun/commit/6dc3e49215b21e5ba42b693dcafebbfe784d6ede) +- Fixes last state in timeline extending to infinity [a13dc44](https://github.com/rerun-io/rerun/commit/a13dc448758bafe355571bfea2b6550c006f4e79) +- Hovered state highlights time range in time-based views [5258852](https://github.com/rerun-io/rerun/commit/52588526b1c0e6c1eb0b4c67b4d8e8ffaf03c665) +- Make webdecoder more robust against spurious decoding problems [fb2e5d1](https://github.com/rerun-io/rerun/commit/fb2e5d19c18c3305466826b58f945730cfc1e4e1) + +#### 🚀 Performance improvements +- Faster queries: do not split or compact chunks [0cb8ffb](https://github.com/rerun-io/rerun/commit/0cb8ffb76146623d425e9d9838ba80573b6850f0) +- re_server: refresh schema cache after add_layer [2e736c4](https://github.com/rerun-io/rerun/commit/2e736c44fcac8144933896dd8b29edfd7566ca3f) +- Separate is keyframe chunk [85fe857](https://github.com/rerun-io/rerun/commit/85fe8576a3d412a8135f3b98723f6a4e15268ead) +- Speed up queries over single columns [5bc08c6](https://github.com/rerun-io/rerun/commit/5bc08c6d1315e7ebaba8bee15bb1c40fb0a41309) +- Enable SIMD on wasm [3bcaca0](https://github.com/rerun-io/rerun/commit/3bcaca0205907a747bb521207798a383047b53eb) +- Making dataloader keyframe aware [203d7ce](https://github.com/rerun-io/rerun/commit/203d7cede037383a6d7e75d63c6a9ba09f32b90d) +- Speedup for points & line rendering on Apple Silicon in some situations [b8b0ced](https://github.com/rerun-io/rerun/commit/b8b0cede37b0243d05d9e33fd40baf517450a07c) +- Cache RGB8 image histograms [#12800](https://github.com/rerun-io/rerun/pull/12800) (thanks [@waamm](https://github.com/waamm)!) + +#### 🧑‍🏫 Examples +- Add example for preprocessing a robot recording via chunk API [396a512](https://github.com/rerun-io/rerun/commit/396a512b1003ce7a761d1bad8aa4587c342fce0b) + +#### 📚 Docs +- Adding overview to Getting Started [473abad](https://github.com/rerun-io/rerun/commit/473abad01fc3279abe32b95761d33bba7ad1e31e) +- Moving install and setup from overview to getting started on resources page [f1de7a4](https://github.com/rerun-io/rerun/commit/f1de7a4551d69045dc39c1586b7c96af26cd66f7) +- Make more reference material available in the side bar [ba868d9](https://github.com/rerun-io/rerun/commit/ba868d9b5badc449b92b1ce16aa7e4bdaa163653) +- Simplifying docs guide [0ca2c5f](https://github.com/rerun-io/rerun/commit/0ca2c5fe1fa482886587e6d703dee914e34a9793) +- Add migration note for legacy ROS 1 data [de6a430](https://github.com/rerun-io/rerun/commit/de6a4300e4e3bcfd67f6f7e3ee5babf0105122f0) +- Fix custom-data doc page claiming you can't visualize custom data, instead redirect to pages that explain how [7bad539](https://github.com/rerun-io/rerun/commit/7bad539208fbaa0cb1294478b0720ab1f922cc91) + +#### 🖼 UI improvements +- Respect Wayland compositor preferences for client/server-side decorations [0c00f57](https://github.com/rerun-io/rerun/commit/0c00f57c3c234dca0ff7e76933a39731f99c2c91) +- Add a nicer About-menu [6645dbe](https://github.com/rerun-io/rerun/commit/6645dbed5488a65e51c5e629488bf247151dbf21) +- Reset states timeline view via double-click [36cf84d](https://github.com/rerun-io/rerun/commit/36cf84d07333e0a4fc843f72afa145ce090f4c6b) + +#### 🧢 MCAP +- Keep MCAP channels without schema as raw data [7b87ed0](https://github.com/rerun-io/rerun/commit/7b87ed00abffc504dc5e2f516baf1431e5f05eb7) +- MCAP: Add lens for ROS 2 geometry_msgs/PoseStamped [be7012c](https://github.com/rerun-io/rerun/commit/be7012cd29b91eb2f10bb6d67efead66edcae34e) +- Move std_msgs/String to lens [06fef1c](https://github.com/rerun-io/rerun/commit/06fef1c32f33732c78b4689c083fd4faa0556f10) +- Move rcl_interfaces/msg/Log to lens [325b28f](https://github.com/rerun-io/rerun/commit/325b28fe01e13a2bfdc447b9e19803e4c0939df5) + +#### 📈 Analytics +- Datafusion metrics [fdbb66f](https://github.com/rerun-io/rerun/commit/fdbb66f9d349dc9e5c5d6c9be637b15e7b36d4a9) + +#### 🧑‍💻 Dev-experience +- Include trace-id in error message on failed registration [bab7682](https://github.com/rerun-io/rerun/commit/bab7682765163e1321fe4fa8605ce5e5b69088fa) + + +## [0.32.2](https://github.com/rerun-io/rerun/compare/0.32.1...0.32.2) - 2026-05-20 + +### 🔎 Details + +#### 🪳 Bug fixes +- Create spatial topology from schema instead of from chunk data (fixing to sometimes never pull data) [6c015b9](https://github.com/rerun-io/rerun/commit/6c015b99335b91b974be6ab2fb524b4381e38a02) +- Respect play state from blueprint with Catalog Server [58aa9c5](https://github.com/rerun-io/rerun/commit/58aa9c55f88b3ef1b2848d1e64422ef5e17303b6) +- Fix orbital zoom clamp panic on tiny scenes [d039a5f](https://github.com/rerun-io/rerun/commit/d039a5f2c92ddecf723624b29291792c136c6593) +- Fix video issues after GC at start of recording [e230962](https://github.com/rerun-io/rerun/commit/e230962e3dbaa9a8400eca8616f9174b8964c654) + +#### 🌁 Viewer improvements +- Opening a url with a timestamp anchor now always pauses the recording [9977cd1](https://github.com/rerun-io/rerun/commit/9977cd103163f4d53e65d1882cc1c30cab8426e3) + +#### 🚀 Performance improvements +- re_server: refresh schema cache after add_layer [fefa95e](https://github.com/rerun-io/rerun/commit/fefa95e07d5a4cff58c40cef8242fb9e011ce996) +- Speed up queries over single columns [0a6e2d8](https://github.com/rerun-io/rerun/commit/0a6e2d8bb8637e59019e48bbf6e78b50018f3994) + +#### 🧢 MCAP +- Keep MCAP channels without schema as raw data [7d3e0d5](https://github.com/rerun-io/rerun/commit/7d3e0d57651ca5c8ac4dded299d9f7bdbe03cc22) + +## [0.32.1](https://github.com/rerun-io/rerun/compare/0.32.0...0.32.1) - 2026-05-15 + +### 🔎 Details + +#### 🪵 Log API +- Fix problem of intermixing different store messages in one rrd [5620f47](https://github.com/rerun-io/rerun/commit/5620f47064a46c4733eea1189e7757d898340a9b) + +## [0.32.0](https://github.com/rerun-io/rerun/compare/0.31.4...0.32.0) - 2026-05-13 - Chunk Processing, Pytorch dataloader, Dataset Review + +🧳 Migration guide: https://rerun.io/docs/reference/migration/migration-0-32 + +### ✨ Overview & highlights + +#### Python chunk processing API + +This release introduces a chunk processing API designed for systematic and efficient wrangling of robotics data. +It includes: +- A `Chunk` object for inspecting, creating, and manipulating chunks. +- Readers for common file formats (RRD, MCAP, Parquet, URDF, and more to come) which output streams of chunks. +- A composable `LazyChunkStream` class to define memory-bounded chunk-based filtering and transformation pipelines. +- Lenses: an expressive and performant API to manipulate component data in chunks. +- A multithreaded, GIL-free, native engine for pipeline execution that is designed for distributed execution in the future. +- Interoperability with a catalog server and the Rerun SDK logging API. + +In addition to enabling powerful data wrangling pipelines, the chunk processing API is significant for offering read/write chunk-level control of RRD files down to the raw Arrow data. + +_Note_: this API is experimental and subject to breaking changes as we continue to improve it. + +#### Experimental dataset review +You can now build tables of recording previews configured with arbitrary blueprints! + + + +https://github.com/user-attachments/assets/7acf9671-c46a-4355-a50f-2670cc80c4d9 + +Clickable flags let you curate data directly from the table: toggles update a boolean flag column and are written back to the server. + +To try it out, enable the experimental options in the Viewer's settings and try the two Python examples: +[`table_grid_with_flags`](./examples/python/table_grid_with_flags/) for basic grids with clickable flags, +and [`table_blueprints`](./examples/python/table_blueprints/) for the full preview experience. + +Limitations, or why this is still experimental: +* previews don't yet work directly on raw datasets; you have to send a special table to the server instead (see examples) +* table blueprints are currently text-encoded in table metadata, this is subject to change +* depending on the number and content of previews, overall runtime performance can be poor, especially in the browser +* many UX details are still unfinished + +#### Experimental state timeline view + +A new experimental view for visualizing discrete state transitions over time as horizontal colored lanes, useful for state machines, mode transitions, and similar discrete signals. Log state changes with the new `StateChange` archetype; configure their display on the UI or using `StateConfiguration` in the blueprint API. + + + State timeline view + + +Read [our guide](https://rerun.io/docs/howto/visualization/state-timeline) to get started. Feedback is appreciated! + +#### `GridMap` archetype and MCAP support for ROS occupancy grids + +Rerun now supports 2D grid maps, as used e.g. in robot mapping & navigation applications, through a new `GridMap` archetype. + +* A `GridMap` is an image buffer with defined cell size per pixel, which can be embedded as a textured rectangle in a 3D scene. +* `GridMap` has a regular `ImageBuffer` component, so you can also send color images (e.g. to do custom color-mapping in your code). +* For layering of multiple maps you can optionally set draw order and opacity when logging, or separately in the viewer / blueprint. +* The visualizer also supports the colormap options that RViz users are familiar with, selectable at log time or in the viewer / blueprint. +* In a 3D scene, the map appears at the entity's coordinate frame (either entity-path based or with TF-style named frame like `CoordinateFrame("map")`). Additionally, an optional translation & rotation offset can be specified. + +For ROS 2 users: +* 🧢 Rerun's MCAP importer automatically loads ROS 2 `nav_msgs/OccupancyGrid` messages as `GridMap`s. +* 📖 Our [ROS 2 guide](https://rerun.io/docs/howto/integrations/ros2-nav-turtlebot) also shows an example how you can log `GridMap` from your custom ROS nodes. + +Here's a demo video showing a typical ROS 2 MCAP recording with multiple map and costmap layers in Rerun: + + + +https://github.com/user-attachments/assets/f31b712d-2dd7-4e45-bb6a-0e103e7016b3 + +#### OSS catalog server now streams from disk + +The OSS server (`rerun server` and `rr.server.Server`) no longer eagerly loads RRDs in memory when registering datasets. +It instead uses the manifest embedded in the RRDs to load chunks on demand when serving requests. +This greatly extends the amount of data that can be registered and queried for a given memory budget, and makes registration orders of magnitude faster. + +_Note_: This requires the RRDs to have a manifest, which most modern RRDs have. +Legacy RRDs are still eagerly loaded. +Use the `rerun rrd optimize` CLI to migrate and optimize legacy RRDs. + +#### Plot improvements + + + new tooltip for plots + + +- Performance improvement for scenes with many series. Moved from egui CPU tesselation to GPU line rendering. +- Redesigned tooltips. Hovering over a plot now shows a cleaner, more compact tooltip with color swatches matching each series. Also it is visually obvious now when events were actually logged. +- Better NaN & Infinity handling. Time series views now gracefully handle non-finite values: the Y-axis range ignores them, isolated data points surrounded by NaN are drawn as dots instead of disappearing, and aggregation skips over non-finite values rather than corrupting nearby points + + +#### Performance improvements + +This release comes with a few significant performance improvements. Among other things: + +* Visualizing scenes with many transforms on the same entity (as it is often the case with `tf`-style named transforms) will now perform vastly better +* Plot line tessellation is now GPU accelerated, using the same rendering path as our 3D lines +* Web viewer now decodes images using the web decoder, resulting in much smoother play of raw-image "videos" +* various improvements to `rrd optimize` (former `rrd compact`) to produce more streaming & object storage friendly data +* MCAP decoder is now multithreaded + +#### New branding + + new rerun app icon +
+ + + new rerun app icon + + +You may have noticed a new Rerun logo and app icon! We've also slightly tweaked our color palette. Stay tuned for more exciting news! + +#### Docs feedback on the website. + +As a part of our [website](https://rerun.io/) update, we've also added a feedback form to all our documentation pages. So you can add your feedback directly to the respective topic. + + + feedback form + + + + +### ☁️ Highlights for Rerun Hub customers + +Several improvements in the open-source Rerun SDK are designed specifically to work with Rerun Hub. + Here are the key updates that are especially relevant if you're a customer of Rerun Hub: + +#### Direct fetch from object storage for commercial `Rerun Hub` customers + +The SDK will now fetch chunk data directly from the object store that holds your recordings, without needing to proxy the data through the server. +This allows for better performance in highly parallel workloads, as well as lower latency when the client is located close to the data store. + +The old proxy path is still supported, and can be opted into using the `RERUN_CHUNK_STRATEGY=grpc` environment variable. + +#### Experimental training dataloader + +You can now train PyTorch models directly against the Rerun OSS server, with no intermediate export step! + +The new highly experimental `rerun.experimental.dataloader` module exposes Rerun recordings as iterable or map-style PyTorch datasets, streaming encoded images, scalars, and compressed video (`h264`/`h265`/`av1`) on the fly. Random access, multi-worker prefetching, and DDP support work out of the box. + +Each field accepts an optional `window=(start_offset, end_offset)` parameter, an inclusive range relative to the current index. When set, the field yields the slice of values across that window instead of a single sample. For example, `window=(1, CHUNK_SIZE)` returns the next `CHUNK_SIZE` action values after every observation, making action-chunking policies a single query per batch. + +See the new [LeRobot ACT training example](https://github.com/rerun-io/rerun/tree/main/examples/python/dataloader). + +Expect breaking changes between releases while we iterate on the design. For large-scale training, Rerun Hub offers a higher-performance backend. + +### ⚠️ Breaking changes + +* "Data loaders" renamed to "importers" to avoid confusion with the widely-used ML/PyTorch "dataloader" concept +* Rust Lenses API has been restructured +* `rerun rrd compact` renamed to `rerun rrd optimize`, has profiles and new defaults +* `DatasetEntry.register` requires a sequence of URIs (Python) +* URDF importer now loads the static transforms of the model to the `/tf_static` entity by default +* MCAP metadata and statistics are now saved to `__mcap_metadata` + +🧳 Full Migration guide: https://rerun.io/docs/reference/migration/migration-0-32 + +### 🔎 Details + +#### 🪵 Log API +- Group URDF collision entity paths by geometry type [a394117](https://github.com/rerun-io/rerun/commit/a3941179b52acc61e1b144c746fef8db8e58f182) +- Add Parquet Dataloader with some workarounds for merging columns [c1ee446](https://github.com/rerun-io/rerun/commit/c1ee446ee9f437847c15033a249be640ac89bc2b) +- Optionally add struct grouping on parquet columns [ae22b4d](https://github.com/rerun-io/rerun/commit/ae22b4d3afa5e71f2a2043c1c45090a5640f2bd1) +- Introduce `GridMap` archetype & visualizer [d74cb27](https://github.com/rerun-io/rerun/commit/d74cb277052ebd41ce5a981d75591a57fea3fc66) +- Rename "DataLoader" to Importer [11cd1c2](https://github.com/rerun-io/rerun/commit/11cd1c24b1c55228c48d3c9e9ca4bc465d73c2d3) +- Use `/tf_static` as default in URDF importer & make configurable in `UrdfTree` [1f01a57](https://github.com/rerun-io/rerun/commit/1f01a572e774ad49112d7e658dab16c60617877a) +- Expose `mimic` joints from URDF [cf4c652](https://github.com/rerun-io/rerun/commit/cf4c65255afed1aeded9f61d513b6fe75fd4b0f5) +- Add RViz-style "Map" and "Costmap" color options for `GridMap` [c452a48](https://github.com/rerun-io/rerun/commit/c452a482638f9a4271596f2ec382f76d5e7695be) +- Add `stream() -> LazyChunkStream` to Python `UrdfTree` [dc51f60](https://github.com/rerun-io/rerun/commit/dc51f60554cc634133b5582e26ccb8c2769d4aa7) +- Renames ChunkBatcherConfig::ALWAYS to ALWAYS_TEST_ONLY [46c20b8](https://github.com/rerun-io/rerun/commit/46c20b85ed0ae39292feae2a50355fdad87d4c0e) +- Opt-out from generating a FileSink footer [0a78f28](https://github.com/rerun-io/rerun/commit/0a78f2817f15c50e00f6ea222227d9358dcf0eea) +- Add `UrdfTree::compute_joint_transform_batches` for lens/chunk pipelines [efd045c](https://github.com/rerun-io/rerun/commit/efd045cdf953d6c3b97f764cdea9560df6fbcab4) +- Add snippet showing a `GridMap` at a specific pose [2e99c68](https://github.com/rerun-io/rerun/commit/2e99c681e9f4789ce92803bda1e5edc76bb5788b) + +#### 🐍 Python API +- Introduce `LazyChunkStream` [a0ce421](https://github.com/rerun-io/rerun/commit/a0ce421f4ca9b3e2406b73efd3e63fb8ee03b8b4) +- Introduce `McapLoader` to produce `LazyChunkStream` from MCAP file [ef51623](https://github.com/rerun-io/rerun/commit/ef51623f8fa45f0bf130b5a55b4b72db7cd43051) +- Expose `Selector` in Python SDK [ffc088d](https://github.com/rerun-io/rerun/commit/ffc088dbf39e767eb02488c0786672517275682b) +- Introduce Python `ChunkStore` object [9294554](https://github.com/rerun-io/rerun/commit/9294554075197fff676000f29f5090f6b35ab771) +- Add ability to compact `ChunkStore` [5dd9f23](https://github.com/rerun-io/rerun/commit/5dd9f2315abb2ae885cb45108512431db32fa4c3) +- Use Lenses to manipulate `ChunkStream` in Python SDK [72ff520](https://github.com/rerun-io/rerun/commit/72ff520451de6c187262fcf10f62e954aa00785e) +- Make notebook display loading spinner when waiting on send_table [ea09906](https://github.com/rerun-io/rerun/commit/ea09906cf3d6b87b16ad55239983a85ec44e5421) +- Make `RrdLoader` produce lazy `ChunkStore` [2e804c4](https://github.com/rerun-io/rerun/commit/2e804c40b316cfcc483a55e2f5edf2b8a227bb8d) +- Add `map` and `flat_map` method to `LazyChunkStream` [393680c](https://github.com/rerun-io/rerun/commit/393680c7dd2337b4fcd59d1851fd38e525ab5664) +- Add `Chunk` construction methods: `from_columns` and `from_record_batch` [547d650](https://github.com/rerun-io/rerun/commit/547d650fcb6665942a06b4c010e65aaead585e14) +- Add `exists_ok` option to `CatalogClient.create_dataset` [8d4e1b3](https://github.com/rerun-io/rerun/commit/8d4e1b307774554255968c485ae28ffb067aeece) +- Add `Chunk.apply_lenses()` API [88fea86](https://github.com/rerun-io/rerun/commit/88fea86e302742ef1982a60dd126829b27f0818c) +- Expose split-size-ratio in python [c21a5b8](https://github.com/rerun-io/rerun/commit/c21a5b8cd0701044ea1f34e0f30be922eb25269b) +- Initial torch dataloader [dca28f2](https://github.com/rerun-io/rerun/commit/dca28f27b0fc936d7e043ceaf41895bab84b84b7) +- Rename `rrd compact` to `rrd optimize` [c5b027b](https://github.com/rerun-io/rerun/commit/c5b027ba02d8fb1fbf30bdda7ab7b430ce90383f) +- Add `apply_selector` methods to `Chunk` [5a20bd6](https://github.com/rerun-io/rerun/commit/5a20bd679677cbbfb0f808e774a3c654c47d7ac2) +- Use `Mapping`-based `LensOutput` and improve naming [2fc409e](https://github.com/rerun-io/rerun/commit/2fc409e5f1a74fa91abf4dcb8fdbb1b3b583830d) +- Add Map-style torch dataset [ebb9953](https://github.com/rerun-io/rerun/commit/ebb9953034baa59817326f3c1fdbaa508149cd2f) +- Make task cancellation API public, add `.cancel()` on RegistrationHandle [260d119](https://github.com/rerun-io/rerun/commit/260d1198c83768c7ef25d35cad1658036006f437) +- Add documentation page for lenses [1cb99e5](https://github.com/rerun-io/rerun/commit/1cb99e5c24ee2fa386d14d2c225fc298873527e2) +- Add include & exclude topic filter options to MCAP importer [1b586fa](https://github.com/rerun-io/rerun/commit/1b586fa604479d55891b057b9dc4c74e44bafdf9) +- Further simplify Lenses API in Python and Rust [65d744a](https://github.com/rerun-io/rerun/commit/65d744ae34b317af2caf36c42ef939477b6ee00c) +- Allow creating a `Chunk` copy with a new entity path [3d8f97c](https://github.com/rerun-io/rerun/commit/3d8f97cf76886f3e6cc8795c19afccabe4ba10f3) +- Make `Selector` picklable [db20691](https://github.com/rerun-io/rerun/commit/db206917263291fb433a1d5db8199fb3e8bc0390) +- Add `dataset.segment_store(segment_id)` to create a lazy `ChunkStore` [524b5cc](https://github.com/rerun-io/rerun/commit/524b5cc3ad543a699cc1579f58fc4b038d0ae97d) +- Introduce optimization profiles and change default [d3488bc](https://github.com/rerun-io/rerun/commit/d3488bc7f5fb640ff3bbe583d1414873d8ea7410) +- Split off `LazyStore` from `ChunkStore` (now returned by `dataset.segment_store()` and `RrdReader.store()`) [fa63189](https://github.com/rerun-io/rerun/commit/fa631894c4f2f0805a1b643dd8d068c841105159) +- Improve dataloader config [b96a985](https://github.com/rerun-io/rerun/commit/b96a9859bcf412f24ba8444138a2ce8fd49d7a3f) +- Add support for multi-store RRD to `RrdReader` [41ed51a](https://github.com/rerun-io/rerun/commit/41ed51ac0d37f4f80c8834a33a38b8587f8a32d1) +- Rename `send_chunk` to `send_chunks` and accepts stores and `LazyChunkStream` [c8e0965](https://github.com/rerun-io/rerun/commit/c8e0965fb50f4d70599f448b4175dd563e72cdf1) +- Deprecate `rerun.recording` [8b52512](https://github.com/rerun-io/rerun/commit/8b52512035c9afc58e543c07862ac14cb4bc677e) +- Fix disconnect footgun [4833706](https://github.com/rerun-io/rerun/commit/483370692c187865c9a694264897a5d4e02a5166) + +#### 🦀 Rust API +- Introduce `at_entity` instead of `*_output_columns_at` [8e65ff0](https://github.com/rerun-io/rerun/commit/8e65ff0504a58753a93a46645ef566cb4f436602) +- Decouple entity path filtering from `Lens` definition [80ab3a9](https://github.com/rerun-io/rerun/commit/80ab3a9db06644caadc646f194f5a6bd64d1a779) +- Pushdown filters for select [215e8aa](https://github.com/rerun-io/rerun/commit/215e8aa3f7f23ce1b84724793ef51c61eda23b27) +- Rework chunk output produced by Lenses [dbeef05](https://github.com/rerun-io/rerun/commit/dbeef05a3457e4def4bc1162719659fae9dbbc11) +- schema evolution: widen record-batch on read [838a669](https://github.com/rerun-io/rerun/commit/838a669d2f2090d22395a481b2dcd08012ebc92c) +- Unify `Chunk`-based APIs between Rust and Python [a171102](https://github.com/rerun-io/rerun/commit/a1711026c976b1a4b9a1dc4669a7994d43da4b4c) +- Add `apply_selector` methods to `Chunk` [5a20bd6](https://github.com/rerun-io/rerun/commit/5a20bd679677cbbfb0f808e774a3c654c47d7ac2) +- Enforce one-to-one mapping of `LensOutput` to target entity [b5709e5](https://github.com/rerun-io/rerun/commit/b5709e5c3ca2c0454ac42bf635a0317a89261ed9) +- Add documentation page for lenses [1cb99e5](https://github.com/rerun-io/rerun/commit/1cb99e5c24ee2fa386d14d2c225fc298873527e2) +- Further simplify Lenses API in Python and Rust [65d744a](https://github.com/rerun-io/rerun/commit/65d744ae34b317af2caf36c42ef939477b6ee00c) +- Allow creating a `Chunk` copy with a new entity path [3d8f97c](https://github.com/rerun-io/rerun/commit/3d8f97cf76886f3e6cc8795c19afccabe4ba10f3) +- Add `GraphView` to rust blueprint api [9327b5f](https://github.com/rerun-io/rerun/commit/9327b5f93e962e4a3e506b8202884f7b82e18b52) + +#### 🪳 Bug fixes +- Fix our python 3.10 support [7d4716d](https://github.com/rerun-io/rerun/commit/7d4716dd5e6bf36203cd246731b2d530ece6dc38) +- Fix MCAP CLI decoder identifier list [5b170a4](https://github.com/rerun-io/rerun/commit/5b170a499f9f016b5fb47abb4b6dedb031b2aaf2) +- Fix bug where shapes defined with UI units were scaled incorrectly [7e7ec15](https://github.com/rerun-io/rerun/commit/7e7ec157f4c772b235f1bbff57a5dcde75db412c) +- Improve `rerun download` [1c9aa10](https://github.com/rerun-io/rerun/commit/1c9aa10c1c8984dd63606f4d3c534cd6505e9cd4) +- Fix off-by-one bug in video stream cache [f0484ee](https://github.com/rerun-io/rerun/commit/f0484eedaaa7263d19a3c3386955930829cfb0d0) +- Fix: Hyperlinks shown in tables wouldn't resize [f16707e](https://github.com/rerun-io/rerun/commit/f16707e0cd651d04453fa6ff8e59e69d39faf528) +- Fix range queries on 3D points in `SpatialView3D` [e8dc5e0](https://github.com/rerun-io/rerun/commit/e8dc5e0a3febf793433c72646bf838a938add107) +- Fix plot view time range ui [45de0cb](https://github.com/rerun-io/rerun/commit/45de0cbcf63b2d2280d1e0136d64ea3fe3a990e4) +- Make arrow keys pause playback [37009c0](https://github.com/rerun-io/rerun/commit/37009c04050e7d5ac968a5b2f50e9f0addfc6f95) +- Fix silent error swallowing in gRPC streaming, add error injection testing infrastructure [ec01f7a](https://github.com/rerun-io/rerun/commit/ec01f7a3c167467a37e5c78e616b7aa2dbc67e17) +- Fix `follow` not being propagated to `http` URLs with extensions [09d5f94](https://github.com/rerun-io/rerun/commit/09d5f94c98bf06aa84ab6f041cc13ef0f21f3537) +- Fix renderer registration order influencing draw order [97db1a1](https://github.com/rerun-io/rerun/commit/97db1a16382f5ae689d2e6a76dfedd7654e2802a) +- Unify `rerun//` and `rerun+https` at parse time, fixing Viewer bugs for incorrectly distinguishing them [69ff58d](https://github.com/rerun-io/rerun/commit/69ff58d66a1e8c7ca524bca4b7daf0f691842710) +- Add `SystemCommand::RemoveRedapServer` for more thorough cleanup [52bc3ea](https://github.com/rerun-io/rerun/commit/52bc3ea69bad2a4b73ea3019e4b9b7a728c281a4) +- Close recordings when a server is removed [ad7371f](https://github.com/rerun-io/rerun/commit/ad7371f3cbaada95147577eed60c7d71da59a516) +- Fix visualizations not showing up when initial data was empty [c867040](https://github.com/rerun-io/rerun/commit/c867040a23cb78b4a27d766fabb5ce9e00c44ca2) +- Fix reflection of "pure-constant" ROS2 message schemas [fefbf6d](https://github.com/rerun-io/rerun/commit/fefbf6d57e68e7e84c04285809188c7107fc1d79) +- Handle large video file error gracefully [#12744](https://github.com/rerun-io/rerun/pull/12744) (thanks [@AyushAgrawal-A2](https://github.com/AyushAgrawal-A2)!) +- Use row id instead of byte span for video streams [645e57b](https://github.com/rerun-io/rerun/commit/645e57b732d09a53a873d00162050a61bbf0b6b0) +- Return empty tensor on video decoder cold-start instead of raising [13f92e5](https://github.com/rerun-io/rerun/commit/13f92e594c909cfd6c1136065ee24a35f945645a) + +#### 🌁 Viewer improvements +- Cluster overlapping coplanar `TexturedRect`s and use draw order for tie-breaking [76b64c1](https://github.com/rerun-io/rerun/commit/76b64c137d96bf189b3cb683d6f6328017ef92a3) +- Always open recording in background from context menu [a173287](https://github.com/rerun-io/rerun/commit/a1732872bf29cd9addff622f343e06d4bd020f07) +- Improve implicit handling of invalid instance pose rotations [e65a6ce](https://github.com/rerun-io/rerun/commit/e65a6ce8f21a41dc3c8ff0bcf2f1ddbdb9367704) +- Use optional `pose` of Foxglove `PointCloud` (if set) [fee2815](https://github.com/rerun-io/rerun/commit/fee28155cd4e077501b67acc36d5aac2aac15467) +- LeRobot loader: Add support for flat feature names [a493658](https://github.com/rerun-io/rerun/commit/a493658e85bd534fff07f1eacdb141d502caae0e) +- Select `message_log_time` as default timeline for MCAP [b687bd6](https://github.com/rerun-io/rerun/commit/b687bd644f405106d8e13357719e68b9f6e4b3d3) +- Properly handle outline masks of overlapping coplanar rectangles [617a7c2](https://github.com/rerun-io/rerun/commit/617a7c2a25f49d80b5a691178dbd75b357275219) +- New liftable shape limit to avoid unresponsive viewer [a4f6223](https://github.com/rerun-io/rerun/commit/a4f62231ae91b8bf19fa646abfd4d0c0b75b881b) +- Add configurable CORS policy for rerun proxy and re_server [8baa142](https://github.com/rerun-io/rerun/commit/8baa142ffc5e81a8574e6bc9629d35d012036af9) +- Smarter `VideoStream` streaming [2f73783](https://github.com/rerun-io/rerun/commit/2f73783025afeb9c026fe7032a73d1a98946506b) +- Support focusing specific 3D points in viewer [ddda5cf](https://github.com/rerun-io/rerun/commit/ddda5cf159f5c50ee6a42a5bc9b6e32190e4b516) +- Experimental grid layout & flagging for tables [9b6bf71](https://github.com/rerun-io/rerun/commit/9b6bf710021a495a7b96e3f24bf73b7b0e634157) +- Make panel state toggable while inspecting tables/server [2eb8fb0](https://github.com/rerun-io/rerun/commit/2eb8fb07d3792d16048195c789f6e8b5ff49d276) +- Streaming info panel (as part of memory panel) [597fdd4](https://github.com/rerun-io/rerun/commit/597fdd4cfbceeb28bd1bddf5ddb11f1a0e4dce5b) +- Respect `up_axis` in Collada (.dae) mesh importer [#12708](https://github.com/rerun-io/rerun/pull/12708) (thanks [@Abhisheklearn12](https://github.com/Abhisheklearn12)!) +- Improve handling of NaN & Infinity values in time series view [055777f](https://github.com/rerun-io/rerun/commit/055777ff5b32eaaeb8fe04f16d28d50449a46e3d) +- By default, fetch similar chunks 30 seconds of playtime forward [7c0680e](https://github.com/rerun-io/rerun/commit/7c0680e5d4b740acc710b366249c32b141ac12dd) +- Remember memory limit between viewer relaunches [1eb763a](https://github.com/rerun-io/rerun/commit/1eb763a34328802de7644bffccd7b1aa156e7997) +- Add support for duration columns in lerobot datasets [1cd0abc](https://github.com/rerun-io/rerun/commit/1cd0abc9cbbe14d6c4c8773432a4cb59ed6eda34) +- Support hierarchical dataset naming in viewer [efa2f23](https://github.com/rerun-io/rerun/commit/efa2f234e968e8888a6ca6d53f39170c5b319b48) +- Display `.` separated dataset in a folder hierarchy [a217309](https://github.com/rerun-io/rerun/commit/a21730943085d8a0742eae8e2d225eef11310535) +- Full VP8+VP9 support on native & web [a1642b4](https://github.com/rerun-io/rerun/commit/a1642b4f8213ac2c00d51bb6a630c30705d84a3b) (thanks [@AyushAgrawal-A2](https://github.com/AyushAgrawal-A2)!) +- Use video player for encoded depth images [c3af7d5](https://github.com/rerun-io/rerun/commit/c3af7d530a5933e624e1404c14c1c62a2ff78a1a) +- Better log console formatting [bd4c866](https://github.com/rerun-io/rerun/commit/bd4c866309dbb7ff0f6bb847d9e897f570d655a1) +- Make text document configuration part of the blueprint [e6ae09a](https://github.com/rerun-io/rerun/commit/e6ae09aa979231084768cd6c8f776f9bf5da563f) +- Experimental preview renders for tables with data set URLs [b133946](https://github.com/rerun-io/rerun/commit/b133946c4fccc0f0f08893e3bf422b6a373cde0d) +- Add `VideoStream.is_keyframe` component [d50eab6](https://github.com/rerun-io/rerun/commit/d50eab6d9d276f88d44c0c133a7e5a2c3e868642) +- Limit 2D & 3D view zoom out [e43c172](https://github.com/rerun-io/rerun/commit/e43c172407300cc4eb8b979a9fa78ea30c73206f) +- Make previews always play looping [90b5b01](https://github.com/rerun-io/rerun/commit/90b5b0134f9eb74c14c34908b30ab43c6af3ca62) +- Fix arrows blowing up when cap behind camera [db571f7](https://github.com/rerun-io/rerun/commit/db571f7035b311d77e9c6015298a4d288faa27f2) +- Add Ellipses2D archetype [2cbf5ff](https://github.com/rerun-io/rerun/commit/2cbf5ff4d5c68850e538ef75a1913b71fbecdcf6) +- Clamp time controls [8af40e2](https://github.com/rerun-io/rerun/commit/8af40e283df04b9d4d26123fc09c201b9751d8df) + +#### 🗄️ OSS server +- Lazy RRD loading in OSS server [4aea4a5](https://github.com/rerun-io/rerun/commit/4aea4a570a245949314f97b282e011c708efce01) +- No longer cache chunks in OSS server [853591a](https://github.com/rerun-io/rerun/commit/853591a3d143da30e79aea113c35af6435728124) + +#### 🚀 Performance improvements +- Decode encoded images using our video-player system, and use the web video decoder [794a722](https://github.com/rerun-io/rerun/commit/794a722d64c46e8c643af7d43c86aa92c2ea7135) +- Drop details from the manifest that aren't needed to reduce manifest memory bloat [612e9ef](https://github.com/rerun-io/rerun/commit/612e9ef5a75a1b9f3e8fa3d81b1142903b83d7a6) +- `rerun rrd compact`: split by video GoP boundaries [2485570](https://github.com/rerun-io/rerun/commit/248557024b14e95257659d63fdadb105ac5a2f87) +- By default, only prefetched what is visible [b509f91](https://github.com/rerun-io/rerun/commit/b509f91d244128339a2e3204a39ca66c6564197d) +- Speed up `DatasetView.reader`: only fetch schema once [b266938](https://github.com/rerun-io/rerun/commit/b26693866da4ae496aceb58042cc232d5b1aeae9) +- Huge speedup transform lookups for overlapping transform chunks [803337d](https://github.com/rerun-io/rerun/commit/803337de7dc94e19613946a9b34d7a914107511d) +- Don't traverse through parent entities in queries if there are no cleared entities at all [a873b22](https://github.com/rerun-io/rerun/commit/a873b22737798756abcac6c25bf8ff7194a6221e) +- Fix not taking fast paths in line/point for using default radii [5fb7c3d](https://github.com/rerun-io/rerun/commit/5fb7c3d00ff2e1a31c9c9e510100d3dfb2253a1f) +- `CatalogClient`: Add RTT and bandwidth probes [87c5e05](https://github.com/rerun-io/rerun/commit/87c5e056326f62a106ce4215775fdb2b53ce3199) +- Improve performance of Protobuf reflection [3969825](https://github.com/rerun-io/rerun/commit/39698253c8346639fc5d5b52c750717a8b4f7eb2) +- `register` now takes a list of URIs [9ec5265](https://github.com/rerun-io/rerun/commit/9ec52651803e0c2c0716e3a16d4631957926b6c2) +- Parallelize mcap decoder [6ccfcbf](https://github.com/rerun-io/rerun/commit/6ccfcbfc6fa8a1b28459440c895a9bbd6cfa6a6b) +- Emit sparse `is_keyframe` marker chunks when running optimize [ec6dff0](https://github.com/rerun-io/rerun/commit/ec6dff03c046f4a2a0e4b2726f208a268d424108) + +#### 🧑‍🏫 Examples +- Add dataloader training example [8cd8acb](https://github.com/rerun-io/rerun/commit/8cd8acb4f3bdad51b97a94174289ed103a8e835c) +- Add snippet demonstrating `LineStrips3D` with `VisibleTimeRange` [80dd138](https://github.com/rerun-io/rerun/commit/80dd1381273fad507f0516e0fe4000b0369f5c79) +- Use blueprint, component ui and type reflection in `custom_view` example [e64abd0](https://github.com/rerun-io/rerun/commit/e64abd0f695be3b7f66b0816c1b53f361c1c96f4) +- Subscribe to occupancy grids in ROS node example [b4a46c5](https://github.com/rerun-io/rerun/commit/b4a46c561713ac895848e122f63b635d800677c8) + +#### 📚 Docs +- Clearer behavior for `CoordinateFrame("")` [5bf9c4a](https://github.com/rerun-io/rerun/commit/5bf9c4a6027ce525a11c35dc9ff43e77324c3e6d) +- Move "Installing Rerun" into Getting Started [0296f67](https://github.com/rerun-io/rerun/commit/0296f67bd56b1ed1625b08e9054d86d13d01767d) +- Reduce python docs footguns [4e158e1](https://github.com/rerun-io/rerun/commit/4e158e1c6221f79ad5dbb4e1cb03b1f73f8c7903) +- Split "Set up a project" out of Log and Ingest [d0d63bc](https://github.com/rerun-io/rerun/commit/d0d63bc775ef6c3964563c7ea2d52df3f9a619b1) + +#### 🖼 UI improvements +- New tooltip redesign [b1a9d82](https://github.com/rerun-io/rerun/commit/b1a9d8285846b4dabce5e29f43af28cb94b7bb07) +- Reduce the size of chevrons in the UI [3f63fa1](https://github.com/rerun-io/rerun/commit/3f63fa12db53e75a6a06f15b2d6d615ff47202a9) +- Highlight invalid frame ID input and show `tf#/` suggestions if applicable [2dbe13a](https://github.com/rerun-io/rerun/commit/2dbe13afaae8627bebe776b1c1231110492d1e38) +- Status visualizer configuration [bb83ed7](https://github.com/rerun-io/rerun/commit/bb83ed70fd5817905a667e93f868986bb11d0e35) +- Update our icon ✨ [0677bbf](https://github.com/rerun-io/rerun/commit/0677bbf87ea4bffce0edd77cc64226a34c9d1d57) +- change colors for new brand colors [27c9036](https://github.com/rerun-io/rerun/commit/27c90366696241da2e2ff7deea9cf97f330fc7eb) + +#### 🕸️ Web +- Add progress bar to rerun-js and handle incomplete wasm downloads [ad551bd](https://github.com/rerun-io/rerun/commit/ad551bdf95f7abe2d1544693042ddf79fdd76e2a) +- Add rerun-js login setting and default to hiding the login button [0d14814](https://github.com/rerun-io/rerun/commit/0d148144f367852084ea48767412f9198c0f1b95) +- web_viewer: support overriding theme via ?theme= URL param [34b9958](https://github.com/rerun-io/rerun/commit/34b9958d5500741787350ae74bf3cf927a7b0fbb) + +#### 🎨 Renderer improvements +- GPU accelerated time series plot drawing [8e9635b](https://github.com/rerun-io/rerun/commit/8e9635b3223528322db2d695b8b6aa45f3a1a037) + +#### 🧢 MCAP +- Add `Selector::pipe` for calling anonymous functions [ea50667](https://github.com/rerun-io/rerun/commit/ea50667657307293eabf14c1b4bb0e6947605928) +- Support Foxglove `LocationFix` & `LocationFixes` [28fe84e](https://github.com/rerun-io/rerun/commit/28fe84e2769ec652919dd0075b5b22010273d62c) +- Split `Runtime` out of `Selector` [0febb36](https://github.com/rerun-io/rerun/commit/0febb36710b8e4efecb0b17890625986e6fa463d) +- Transition Lenses to be `Selector`-based [75e965a](https://github.com/rerun-io/rerun/commit/75e965a4c18784cb645ddf2f1765e80dc16da1c3) +- Lenses should not drop unrelated columns [849efb4](https://github.com/rerun-io/rerun/commit/849efb4a17b37882d2113f35af03bfb72d949971) +- MCAP: Add lens for ROS 2 `nav_msgs/OccupancyGrid` [c87a9ae](https://github.com/rerun-io/rerun/commit/c87a9aeeaded70cad02d2b802913b9d4d8fec02f) +- Write MCAP metadata to `__mcap_metadata` instead of `__properties` [3352bb6](https://github.com/rerun-io/rerun/commit/3352bb63b8bd3732e26d17da13c01f0a4c87e873) +- Write MCAP stats & info to `__mcap_properties` instead of `__properties` [31159e1](https://github.com/rerun-io/rerun/commit/31159e18323d2b73ab8f22ffa64144977d6296f4) +- Decode MCAP attachment records into `__mcap_attachments` [004539a](https://github.com/rerun-io/rerun/commit/004539a793449ae28df1561c309d93715bedf60f) + +#### 📈 Analytics +- More SDK analytics [fc6c8c7](https://github.com/rerun-io/rerun/commit/fc6c8c79fdd8201fef8119a66f9b1540d2a2f8da) + +#### 🧑‍💻 Dev-experience +- Add `rerun.tracing_session()` for support correlation [ec9f048](https://github.com/rerun-io/rerun/commit/ec9f048dfbd83fef934dcd4f19c752618c2ec81a) + +#### 📦 Dependencies +- Update datafusion to 52.5.0 [2832f82](https://github.com/rerun-io/rerun/commit/2832f8264a303e5ad8187183178c0f399a845205) +- Unpin wasm-bindgen [#12737](https://github.com/rerun-io/rerun/pull/12737) (thanks [@anassinator](https://github.com/anassinator)!) + +#### 🤷‍ Other +- Add option to split chunks with large component size differences for different archetypes [b0e6f90](https://github.com/rerun-io/rerun/commit/b0e6f90b3abcb7599d94d561774ff03b0b310dc4) +- Run `rerun rrd optimize` on a folder of recordings [9ccb8b2](https://github.com/rerun-io/rerun/commit/9ccb8b2f8d731e78f2ffb8b04dbe0e1d60fe54ee) ## [0.31.4](https://github.com/rerun-io/rerun/compare/0.31.3...0.31.4) - 2026-04-29 @@ -428,7 +1438,7 @@ And finally, thanks to a contribution from [@vfilter](https://github.com/vfilter #### 📡 On-demand streaming / larger-than-RAM -The Rerun Viewer now supports _on-demand streaming_, when connected to either the OSS server or [Rerun Cloud](https://5li7zhj98k8.typeform.com/to/a5XDpBkZ?typeform-source=rerun.io). +The Rerun Viewer now supports _on-demand streaming_, when connected to either the OSS server or [Rerun Hub](https://5li7zhj98k8.typeform.com/to/a5XDpBkZ?typeform-source=rerun.io). With on-demand streaming, whatever you are currently viewing will be downloaded first. This includes time-scrubbing to the end of a very long recording and quickly seeing what is there, or viewing only one camera feed of many. @@ -437,7 +1447,7 @@ Of course, your memory limit will be respected, and when you change your view or This also means that the web viewer can finally view recordings larger than the 4GiB limit enforced by Wasm32, as long as those recordings are served by a Rerun server. -It also means that Rerun Cloud users can view huge recordings, larger than what fits into RAM. +It also means that Rerun Hub users can view huge recordings, larger than what fits into RAM. The OSS server, however, still loads everything into RAM before serving it. Usage: @@ -803,7 +1813,7 @@ You can now get some insight on which parts of your recording use how much memor - **Python**: Internal submodules moved to underscore-prefixed names (e.g., `rr.color_conversion` → `rr._color_conversion`) - **CLI**: `rerun server --addr` renamed to `rerun server --host` - **Blueprint**: Component overrides from `.rbl` files created in previous versions cannot be loaded in 0.29 -- **Data Platform**: Datasets need re-registration to populate `name` and `start_time` in segment table +- **Catalog server**: Datasets need re-registration to populate `name` and `start_time` in segment table 🧳 Check the migration guide for details: https://rerun.io/docs/reference/migration/migration-0-29 @@ -961,7 +1971,7 @@ This patch adds native support for Collada (`.dae`) meshes, a common format used ### ✨ Overview & highlights -**Transform system overhaul.** This release brings significant improvements to how transforms are handled, especially from ROS or MCAP-based systems. You can now decouple spatial relationships from entity paths by using `CoordinateFrame` to associate entities with named frames, and `Transform3D` with `child_frame`/`parent_frame` parameters to define relationships between frames—similar to ROS tf2. Pinhole cameras also support this system. Additionally, axis visualization has moved to its own `TransformAxes3D` archetype. +**Transform system overhaul.** This release brings significant improvements to how transforms are handled, especially from ROS or MCAP-based systems. You can now decouple spatial relationships from entity paths by using `CoordinateFrame` to associate entities with named frames, and `Transform3D` with `child_frame`/`parent_frame` parameters to define relationships between frames — similar to ROS tf2. Pinhole cameras also support this system. Additionally, axis visualization has moved to its own `TransformAxes3D` archetype. Much more can be found at our revamped docs page [here](https://rerun.io/docs/concepts/transforms). @@ -982,11 +1992,11 @@ Forward/back navigation is now available on native viewers as well. 🧳 Migration guide: [https://rerun.io/docs/reference/migration/migration-0-28](https://rerun.io/docs/reference/migration/migration-0-28) **Transactional transform behavior (important!):** -Changes to `Transform3D`, `InstancePose3D`, or `Pinhole` transform properties are now treated transactionally. Updating any component resets all other transform components—the viewer no longer looks back in time for previously logged values. If you relied on partial updates (e.g., logging only rotation while keeping a previous translation), you must now re-log all components together. If you always logged the same components on every call or used the standard constructors, no changes are needed. [#11911](https://github.com/rerun-io/rerun/pull/11911) +Changes to `Transform3D`, `InstancePose3D`, or `Pinhole` transform properties are now treated transactionally. Updating any component resets all other transform components — the viewer no longer looks back in time for previously logged values. If you relied on partial updates (e.g., logging only rotation while keeping a previous translation), you must now re-log all components together. If you always logged the same components on every call or used the standard constructors, no changes are needed. [#11911](https://github.com/rerun-io/rerun/pull/11911) ```python rr.log("simple", rr.Transform3D(translation=[1.0, 2.0, 3.0])) -# In 0.27: This clears the translation—it will NOT inherit the previous value +# In 0.27: This clears the translation — it will NOT inherit the previous value rr.log("simple", rr.Transform3D.from_fields(scale=2)) ``` @@ -1009,7 +2019,7 @@ MCAP timelines renamed from `log_time`/`publish_time` to `message_log_time`/`mes - The `rerun_partition_id` column is now `rerun_segment_id` - `entries()`, `datasets()`, `tables()` now return lists of entry objects instead of DataFrames -- `get_table()` returns a `TableEntry` object instead of a DataFrame—use `.reader()` to get the DataFrame +- `get_table()` returns a `TableEntry` object instead of a DataFrame — use `.reader()` to get the DataFrame - `DataframeQueryView` removed; use `filter_segments()`, `filter_contents()`, and `reader()` instead [#12151](https://github.com/rerun-io/rerun/pull/12151) - `register()` and `register_batch()` merged into single `register()` returning `RegistrationHandle` [#12187](https://github.com/rerun-io/rerun/pull/12187) - `search_fts()` and `search_vector()` now return DataFrames directly (no `.df()` needed) [#12198](https://github.com/rerun-io/rerun/pull/12198) @@ -1160,7 +2170,7 @@ MCAP timelines renamed from `log_time`/`publish_time` to `message_log_time`/`mes - Implement the search service [#11954](https://github.com/rerun-io/rerun/pull/11954) - Add `MapProvider::MapboxLight` [#12083](https://github.com/rerun-io/rerun/pull/12083) (thanks [@sectore](https://github.com/sectore)!) - Implement streaming for datafusion table [#12162](https://github.com/rerun-io/rerun/pull/12162) -- Add rerun cloud section to welcome page [#12051](https://github.com/rerun-io/rerun/pull/12051) +- Add Rerun Hub section to welcome page [#12051](https://github.com/rerun-io/rerun/pull/12051) - Add support for server side filtering of DataFusion DataFrames [#12147](https://github.com/rerun-io/rerun/pull/12147) - Fix compaction of recordings containing video streams [35810c74187c250925e958a8f095756915313ce7](https://github.com/rerun-io/rerun/commit/35810c74187c250925e958a8f095756915313ce7) - Python SDK: Add timeout_sec argument to flush [f69d249e5c6bc5225d8f2f0be384243ab9dacf03](https://github.com/rerun-io/rerun/commit/f69d249e5c6bc5225d8f2f0be384243ab9dacf03) @@ -1560,7 +2570,7 @@ See the - Allow opening web viewer links directly [#10928](https://github.com/rerun-io/rerun/pull/10928) - Add keyboard shortcut to copy entity hierarchy [#10938](https://github.com/rerun-io/rerun/pull/10938) - Add H.265 support for native & `VideoStream` [#10994](https://github.com/rerun-io/rerun/pull/10994) -- Support sharing URLs for Data Platform datasets & tables [#11038](https://github.com/rerun-io/rerun/pull/11038) +- Support sharing URLs for catalog server datasets & tables [#11038](https://github.com/rerun-io/rerun/pull/11038) - New open from URL dialog & main menu entry [#11040](https://github.com/rerun-io/rerun/pull/11040) - Add archetypes for MCAP metadata [#11062](https://github.com/rerun-io/rerun/pull/11062) - Add `opacity` setting for `VideoStream` & `VideoAsset` [#11113](https://github.com/rerun-io/rerun/pull/11113) @@ -1592,7 +2602,7 @@ See the - Add share link button to time panel context menu [#11186](https://github.com/rerun-io/rerun/pull/11186) #### 🕸️ Web -- Improve browser navigation for http & Rerun Data Platform links [#10863](https://github.com/rerun-io/rerun/pull/10863) +- Improve browser navigation for http & catalog server links [#10863](https://github.com/rerun-io/rerun/pull/10863) - pixi: Use llvm-ar from PATH on macOS, avoid unexpanded ${PIXI_PROJECT… [#10910](https://github.com/rerun-io/rerun/pull/10910) (thanks [@matildasmeds](https://github.com/matildasmeds)!) #### 🧑‍💻 Dev-experience @@ -1759,7 +2769,7 @@ You can now log URDF files directly to Rerun using the `log_file` API. #### 🐍 Python API - Add `Dataset.register_batch` and wrappers for task ids [#9895](https://github.com/rerun-io/rerun/pull/9895) -- Introduce `ConnectionRegistry` for centralised redap client and token management [#10078](https://github.com/rerun-io/rerun/pull/10078) +- Introduce `ConnectionRegistry` for centralized redap client and token management [#10078](https://github.com/rerun-io/rerun/pull/10078) - Build in `manylinux_2_28` container [#10148](https://github.com/rerun-io/rerun/pull/10148) - Add APIs to Dataset to query and update the associated blueprint [#10156](https://github.com/rerun-io/rerun/pull/10156) - Support for seconds-since-Epoch numpy arrays for constructing `TimeColumn` [#10168](https://github.com/rerun-io/rerun/pull/10168) (thanks [@MichaelGrupp](https://github.com/MichaelGrupp)!) diff --git a/CLAUDE.md b/CLAUDE.md index 74d24d762a82..86722a954fe9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,6 +73,11 @@ To add custom functionality to generated types, create `_ext` files: - validate various custom conventions via `pixi run lint-rerun ` (not passing any file will check everything) - Use `format!("{x}")` over `format!("{}, x)` (same in log calls etc) - Don't write trivial comments that add nothing new +- In error and log messages, put the error first and any file path at the end (e.g. `Failed to import: {err}\nFile path: {path}`), never in the middle. + Paths can be long or sensitive, so trailing placement makes them easy to strip when copy-pasting. +- Prose style (em vs en dash, sentence endings, casing) — see [`DESIGN.md`](DESIGN.md). In short: spaced em dash ` — `, never unspaced `word—word`, and don't use `–` as a sentence dash (it's for numeric ranges only) +- One sentence per line in markdown files. + Markdown joins consecutive lines into a paragraph, so rendering is unchanged — but diffs become much easier to review. ## Architecture overview @@ -88,6 +93,9 @@ crates/ For more details about the architecture see `ARCHITECTURE.md`. +**When adding, removing, or renaming a crate**, update `ARCHITECTURE.md`: +add the crate to the appropriate crate table, and flag for the author that the crate-organization diagram (FigJam) needs a manual update — see the HTML comment next to the diagram in `ARCHITECTURE.md` for instructions. + ### Type system hierarchy The type system has three levels (generated from .fbs files): diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5dfee1e0f4ee..e27ed3ee5dc0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,5 @@ # Contributing to Rerun -This guide is for anyone who wants to contribute to the Rerun repository. - +This guide is for anyone who wants to contribute to the Rerun repository, for employees and outside contributors alike. ## See also * [`ARCHITECTURE.md`](ARCHITECTURE.md) @@ -30,15 +29,49 @@ You can discuss these changes by: > [!NOTE] > PRs containing large undiscussed changes may be closed without comment. +> The same applies to issues and PRs opened by bot accounts (e.g. OpenClaw) or that are clearly agent-generated without human review — see [Agents](#agents). ## Pull requests We use [Trunk Based Development](https://trunkbaseddevelopment.com/), which means we encourage small, short-lived branches. -* Open draft PRs early to get feedback before a full review. * Don't PR from your own `main` branch — it makes it hard for reviewers to add fixes. -* Add improvements as new commits rather than rebasing, so reviewers can follow progress (add images if possible!). +* Add improvements as new commits rather than rebasing, so reviewers can follow progress. * All PRs are merged with [`Squash and Merge`](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits), so you don't need a clean commit history on feature branches. Prefer new commits over rebasing — force-pushing discourages collaboration. +### PR draft +It can be useful to open a PR in _draft_ mode first. On reason is to get CI to run on it. + +Another reason is to ask for early feedback on e.g. the user interaction or the overall design of the PR. +This can be a great way to discuss architectural ideas before doing the full work of implementing it. +If you want such early feedback, ask for it explicitly (e.g. ping someone relevant). + +Do not un-draft until you have read all your code and _you_ think it is ready to merge. + +An un-drafted PR means "ready for review". + +### PR description +- Make sure the PR description is _inviting_ - not too long, not too short +- Write it yourself +- Describe _why_ you made this change (and link to any relevant issue/PR) +- If it makes sense, include an image or a video +- Describe what you want reviewed, e.g. + - The UX — does this feature feel nice to use? + - The architecture / design — explain the proposed design in the PR description, and keep the PR in draft mode + - The code +- Express your own confidence in your work + - Is this a simple fix for something you understand well, or maybe something well outside your domain that an agent wrote for you? + +### Agents +Coding agents are powerful tools, but like any tool should be used wisely. + +If you use an agent to prototype some feature, then the PR should be in draft mode, and you should ask for feedback on the _effect_ of the PR, rather than its contents. + +If you use an agent to implement a solution, then you should be able to understand that solution. +Asking the agent to walk you through the code can help, but doesn't replace reading it yourself. +LLMs make it easy to produce code quickly, while understanding takes longer. +Please disclose the level of confidence that you have in your solution. + +### Other Our CI will [record binary sizes](https://build.rerun.io/graphs/sizes.html) and run [benchmarks](https://build.rerun.io/graphs/crates.html) on each merged PR. Pull requests from external contributors require approval for CI runs. Click the `Approve and run` button: diff --git a/Cargo.lock b/Cargo.lock index 877613d6192e..ab929c668572 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "accesskit" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" dependencies = [ "enumn", "serde", @@ -74,7 +74,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "842fd8203e6dfcf531d24f5bac792088edfba7d6b35844fead191603fb32a260" dependencies = [ "accesskit", - "accesskit_consumer", + "accesskit_consumer 0.35.0", "atspi-common", "phf 0.13.1", "serde", @@ -91,6 +91,16 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "accesskit_consumer" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950720ce064757a1b629caad3a408e8d2c63bb01f29b8a3ff8daa331053ffeb" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + [[package]] name = "accesskit_macos" version = "0.26.0" @@ -98,7 +108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534bc3fdc89a64a1db3c46b33c198fde2b7c3c7d094e5809c8c8bf2970c18243" dependencies = [ "accesskit", - "accesskit_consumer", + "accesskit_consumer 0.35.0", "hashbrown 0.16.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -130,7 +140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" dependencies = [ "accesskit", - "accesskit_consumer", + "accesskit_consumer 0.35.0", "hashbrown 0.16.1", "static_assertions", "windows", @@ -205,6 +215,15 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -218,11 +237,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.11.0", + "bitflags 2.13.0", "cc", "cesu8", - "jni", - "jni-sys", + "jni 0.21.1", + "jni-sys 0.3.0", "libc", "log", "ndk", @@ -255,7 +274,7 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "animated_urdf" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", @@ -265,9 +284,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -286,9 +305,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -315,9 +334,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "approx" @@ -356,9 +375,9 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "argh" -version = "0.1.15" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32c2462e89541e6687e684d97310015d64a0627b61106fc472156a38f61cd1e" +checksum = "211818e820cda9ca6f167a64a5c808837366a6dfd807157c64c1304c486cd033" dependencies = [ "argh_derive", "argh_shared", @@ -366,9 +385,9 @@ dependencies = [ [[package]] name = "argh_derive" -version = "0.1.15" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc2a031b364bd099fed016feb1ccfca2c3549d63c16f330cfc40b27b7692231" +checksum = "c442a9d18cef5dde467405d27d461d080d68972d6d0dfd0408265b6749ec427d" dependencies = [ "argh_shared", "proc-macro2", @@ -378,9 +397,9 @@ dependencies = [ [[package]] name = "argh_shared" -version = "0.1.15" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9abea17ef74821d1d3490aee9e0749d731445d965b7512308b2aa00c90079e" +checksum = "e5ade012bac4db278517a0132c8c10c6427025868dca16c801087c28d5a411f1" dependencies = [ "serde", ] @@ -405,9 +424,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8" +checksum = "378530e55cd479eda3c14eb345310799717e6f76d0c332041e8487022166b471" dependencies = [ "arrow-arith", "arrow-array", @@ -427,9 +446,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b" +checksum = "a0ab212d2c1886e802f51c5212d78ebbcbb0bec980fff9dadc1eb8d45cd0b738" dependencies = [ "arrow-array", "arrow-buffer", @@ -441,9 +460,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +checksum = "cfd33d3e92f207444098c75b42de99d329562be0cf686b307b097cc52b4e999e" dependencies = [ "ahash", "arrow-buffer", @@ -452,7 +471,7 @@ dependencies = [ "chrono", "chrono-tz", "half", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "num-complex", "num-integer", "num-traits", @@ -460,9 +479,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +checksum = "0c6cd424c2693bcdbc150d843dc9d4d137dd2de4782ce6df491ad11a3a0416c0" dependencies = [ "bytes", "half", @@ -472,9 +491,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +checksum = "4c5aefb56a2c02e9e2b30746241058b85f8983f0fcff2ba0c6d09006e1cded7f" dependencies = [ "arrow-array", "arrow-buffer", @@ -494,9 +513,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da746f4180004e3ce7b83c977daf6394d768332349d3d913998b10a120b790a" +checksum = "e94e8cf7e517657a52b91ea1263acf38c4ca62a84655d72458a3359b12ab97de" dependencies = [ "arrow-array", "arrow-cast", @@ -509,9 +528,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +checksum = "3c88210023a2bfee1896af366309a3028fc3bcbd6515fa29a7990ee1baa08ee0" dependencies = [ "arrow-buffer", "arrow-schema", @@ -522,9 +541,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf7df950701ab528bf7c0cf7eeadc0445d03ef5d6ffc151eaae6b38a58feff1" +checksum = "238438f0834483703d88896db6fe5a7138b2230debc31b34c0336c2996e3c64f" dependencies = [ "arrow-array", "arrow-buffer", @@ -532,24 +551,25 @@ dependencies = [ "arrow-schema", "arrow-select", "flatbuffers", - "lz4_flex 0.12.1", + "lz4_flex", "zstd", ] [[package]] name = "arrow-json" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ff8357658bedc49792b13e2e862b80df908171275f8e6e075c460da5ee4bf86" +checksum = "205ca2119e6d679d5c133c6f30e68f027738d95ed948cf77677ea69c7800036b" dependencies = [ "arrow-array", "arrow-buffer", "arrow-cast", - "arrow-data", + "arrow-ord", "arrow-schema", + "arrow-select", "chrono", "half", - "indexmap", + "indexmap 2.14.0", "itoa", "lexical-core", "memchr", @@ -562,9 +582,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +checksum = "1bffd8fd2579286a5d63bac898159873e5094a79009940bcb42bbfce4f19f1d0" dependencies = [ "arrow-array", "arrow-buffer", @@ -575,9 +595,9 @@ dependencies = [ [[package]] name = "arrow-pyarrow" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18c442b4c266aaf3d7f7dd40fd7ae058cef7f113b00ff0cd8256e1e218ec544" +checksum = "d29abdf672a81c1aeb57fd2661457f9918964d49aed0e9f18932535f2a9e49ce" dependencies = [ "arrow-array", "arrow-data", @@ -587,9 +607,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0" +checksum = "bab5994731204603c73ba69267616c50f80780774c6bb0476f1f830625115e0c" dependencies = [ "arrow-array", "arrow-buffer", @@ -600,20 +620,20 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" +checksum = "f633dbfdf39c039ada1bf9e34c694816eb71fbb7dc78f613993b7245e078a1ed" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "serde_core", "serde_json", ] [[package]] name = "arrow-select" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +checksum = "8cd065c54172ac787cf3f2f8d4107e0d3fdc26edba76fdf4f4cc170258942222" dependencies = [ "ahash", "arrow-array", @@ -625,9 +645,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8" +checksum = "29dd7cda3ab9692f43a2e4acc444d760cc17b12bb6d8232ddf64e9bab7c06b42" dependencies = [ "arrow-array", "arrow-buffer", @@ -956,9 +976,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -1045,12 +1065,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "beef" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" - [[package]] name = "bigdecimal" version = "0.4.8" @@ -1081,9 +1095,9 @@ dependencies = [ [[package]] name = "binrw" -version = "0.12.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8318fda24dc135cdd838f57a2b5ccb6e8f04ff6b6c65528c4bd9b5fcdc5cf6" +checksum = "d53195f985e88ab94d1cc87e80049dd2929fd39e4a772c5ae96a7e5c4aad3642" dependencies = [ "array-init", "binrw_derive", @@ -1092,15 +1106,15 @@ dependencies = [ [[package]] name = "binrw_derive" -version = "0.12.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0832bed83248115532dfb25af54fae1c83d67a2e4e3e2f591c13062e372e7e" +checksum = "5910da05ee556b789032c8ff5a61fb99239580aa3fd0bfaa8f4d094b2aee00ad" dependencies = [ "either", "owo-colors", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -1129,9 +1143,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "bytemuck", "serde_core", @@ -1228,7 +1242,7 @@ dependencies = [ [[package]] name = "blueprint" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "clap", "rerun", @@ -1236,7 +1250,7 @@ dependencies = [ [[package]] name = "blueprint_stocks" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "chrono", @@ -1247,31 +1261,6 @@ dependencies = [ "strum", ] -[[package]] -name = "bon" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2529c31017402be841eb45892278a6c21a000c0a17643af326c73a73f83f0fb" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82020dadcb845a345591863adb65d74fa8dc5c18a0b6d408470e13b7adc7005" -dependencies = [ - "darling", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.117", -] - [[package]] name = "brotli" version = "8.0.2" @@ -1293,6 +1282,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.0" @@ -1350,9 +1348,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bytestring" @@ -1396,7 +1394,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "log", "polling", "rustix 0.38.44", @@ -1418,9 +1416,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" dependencies = [ "serde_core", ] @@ -1478,27 +1476,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cdr-encoding" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f0cdb643d85bb03b1b786ab8d332747f386f0a25b19367d559b437f44f1e46" -dependencies = [ - "byteorder", - "log", - "paste", - "serde", - "serde_repr", - "static_assertions", - "thiserror 1.0.69", -] - -[[package]] -name = "census" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" - [[package]] name = "cesu8" version = "1.1.0" @@ -1539,9 +1516,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1605,9 +1582,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -1615,9 +1592,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -1627,9 +1604,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -1660,7 +1637,7 @@ dependencies = [ [[package]] name = "clock" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", @@ -1680,9 +1657,9 @@ dependencies = [ [[package]] name = "color" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" dependencies = [ "bytemuck", ] @@ -1806,11 +1783,12 @@ dependencies = [ [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -1880,7 +1858,7 @@ dependencies = [ "cookie", "document-features", "idna", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -1974,25 +1952,24 @@ dependencies = [ [[package]] name = "criterion" -version = "0.5.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ + "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", - "itertools 0.10.5", + "itertools 0.13.0", "num-traits", - "once_cell", "oorandom", + "page_size", "plotters", "rayon", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -2000,12 +1977,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools 0.10.5", + "itertools 0.13.0", ] [[package]] @@ -2051,9 +2028,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -2089,7 +2066,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "crossterm_winapi", "document-features", "parking_lot", @@ -2151,7 +2128,7 @@ checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] name = "custom_callback" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "bincode", "mimalloc", @@ -2163,7 +2140,7 @@ dependencies = [ [[package]] name = "custom_importer" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "crossbeam", "re_build_tools", @@ -2173,7 +2150,7 @@ dependencies = [ [[package]] name = "custom_store_subscriber" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "re_build_tools", "rerun", @@ -2181,7 +2158,7 @@ dependencies = [ [[package]] name = "custom_view" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "mimalloc", "rerun", @@ -2189,7 +2166,7 @@ dependencies = [ [[package]] name = "custom_visualizer" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "bytemuck", "mimalloc", @@ -2214,8 +2191,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -2225,6 +2212,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ "ident_case", "proc-macro2", "quote", @@ -2238,7 +2237,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -2259,9 +2269,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "data-url" @@ -2271,7 +2281,7 @@ checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" [[package]] name = "dataframe_query" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "itertools 0.14.0", "rerun", @@ -2280,9 +2290,9 @@ dependencies = [ [[package]] name = "datafusion" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7541353e77dc7262b71ca27be07d8393661737e3a73b5d1b1c6f7d814c64fa2a" +checksum = "93db0e623840612f7f2cd757f7e8a8922064192363732c88692e0870016e141b" dependencies = [ "arrow", "arrow-schema", @@ -2318,7 +2328,7 @@ dependencies = [ "log", "object_store", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "regex", "sqlparser", "tempfile", @@ -2329,9 +2339,9 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9997731f90fa5398ef831ad0e69600f92c861b79c0d38bd1a29b6f0e3a0ce4c8" +checksum = "37cefde60b26a7f4ff61e9d2ff2833322f91df2b568d7238afe67bde5bdffb66" dependencies = [ "arrow", "async-trait", @@ -2354,9 +2364,9 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b30a3dd50dec860c9559275c8d97d9de602e611237a6ecfbda0b3b63b872352" +checksum = "17e112307715d6a7a331111a4c2330ff54bc237183511c319e3708a4cff431fb" dependencies = [ "arrow", "async-trait", @@ -2377,9 +2387,9 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d551054acec0398ca604512310b77ce05c46f66e54b54d48200a686e385cca4e" +checksum = "d72a11ca44a95e1081870d3abb80c717496e8a7acb467a1d3e932bb636af5cc2" dependencies = [ "ahash", "arrow", @@ -2387,7 +2397,8 @@ dependencies = [ "chrono", "half", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.14.0", + "itertools 0.14.0", "libc", "log", "object_store", @@ -2400,9 +2411,9 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567d40e285f5b79f8737b576605721cd6c1133b5d2b00bdbd5d9838d90d0812f" +checksum = "89f4afaed29670ec4fd6053643adc749fe3f4bc9d1ce1b8c5679b22c67d12def" dependencies = [ "futures", "log", @@ -2411,9 +2422,9 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27d2668f51b3b30befae2207472569e37807fdedd1d14da58acc6f8ca6257eae" +checksum = "e9fb386e1691355355a96419978a0022b7947b44d4a24a6ea99f00b6b485cbb6" dependencies = [ "arrow", "async-trait", @@ -2433,16 +2444,16 @@ dependencies = [ "itertools 0.14.0", "log", "object_store", - "rand 0.9.3", + "rand 0.9.4", "tokio", "url", ] [[package]] name = "datafusion-datasource-arrow" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02e1b3e3a8ec55f1f62de4252b0407c8567363d056078769a197e24fc834a0f" +checksum = "ffa6c52cfed0734c5f93754d1c0175f558175248bf686c944fb05c373e5fc096" dependencies = [ "arrow", "arrow-ipc", @@ -2464,9 +2475,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b559d7bf87d4f900f847baba8509634f838d9718695389e903604cdcccdb01f3" +checksum = "503f29e0582c1fc189578d665ff57d9300da1f80c282777d7eb67bb79fb8cdca" dependencies = [ "arrow", "async-trait", @@ -2487,9 +2498,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250e2d7591ba8b638f063854650faa40bca4e8bd4059b2ece8836f6388d02db4" +checksum = "e33804749abc8d0c8cb7473228483cb8070e524c6f6086ee1b85a64debe2b3d2" dependencies = [ "arrow", "async-trait", @@ -2504,14 +2515,16 @@ dependencies = [ "datafusion-session", "futures", "object_store", + "serde_json", "tokio", + "tokio-stream", ] [[package]] name = "datafusion-datasource-parquet" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b043149f2c3557ca94abc58de40f68a8d412ff53365c06126ed234f8596399d" +checksum = "32a8e0365e0e08e8ff94d912f0ababcf9065a1a304018ba90b1fc83c855b4997" dependencies = [ "arrow", "async-trait", @@ -2539,36 +2552,38 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9496cb0db222dbb9a3735760ceca7fc56f35e1d5502c38d0caa77a81e9c1f6a" +checksum = "8de6ac0df1662b9148ad3c987978b32cbec7c772f199b1d53520c8fa764a87ee" [[package]] name = "datafusion-execution" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc45d23c516ed8d3637751e44e09e21b45b3f58b473c802dddd1f1ad4fe435ff" +checksum = "c03c7fbdaefcca4ef6ffe425a5fc2325763bfb426599bb0bf4536466efabe709" dependencies = [ "arrow", + "arrow-buffer", "async-trait", "chrono", "dashmap", "datafusion-common", "datafusion-expr", + "datafusion-physical-expr-common", "futures", "log", "object_store", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "tempfile", "url", ] [[package]] name = "datafusion-expr" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63dd30526d2db4fda6440806a41e4676334a94bc0596cc9cc2a0efed20ef2c44" +checksum = "574b9b6977fedbd2a611cbff12e5caf90f31640ad9dc5870f152836d94bad0dd" dependencies = [ "arrow", "async-trait", @@ -2579,7 +2594,7 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-functions-window-common", "datafusion-physical-expr-common", - "indexmap", + "indexmap 2.14.0", "itertools 0.14.0", "paste", "serde_json", @@ -2588,22 +2603,22 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b486b5f6255d40976b88bb83813b0d035a8333e0ec39864824e78068cf42fa6" +checksum = "7d7c3adf3db8bf61e92eb90cb659c8e8b734593a8f7c8e12a843c7ddba24b87e" dependencies = [ "arrow", "datafusion-common", - "indexmap", + "indexmap 2.14.0", "itertools 0.14.0", "paste", ] [[package]] name = "datafusion-ffi" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b04181cffefd632e57acfc233ed239626863682dd8bb30ab366293f441bba8" +checksum = "b95173344d04ba62755c949bf44f8d1a6e4414cf6392a635db96c07e711b9a3c" dependencies = [ "abi_stable", "arrow", @@ -2631,9 +2646,9 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07356c94118d881130dd0ffbff127540407d969c8978736e324edcd6c41cd48f" +checksum = "f28aa4e10384e782774b10e72aca4d93ef7b31aa653095d9d4536b0a3dbc51b6" dependencies = [ "arrow", "arrow-buffer", @@ -2652,8 +2667,9 @@ dependencies = [ "itertools 0.14.0", "log", "md-5", + "memchr", "num-traits", - "rand 0.9.3", + "rand 0.9.4", "regex", "sha2", "unicode-segmentation", @@ -2662,9 +2678,9 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b644f9cf696df9233ce6958b9807666d78563b56f923267474dd6c07795f1f8f" +checksum = "00aa6217e56098ba84e0a338176fe52f0a84cca398021512c6c8c5eff806d0ad" dependencies = [ "ahash", "arrow", @@ -2678,14 +2694,15 @@ dependencies = [ "datafusion-physical-expr-common", "half", "log", + "num-traits", "paste", ] [[package]] name = "datafusion-functions-aggregate-common" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1de2deaaabe8923ce9ea9f29c47bbb4ee14f67ea2fe1ab5398d9bbebcf86e56" +checksum = "b511250349407db7c43832ab2de63f5557b19a20dfd236b39ca2c04468b50d47" dependencies = [ "ahash", "arrow", @@ -2696,9 +2713,9 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552f8d92e4331ee91d23c02d12bb6acf32cbfd5215117e01c0fb63cd4b15af1a" +checksum = "ef13a858e20d50f0a9bb5e96e7ac82b4e7597f247515bccca4fdd2992df0212a" dependencies = [ "arrow", "arrow-ord", @@ -2712,16 +2729,18 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-macros", "datafusion-physical-expr-common", + "hashbrown 0.16.1", "itertools 0.14.0", + "itoa", "log", "paste", ] [[package]] name = "datafusion-functions-table" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970fd0cdd3df8802b9a9975ff600998289ba9d46682a4f7285cba4820c9ada78" +checksum = "72b40d3f5bbb3905f9ccb1ce9485a9595c77b69758a7c24d3ba79e334ff51e7e" dependencies = [ "arrow", "async-trait", @@ -2735,9 +2754,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b4c21a7c8a986a1866c0a87ab756d0bbf7b5f41f306009fa2d9af79c52ed31" +checksum = "d4e88ec9d57c9b685d02f58bfee7be62d72610430ddcedb82a08e5d9925dbfb6" dependencies = [ "arrow", "datafusion-common", @@ -2753,9 +2772,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1210ad73b8b3211aeaf4a42bef9bd7a2b7fce3ec119a478831f18c6ff7f7b93" +checksum = "8307bb93519b1a91913723a1130cfafeee3f72200d870d88e91a6fc5470ede5c" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -2763,9 +2782,9 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa566a963013a38681ad82a727a654bc7feb19632426aea8c3412d415d200c5" +checksum = "2e367e6a71051d0ebdd29b2f85d12059b38b1d1f172c6906e80016da662226bd" dependencies = [ "datafusion-doc", "quote", @@ -2774,9 +2793,9 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9aa82b240252a88dee118372f9b9757c545ab9e53c0736bebab2e7da0ef1f2" +checksum = "e929015451a67f77d9d8b727b2bf3a40c4445fdef6cdc53281d7d97c76888ace" dependencies = [ "arrow", "chrono", @@ -2784,7 +2803,7 @@ dependencies = [ "datafusion-expr", "datafusion-expr-common", "datafusion-physical-expr", - "indexmap", + "indexmap 2.14.0", "itertools 0.14.0", "log", "regex", @@ -2793,9 +2812,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d48022b8af9988c1d852644f9e8b5584c490659769a550c5e8d39457a1da0a5" +checksum = "4b1e68aba7a4b350401cfdf25a3d6f989ad898a7410164afe9ca52080244cb59" dependencies = [ "ahash", "arrow", @@ -2806,7 +2825,7 @@ dependencies = [ "datafusion-physical-expr-common", "half", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.14.0", "itertools 0.14.0", "parking_lot", "paste", @@ -2816,9 +2835,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae7a8abc0b4fe624000972a9b145b30b7f1b680bffaa950ea53f78d9b21c27c3" +checksum = "ea22315f33cf2e0adc104e8ec42e285f6ed93998d565c65e82fec6a9ee9f9db4" dependencies = [ "arrow", "datafusion-common", @@ -2831,9 +2850,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147253ca3e6b9d59c162de64c02800973018660e13340dd1886dd038d17ac429" +checksum = "b04b45ea8ad3ac2d78f2ea2a76053e06591c9629c7a603eda16c10649ecf4362" dependencies = [ "ahash", "arrow", @@ -2841,16 +2860,16 @@ dependencies = [ "datafusion-common", "datafusion-expr-common", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.14.0", "itertools 0.14.0", "parking_lot", ] [[package]] name = "datafusion-physical-optimizer" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "689156bb2282107b6239db8d7ef44b4dab10a9b33d3491a0c74acac5e4fedd72" +checksum = "7cb13397809a425918f608dfe8653f332015a3e330004ab191b4404187238b95" dependencies = [ "arrow", "datafusion-common", @@ -2866,9 +2885,9 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68253dc0ee5330aa558b2549c9b0da5af9fc17d753ae73022939014ad616fc28" +checksum = "5edc023675791af9d5fb4cc4c24abf5f7bd3bd4dcf9e5bd90ea1eff6976dcc79" dependencies = [ "ahash", "arrow", @@ -2887,9 +2906,10 @@ dependencies = [ "futures", "half", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.14.0", "itertools 0.14.0", "log", + "num-traits", "parking_lot", "pin-project-lite", "tokio", @@ -2897,9 +2917,9 @@ dependencies = [ [[package]] name = "datafusion-proto" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f5ab57d0b5a368258fff1d828f1619a10541fa5c4ec4930a383deb3a23204c8" +checksum = "6a387aaef949dc16bb6abc81bd1af850ec7449183aef011214f9724957495738" dependencies = [ "arrow", "chrono", @@ -2920,13 +2940,14 @@ dependencies = [ "datafusion-proto-common", "object_store", "prost", + "rand 0.9.4", ] [[package]] name = "datafusion-proto-common" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd21d2c804802ca4b1719191dfe8e3d0860686649de6375ddc9237f85beb82b3" +checksum = "16e614c7c53a9c304c6a850b821010bb492e57300311835f1180613f9d2c63d9" dependencies = [ "arrow", "datafusion-common", @@ -2935,9 +2956,9 @@ dependencies = [ [[package]] name = "datafusion-pruning" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcad240a54d0b1d3e8f668398900260a53122d522b2102ab57218590decacd6" +checksum = "ac8c76860e355616555081cab5968cec1af7a80701ff374510860bcd567e365a" dependencies = [ "arrow", "datafusion-common", @@ -2952,9 +2973,9 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e83a68bb67007a8fcbf005c44cefe441270c7ee7f6dee10c0e0109b556f6d" +checksum = "5412111aa48e2424ba926112e192f7a6b7e4ccb450145d25ce5ede9f19dc491e" dependencies = [ "async-trait", "datafusion-common", @@ -2966,39 +2987,52 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "52.5.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be53e9eb55db0fbb8980bb6d87f2435b0524acf4c718ed54a57cabbb299b2ab3" +checksum = "fa0d133ddf8b9b3b872acac900157f783e7b879fe9a6bccf389abebbfac45ec1" dependencies = [ "arrow", "bigdecimal", "chrono", "datafusion-common", "datafusion-expr", - "indexmap", + "datafusion-functions-nested", + "indexmap 2.14.0", "log", "regex", "sqlparser", ] [[package]] -name = "deepsize" -version = "0.2.0" +name = "defmt" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb987ec36f6bf7bfbea3f928b75590b736fc42af8e54d97592481351b2b96c" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" dependencies = [ - "deepsize_derive", + "bitflags 1.3.2", + "defmt-macros", ] [[package]] -name = "deepsize_derive" -version = "0.1.2" +name = "defmt-macros" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990101d41f3bc8c1a45641024377ee284ecc338e5ecf3ea0f0e236d897c72796" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" dependencies = [ + "defmt-parser", + "proc-macro-error2", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", ] [[package]] @@ -3043,7 +3077,7 @@ dependencies = [ [[package]] name = "dimos-viewer" -version = "0.32.0-alpha.2" +version = "0.35.0-alpha.1" dependencies = [ "bincode", "clap", @@ -3098,7 +3132,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -3126,10 +3160,10 @@ dependencies = [ [[package]] name = "dna" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "itertools 0.14.0", - "rand 0.9.3", + "rand 0.9.4", "rerun", ] @@ -3148,12 +3182,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - [[package]] name = "dpi" version = "0.1.2" @@ -3166,11 +3194,17 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecolor" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb993f120d46ca077a18f166175b94c6edeb994d812cbd07a4a03cfced2713c" +checksum = "6758be723a3f298bbfda4db75748bc2ba0abafe096b6383c7c32da264764fbc3" dependencies = [ "bytemuck", "color-hex", @@ -3186,9 +3220,9 @@ checksum = "18aade80d5e09429040243ce1143ddc08a92d7a22820ac512610410a4dd5214f" [[package]] name = "eframe" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd6562766ec6b921232ceb960acdb405f91971f59c17604840fd9485fee0dc3" +checksum = "8dc8234e2f681b2afd2b2e8c332fa614de2fddbdcb28d0acf5c420449682ea90" dependencies = [ "ahash", "bytemuck", @@ -3197,6 +3231,7 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", + "egui_inspection", "glutin", "glutin-winit", "home", @@ -3225,16 +3260,17 @@ dependencies = [ [[package]] name = "egui" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b71aaacbe69e214aebf7b8d5eacd6256d0d2ca5ff9525c28d1dd7f377f430a9" +checksum = "2796c98d50b79631281d516343a6f6e93c0666462ca36e2c93b39f25d7793325" dependencies = [ "accesskit", "ahash", "backtrace", - "bitflags 2.11.0", + "bitflags 2.13.0", "emath", "epaint", + "itertools 0.14.0", "log", "nohash-hasher", "profiling", @@ -3246,9 +3282,9 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111e54ac54bf9d767ce8970a454f720878875bca1f2e63f781c722e98f466971" +checksum = "d2e6cfac0725563555fa4f91e9f799b9d7c6c5dd831fca6abc8234afc64b7a34" dependencies = [ "ahash", "bytemuck", @@ -3266,9 +3302,9 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6af7dad7dd045b77731fe6dc291b835489a104d3cd2428a945e71e2265df3" +checksum = "9ea6bf3608db949588b95b8b341ee358d0c3f95cf4dc3f53d8d76717edee87db" dependencies = [ "accesskit_winit", "arboard", @@ -3289,9 +3325,9 @@ dependencies = [ [[package]] name = "egui_animation" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd9bc6d586df44e01b90715eec1eb8d73a61c3c3f554edffb01eb0894a8107ef" +checksum = "d4434e99040717912350c108ffe543ef68d34147691523fb726ae99adf0935b4" dependencies = [ "egui", "hello_egui_utils", @@ -3300,9 +3336,9 @@ dependencies = [ [[package]] name = "egui_commonmark" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55df531ff51161b3c6212e0ee2166b370f150254bf4448a8a15c3d26fec87958" +checksum = "c833127228cc61cef806166adadc23572431fe2ea4cf753e60f654f82a2b3270" dependencies = [ "egui", "egui_commonmark_backend", @@ -3312,9 +3348,9 @@ dependencies = [ [[package]] name = "egui_commonmark_backend" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe04399ca5a2196965833a2918e50400449721fd9350e31ae7d84d6690859437" +checksum = "5758a1110eb8259743c9b220241c30ff014b2dec53c1a2b861d464e41fa3483e" dependencies = [ "egui", "egui_extras", @@ -3323,9 +3359,9 @@ dependencies = [ [[package]] name = "egui_dnd" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51a348b3fdbc048c4241aaa2865255e1fdebbc0099324ded8c5b534e598e600c" +checksum = "b1e40c74727cb2a7db02712604dbd293bb2a1dc8fa461e901612d280678fe56c" dependencies = [ "egui", "egui_animation", @@ -3335,15 +3371,16 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8844ef47969c00cc186e06e9721ac952758c0e5cd5f579468037f19c7f49566d" +checksum = "e2bd33be7338367bf21f54d62e069d58a5fb3ae033fa8bc6df7a77b9ef4cf957" dependencies = [ "ahash", "egui", "ehttp", "enum-map", "image", + "itertools 0.14.0", "log", "mime_guess2", "profiling", @@ -3353,9 +3390,9 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c5dea15932abd0a953fefc8d8490002204808a2e087147aac48608eea9ef79" +checksum = "52274b9bfb8d8e252cd0f00c9f6214f360d75a0962baa77a2d62ddb002fe99d4" dependencies = [ "bytemuck", "egui", @@ -3363,16 +3400,28 @@ dependencies = [ "log", "memoffset", "profiling", - "wasm-bindgen", - "web-sys", "winit", ] +[[package]] +name = "egui_inspection" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16f113bb82ab665fce1af4da8941412e7991c9245c043bf1e14412089253735" +dependencies = [ + "egui", + "image", + "log", + "rmp-serde", + "serde", + "serde_bytes", +] + [[package]] name = "egui_kittest" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1077ec995dbc754f22afcca9bbab1329071737d749a953e7b430d9b825d40c32" +checksum = "002c31e1f41461ce206333ffbd2e07563b79e87eb71c27e026151d12ee7b78e0" dependencies = [ "dify", "eframe", @@ -3380,6 +3429,7 @@ dependencies = [ "egui-wgpu", "image", "kittest", + "log", "open", "pollster", "serde", @@ -3388,11 +3438,30 @@ dependencies = [ "wgpu", ] +[[package]] +name = "egui_mcp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5cd8ea7142ea6a93fdaac2ba44b34f93352e0fa2800719d5d55389947c2e31" +dependencies = [ + "accesskit", + "accesskit_consumer 0.37.0", + "base64 0.22.1", + "egui", + "egui_inspection", + "rmcp", + "schemars 1.2.1", + "serde", + "serde_json", + "tokio", + "tracing-subscriber", +] + [[package]] name = "egui_plot" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bd66213736bf9a9a53dc4888570b9194fc0db906507517a7fcc787e888ac47" +checksum = "302515219c63e7f380058ecc899c5a6b2c5fe9f140ef1d91c3b685c9f0355fa3" dependencies = [ "ahash", "egui", @@ -3401,9 +3470,9 @@ dependencies = [ [[package]] name = "egui_table" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8512decdd471a2b6106d0b42cc0662f0e94b0ca8f21bc1b0075f455f58901010" +checksum = "243d2531d1008c778a5b78d5b46858f70e78229d518885824af40fa673e44a63" dependencies = [ "egui", "serde", @@ -3412,9 +3481,9 @@ dependencies = [ [[package]] name = "egui_tiles" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08e570b77f6cce3292eba4aee9b9c08cf11dfc68430f4dc9613d939628498647" +checksum = "9eb8fef6130bd04fcb7bb3584845605e57c56fed249bc3ca5a568e696cc0a174" dependencies = [ "ahash", "egui", @@ -3450,9 +3519,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "emath" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8490360fc83eb3a2b20aba120457f7d91c3042d7c628e4f90b43a97b6adf1255" +checksum = "cd4ec073c9898516584d8c6cfdcee95b530b3d941cd5031ef4050aa36812308b" dependencies = [ "bytemuck", "serde", @@ -3534,20 +3603,20 @@ dependencies = [ [[package]] name = "enumset" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.117", @@ -3555,31 +3624,18 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" -dependencies = [ - "log", -] - -[[package]] -name = "env_logger" -version = "0.11.9" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", "log", ] [[package]] name = "epaint" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76f9fdbf72eaf0dc3198d90e7bff267b45866f660f8cf49caeb9dcab93342e0" +checksum = "4e60a8888b51da911df23918fd7301359b1d43a406a0ff3b8863af093dd7fc6c" dependencies = [ "ahash", "bytemuck", @@ -3587,6 +3643,7 @@ dependencies = [ "emath", "epaint_default_fonts", "font-types", + "harfrust", "log", "nohash-hasher", "parking_lot", @@ -3596,14 +3653,16 @@ dependencies = [ "serde", "skrifa", "smallvec", + "unicode-general-category", + "unicode-segmentation", "vello_cpu", ] [[package]] name = "epaint_default_fonts" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1a65f42685d25419a67fd3175e11dfecdd268b0478aaba76785b0a896884e0" +checksum = "13ee4e1f553a3584c301f3a56ff1a775f1384781396cea301c8d952e9b93f560" [[package]] name = "equivalent" @@ -3629,15 +3688,15 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "ethnum" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "euclid" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ "num-traits", ] @@ -3665,7 +3724,7 @@ dependencies = [ [[package]] name = "extend_viewer_ui" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "mimalloc", "rerun", @@ -3683,12 +3742,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" -[[package]] -name = "fastdivide" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" - [[package]] name = "fastrand" version = "2.3.0" @@ -3706,18 +3759,15 @@ dependencies = [ [[package]] name = "fearless_simd" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" -dependencies = [ - "bytemuck", -] +checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" [[package]] name = "ffmpeg-sidecar" -version = "2.4.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f076483fb6efcf02e4abcf3e9388d30123346f85b9a96e8fe834718951b945ed" +checksum = "126522985748cb6a56a966037c3d86d94414be7a01d7749eccf71f7290f6eacd" dependencies = [ "anyhow", ] @@ -3765,7 +3815,7 @@ version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "rustc_version", ] @@ -3786,15 +3836,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits", -] - [[package]] name = "fnv" version = "1.0.7" @@ -3815,9 +3856,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.11.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" dependencies = [ "bytemuck", "serde", @@ -3865,16 +3906,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "795cbfc56d419a7ce47ccbb7504dd9a5b7c484c083c356e797de08bd988d9629" -[[package]] -name = "fs4" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" -dependencies = [ - "rustix 0.38.44", - "windows-sys 0.52.0", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -3886,12 +3917,12 @@ dependencies = [ [[package]] name = "fsst" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae4126c38f86d37d5479295c135a1b81688b6c799d6c39d44b1855f9a0e712c" +checksum = "7af6e24ed12cf382082d5a7f365df2b8e3d6b1518f615d54c85165e9b9718250" dependencies = [ "arrow-array", - "rand 0.9.3", + "rand 0.9.4", ] [[package]] @@ -4114,6 +4145,40 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gilrs" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa85c2e35dc565c90511917897ea4eae16b77f2773d5223536f7b602536d462" +dependencies = [ + "fnv", + "gilrs-core", + "log", + "uuid", + "vec_map", +] + +[[package]] +name = "gilrs-core" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23f2cc5144060a7f8d9e02d3fce5d06705376568256a509cdbc3c24d47e4f04" +dependencies = [ + "inotify", + "js-sys", + "libc", + "libudev-sys", + "log", + "nix", + "objc2-core-foundation", + "objc2-io-kit", + "uuid", + "vec_map", + "wasm-bindgen", + "web-sys", + "windows", +] + [[package]] name = "gimli" version = "0.32.3" @@ -4121,7 +4186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", - "indexmap", + "indexmap 2.14.0", "stable_deref_trait", ] @@ -4146,6 +4211,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "glifo" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +dependencies = [ + "bytemuck", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "log", + "peniko", + "skrifa", + "smallvec", + "vello_common", +] + [[package]] name = "glob" version = "0.3.3" @@ -4222,7 +4303,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "cfg_aliases", "cgl", "dispatch2", @@ -4302,7 +4383,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -4313,19 +4394,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", ] [[package]] name = "graph_lattice" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", - "clap", "itertools 0.14.0", "rerun", ] +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + [[package]] name = "h2" version = "0.4.12" @@ -4338,7 +4427,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -4371,6 +4460,24 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "harfrust" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431e8e389aa0f1e72bb9d1c2db8957a1a7a3580e8ed97db819c14837aac9b3e" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "read-fonts", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -4383,8 +4490,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -4401,6 +4506,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hdf5-pure" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c36cb09ddc5a5a0d10680cf7fe4b82173b1d15c2b2277b868005add93186682" +dependencies = [ + "byteorder", + "flate2", +] + [[package]] name = "heck" version = "0.5.0" @@ -4409,9 +4535,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hello_egui_utils" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34bfd8bff6f6df43b0b73ed7949a7aff0c98c2c1bd4c2f2771f5f2f6d98ced0" +checksum = "7e31f814d47d37c03981b24094192586560fbe490c0b4ae94e808bdf076610b6" dependencies = [ "concat-idents", "egui", @@ -4470,17 +4596,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "htmlescape" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" - [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -4638,14 +4758,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -4654,7 +4773,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -4695,37 +4814,59 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", ] +[[package]] +name = "icu_locale" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -4736,42 +4877,40 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", + "serde", "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -4779,6 +4918,27 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_segmenter" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0794db0b1a86193ac9c48768d0e6c52c54448e0870ad87907d456ee0dac964" +dependencies = [ + "icu_collections", + "icu_locale", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a2c462a4d927d512f5f882a033ddd62f33a05bb9f230d98f736ac3dc85938f" + [[package]] name = "id-arena" version = "2.3.0" @@ -4838,11 +4998,11 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "incremental_logging" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", - "rand 0.9.3", + "rand 0.9.4", "rerun", ] @@ -4854,12 +5014,23 @@ checksum = "d9f1a0777d972970f204fdf8ef319f1f4f8459131636d7e3c96c5d59570d0fa6" [[package]] name = "indexmap" -version = "2.13.0" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -4877,17 +5048,11 @@ dependencies = [ "web-time", ] -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "infer" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" dependencies = [ "cfb", ] @@ -4904,7 +5069,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "inotify-sys", "libc", ] @@ -4920,11 +5085,11 @@ dependencies = [ [[package]] name = "insta" -version = "1.46.3" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ - "console 0.15.11", + "console 0.16.1", "globset", "once_cell", "pest", @@ -4932,6 +5097,7 @@ dependencies = [ "regex", "serde", "similar", + "strip-ansi-escapes", "tempfile", "walkdir", ] @@ -4943,20 +5109,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] -name = "ipnet" -version = "2.11.0" +name = "io-uring" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "libc", +] [[package]] -name = "iri-string" -version = "0.7.8" +name = "ipnet" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-docker" @@ -4967,17 +5134,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -4994,15 +5150,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -5029,10 +5176,11 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46" dependencies = [ + "defmt", "jiff-static", "jiff-tzdb-platform", "js-sys", @@ -5041,14 +5189,14 @@ dependencies = [ "portable-atomic-util", "serde_core", "wasm-bindgen", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f" dependencies = [ "proc-macro2", "quote", @@ -5079,19 +5227,68 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -5131,10 +5328,10 @@ dependencies = [ "fast-float2", "itoa", "jiff", - "nom 8.0.0", + "nom", "num-traits", - "ordered-float 5.1.0", - "rand 0.9.3", + "ordered-float 5.3.0", + "rand 0.9.4", "ryu", "serde", "serde_json", @@ -5142,9 +5339,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "base64 0.22.1", "getrandom 0.2.17", @@ -5152,6 +5349,7 @@ dependencies = [ "serde", "serde_json", "signature", + "zeroize", ] [[package]] @@ -5178,9 +5376,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5" dependencies = [ "accesskit", - "accesskit_consumer", + "accesskit_consumer 0.35.0", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "kqueue" version = "1.1.1" @@ -5213,25 +5426,28 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" dependencies = [ "arrayvec", "euclid", + "polycool", "smallvec", ] [[package]] name = "lance" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c38e7c7c448a77203b50c05f5bf308cadb3fe074e7dde22e4302206ea44d7a" +checksum = "e2122e9f5f5f4b38bb9f0c4991c8d5171696402b3cc6187885c8385875f6df2e" dependencies = [ + "arc-swap", "arrow", "arrow-arith", "arrow-array", "arrow-buffer", + "arrow-cast", "arrow-ipc", "arrow-ord", "arrow-row", @@ -5240,9 +5456,11 @@ dependencies = [ "async-recursion", "async-trait", "async_cell", + "bitpacking", "byteorder", "bytes", "chrono", + "crossbeam-queue", "crossbeam-skiplist", "dashmap", "datafusion", @@ -5250,8 +5468,8 @@ dependencies = [ "datafusion-functions", "datafusion-physical-expr", "datafusion-physical-plan", - "deepsize", "either", + "fst", "futures", "half", "humantime", @@ -5265,21 +5483,25 @@ dependencies = [ "lance-io", "lance-linalg", "lance-namespace", + "lance-select", "lance-table", + "lance-tokenizer", "log", "moka", "object_store", "permutation", "pin-project", "prost", + "prost-build", "prost-types", - "rand 0.9.3", + "rand 0.9.4", + "rayon", "roaring", + "rustc-hash 2.1.1", "semver", "serde", "serde_json", "snafu", - "tantivy", "tokio", "tokio-stream", "tokio-util", @@ -5290,14 +5512,14 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "174bae71821e5535a594f9ecd64b07e6ffe729498a5d27a0ca926b4ff2714664" +checksum = "95f45fb7e0822cc0233d686222ab451f6ec58879620029e0b7bae8192c2f7c7e" dependencies = [ "arrow-array", "arrow-buffer", - "arrow-cast", "arrow-data", + "arrow-ipc", "arrow-ord", "arrow-schema", "arrow-select", @@ -5307,14 +5529,41 @@ dependencies = [ "half", "jsonb", "num-traits", - "rand 0.9.3", + "rand 0.9.4", +] + +[[package]] +name = "lance-arrow-scalar" +version = "58.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771f68b04b47f3addf781116f65061808de94b05e1e9411c23c18f32d14ebe79" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-row", + "arrow-schema", + "half", +] + +[[package]] +name = "lance-arrow-stats" +version = "58.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd47ec33c90bf29f688fd02118e37d3a5ad5c339caa3163f89e417dc0867001f" +dependencies = [ + "arrow-array", + "arrow-schema", + "half", + "lance-arrow-scalar", ] [[package]] name = "lance-bitpacking" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4494187b4244fa56c8cf911d7358e5322fa1cf7d8f6a213b3155a4139eb556b1" +checksum = "c56c21a39860ca7bae712b4e24b9fd066029c570a72428a579faf697de5b8822" dependencies = [ "arrayref", "paste", @@ -5323,32 +5572,32 @@ dependencies = [ [[package]] name = "lance-core" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0a5d4427c42f7d9302771bb2aa474b09316a8919633abad0030c4d58013e89" +checksum = "4ccc7b0b61c11bdb74f929111214553b36b1ee43c88445edda17bc9a2bda75e9" dependencies = [ "arrow-array", "arrow-buffer", + "arrow-data", "arrow-schema", "async-trait", "byteorder", "bytes", - "chrono", "datafusion-common", "datafusion-sql", - "deepsize", "futures", "itertools 0.13.0", "lance-arrow", + "lance-derive", "libc", + "libm", "log", - "mock_instant", "moka", "num_cpus", "object_store", "pin-project", "prost", - "rand 0.9.3", + "rand 0.9.4", "roaring", "serde_json", "snafu", @@ -5357,18 +5606,20 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", + "twox-hash", "url", ] [[package]] name = "lance-datafusion" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c6151ee46a35886ac6c804f870de2992c36de4198da1c64d806529f246d2d7" +checksum = "f0932140306faa6c3c4e17f0478c031e2be659ea2721311a530fb7c664e1b9a0" dependencies = [ "arrow", "arrow-array", "arrow-buffer", + "arrow-cast", "arrow-ord", "arrow-schema", "arrow-select", @@ -5387,16 +5638,15 @@ dependencies = [ "pin-project", "prost", "prost-build", - "snafu", "tokio", "tracing", ] [[package]] name = "lance-datagen" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d130ec0426173daeb14a6032d18a1eb8c1f8858dca3f42735f0d2c7dde3c47f" +checksum = "075c8154e6b27ff6859f90886efd8cbac348cca255c8b855da630994b4fed714" dependencies = [ "arrow", "arrow-array", @@ -5406,17 +5656,27 @@ dependencies = [ "futures", "half", "hex", - "rand 0.9.3", - "rand_distr 0.5.1", + "rand 0.9.4", + "rand_distr", "rand_xoshiro", - "random_word", +] + +[[package]] +name = "lance-derive" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7056da2e742fd466f6bdfaa876c91d3e0e99bb4f756be058e374ceae2630ec2f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "lance-encoding" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965281449dc6b47d4669e11572f7b2a5e0e6b206dc1f21bce0f3b5f7dc533ece" +checksum = "5e165c668ae61fce336d5f6f9a999ee99b963bb395a3fb818737c533fc9528d1" dependencies = [ "arrow-arith", "arrow-array", @@ -5441,9 +5701,7 @@ dependencies = [ "num-traits", "prost", "prost-build", - "prost-types", - "rand 0.9.3", - "snafu", + "rand 0.9.4", "strum", "tokio", "tracing", @@ -5453,9 +5711,9 @@ dependencies = [ [[package]] name = "lance-file" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8230f9a2e63170eef3d4f8f7576593a73ff8a2f8d5a75400e511e74d4d308f" +checksum = "1691bd5c37bc5e07bde00e32950b5526c2e5653bd2493a3773712574366a37a1" dependencies = [ "arrow-arith", "arrow-array", @@ -5468,7 +5726,6 @@ dependencies = [ "byteorder", "bytes", "datafusion-common", - "deepsize", "futures", "lance-arrow", "lance-core", @@ -5480,17 +5737,17 @@ dependencies = [ "prost", "prost-build", "prost-types", - "snafu", "tokio", "tracing", ] [[package]] name = "lance-index" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950a0bc24e01044fc86260c54e810f2d051660d89caa727234f09b252446c425" +checksum = "9b4792b56f0e047eed706d05a1eedc5f549090913fb4527585bb3a331b83416b" dependencies = [ + "arc-swap", "arrow", "arrow-arith", "arrow-array", @@ -5503,13 +5760,12 @@ dependencies = [ "bitpacking", "bitvec", "bytes", + "chrono", "crossbeam-queue", "datafusion", "datafusion-common", "datafusion-expr", "datafusion-physical-expr", - "datafusion-sql", - "deepsize", "dirs", "fst", "futures", @@ -5517,6 +5773,7 @@ dependencies = [ "itertools 0.13.0", "jsonb", "lance-arrow", + "lance-arrow-stats", "lance-core", "lance-datafusion", "lance-datagen", @@ -5524,8 +5781,10 @@ dependencies = [ "lance-file", "lance-io", "lance-linalg", + "lance-select", "lance-table", - "libm", + "lance-tokenizer", + "libsais-rs", "log", "ndarray", "num-traits", @@ -5533,28 +5792,26 @@ dependencies = [ "prost", "prost-build", "prost-types", - "rand 0.9.3", - "rand_distr 0.5.1", + "rand 0.9.4", + "rand_distr", "rangemap", "rayon", + "regex-syntax", "roaring", "serde", "serde_json", "smallvec", - "snafu", - "tantivy", "tempfile", "tokio", "tracing", - "twox-hash", "uuid", ] [[package]] name = "lance-io" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023188a98822bc87c87726893fbd6c1deea2dccf5c4214051b789b6047bdc2a4" +checksum = "6c81dda99d5b8c8741f413d65650a28ff203d94eda3cbbdda6fd3af3ea9b2a69" dependencies = [ "arrow", "arrow-arith", @@ -5569,20 +5826,20 @@ dependencies = [ "byteorder", "bytes", "chrono", - "deepsize", "futures", "http", + "io-uring", "lance-arrow", "lance-core", "lance-namespace", "log", + "moka", "object_store", "path_abs", "pin-project", "prost", - "rand 0.9.3", + "rand 0.9.4", "serde", - "snafu", "tempfile", "tokio", "tracing", @@ -5591,27 +5848,26 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc4b0c9892f985dac4d27c058d21b6adfda5c73a7aaf697a92f300f36995ded" +checksum = "5c0a8d2a2bc7e6b6229abe320ec6d9e2049a0e65e084684cba01cf032fc71896" dependencies = [ "arrow-array", "arrow-buffer", "arrow-schema", "cc", - "deepsize", "half", "lance-arrow", "lance-core", "num-traits", - "rand 0.9.3", + "rand 0.9.4", ] [[package]] name = "lance-namespace" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8aab88d6c91b045ac3d3967c73b547d9fed5559a91a907f3f4586d0f2d6cc6" +checksum = "3ee2d75929caec41747e58ce090b1a061b650a16cb10d09998128d7912203b1d" dependencies = [ "arrow", "async-trait", @@ -5623,22 +5879,40 @@ dependencies = [ [[package]] name = "lance-namespace-reqwest-client" -version = "0.5.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9008f9825066088178c10599130c8bb0b9c79a39a479e8c51201620c43864a" +checksum = "ba3f0a235e3ed5f8805205649ccc7d7d0f3df23ce1294242c9265ad488d7f19d" dependencies = [ "reqwest", "serde", "serde_json", "serde_repr", + "serde_with", "url", ] +[[package]] +name = "lance-select" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3a2162385a4392376ed7a732e070afd1432bb6f86b0ebcb7c4304c7196953b" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-schema", + "byteorder", + "bytes", + "itertools 0.13.0", + "lance-core", + "roaring", + "tracing", +] + [[package]] name = "lance-table" -version = "3.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7511e4e951d3938316b16f1a64360cfae0b44d22d9c5aca971ed7b5bc83caea7" +checksum = "73da3932a85489e80d5458d4bbc1d340e15c72b89bab529723f575d4d8fda5eb" dependencies = [ "arrow", "arrow-array", @@ -5649,18 +5923,18 @@ dependencies = [ "byteorder", "bytes", "chrono", - "deepsize", "futures", "lance-arrow", "lance-core", "lance-file", "lance-io", + "lance-select", "log", "object_store", "prost", "prost-build", "prost-types", - "rand 0.9.3", + "rand 0.9.4", "rangemap", "roaring", "semver", @@ -5673,6 +5947,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "lance-tokenizer" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16326f6b6d3b736aac40a0ffa78d8aa17d3a53a3766cd0af6d23db87472bd5e" +dependencies = [ + "icu_segmenter", + "rust-stemmers", + "serde", + "stop-words", + "unicode-normalization", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -5693,19 +5980,13 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lenses" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "arrow", "rerun", ] -[[package]] -name = "levenshtein_automata" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" - [[package]] name = "lexical-core" version = "1.0.6" @@ -5765,9 +6046,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -5796,24 +6077,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] -name = "libmimalloc-sys" -version = "0.1.44" +name = "libmimalloc-sys" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libredox" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +dependencies = [ + "bitflags 2.13.0", + "libc", + "redox_syscall 0.5.18", +] + +[[package]] +name = "libsais-rs" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "40fe164dbd47ea0c20e78a121c980ef673326905f1d4fba55e3645a20ef6717f" dependencies = [ - "cc", - "libc", + "rayon", ] [[package]] -name = "libredox" +name = "libudev-sys" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" dependencies = [ - "bitflags 2.11.0", "libc", - "redox_syscall 0.5.18", + "pkg-config", ] [[package]] @@ -5873,9 +6173,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "log-once" @@ -5888,20 +6188,21 @@ dependencies = [ [[package]] name = "log_benchmark" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", "emath", "glam", - "rand 0.9.3", + "itertools 0.14.0", + "rand 0.9.4", "re_tracing", "rerun", ] [[package]] name = "log_file" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", @@ -5910,34 +6211,32 @@ dependencies = [ [[package]] name = "logos" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" dependencies = [ - "beef", "fnv", - "lazy_static", "proc-macro2", "quote", + "regex-automata", "regex-syntax", - "rustc_version", "syn 2.0.117", ] [[package]] name = "logos-derive" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" dependencies = [ "logos-codegen", ] @@ -5957,20 +6256,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -5998,21 +6288,6 @@ dependencies = [ "libc", ] -[[package]] -name = "lz4_flex" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" - -[[package]] -name = "lz4_flex" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98c23545df7ecf1b16c303910a69b079e8e251d60f7dd2cc9b4177f2afaf1746" -dependencies = [ - "twox-hash", -] - [[package]] name = "lz4_flex" version = "0.13.0" @@ -6063,9 +6338,9 @@ dependencies = [ [[package]] name = "mcap" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43908ab970f3a880b02834055a1e04221a3056f442a65ae9111f63e550e7daa5" +checksum = "b215e79631de2a19ed76fda13cd52950c3057b487a2f41d9dc85c4079b969c40" dependencies = [ "bimap", "binrw", @@ -6077,7 +6352,7 @@ dependencies = [ "num_cpus", "paste", "static_assertions", - "thiserror 1.0.69", + "thiserror 2.0.18", "zstd", ] @@ -6091,20 +6366,11 @@ dependencies = [ "digest", ] -[[package]] -name = "measure_time" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" -dependencies = [ - "log", -] - [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" @@ -6117,9 +6383,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -6203,6 +6469,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minidom-14" version = "0.17.0" @@ -6214,20 +6490,14 @@ dependencies = [ [[package]] name = "minimal" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "rerun", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "minimal_options" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", @@ -6237,7 +6507,7 @@ dependencies = [ [[package]] name = "minimal_serve" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "rerun", ] @@ -6260,22 +6530,16 @@ checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" [[package]] name = "mio" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] -[[package]] -name = "mock_instant" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" - [[package]] name = "moka" version = "0.12.11" @@ -6318,28 +6582,22 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "murmurhash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" - [[package]] name = "naga" -version = "29.0.1" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2630921705b9b01dcdd0b6864b9562ca3c1951eecd0f0c4f5f04f61e412647" +checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.11.0", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "codespan-reporting", "half", "hashbrown 0.16.1", "hexf-parse", - "indexmap", + "indexmap 2.14.0", "libm", "log", "num-traits", @@ -6386,8 +6644,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.0", - "jni-sys", + "bitflags 2.13.0", + "jni-sys 0.3.0", "log", "ndk-sys", "num_enum", @@ -6407,7 +6665,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.0", ] [[package]] @@ -6422,7 +6680,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -6435,16 +6693,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nom" version = "8.0.0" @@ -6460,8 +6708,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.0", - "crossbeam-channel", + "bitflags 2.13.0", "fsevent-sys", "inotify", "kqueue", @@ -6497,6 +6744,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -6598,9 +6854,9 @@ dependencies = [ [[package]] name = "numpy" -version = "0.26.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2dba356160b54f5371b550575b78130a54718b4c6e46b3f33a6da74a27e78b" +checksum = "778da78c64ddc928ebf5ad9df5edf0789410ff3bdbf3619aed51cd789a6af1e2" dependencies = [ "libc", "ndarray", @@ -6643,7 +6899,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -6659,7 +6915,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", @@ -6673,7 +6929,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -6697,7 +6953,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6709,7 +6965,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "dispatch2", "objc2 0.6.4", ] @@ -6720,7 +6976,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -6763,7 +7019,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "dispatch", "libc", @@ -6776,7 +7032,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", @@ -6788,6 +7044,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ + "bitflags 2.13.0", "libc", "objc2-core-foundation", ] @@ -6798,7 +7055,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", ] @@ -6821,7 +7078,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6833,7 +7090,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.6.2", "objc2 0.6.4", "objc2-foundation 0.3.2", @@ -6845,7 +7102,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6858,7 +7115,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -6881,7 +7138,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -6902,7 +7159,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -6925,7 +7182,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -6943,14 +7200,16 @@ dependencies = [ [[package]] name = "object_store" -version = "0.12.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c1be0c6c22ec0817cdc77d3842f721a17fd30ab6965001415b5402a74e6b740" +checksum = "622acbc9100d3c10e2ee15804b0caa40e55c933d5aa53814cd520805b7958a49" dependencies = [ "async-trait", "bytes", "chrono", - "futures", + "futures-channel", + "futures-core", + "futures-util", "http", "humantime", "itertools 0.14.0", @@ -6967,7 +7226,7 @@ dependencies = [ [[package]] name = "objectron" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", @@ -6991,12 +7250,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" -[[package]] -name = "oneshot" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce66197e99546da6c6d991285f605192e794ceae69686c17163844a7bf8fcc2" - [[package]] name = "oorandom" version = "11.1.5" @@ -7022,9 +7275,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -7036,35 +7289,37 @@ dependencies = [ [[package]] name = "opentelemetry-appender-tracing" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" +checksum = "2c0080f0dc1d7c786f467cd85a4e395fcab11ee852004f39a29a18ab7c25d837" dependencies = [ "opentelemetry", "tracing", "tracing-core", - "tracing-opentelemetry", "tracing-subscriber", ] [[package]] name = "opentelemetry-http" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", "bytes", "http", + "http-body-util", + "hyper", + "hyper-util", "opentelemetry", - "reqwest", + "tokio", ] [[package]] name = "opentelemetry-otlp" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", "opentelemetry", @@ -7072,18 +7327,17 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest", "thiserror 2.0.18", "tokio", "tonic", - "tracing", + "tonic-types", ] [[package]] name = "opentelemetry-proto" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -7094,16 +7348,17 @@ dependencies = [ [[package]] name = "opentelemetry_sdk" -version = "0.31.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.3", + "portable-atomic", + "rand 0.9.4", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -7135,9 +7390,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "5.1.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", ] @@ -7153,19 +7408,20 @@ dependencies = [ ] [[package]] -name = "ownedbytes" -version = "0.9.0" +name = "owo-colors" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" -dependencies = [ - "stable_deref_trait", -] +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] -name = "owo-colors" -version = "3.5.0" +name = "page_size" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "parking" @@ -7200,14 +7456,13 @@ dependencies = [ [[package]] name = "parquet" -version = "57.3.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee96b29972a257b855ff2341b37e61af5f12d6af1158b6dcdb5b31ea07bb3cb" +checksum = "5dafa7d01085b62a47dd0c1829550a0a36710ea9c4fe358a05a85477cec8a908" dependencies = [ "ahash", "arrow-array", "arrow-buffer", - "arrow-cast", "arrow-data", "arrow-ipc", "arrow-schema", @@ -7219,8 +7474,8 @@ dependencies = [ "flate2", "futures", "half", - "hashbrown 0.16.1", - "lz4_flex 0.12.1", + "hashbrown 0.17.1", + "lz4_flex", "num-bigint", "num-integer", "num-traits", @@ -7241,6 +7496,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "path_abs" version = "0.5.1" @@ -7288,13 +7549,13 @@ checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" [[package]] name = "peniko" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2b6aadb221872732e87d465213e9be5af2849b0e8cc5300a8ba98fffa2e00a" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" dependencies = [ "bytemuck", "color", - "kurbo 0.13.0", + "kurbo 0.13.1", "linebender_resource_handle", "smallvec", ] @@ -7361,7 +7622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap", + "indexmap 2.14.0", ] [[package]] @@ -7372,7 +7633,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "serde", ] @@ -7413,7 +7674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -7538,12 +7799,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plot_dashboard_stress" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", - "rand 0.9.3", - "rand_distr 0.5.1", + "rand 0.9.4", + "rand_distr", "re_log", "rerun", ] @@ -7583,7 +7844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe55bbee2b70d1c1e58d8340eda9a80c5ce11fb9b1bc10b5fc1575c490d38fa9" dependencies = [ "byteorder", - "indexmap", + "indexmap 2.14.0", "peg", ] @@ -7633,6 +7894,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -7654,6 +7924,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ + "serde", "zerovec", ] @@ -7690,13 +7961,35 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -7708,9 +8001,9 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" dependencies = [ "profiling-procmacros", "puffin", @@ -7718,9 +8011,9 @@ dependencies = [ [[package]] name = "profiling-procmacros" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", "syn 2.0.117", @@ -7728,9 +8021,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4500adecd7af8e0e9f4dbce15cfee07ce913fbf6ad605cc468b83f2d531ee94" +checksum = "ba70bf887030e45213b4a95c9b08d5a450b157f87c1d63661ed0847a12fa2aad" dependencies = [ "dtoa", "itoa", @@ -7751,9 +8044,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -7761,9 +8054,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", "itertools 0.14.0", @@ -7780,9 +8073,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", @@ -7793,9 +8086,9 @@ dependencies = [ [[package]] name = "prost-reflect" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89455ef41ed200cafc47c76c552ee7792370ac420497e551f16123a9135f76e" +checksum = "590aa145fee8f7a26b5a6055365e7c5e89a5c1caae9869de76ec0ee73181a2f9" dependencies = [ "logos", "prost", @@ -7804,9 +8097,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -7823,26 +8116,25 @@ dependencies = [ [[package]] name = "puffin" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f" +checksum = "84b514d95a258be801fde8a1ff1c974f4a4841d9750f5d1d6690fc07a5ad4049" dependencies = [ "anyhow", "bincode", "byteorder", "cfg-if", - "itertools 0.10.5", - "lz4_flex 0.11.6", - "once_cell", + "itertools 0.14.0", + "lz4_flex", "parking_lot", "serde", ] [[package]] name = "puffin_http" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739a3c7f56604713b553d7addd7718c226e88d598979ae3450320800bd0e9810" +checksum = "4f912991aab1adae69d2be9455e8db0f41e5ad3da87706ab2de64908678c2c76" dependencies = [ "anyhow", "crossbeam-channel", @@ -7857,43 +8149,40 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "memchr", "unicase", ] [[package]] name = "pyo3" -version = "0.26.0" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" dependencies = [ "chrono", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.26.0" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.26.0" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" dependencies = [ "libc", "pyo3-build-config", @@ -7901,9 +8190,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.26.0" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -7913,9 +8202,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.26.0" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" dependencies = [ "heck", "proc-macro2", @@ -7936,9 +8225,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] @@ -7972,7 +8261,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.3", + "rand 0.9.4", "ring", "rustc-hash 2.1.1", "rustls", @@ -7998,6 +8287,38 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "quiver" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d45b102e73d519fc2b42a01f7e5ea3703087a84c6ac530f9bc7a013ed187c12" +dependencies = [ + "quiver_derive", + "quiver_types", +] + +[[package]] +name = "quiver_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4908b6ee5c1792ad8fe4c98fa5a4d674205754bbbd4d1f7967e62e820b2add41" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "quiver_types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fec5e9c41b28a71f8721ea034c03ac0ef2faf28d461c248b9abfcdfec6a784" +dependencies = [ + "arrow", + "half", + "thiserror 2.0.18", +] + [[package]] name = "quote" version = "1.0.45" @@ -8027,35 +8348,23 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ - "libc", - "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.3", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -8084,16 +8393,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - [[package]] name = "rand_distr" version = "0.5.1" @@ -8101,7 +8400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.3", + "rand 0.9.4", ] [[package]] @@ -8113,19 +8412,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "random_word" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47a395bdb55442b883c89062d6bcff25dc90fa5f8369af81e0ac6d49d78cf81" -dependencies = [ - "ahash", - "brotli", - "paste", - "rand 0.9.3", - "unicase", -] - [[package]] name = "range-alloc" version = "0.1.4" @@ -8144,7 +8430,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", ] [[package]] @@ -8167,7 +8453,7 @@ dependencies = [ [[package]] name = "raw_mesh" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "bytes", @@ -8184,9 +8470,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -8204,7 +8490,7 @@ dependencies = [ [[package]] name = "re_analytics" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "crossbeam", "directories", @@ -8225,12 +8511,13 @@ dependencies = [ [[package]] name = "re_arrow_ui" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", "egui", "egui_kittest", "insta", + "itertools 0.14.0", "jiff", "re_arrow_util", "re_format", @@ -8242,7 +8529,7 @@ dependencies = [ [[package]] name = "re_arrow_util" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "arrow", @@ -8260,7 +8547,7 @@ dependencies = [ [[package]] name = "re_auth" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "async-trait", "base64 0.22.1", @@ -8275,7 +8562,7 @@ dependencies = [ "js-sys", "jsonwebtoken", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "re_analytics", "re_log", "ring", @@ -8298,11 +8585,12 @@ dependencies = [ [[package]] name = "re_backoff" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ + "futures", "getrandom 0.3.4", "js-sys", - "rand 0.9.3", + "rand 0.9.4", "tokio", "wasm-bindgen-futures", "web-sys", @@ -8310,7 +8598,7 @@ dependencies = [ [[package]] name = "re_blueprint_tree" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "egui", "egui_kittest", @@ -8337,7 +8625,7 @@ dependencies = [ [[package]] name = "re_build_info" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "re_byte_size", "serde", @@ -8345,7 +8633,7 @@ dependencies = [ [[package]] name = "re_build_tools" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "cargo_metadata", @@ -8359,21 +8647,34 @@ dependencies = [ [[package]] name = "re_byte_size" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", "ecolor", + "egui", "glam", "half", "insta", + "macaw", "parking_lot", + "re_byte_size_derive", "smallvec", "vec1", ] +[[package]] +name = "re_byte_size_derive" +version = "0.35.0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "re_capabilities" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "document-features", "egui", @@ -8383,14 +8684,26 @@ dependencies = [ [[package]] name = "re_case" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "convert_case", ] +[[package]] +name = "re_cdr" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba10c21433b597481f366b978cdf2617e56785b38cb0dcc3833c6b8ad7e061a" +dependencies = [ + "bytemuck", + "byteorder", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "re_chunk" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", @@ -8402,7 +8715,7 @@ dependencies = [ "insta", "itertools 0.14.0", "nohash-hasher", - "rand 0.9.3", + "rand 0.9.4", "re_arrow_util", "re_byte_size", "re_error", @@ -8413,29 +8726,28 @@ dependencies = [ "re_sorbet", "re_span", "re_tracing", - "re_tuid", "re_types_core", "similar-asserts", "thiserror 2.0.18", - "tracing", ] [[package]] name = "re_chunk_store" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "arrow", "criterion", "document-features", + "futures", "indent", "insta", "itertools 0.14.0", "mimalloc", "nohash-hasher", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "re_arrow_util", "re_byte_size", "re_chunk", @@ -8456,7 +8768,7 @@ dependencies = [ [[package]] name = "re_chunk_store_ui" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", "egui", @@ -8467,6 +8779,7 @@ dependencies = [ "re_byte_size", "re_chunk_store", "re_format", + "re_log", "re_log_types", "re_types_core", "re_ui", @@ -8475,16 +8788,18 @@ dependencies = [ [[package]] name = "re_component_fallbacks" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "re_log_types", + "re_rvl", "re_sdk_types", + "re_video", "re_viewer_context", ] [[package]] name = "re_component_ui" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", "egui", @@ -8510,7 +8825,7 @@ dependencies = [ [[package]] name = "re_context_menu" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "egui", "egui_tiles", @@ -8531,7 +8846,7 @@ dependencies = [ [[package]] name = "re_crash_handler" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "backtrace", "econtext", @@ -8544,7 +8859,7 @@ dependencies = [ [[package]] name = "re_data_source" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "ehttp", @@ -8567,7 +8882,7 @@ dependencies = [ [[package]] name = "re_data_ui" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", @@ -8578,7 +8893,6 @@ dependencies = [ "itertools 0.14.0", "jiff", "re_arrow_ui", - "re_arrow_util", "re_capabilities", "re_chunk_store", "re_entity_db", @@ -8602,12 +8916,14 @@ dependencies = [ [[package]] name = "re_dataframe" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "arrow", + "criterion", "insta", "itertools 0.14.0", + "mimalloc", "nohash-hasher", "rayon", "re_arrow_util", @@ -8618,6 +8934,7 @@ dependencies = [ "re_query", "re_sdk_types", "re_sorbet", + "re_span", "re_tracing", "re_types_core", "similar-asserts", @@ -8628,12 +8945,11 @@ dependencies = [ [[package]] name = "re_dataframe_ui" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", "async-trait", - "cfg-if", "crossbeam", "datafusion", "egui", @@ -8644,10 +8960,14 @@ dependencies = [ "insta", "itertools 0.14.0", "jiff", - "ordered-float 5.1.0", + "nohash-hasher", + "ordered-float 5.3.0", "re_arrow_util", + "re_chunk_store", "re_component_ui", + "re_data_source", "re_dataframe", + "re_entity_db", "re_format", "re_log", "re_log_types", @@ -8663,6 +8983,8 @@ dependencies = [ "re_ui", "re_uri", "re_viewer_context", + "re_viewport", + "re_viewport_blueprint", "serde", "static_assertions", "strum", @@ -8672,25 +8994,30 @@ dependencies = [ [[package]] name = "re_datafusion" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", "async-stream", "async-trait", + "bytes", "chrono", "datafusion", "futures", "futures-util", "getrandom 0.3.4", - "http", + "itertools 0.14.0", + "jiff", "opentelemetry", "opentelemetry-proto", "parking_lot", + "quiver", "re_analytics", "re_arrow_util", "re_backoff", + "re_byte_size", "re_dataframe", + "re_format", "re_log", "re_log_encoding", "re_log_types", @@ -8698,13 +9025,13 @@ dependencies = [ "re_protos", "re_redap_client", "re_sorbet", + "re_tracing", + "re_types_core", "re_uri", "reqwest", "tokio", "tokio-stream", "tonic", - "tonic-prost", - "tonic-web-wasm-client", "tracing", "wasm-bindgen-futures", "web-time", @@ -8712,7 +9039,7 @@ dependencies = [ [[package]] name = "re_dev_tools" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "argh", @@ -8736,14 +9063,14 @@ dependencies = [ [[package]] name = "re_entity_db" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "arrow", "document-features", "emath", - "indexmap", + "indexmap 2.14.0", "insta", "itertools 0.14.0", "nohash-hasher", @@ -8775,14 +9102,14 @@ dependencies = [ [[package]] name = "re_error" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", ] [[package]] name = "re_format" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "half", "itertools 0.14.0", @@ -8790,9 +9117,19 @@ dependencies = [ "re_log", ] +[[package]] +name = "re_gamepad" +version = "0.35.0" +dependencies = [ + "gilrs", + "glam", + "parking_lot", + "re_log", +] + [[package]] name = "re_grpc_client" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "async-stream", "crossbeam", @@ -8807,21 +9144,32 @@ dependencies = [ "re_sorbet", "re_tracing", "re_uri", + "rerun-tonic-web-wasm-client", "thiserror 2.0.18", "tokio", "tokio-stream", "tonic", - "tonic-web-wasm-client", "wasm-bindgen-futures", "web-time", ] +[[package]] +name = "re_grpc_headers" +version = "0.35.0" +dependencies = [ + "http", + "pin-project-lite", + "tonic", + "tower", +] + [[package]] name = "re_grpc_server" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "async-stream", + "futures", "itertools 0.14.0", "parking_lot", "re_byte_size", @@ -8849,30 +9197,42 @@ dependencies = [ "wildmatch", ] +[[package]] +name = "re_hdf5" +version = "0.35.0" +dependencies = [ + "arrow", + "hdf5-pure", + "itertools 0.14.0", + "re_chunk", + "re_log", + "re_log_types", + "re_sdk_types", + "re_tracing", + "tempfile", + "thiserror 2.0.18", +] + [[package]] name = "re_importer" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "arrow", - "cfg-if", "crossbeam", "image", - "indexmap", + "indexmap 2.14.0", "insta", "itertools 0.14.0", "mcap", - "memmap2 0.9.10", - "notify", + "memmap2 0.9.11", "parking_lot", "parquet", "rayon", "re_arrow_util", - "re_build_info", "re_chunk", "re_chunk_store", - "re_crash_handler", "re_error", "re_format", "re_lenses", @@ -8882,6 +9242,7 @@ dependencies = [ "re_log_encoding", "re_log_types", "re_mcap", + "re_mp4_reader", "re_parquet", "re_quota_channel", "re_sdk_types", @@ -8889,6 +9250,7 @@ dependencies = [ "re_video", "serde", "serde_json", + "tempfile", "thiserror 2.0.18", "urdf-rs", "walkdir", @@ -8896,9 +9258,11 @@ dependencies = [ [[package]] name = "re_integration_test" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", + "datafusion", + "directories", "egui", "egui_kittest", "egui_tiles", @@ -8908,7 +9272,9 @@ dependencies = [ "parking_lot", "re_build_info", "re_dataframe_ui", + "re_datafusion", "re_log_encoding", + "re_log_types", "re_protos", "re_redap_client", "re_sdk", @@ -8917,6 +9283,7 @@ dependencies = [ "re_test_context", "re_uri", "re_view_bar_chart", + "re_view_state_timeline", "re_view_tensor", "re_view_text_document", "re_view_text_log", @@ -8924,15 +9291,20 @@ dependencies = [ "re_viewer", "re_viewer_context", "re_viewport_blueprint", + "reqwest", + "serde", "tempfile", "tokio", + "uuid", ] [[package]] name = "re_lenses" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", + "insta", + "re_arrow_util", "re_lenses_core", "re_log", "re_sdk_types", @@ -8940,7 +9312,7 @@ dependencies = [ [[package]] name = "re_lenses_core" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", @@ -8959,29 +9331,30 @@ dependencies = [ [[package]] name = "re_log" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "crossbeam", - "env_filter", - "env_logger", - "js-sys", "log", "log-once", "parking_lot", "tracing", - "wasm-bindgen", + "tracing-log", + "tracing-subscriber", + "tracing-web", ] [[package]] name = "re_log_channel" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "camino", "crossbeam", + "futures", "parking_lot", "re_byte_size", "re_log_encoding", "re_log_types", + "re_protos", "re_quota_channel", "re_tracing", "re_uri", @@ -8991,21 +9364,24 @@ dependencies = [ [[package]] name = "re_log_encoding" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", + "async-trait", "bytes", "criterion", "crossbeam", "ehttp", + "futures", "insta", "itertools 0.14.0", "js-sys", - "lz4_flex 0.13.0", + "lz4_flex", "mimalloc", "parking_lot", "re_arrow_util", "re_build_info", + "re_byte_size", "re_chunk", "re_log", "re_log_types", @@ -9020,8 +9396,6 @@ dependencies = [ "similar-asserts", "tempfile", "thiserror 2.0.18", - "tokio", - "tokio-stream", "tracing", "wasm-bindgen", "wasm-bindgen-futures", @@ -9032,7 +9406,7 @@ dependencies = [ [[package]] name = "re_log_types" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", @@ -9050,6 +9424,7 @@ dependencies = [ "num-derive", "num-traits", "parking_lot", + "quiver", "re_arrow_util", "re_build_info", "re_byte_size", @@ -9071,20 +9446,24 @@ dependencies = [ [[package]] name = "re_mcap" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "arrow", "byteorder", - "cdr-encoding", + "crossbeam", "insta", + "itertools 0.14.0", "mcap", "prost-reflect", + "rayon", "re_arrow_util", + "re_cdr", "re_chunk", "re_log", "re_log_types", + "re_quota_channel", "re_ros_msg", "re_sdk_types", "re_tracing", @@ -9098,7 +9477,7 @@ dependencies = [ [[package]] name = "re_memory" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "backtrace", @@ -9121,7 +9500,7 @@ dependencies = [ [[package]] name = "re_memory_view" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "eframe", "egui", @@ -9132,36 +9511,54 @@ dependencies = [ [[package]] name = "re_mp4" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cf24563b952aa0d55031cca30e58c11f853e59806ba5ac2a28054caffc3eb5" +checksum = "63d71dca1bc07c053ed67ccfd0b52df652192a6541fbdf85cc3d46142e20d709" dependencies = [ "byteorder", "bytes", "num-rational", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.18", +] + +[[package]] +name = "re_mp4_reader" +version = "0.35.0" +dependencies = [ + "arrow", + "itertools 0.14.0", + "re_chunk", + "re_log", + "re_log_types", + "re_sdk_types", + "re_tracing", + "re_video", + "thiserror 2.0.18", ] [[package]] name = "re_mutex" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ - "cfg-if", "parking_lot", + "re_byte_size", "re_log", ] [[package]] name = "re_parquet" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "arrow", "bytes", + "itertools 0.14.0", "parquet", "re_chunk", + "re_lenses", + "re_lenses_core", "re_log", "re_log_types", "re_sdk_types", @@ -9171,7 +9568,7 @@ dependencies = [ [[package]] name = "re_perf_telemetry" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", @@ -9187,7 +9584,10 @@ dependencies = [ "opentelemetry_sdk", "parking_lot", "prometheus-client", - "pyo3", + "rand 0.9.4", + "re_auth", + "re_grpc_headers", + "re_test_mocks", "serde", "serde_json", "tokio", @@ -9202,31 +9602,34 @@ dependencies = [ [[package]] name = "re_plot" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "egui", - "indexmap", + "indexmap 2.14.0", "re_ui", ] [[package]] name = "re_protos" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", "http", + "insta", + "itertools 0.14.0", "jiff", - "lz4_flex 0.13.0", + "lz4_flex", "opentelemetry", - "pin-project-lite", "prost", "prost-types", "pyo3", + "quiver", "re_arrow_util", "re_build_info", "re_byte_size", "re_chunk", + "re_grpc_headers", "re_log_types", "re_sorbet", "re_tracing", @@ -9243,7 +9646,7 @@ dependencies = [ [[package]] name = "re_protos_builder" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "camino", "re_log", @@ -9252,7 +9655,7 @@ dependencies = [ [[package]] name = "re_query" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", @@ -9265,7 +9668,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "paste", - "rand 0.9.3", + "rand 0.9.4", "re_arrow_util", "re_byte_size", "re_chunk", @@ -9285,7 +9688,7 @@ dependencies = [ [[package]] name = "re_quota_channel" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "crossbeam", "parking_lot", @@ -9304,7 +9707,7 @@ dependencies = [ "assert_matches", "atomig", "av-data", - "bitflags 2.11.0", + "bitflags 2.13.0", "cc", "cfg-if", "libc", @@ -9320,7 +9723,7 @@ dependencies = [ [[package]] name = "re_recording_panel" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "egui", @@ -9343,10 +9746,9 @@ dependencies = [ [[package]] name = "re_redap_browser" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", - "cfg-if", "crossbeam", "datafusion", "egui", @@ -9355,6 +9757,7 @@ dependencies = [ "itertools 0.14.0", "js-sys", "re_auth", + "re_backoff", "re_component_ui", "re_dataframe_ui", "re_datafusion", @@ -9378,15 +9781,21 @@ dependencies = [ [[package]] name = "re_redap_client" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", + "async-trait", + "bitflags 2.13.0", "ehttp", "futures", + "http", + "http-body", "itertools 0.14.0", "jiff", "opentelemetry", + "opentelemetry-proto", + "quiver", "re_arrow_util", "re_auth", "re_backoff", @@ -9400,13 +9809,16 @@ dependencies = [ "re_log_types", "re_perf_telemetry", "re_protos", + "re_tracing", + "re_types_core", "re_uri", + "rerun-tonic-web-wasm-client", "serde", "thiserror 2.0.18", "tokio", "tokio-stream", "tonic", - "tonic-web-wasm-client", + "tonic-prost", "tower", "tracing", "url", @@ -9415,7 +9827,7 @@ dependencies = [ [[package]] name = "re_redap_tests" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "arrow", @@ -9425,6 +9837,7 @@ dependencies = [ "insta", "itertools 0.14.0", "lance", + "parking_lot", "prost-types", "re_arrow_util", "re_chunk", @@ -9445,11 +9858,11 @@ dependencies = [ [[package]] name = "re_renderer" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", - "bitflags 2.11.0", + "bitflags 2.13.0", "bytemuck", "cfg_aliases", "clean-path", @@ -9468,7 +9881,7 @@ dependencies = [ "macaw", "never", "notify", - "ordered-float 5.1.0", + "ordered-float 5.3.0", "parking_lot", "pathdiff", "pollster", @@ -9480,7 +9893,6 @@ dependencies = [ "re_mutex", "re_quota_channel", "re_tracing", - "re_tuid", "re_video", "regex-lite", "serde", @@ -9496,23 +9908,23 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu", - "windows-core", ] [[package]] name = "re_renderer_examples" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "bytemuck", + "clap", "console_error_panic_hook", "glam", "image", "itertools 0.14.0", "macaw", "pollster", - "rand 0.9.3", + "rand 0.9.4", "re_log", "re_renderer", "wasm-bindgen-futures", @@ -9524,16 +9936,17 @@ dependencies = [ [[package]] name = "re_ros_msg" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", - "serde", + "itertools 0.14.0", + "re_cdr", "thiserror 2.0.18", ] [[package]] name = "re_rvl" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "byteorder", "thiserror 2.0.18", @@ -9541,7 +9954,7 @@ dependencies = [ [[package]] name = "re_sdk" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", @@ -9585,7 +9998,7 @@ dependencies = [ [[package]] name = "re_sdk_types" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "array-init", "arrow", @@ -9597,7 +10010,6 @@ dependencies = [ "glam", "half", "image", - "indexmap", "infer", "itertools 0.14.0", "macaw", @@ -9606,7 +10018,7 @@ dependencies = [ "ndarray", "nohash-hasher", "ply-rs-bw", - "rand 0.9.3", + "rand 0.9.4", "re_byte_size", "re_error", "re_format", @@ -9627,7 +10039,7 @@ dependencies = [ [[package]] name = "re_selection_panel" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", "egui", @@ -9661,7 +10073,7 @@ dependencies = [ [[package]] name = "re_server" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", @@ -9669,25 +10081,27 @@ dependencies = [ "axum", "bincode", "bytes", - "cfg-if", + "chrono", "clap", "datafusion", "ehttp", "futures", + "getrandom 0.3.4", "http", "http-body", "itertools 0.14.0", "jiff", + "js-sys", "lance", - "lance-index", - "lance-linalg", "nohash-hasher", "opentelemetry", "parking_lot", + "percent-encoding", "re_arrow_util", "re_build_info", "re_build_tools", "re_byte_size", + "re_chunk", "re_chunk_store", "re_entity_db", "re_format", @@ -9713,11 +10127,15 @@ dependencies = [ "tower-service", "tracing", "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", ] [[package]] name = "re_sorbet" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "arrow", "itertools 0.14.0", @@ -9738,18 +10156,19 @@ dependencies = [ [[package]] name = "re_span" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "num-traits", ] [[package]] name = "re_string_interner" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "nohash-hasher", "parking_lot", + "paste", "re_byte_size", "serde", "static_assertions", @@ -9757,7 +10176,7 @@ dependencies = [ [[package]] name = "re_test_context" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", @@ -9787,13 +10206,29 @@ dependencies = [ "wgpu", ] +[[package]] +name = "re_test_mocks" +version = "0.35.0" +dependencies = [ + "axum", + "opentelemetry-proto", + "parking_lot", + "reqwest", + "serde_json", + "tokio", + "tokio-stream", + "tonic", +] + [[package]] name = "re_test_viewport" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "egui", + "re_byte_size", "re_chunk", + "re_context_menu", "re_entity_db", "re_log_types", "re_sdk_types", @@ -9806,11 +10241,11 @@ dependencies = [ [[package]] name = "re_tf" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", - "bitflags 2.11.0", + "bitflags 2.13.0", "criterion", "glam", "insta", @@ -9833,7 +10268,7 @@ dependencies = [ [[package]] name = "re_time_panel" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", @@ -9842,7 +10277,7 @@ dependencies = [ "insta", "itertools 0.14.0", "nohash-hasher", - "rand 0.9.3", + "rand 0.9.4", "re_byte_size", "re_chunk", "re_chunk_store", @@ -9855,6 +10290,7 @@ dependencies = [ "re_log_types", "re_sdk_types", "re_test_context", + "re_time_ruler", "re_tracing", "re_ui", "re_viewer_context", @@ -9864,9 +10300,24 @@ dependencies = [ "vec1", ] +[[package]] +name = "re_time_ruler" +version = "0.35.0" +dependencies = [ + "egui", + "itertools 0.14.0", + "re_format", + "re_log", + "re_log_types", + "re_sdk_types", + "re_tracing", + "re_ui", + "re_viewer_context", +] + [[package]] name = "re_tracing" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "parking_lot", "puffin", @@ -9878,13 +10329,14 @@ dependencies = [ [[package]] name = "re_tuid" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "bytemuck", "criterion", "document-features", "getrandom 0.3.4", - "rand 0.9.3", + "quiver", + "rand 0.9.4", "re_byte_size", "re_log", "serde", @@ -9893,14 +10345,14 @@ dependencies = [ [[package]] name = "re_types" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "re_sdk_types", ] [[package]] name = "re_types_builder" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "camino", @@ -9931,17 +10383,18 @@ dependencies = [ [[package]] name = "re_types_core" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "arrow", - "bitflags 2.11.0", + "bitflags 2.13.0", "bytemuck", "criterion", "document-features", "half", "itertools 0.14.0", "nohash-hasher", + "quiver", "re_arrow_util", "re_byte_size", "re_case", @@ -9951,13 +10404,14 @@ dependencies = [ "re_tracing", "re_tuid", "serde", + "serde_json", "similar-asserts", "thiserror 2.0.18", ] [[package]] name = "re_ui" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", @@ -9972,9 +10426,10 @@ dependencies = [ "itertools 0.14.0", "jiff", "notify", + "nucleo-matcher", "num-traits", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "raw-window-handle", "re_analytics", "re_build_tools", @@ -9986,24 +10441,29 @@ dependencies = [ "re_mutex", "re_quota_channel", "re_tracing", + "re_uri", "ron", "serde", "smallvec", "strum", "strum_macros", - "sublime_fuzzy", + "thiserror 2.0.18", "url", + "wayland-client", + "wayland-protocols", "web-time", ] [[package]] name = "re_uri" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "percent-encoding", + "re_byte_size", "re_log", "re_log_types", "re_tuid", + "re_types_core", "serde", "static_assertions", "thiserror 2.0.18", @@ -10012,7 +10472,7 @@ dependencies = [ [[package]] name = "re_video" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "bit-vec", @@ -10022,6 +10482,7 @@ dependencies = [ "cros-codecs", "econtext", "ffmpeg-sidecar", + "getrandom 0.3.4", "h264-reader", "image", "indicatif", @@ -10034,6 +10495,7 @@ dependencies = [ "re_mutex", "re_quota_channel", "re_rav1d", + "re_rvl", "re_span", "re_tracing", "re_tuid", @@ -10051,7 +10513,7 @@ dependencies = [ [[package]] name = "re_view" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "egui", @@ -10076,12 +10538,13 @@ dependencies = [ [[package]] name = "re_view_bar_chart" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", "egui", "egui_plot", + "itertools 0.14.0", "re_chunk_store", "re_format", "re_log_types", @@ -10098,7 +10561,7 @@ dependencies = [ [[package]] name = "re_view_dataframe" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "arrow", @@ -10106,6 +10569,7 @@ dependencies = [ "egui_dnd", "egui_table", "itertools 0.14.0", + "re_byte_size", "re_chunk_store", "re_component_ui", "re_dataframe", @@ -10127,13 +10591,14 @@ dependencies = [ [[package]] name = "re_view_graph" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "egui", "fjadra", "itertools 0.14.0", "nohash-hasher", + "re_byte_size", "re_chunk", "re_chunk_store", "re_data_ui", @@ -10154,13 +10619,14 @@ dependencies = [ [[package]] name = "re_view_map" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "bytemuck", "egui", "glam", "itertools 0.14.0", "macaw", + "re_byte_size", "re_data_ui", "re_entity_db", "re_log", @@ -10179,12 +10645,12 @@ dependencies = [ [[package]] name = "re_view_spatial" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "arrow", - "bitflags 2.11.0", + "bitflags 2.13.0", "bytemuck", "egui", "glam", @@ -10196,8 +10662,8 @@ dependencies = [ "macaw", "ndarray", "nohash-hasher", - "ordered-float 5.1.0", - "re_arrow_util", + "ordered-float 5.3.0", + "parking_lot", "re_byte_size", "re_chunk_store", "re_component_ui", @@ -10205,6 +10671,7 @@ dependencies = [ "re_entity_db", "re_error", "re_format", + "re_gamepad", "re_log", "re_log_types", "re_mp4", @@ -10230,31 +10697,38 @@ dependencies = [ ] [[package]] -name = "re_view_status" -version = "0.32.0-alpha.1" +name = "re_view_state_timeline" +version = "0.35.0" dependencies = [ "egui", + "nohash-hasher", + "re_byte_size", "re_chunk_store", + "re_component_ui", "re_log_types", "re_sdk_types", + "re_selection_panel", "re_test_context", "re_test_viewport", + "re_time_ruler", "re_tracing", "re_ui", "re_view", "re_viewer_context", + "re_viewport", "re_viewport_blueprint", ] [[package]] name = "re_view_tensor" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "bytemuck", "egui", "half", "ndarray", + "re_byte_size", "re_chunk_store", "re_data_ui", "re_log_types", @@ -10274,10 +10748,11 @@ dependencies = [ [[package]] name = "re_view_text_document" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "egui", "egui_commonmark", + "re_byte_size", "re_chunk_store", "re_log_types", "re_sdk_types", @@ -10292,11 +10767,13 @@ dependencies = [ [[package]] name = "re_view_text_log" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "egui", "egui_extras", "itertools 0.14.0", + "re_byte_size", + "re_chunk", "re_chunk_store", "re_data_ui", "re_entity_db", @@ -10305,6 +10782,7 @@ dependencies = [ "re_query", "re_sdk_types", "re_test_context", + "re_test_viewport", "re_tracing", "re_ui", "re_view", @@ -10314,7 +10792,7 @@ dependencies = [ [[package]] name = "re_view_time_series" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrayvec", @@ -10326,6 +10804,7 @@ dependencies = [ "nohash-hasher", "rayon", "re_byte_size", + "re_chunk", "re_chunk_store", "re_component_ui", "re_format", @@ -10348,21 +10827,23 @@ dependencies = [ [[package]] name = "re_viewer" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "arrow", "bytemuck", - "cfg-if", + "camino", "crossbeam", "eframe", "egui", "egui-wgpu", + "egui_inspection", "egui_kittest", "egui_plot", "ehttp", "emath", + "futures", "glam", "image", "insta", @@ -10391,6 +10872,7 @@ dependencies = [ "re_entity_db", "re_error", "re_format", + "re_gamepad", "re_importer", "re_log", "re_log_channel", @@ -10400,6 +10882,7 @@ dependencies = [ "re_memory_view", "re_mutex", "re_perf_telemetry", + "re_protos", "re_query", "re_recording_panel", "re_redap_browser", @@ -10407,6 +10890,7 @@ dependencies = [ "re_renderer", "re_sdk_types", "re_selection_panel", + "re_server", "re_sorbet", "re_string_interner", "re_test_context", @@ -10423,7 +10907,7 @@ dependencies = [ "re_view_graph", "re_view_map", "re_view_spatial", - "re_view_status", + "re_view_state_timeline", "re_view_tensor", "re_view_text_document", "re_view_text_log", @@ -10443,22 +10927,24 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-stream", + "tokio-util", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "web-time", "wgpu", + "winit", ] [[package]] name = "re_viewer_context" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "arrow", - "bitflags 2.11.0", + "bitflags 2.13.0", "bytemuck", "camino", "crossbeam", @@ -10466,20 +10952,21 @@ dependencies = [ "directories", "egui", "egui-wgpu", + "egui_kittest", "egui_tiles", "emath", "glam", "half", "home", "image", - "indexmap", + "indexmap 2.14.0", "itertools 0.14.0", "linked-hash-map", "macaw", "ndarray", "nohash-hasher", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "rayon", "re_arrow_ui", "re_arrow_util", @@ -10526,9 +11013,28 @@ dependencies = [ "wgpu", ] +[[package]] +name = "re_viewer_mcp" +version = "0.35.0" +dependencies = [ + "anyhow", + "egui_inspection", + "egui_mcp", + "insta", + "parking_lot", + "re_log", + "re_protos", + "rmcp", + "schemars 1.2.1", + "serde", + "serde_json", + "tokio", + "tonic", +] + [[package]] name = "re_viewport" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "egui", @@ -10551,7 +11057,7 @@ dependencies = [ [[package]] name = "re_viewport_blueprint" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", @@ -10561,7 +11067,7 @@ dependencies = [ "itertools 0.14.0", "mimalloc", "nohash-hasher", - "rand 0.9.3", + "rand 0.9.4", "re_chunk", "re_chunk_store", "re_entity_db", @@ -10581,7 +11087,7 @@ dependencies = [ [[package]] name = "re_web_viewer_server" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "document-features", "re_analytics", @@ -10589,14 +11095,14 @@ dependencies = [ "re_log", "thiserror 2.0.18", "tiny_http", - "zip 8.2.0", + "zip 8.6.0", ] [[package]] name = "read-fonts" -version = "0.37.0" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" dependencies = [ "bytemuck", "font-types", @@ -10617,7 +11123,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", ] [[package]] @@ -10694,9 +11200,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "renderdoc-sys" @@ -10723,7 +11229,6 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", @@ -10778,22 +11283,22 @@ dependencies = [ [[package]] name = "rerun" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "anyhow", "arrow", "camino", - "cfg-if", "clap", "crossbeam", "document-features", "env_filter", - "indexmap", + "indexmap 2.14.0", "indicatif", "itertools 0.14.0", "jiff", "log", + "mcap", "parking_lot", "puffin", "rayon", @@ -10831,16 +11336,18 @@ dependencies = [ "re_uri", "re_video", "re_viewer", + "re_viewer_mcp", "re_web_viewer_server", "similar-asserts", "tokio", "unindent", "url", + "walkdir", ] [[package]] name = "rerun-cli" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "document-features", "mimalloc", @@ -10855,20 +11362,46 @@ dependencies = [ [[package]] name = "rerun-importer-rust-file" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "argh", "rerun", ] +[[package]] +name = "rerun-tonic-web-wasm-client" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cf00a06feab41977adfc4f2593cb60ed937351924d5bb0edeaa3eafbc02a6a" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "httparse", + "js-sys", + "pin-project", + "thiserror 2.0.18", + "tonic", + "tower-service", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "rerun_c" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "ahash", "arrow", "infer", + "itertools 0.14.0", "parking_lot", "re_arrow_util", "re_build_info", @@ -10881,11 +11414,10 @@ dependencies = [ [[package]] name = "rerun_py" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "arrow", - "bytes", "chrono", "comfy-table", "crossbeam", @@ -10896,18 +11428,17 @@ dependencies = [ "infer", "itertools 0.14.0", "jiff", - "memmap2 0.9.10", + "memmap2 0.9.11", "mimalloc", "numpy", "parking_lot", "pyo3", "pyo3-build-config", - "rand 0.9.3", + "rand 0.9.4", "re_arrow_util", "re_auth", "re_build_info", "re_build_tools", - "re_byte_size", "re_chunk", "re_chunk_store", "re_datafusion", @@ -10915,21 +11446,26 @@ dependencies = [ "re_format", "re_grpc_client", "re_grpc_server", + "re_hdf5", "re_importer", + "re_lenses", "re_lenses_core", "re_log", "re_log_encoding", "re_log_types", "re_mcap", "re_memory", + "re_mp4_reader", "re_parquet", "re_perf_telemetry", "re_protos", "re_quota_channel", "re_redap_client", "re_sdk", + "re_sdk_types", "re_server", "re_sorbet", + "re_tracing", "re_tuid", "re_types_core", "re_uri", @@ -10938,6 +11474,7 @@ dependencies = [ "rustls", "strum", "strum_macros", + "tempfile", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -11024,11 +11561,65 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmcp" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "futures", + "pastey", + "pin-project-lite", + "rmcp-macros", + "schemars 1.2.1", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "rmcp-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.117", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "roaring" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" dependencies = [ "bytemuck", "byteorder", @@ -11036,11 +11627,11 @@ dependencies = [ [[package]] name = "ron" -version = "0.12.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "once_cell", "serde", "serde_derive", @@ -11056,7 +11647,7 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "run_wasm" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "cargo-run-wasm", "pico-args", @@ -11106,7 +11697,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -11119,7 +11710,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.12.1", @@ -11128,9 +11719,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -11210,6 +11801,44 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -11258,7 +11887,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -11283,9 +11912,9 @@ checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -11360,6 +11989,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_json" version = "1.0.149" @@ -11397,9 +12037,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -11416,6 +12056,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sha-1" version = "0.10.1" @@ -11460,7 +12132,7 @@ dependencies = [ [[package]] name = "shared_recording" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "rerun", ] @@ -11495,6 +12167,16 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -11542,20 +12224,11 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" -[[package]] -name = "sketches-ddsketch" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" -dependencies = [ - "serde", -] - [[package]] name = "skrifa" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" dependencies = [ "bytemuck", "read-fonts", @@ -11592,13 +12265,13 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "calloop", "calloop-wayland-source", "cursor-icon", "libc", "log", - "memmap2 0.9.10", + "memmap2 0.9.11", "rustix 0.38.44", "thiserror 1.0.69", "wayland-backend", @@ -11660,13 +12333,13 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" [[package]] name = "snippets" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "crossbeam", "itertools 0.14.0", "ndarray", - "rand 0.9.3", - "rand_distr 0.5.1", + "rand 0.9.4", + "rand_distr", "re_build_tools", "rerun", "similar-asserts", @@ -11684,17 +12357,17 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "spawn_viewer" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "rerun", ] @@ -11705,14 +12378,14 @@ version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", ] [[package]] name = "sqlparser" -version = "0.59.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f" +checksum = "dbf5ea8d4d7c808e1af1cbabebca9a2abe603bcefc22294c5b95018d53200cb7" dependencies = [ "log", "sqlparser_derive", @@ -11720,9 +12393,9 @@ dependencies = [ [[package]] name = "sqlparser_derive" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5fc6819faabb412da764b99d3b713bb55083c11e7e0c00144d386cd6a1939c" +checksum = "a6dd45d8fc1c79299bfbb7190e42ccbbdf6a5f52e4a6ad98d92357ea965bd289" dependencies = [ "proc-macro2", "quote", @@ -11752,19 +12425,21 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "state_timeline_example" +version = "0.35.0" +dependencies = [ + "anyhow", + "arrow", + "rerun", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "status_example" -version = "0.32.0-alpha.1" -dependencies = [ - "rerun", -] - [[package]] name = "std_prelude" version = "0.2.12" @@ -11773,8 +12448,9 @@ checksum = "8207e78455ffdf55661170876f88daf85356e4edd54e0a3dbc79586ca1e50cbe" [[package]] name = "stdio" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ + "itertools 0.14.0", "rerun", ] @@ -11786,21 +12462,35 @@ checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978" [[package]] name = "stl_io" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567641995c51a3b8befddb13e1826187bcf7eb7ca8d13746bfd1cc5a22e89fa8" + +[[package]] +name = "stop-words" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da63e75b86345156b191c021b3ce2a13b973941ecdb8c70d6f00cbbfe0076ed7" +checksum = "d68df56303396bcfb639455b3c166804aeb7994005010aab5e9e8a1277b8871d" dependencies = [ - "byteorder", - "float-cmp 0.10.0", + "serde_json", ] [[package]] name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" dependencies = [ - "float-cmp 0.9.0", + "vte", ] [[package]] @@ -11831,12 +12521,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "sublime_fuzzy" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7986063f7c0ab374407e586d7048a3d5aac94f103f751088bf398e07cd5400" - [[package]] name = "subtle" version = "2.6.1" @@ -11915,152 +12599,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "tantivy" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a966cb0e76e311f09cf18507c9af192f15d34886ee43d7ba7c7e3803660c43" -dependencies = [ - "aho-corasick", - "arc-swap", - "base64 0.22.1", - "bitpacking", - "bon", - "byteorder", - "census", - "crc32fast", - "crossbeam-channel", - "downcast-rs 2.0.2", - "fastdivide", - "fnv", - "fs4", - "htmlescape", - "hyperloglogplus", - "itertools 0.14.0", - "levenshtein_automata", - "log", - "lru 0.12.5", - "lz4_flex 0.11.6", - "measure_time", - "memmap2 0.9.10", - "once_cell", - "oneshot", - "rayon", - "regex", - "rust-stemmers", - "rustc-hash 2.1.1", - "serde", - "serde_json", - "sketches-ddsketch", - "smallvec", - "tantivy-bitpacker", - "tantivy-columnar", - "tantivy-common", - "tantivy-fst", - "tantivy-query-grammar", - "tantivy-stacker", - "tantivy-tokenizer-api", - "tempfile", - "thiserror 2.0.18", - "time", - "uuid", - "winapi", -] - -[[package]] -name = "tantivy-bitpacker" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adc286a39e089ae9938935cd488d7d34f14502544a36607effd2239ff0e2494" -dependencies = [ - "bitpacking", -] - -[[package]] -name = "tantivy-columnar" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6300428e0c104c4f7db6f95b466a6f5c1b9aece094ec57cdd365337908dc7344" -dependencies = [ - "downcast-rs 2.0.2", - "fastdivide", - "itertools 0.14.0", - "serde", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-sstable", - "tantivy-stacker", -] - -[[package]] -name = "tantivy-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b6ea6090ce03dc72c27d0619e77185d26cc3b20775966c346c6d4f7e99d7f" -dependencies = [ - "async-trait", - "byteorder", - "ownedbytes", - "serde", - "time", -] - -[[package]] -name = "tantivy-fst" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" -dependencies = [ - "byteorder", - "regex-syntax", - "utf8-ranges", -] - -[[package]] -name = "tantivy-query-grammar" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e810cdeeebca57fc3f7bfec5f85fdbea9031b2ac9b990eb5ff49b371d52bbe6a" -dependencies = [ - "nom 7.1.3", - "serde", - "serde_json", -] - -[[package]] -name = "tantivy-sstable" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709f22c08a4c90e1b36711c1c6cad5ae21b20b093e535b69b18783dd2cb99416" -dependencies = [ - "futures-util", - "itertools 0.14.0", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-fst", - "zstd", -] - -[[package]] -name = "tantivy-stacker" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bcdebb267671311d1e8891fd9d1301803fdb8ad21ba22e0a30d0cab49ba59c1" -dependencies = [ - "murmurhash32", - "rand_distr 0.4.3", - "tantivy-common", -] - -[[package]] -name = "tantivy-tokenizer-api" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa942fcee81e213e09715bbce8734ae2180070b97b33839a795ba1de201547d" -dependencies = [ - "serde", -] - [[package]] name = "tap" version = "1.0.1" @@ -12075,9 +12613,9 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -12088,7 +12626,7 @@ dependencies = [ [[package]] name = "template" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "rerun", ] @@ -12104,17 +12642,17 @@ dependencies = [ [[package]] name = "test_data_density_graph" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", - "rand 0.9.3", + "rand 0.9.4", "re_log", "rerun", ] [[package]] name = "test_image_memory" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "mimalloc", "re_format", @@ -12124,7 +12662,7 @@ dependencies = [ [[package]] name = "test_label_compaction" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", @@ -12134,7 +12672,7 @@ dependencies = [ [[package]] name = "test_out_of_order_transforms" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", @@ -12145,7 +12683,7 @@ dependencies = [ [[package]] name = "test_ui_wakeup" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anyhow", "clap", @@ -12313,11 +12851,12 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -12363,25 +12902,25 @@ dependencies = [ [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.0", + "socket2 0.6.4", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -12430,6 +12969,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -12437,69 +12977,60 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", + "winnow 1.0.3", ] [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime", "toml_parser", - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" -version = "0.14.2" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", @@ -12516,7 +13047,7 @@ dependencies = [ "percent-encoding", "pin-project", "rustls-native-certs", - "socket2 0.6.0", + "socket2 0.6.4", "sync_wrapper", "tokio", "tokio-rustls", @@ -12529,9 +13060,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.14.2" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" dependencies = [ "prettyplease", "proc-macro2", @@ -12541,9 +13072,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.2" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -12552,9 +13083,9 @@ dependencies = [ [[package]] name = "tonic-prost-build" -version = "0.14.2" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" dependencies = [ "prettyplease", "proc-macro2", @@ -12567,46 +13098,32 @@ dependencies = [ ] [[package]] -name = "tonic-web" -version = "0.14.2" +name = "tonic-types" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75214f6b6bd28c19aa752ac09fdf0eea546095670906c21fe3940e180a4c43f2" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" dependencies = [ - "base64 0.22.1", - "bytes", - "http", - "http-body", - "pin-project", - "tokio-stream", + "prost", + "prost-types", "tonic", - "tower-layer", - "tower-service", - "tracing", ] [[package]] -name = "tonic-web-wasm-client" -version = "0.8.0" +name = "tonic-web" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898cd44be5e23e59d2956056538f1d6b3c5336629d384ffd2d92e76f87fb98ff" +checksum = "b5e6a1b6319ca4b61a4c0f0c94d439c8f3ed344cca56fe0df40e1fe4be11380b" dependencies = [ "base64 0.22.1", - "byteorder", "bytes", - "futures-util", "http", "http-body", - "http-body-util", - "httparse", - "js-sys", "pin-project", - "thiserror 2.0.18", + "tokio-stream", "tonic", + "tower-layer", "tower-service", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", + "tracing", ] [[package]] @@ -12617,7 +13134,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -12630,21 +13147,21 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", "tracing", + "url", ] [[package]] @@ -12705,9 +13222,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", "opentelemetry", @@ -12731,9 +13248,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -12761,6 +13278,19 @@ dependencies = [ "tracy-client", ] +[[package]] +name = "tracing-web" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e6a141feebd51f8d91ebfd785af50fca223c570b86852166caa3b141defe7c" +dependencies = [ + "js-sys", + "tracing-core", + "tracing-subscriber", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "tracy-client" version = "0.18.4" @@ -12779,7 +13309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f7c95348f20c1c913d72157b3c6dee6ea3e30b3d19502c5a7f6d3f160dacbf" dependencies = [ "cc", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -12814,7 +13344,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.3", + "rand 0.9.4", "sha1", "thiserror 2.0.18", "utf-8", @@ -12826,7 +13356,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" dependencies = [ - "rand 0.9.3", + "rand 0.9.4", ] [[package]] @@ -12858,9 +13388,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -12885,17 +13415,32 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -13067,9 +13612,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -13093,29 +13638,37 @@ dependencies = [ "smallvec", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "vello_common" -version = "0.0.6" +version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" +checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" dependencies = [ "bytemuck", "fearless_simd", - "hashbrown 0.16.1", + "guillotiere", + "hashbrown 0.17.1", "log", "peniko", - "skrifa", "smallvec", + "thiserror 2.0.18", ] [[package]] name = "vello_cpu" -version = "0.0.6" +version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" +checksum = "588691169aed86b5c8fb487266afee01323234e6fd0a3f2aaec0eaa8e4007f23" dependencies = [ "bytemuck", - "hashbrown 0.16.1", + "glifo", + "hashbrown 0.17.1", "vello_common", ] @@ -13127,12 +13680,21 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "viewer_callbacks" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "mimalloc", "rerun", ] +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -13145,9 +13707,9 @@ dependencies = [ [[package]] name = "walkers" -version = "0.53.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c81bab51dc24106d35edce41958ed16e3a0c1c0b45f40685ffd3f4b5691490c" +checksum = "936a0d5741eb3dcd0fc2da5ec819447147f0c296492a54f20318e76f3b2f37bc" dependencies = [ "bytes", "egui", @@ -13158,7 +13720,7 @@ dependencies = [ "http-cache-reqwest", "image", "log", - "lru 0.16.3", + "lru", "reqwest", "reqwest-middleware", "thiserror 2.0.18", @@ -13301,6 +13863,45 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941c102b3f0c15b6d72a53205e09e6646aafcf2991e18412cc331dbac1806bc0" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26bd6570f39bb1440fd8f01b63461faaf2a3f6078a508e4e54efa99363108d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c29582b14d5bf030b02fa232b9b57faf2afc322d2c61964dd80bad02bf76207" + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -13328,7 +13929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder 0.244.0", "wasmparser 0.244.0", ] @@ -13352,9 +13953,9 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -13364,21 +13965,21 @@ version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.14.0", "semver", "serde", ] [[package]] name = "wayland-backend" -version = "0.3.11" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", - "downcast-rs 1.2.1", + "downcast-rs", "rustix 1.1.4", "scoped-tls", "smallvec", @@ -13387,11 +13988,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "rustix 1.1.4", "wayland-backend", "wayland-scanner", @@ -13403,7 +14004,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "cursor-icon", "wayland-backend", ] @@ -13421,11 +14022,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -13437,7 +14038,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -13450,7 +14051,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -13459,20 +14060,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml 0.39.4", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -13502,12 +14103,12 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", - "jni", + "jni 0.22.4", "log", "ndk-context", "objc2 0.6.4", @@ -13542,12 +14143,12 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wgpu" -version = "29.0.1" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c239a9a747bbd379590985bac952c2e53cb19873f7072b3370c6a6a8e06837" +checksum = "bb3feacc458f7bee8bc1737149b42b6c731aa461039a4264a67bb6681646b250" dependencies = [ "arrayvec", - "bitflags 2.11.0", + "bitflags 2.13.0", "bytemuck", "cfg-if", "cfg_aliases", @@ -13572,19 +14173,19 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "29.0.1" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e80ac6cf1895df6342f87d975162108f9d98772a0d74bc404ab7304ac29469e" +checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7" dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.11.0", + "bitflags 2.13.0", "bytemuck", "cfg_aliases", "document-features", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.14.0", "log", "naga", "once_cell", @@ -13642,15 +14243,15 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "29.0.1" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a47aef47636562f3937285af4c44b4b5b404b46577471411cc5313a921da7e" +checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.6.2", "bytemuck", "cfg-if", @@ -13673,7 +14274,7 @@ dependencies = [ "objc2-metal 0.3.2", "objc2-quartz-core 0.3.2", "once_cell", - "ordered-float 5.1.0", + "ordered-float 5.3.0", "parking_lot", "portable-atomic", "portable-atomic-util", @@ -13691,13 +14292,14 @@ dependencies = [ "wgpu-types", "windows", "windows-core", + "windows-result", ] [[package]] name = "wgpu-naga-bridge" -version = "29.0.1" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4684f4410da0cf95a4cb63bb5edaac022461dedb6adf0b64d0d9b5f6890d51" +checksum = "59c654c483f058800972c3645e95388a7eca31bf9fe1933bc20e036588a0be02" dependencies = [ "naga", "wgpu-types", @@ -13705,11 +14307,11 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "29.0.1" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2675540fb1a5cfa5ef122d3d5f390e2c75711a0b946410f2d6ac3a0f77d1f6" +checksum = "a9bcc31518a0e9735aefebedb5f7a9ef3ed1c42549c9f4c882fa9060ceaac639" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "bytemuck", "js-sys", "log", @@ -14161,7 +14763,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.11.0", + "bitflags 2.13.0", "block2 0.5.1", "bytemuck", "calloop", @@ -14173,7 +14775,7 @@ dependencies = [ "dpi", "js-sys", "libc", - "memmap2 0.9.10", + "memmap2 0.9.11", "ndk", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -14212,6 +14814,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -14246,7 +14857,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -14276,8 +14887,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", - "indexmap", + "bitflags 2.13.0", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -14296,7 +14907,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", @@ -14308,9 +14919,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -14365,7 +14976,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "dlib", "log", "once_cell", @@ -14413,11 +15024,10 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -14425,9 +15035,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -14462,7 +15072,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.60.2", - "winnow", + "winnow 0.7.13", "zbus_macros", "zbus_names", "zvariant", @@ -14515,7 +15125,7 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow", + "winnow 0.7.13", "zvariant", ] @@ -14599,24 +15209,40 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -14624,9 +15250,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -14647,13 +15273,13 @@ dependencies = [ [[package]] name = "zip" -version = "8.2.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "crc32fast", "flate2", - "indexmap", + "indexmap 2.14.0", "memchr", "typed-path", "zopfli", @@ -14735,7 +15361,7 @@ dependencies = [ "endi", "enumflags2", "serde", - "winnow", + "winnow 0.7.13", "zvariant_derive", "zvariant_utils", ] @@ -14763,5 +15389,5 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "winnow", + "winnow 0.7.13", ] diff --git a/Cargo.toml b/Cargo.toml index 6e7969a460e3..12e3392d9830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,8 @@ include = [ ] license = "MIT OR Apache-2.0" repository = "https://github.com/rerun-io/rerun" -rust-version = "1.92" -version = "0.32.0-alpha.1" +rust-version = "1.95" +version = "0.35.0" [workspace.metadata.cargo-shear] ignored = [ @@ -47,6 +47,7 @@ ignored = [ # used for specific targets or features "home", "profiling", + "wayland-sys", # only enabled via the `server` feature of `re_tracing` "windows-core", ] ignored-paths = [ @@ -60,109 +61,118 @@ ignored-paths = [ # re_log_types 0.3.0-alpha.0, NOT 0.3.0-alpha.4 even though it is newer and semver-compatible. # crates/build: -re_build_info = { path = "crates/build/re_build_info", version = "=0.32.0-alpha.1", default-features = false } -re_build_tools = { path = "crates/build/re_build_tools", version = "=0.32.0-alpha.1", default-features = false } -re_dev_tools = { path = "crates/build/re_dev_tools", version = "=0.32.0-alpha.1", default-features = false } -re_protos_builder = { path = "crates/build/re_protos_builder", version = "=0.32.0-alpha.1", default-features = false } -re_types_builder = { path = "crates/build/re_types_builder", version = "=0.32.0-alpha.1", default-features = false } +re_build_info = { path = "crates/build/re_build_info", version = "0.35.0", default-features = false } +re_build_tools = { path = "crates/build/re_build_tools", version = "0.35.0", default-features = false } +re_dev_tools = { path = "crates/build/re_dev_tools", version = "0.35.0", default-features = false } +re_protos_builder = { path = "crates/build/re_protos_builder", version = "0.35.0", default-features = false } +re_types_builder = { path = "crates/build/re_types_builder", version = "0.35.0", default-features = false } # crates/store: -re_lenses_core = { path = "crates/store/re_lenses_core", version = "=0.32.0-alpha.1", default-features = false } -re_chunk = { path = "crates/store/re_chunk", version = "=0.32.0-alpha.1", default-features = false } -re_chunk_store = { path = "crates/store/re_chunk_store", version = "=0.32.0-alpha.1", default-features = false } -re_importer = { path = "crates/store/re_importer", version = "=0.32.0-alpha.1", default-features = false } -re_data_source = { path = "crates/store/re_data_source", version = "=0.32.0-alpha.1", default-features = false } -re_dataframe = { path = "crates/store/re_dataframe", version = "=0.32.0-alpha.1", default-features = false } -re_datafusion = { path = "crates/store/re_datafusion", version = "=0.32.0-alpha.1", default-features = false } -re_entity_db = { path = "crates/store/re_entity_db", version = "=0.32.0-alpha.1", default-features = false } -re_grpc_client = { path = "crates/store/re_grpc_client", version = "=0.32.0-alpha.1", default-features = false } -re_grpc_server = { path = "crates/store/re_grpc_server", version = "=0.32.0-alpha.1", default-features = false } -re_lenses = { path = "crates/store/re_lenses", version = "=0.32.0-alpha.1", default-features = false } -re_log_channel = { path = "crates/store/re_log_channel", version = "=0.32.0-alpha.1", default-features = false } -re_log_encoding = { path = "crates/store/re_log_encoding", version = "=0.32.0-alpha.1", default-features = false } -re_log_types = { path = "crates/store/re_log_types", version = "=0.32.0-alpha.1", default-features = false } -re_mcap = { path = "crates/store/re_mcap", version = "=0.32.0-alpha.1", default-features = false } -re_parquet = { path = "crates/store/re_parquet", version = "=0.32.0-alpha.1", default-features = false } -re_protos = { path = "crates/store/re_protos", version = "=0.32.0-alpha.1", default-features = false } -re_query = { path = "crates/store/re_query", version = "=0.32.0-alpha.1", default-features = false } -re_redap_client = { path = "crates/store/re_redap_client", version = "=0.32.0-alpha.1", default-features = false } -re_redap_tests = { path = "crates/store/re_redap_tests", version = "=0.32.0-alpha.1", default-features = false } -re_sdk_types = { path = "crates/store/re_sdk_types", version = "=0.32.0-alpha.1", default-features = false } -re_server = { path = "crates/store/re_server", version = "=0.32.0-alpha.1", default-features = false } -re_sorbet = { path = "crates/store/re_sorbet", version = "=0.32.0-alpha.1", default-features = false } -re_tf = { path = "crates/store/re_tf", version = "=0.32.0-alpha.1", default-features = false } -re_types_core = { path = "crates/store/re_types_core", version = "=0.32.0-alpha.1", default-features = false } -re_uri = { path = "crates/store/re_uri", version = "=0.32.0-alpha.1", default-features = false } +re_lenses_core = { path = "crates/store/re_lenses_core", version = "0.35.0", default-features = false } +re_chunk = { path = "crates/store/re_chunk", version = "0.35.0", default-features = false } +re_chunk_store = { path = "crates/store/re_chunk_store", version = "0.35.0", default-features = false } +re_importer = { path = "crates/store/re_importer", version = "0.35.0", default-features = false } +re_data_source = { path = "crates/store/re_data_source", version = "0.35.0", default-features = false } +re_dataframe = { path = "crates/store/re_dataframe", version = "0.35.0", default-features = false } +re_datafusion = { path = "crates/store/re_datafusion", version = "0.35.0", default-features = false } +re_entity_db = { path = "crates/store/re_entity_db", version = "0.35.0", default-features = false } +re_grpc_client = { path = "crates/store/re_grpc_client", version = "0.35.0", default-features = false } +re_grpc_server = { path = "crates/store/re_grpc_server", version = "0.35.0", default-features = false } +re_hdf5 = { path = "crates/store/re_hdf5", version = "0.35.0", default-features = false } +re_lenses = { path = "crates/store/re_lenses", version = "0.35.0", default-features = false } +re_log_channel = { path = "crates/store/re_log_channel", version = "0.35.0", default-features = false } +re_log_encoding = { path = "crates/store/re_log_encoding", version = "0.35.0", default-features = false } +re_log_types = { path = "crates/store/re_log_types", version = "0.35.0", default-features = false } +re_mcap = { path = "crates/store/re_mcap", version = "0.35.0", default-features = false } +re_mp4_reader = { path = "crates/store/re_mp4_reader", version = "0.35.0", default-features = false } +re_parquet = { path = "crates/store/re_parquet", version = "0.35.0", default-features = false } +re_protos = { path = "crates/store/re_protos", version = "0.35.0", default-features = false } +re_query = { path = "crates/store/re_query", version = "0.35.0", default-features = false } +re_redap_client = { path = "crates/store/re_redap_client", version = "0.35.0", default-features = false } +re_redap_tests = { path = "crates/store/re_redap_tests", version = "0.35.0", default-features = false } +re_sdk_types = { path = "crates/store/re_sdk_types", version = "0.35.0", default-features = false } +re_server = { path = "crates/store/re_server", version = "0.35.0", default-features = false } +re_sorbet = { path = "crates/store/re_sorbet", version = "0.35.0", default-features = false } +re_tf = { path = "crates/store/re_tf", version = "0.35.0", default-features = false } +re_types_core = { path = "crates/store/re_types_core", version = "0.35.0", default-features = false } +re_uri = { path = "crates/store/re_uri", version = "0.35.0", default-features = false } # crates/top: -re_sdk = { path = "crates/top/re_sdk", version = "=0.32.0-alpha.1", default-features = false } -rerun = { path = "crates/top/rerun", version = "=0.32.0-alpha.1", default-features = false } -rerun_c = { path = "crates/top/rerun_c", version = "=0.32.0-alpha.1", default-features = false } -rerun-cli = { path = "crates/top/rerun-cli", version = "=0.32.0-alpha.1", default-features = false } +re_sdk = { path = "crates/top/re_sdk", version = "0.35.0", default-features = false } +rerun = { path = "crates/top/rerun", version = "0.35.0", default-features = false } +rerun_c = { path = "crates/top/rerun_c", version = "0.35.0", default-features = false } +rerun-cli = { path = "crates/top/rerun-cli", version = "0.35.0", default-features = false } # crates/utils: -re_analytics = { path = "crates/utils/re_analytics", version = "=0.32.0-alpha.1", default-features = false } -re_arrow_util = { path = "crates/utils/re_arrow_util", version = "=0.32.0-alpha.1", default-features = false } -re_auth = { path = "crates/utils/re_auth", version = "=0.32.0-alpha.1", default-features = false } -re_backoff = { path = "crates/utils/re_backoff", version = "=0.32.0-alpha.1", default-features = false } -re_byte_size = { path = "crates/utils/re_byte_size", version = "=0.32.0-alpha.1", default-features = false } -re_capabilities = { path = "crates/utils/re_capabilities", version = "=0.32.0-alpha.1", default-features = false } -re_case = { path = "crates/utils/re_case", version = "=0.32.0-alpha.1", default-features = false } -re_crash_handler = { path = "crates/utils/re_crash_handler", version = "=0.32.0-alpha.1", default-features = false } -re_error = { path = "crates/utils/re_error", version = "=0.32.0-alpha.1", default-features = false } -re_format = { path = "crates/utils/re_format", version = "=0.32.0-alpha.1", default-features = false } -re_log = { path = "crates/utils/re_log", version = "=0.32.0-alpha.1", default-features = false } -re_memory = { path = "crates/utils/re_memory", version = "=0.32.0-alpha.1", default-features = false } -re_mutex = { path = "crates/utils/re_mutex", version = "=0.32.0-alpha.1", default-features = false } -re_perf_telemetry = { path = "crates/utils/re_perf_telemetry", version = "=0.32.0-alpha.1", default-features = false } -re_quota_channel = { path = "crates/utils/re_quota_channel", version = "=0.32.0-alpha.1", default-features = false } -re_ros_msg = { path = "crates/utils/re_ros_msg", version = "=0.32.0-alpha.1", default-features = false } -re_rvl = { path = "crates/utils/re_rvl", version = "=0.32.0-alpha.1", default-features = false } -re_span = { path = "crates/utils/re_span", version = "=0.32.0-alpha.1", default-features = false } -re_string_interner = { path = "crates/utils/re_string_interner", version = "=0.32.0-alpha.1", default-features = false } -re_tracing = { path = "crates/utils/re_tracing", version = "=0.32.0-alpha.1", default-features = false } -re_tuid = { path = "crates/utils/re_tuid", version = "=0.32.0-alpha.1", default-features = false } -re_video = { path = "crates/utils/re_video", version = "=0.32.0-alpha.1", default-features = false } +re_analytics = { path = "crates/utils/re_analytics", version = "0.35.0", default-features = false } +re_arrow_util = { path = "crates/utils/re_arrow_util", version = "0.35.0", default-features = false } +re_auth = { path = "crates/utils/re_auth", version = "0.35.0", default-features = false } +re_backoff = { path = "crates/utils/re_backoff", version = "0.35.0", default-features = false } +re_byte_size = { path = "crates/utils/re_byte_size", version = "0.35.0", default-features = false } +re_byte_size_derive = { path = "crates/utils/re_byte_size_derive", version = "0.35.0", default-features = false } +re_capabilities = { path = "crates/utils/re_capabilities", version = "0.35.0", default-features = false } +re_case = { path = "crates/utils/re_case", version = "0.35.0", default-features = false } +re_crash_handler = { path = "crates/utils/re_crash_handler", version = "0.35.0", default-features = false } +re_error = { path = "crates/utils/re_error", version = "0.35.0", default-features = false } +re_format = { path = "crates/utils/re_format", version = "0.35.0", default-features = false } +re_grpc_headers = { path = "crates/utils/re_grpc_headers", version = "0.35.0", default-features = false } +re_log = { path = "crates/utils/re_log", version = "0.35.0", default-features = false } +re_memory = { path = "crates/utils/re_memory", version = "0.35.0", default-features = false } +re_mutex = { path = "crates/utils/re_mutex", version = "0.35.0", default-features = false } +re_perf_telemetry = { path = "crates/utils/re_perf_telemetry", version = "0.35.0", default-features = false } +re_quota_channel = { path = "crates/utils/re_quota_channel", version = "0.35.0", default-features = false } +re_ros_msg = { path = "crates/utils/re_ros_msg", version = "0.35.0", default-features = false } +re_rvl = { path = "crates/utils/re_rvl", version = "0.35.0", default-features = false } +re_span = { path = "crates/utils/re_span", version = "0.35.0", default-features = false } +re_string_interner = { path = "crates/utils/re_string_interner", version = "0.35.0", default-features = false } +re_test_mocks = { path = "crates/utils/re_test_mocks", version = "0.35.0", default-features = false } +re_tracing = { path = "crates/utils/re_tracing", version = "0.35.0", default-features = false } +re_tuid = { path = "crates/utils/re_tuid", version = "0.35.0", default-features = false } +re_video = { path = "crates/utils/re_video", version = "0.35.0", default-features = false } # crates/viewer: -re_arrow_ui = { path = "crates/viewer/re_arrow_ui", version = "=0.32.0-alpha.1", default-features = false } -re_blueprint_tree = { path = "crates/viewer/re_blueprint_tree", version = "=0.32.0-alpha.1", default-features = false } -re_chunk_store_ui = { path = "crates/viewer/re_chunk_store_ui", version = "=0.32.0-alpha.1", default-features = false } -re_component_fallbacks = { path = "crates/viewer/re_component_fallbacks", version = "=0.32.0-alpha.1", default-features = false } -re_component_ui = { path = "crates/viewer/re_component_ui", version = "=0.32.0-alpha.1", default-features = false } -re_context_menu = { path = "crates/viewer/re_context_menu", version = "=0.32.0-alpha.1", default-features = false } -re_data_ui = { path = "crates/viewer/re_data_ui", version = "=0.32.0-alpha.1", default-features = false } -re_dataframe_ui = { path = "crates/viewer/re_dataframe_ui", version = "=0.32.0-alpha.1", default-features = false } -re_memory_view = { path = "crates/viewer/re_memory_view", version = "=0.32.0-alpha.1", default-features = false } -re_plot = { path = "crates/viewer/re_plot", version = "=0.32.0-alpha.1", default-features = false } -re_recording_panel = { path = "crates/viewer/re_recording_panel", version = "=0.32.0-alpha.1", default-features = false } -re_redap_browser = { path = "crates/viewer/re_redap_browser", version = "=0.32.0-alpha.1", default-features = false } -re_renderer = { path = "crates/viewer/re_renderer", version = "=0.32.0-alpha.1", default-features = false } -re_renderer_examples = { path = "crates/viewer/re_renderer_examples", version = "=0.32.0-alpha.1", default-features = false } -re_selection_panel = { path = "crates/viewer/re_selection_panel", version = "=0.32.0-alpha.1", default-features = false } -re_test_context = { path = "crates/viewer/re_test_context", version = "=0.32.0-alpha.1", default-features = false } -re_test_viewport = { path = "crates/viewer/re_test_viewport", version = "=0.32.0-alpha.1", default-features = false } -re_time_panel = { path = "crates/viewer/re_time_panel", version = "=0.32.0-alpha.1", default-features = false } -re_ui = { path = "crates/viewer/re_ui", version = "=0.32.0-alpha.1", default-features = false } -re_view = { path = "crates/viewer/re_view", version = "=0.32.0-alpha.1", default-features = false } -re_view_bar_chart = { path = "crates/viewer/re_view_bar_chart", version = "=0.32.0-alpha.1", default-features = false } -re_view_dataframe = { path = "crates/viewer/re_view_dataframe", version = "=0.32.0-alpha.1", default-features = false } -re_view_graph = { path = "crates/viewer/re_view_graph", version = "=0.32.0-alpha.1", default-features = false } -re_view_map = { path = "crates/viewer/re_view_map", version = "=0.32.0-alpha.1", default-features = false } -re_view_spatial = { path = "crates/viewer/re_view_spatial", version = "=0.32.0-alpha.1", default-features = false } -re_view_status = { path = "crates/viewer/re_view_status", version = "=0.32.0-alpha.1", default-features = false } -re_view_tensor = { path = "crates/viewer/re_view_tensor", version = "=0.32.0-alpha.1", default-features = false } -re_view_text_document = { path = "crates/viewer/re_view_text_document", version = "=0.32.0-alpha.1", default-features = false } -re_view_text_log = { path = "crates/viewer/re_view_text_log", version = "=0.32.0-alpha.1", default-features = false } -re_view_time_series = { path = "crates/viewer/re_view_time_series", version = "=0.32.0-alpha.1", default-features = false } -re_viewer = { path = "crates/viewer/re_viewer", version = "=0.32.0-alpha.1", default-features = false } -re_viewer_context = { path = "crates/viewer/re_viewer_context", version = "=0.32.0-alpha.1", default-features = false } -re_viewport = { path = "crates/viewer/re_viewport", version = "=0.32.0-alpha.1", default-features = false } -re_viewport_blueprint = { path = "crates/viewer/re_viewport_blueprint", version = "=0.32.0-alpha.1", default-features = false } -re_web_viewer_server = { path = "crates/viewer/re_web_viewer_server", version = "=0.32.0-alpha.1", default-features = false } +re_arrow_ui = { path = "crates/viewer/re_arrow_ui", version = "0.35.0", default-features = false } +re_blueprint_tree = { path = "crates/viewer/re_blueprint_tree", version = "0.35.0", default-features = false } +re_chunk_store_ui = { path = "crates/viewer/re_chunk_store_ui", version = "0.35.0", default-features = false } +re_component_fallbacks = { path = "crates/viewer/re_component_fallbacks", version = "0.35.0", default-features = false } +re_component_ui = { path = "crates/viewer/re_component_ui", version = "0.35.0", default-features = false } +re_context_menu = { path = "crates/viewer/re_context_menu", version = "0.35.0", default-features = false } +re_data_ui = { path = "crates/viewer/re_data_ui", version = "0.35.0", default-features = false } +re_dataframe_ui = { path = "crates/viewer/re_dataframe_ui", version = "0.35.0", default-features = false } +re_gamepad = { path = "crates/viewer/re_gamepad", version = "0.35.0", default-features = false } +re_memory_view = { path = "crates/viewer/re_memory_view", version = "0.35.0", default-features = false } +re_plot = { path = "crates/viewer/re_plot", version = "0.35.0", default-features = false } +re_recording_panel = { path = "crates/viewer/re_recording_panel", version = "0.35.0", default-features = false } +re_redap_browser = { path = "crates/viewer/re_redap_browser", version = "0.35.0", default-features = false } +re_renderer = { path = "crates/viewer/re_renderer", version = "0.35.0", default-features = false } +re_renderer_examples = { path = "crates/viewer/re_renderer_examples", version = "0.35.0", default-features = false } +re_selection_panel = { path = "crates/viewer/re_selection_panel", version = "0.35.0", default-features = false } +re_test_context = { path = "crates/viewer/re_test_context", version = "0.35.0", default-features = false } +re_test_viewport = { path = "crates/viewer/re_test_viewport", version = "0.35.0", default-features = false } +re_time_panel = { path = "crates/viewer/re_time_panel", version = "0.35.0", default-features = false } +re_time_ruler = { path = "crates/viewer/re_time_ruler", version = "0.35.0", default-features = false } +re_ui = { path = "crates/viewer/re_ui", version = "0.35.0", default-features = false } +re_view = { path = "crates/viewer/re_view", version = "0.35.0", default-features = false } +re_view_bar_chart = { path = "crates/viewer/re_view_bar_chart", version = "0.35.0", default-features = false } +re_view_dataframe = { path = "crates/viewer/re_view_dataframe", version = "0.35.0", default-features = false } +re_view_graph = { path = "crates/viewer/re_view_graph", version = "0.35.0", default-features = false } +re_view_map = { path = "crates/viewer/re_view_map", version = "0.35.0", default-features = false } +re_view_spatial = { path = "crates/viewer/re_view_spatial", version = "0.35.0", default-features = false } +re_view_state_timeline = { path = "crates/viewer/re_view_state_timeline", version = "0.35.0", default-features = false } +re_view_tensor = { path = "crates/viewer/re_view_tensor", version = "0.35.0", default-features = false } +re_view_text_document = { path = "crates/viewer/re_view_text_document", version = "0.35.0", default-features = false } +re_view_text_log = { path = "crates/viewer/re_view_text_log", version = "0.35.0", default-features = false } +re_view_time_series = { path = "crates/viewer/re_view_time_series", version = "0.35.0", default-features = false } +re_viewer = { path = "crates/viewer/re_viewer", version = "0.35.0", default-features = false } +re_viewer_context = { path = "crates/viewer/re_viewer_context", version = "0.35.0", default-features = false } +re_viewer_mcp = { path = "crates/viewer/re_viewer_mcp", version = "0.35.0", default-features = false } +re_viewport = { path = "crates/viewer/re_viewport", version = "0.35.0", default-features = false } +re_viewport_blueprint = { path = "crates/viewer/re_viewport_blueprint", version = "0.35.0", default-features = false } +re_web_viewer_server = { path = "crates/viewer/re_web_viewer_server", version = "0.35.0", default-features = false } # Rerun crates in other repos: -re_mp4 = "0.4.0" +quiver = { version = "0.5.0", default-features = false } +re_mp4 = "0.5.1" # If this package fails to build, install `nasm` locally, or build through `pixi`. # NOTE: we use `dav1d` as an alias for our own re_rav1d crate @@ -171,74 +181,75 @@ dav1d = { package = "re_rav1d", version = "0.1.3", default-features = false } # dav1d = { version = "0.10.3" } # Requires separate install of `dav1d` library. Fast in debug builds. Useful for development. # core egui-crates: -ecolor = "0.34.0" -eframe = { version = "0.34.0", default-features = false, features = [ +ecolor = "0.35.0" +eframe = { version = "0.35.0", default-features = false, features = [ "accesskit", "default_fonts", + "inspection", "wayland", "x11", ] } -egui = { version = "0.34.0", features = ["callstack", "color-hex", "rayon"] } -egui_extras = { version = "0.34.0", features = ["http", "image", "serde", "svg"] } -egui_kittest = { version = "0.34.0", features = ["wgpu", "snapshot", "eframe"] } -egui-wgpu = "0.34.0" -emath = "0.34.0" +egui = { version = "0.35.0", features = ["callstack", "color-hex", "rayon"] } +egui_extras = { version = "0.35.0", features = ["http", "image", "serde", "svg"] } +egui_inspection = { version = "0.35.0", default-features = false, features = ["plugin"] } +egui_kittest = { version = "0.35.0", features = ["wgpu", "snapshot", "eframe"] } +egui_mcp = { version = "0.1.0" } +egui-wgpu = "0.35.0" +emath = "0.35.0" # other egui crates: -egui_commonmark = { version = "0.23.0", default-features = false } -egui_dnd = { version = "0.15.0" } -egui_plot = "0.35.0" # https://github.com/emilk/egui_plot -egui_table = "0.8.0" # https://github.com/rerun-io/egui_table -egui_tiles = "0.15.0" # https://github.com/rerun-io/egui_tiles -walkers = "0.53.0" +egui_commonmark = { version = "0.24.0", default-features = false } +egui_dnd = { version = "0.16.0" } +egui_plot = "0.36.0" # https://github.com/emilk/egui_plot +egui_table = "0.9.0" # https://github.com/rerun-io/egui_table +egui_tiles = "0.16.0" # https://github.com/rerun-io/egui_tiles +walkers = "0.56.0" # All of our direct external dependencies should be found here: -ahash = "0.8" -anyhow = { version = "1.0.102", default-features = false } -argh = "0.1.15" -arrayvec = "0.7" +ahash = "0.8.12" +anyhow = { version = "1.0", default-features = false } +argh = "0.1.19" +arrayvec = "0.7.6" array-init = "2.1" -arrow = { version = "57.3.0", default-features = false, features = [ +arrow = { version = "58.3", default-features = false, features = [ # NOTE: Similar to `datafusion`, we enable many features on a workspace level # to avoid re-compilation when changing compile targets. "ffi", "ipc", "json", ] } -async-stream = "0.3" +async-stream = "0.3.6" async-trait = "0.1.89" -axum = "0.8.8" -backtrace = "0.3" -base64 = "0.22" +axum = "0.8.9" +backtrace = "0.3.76" +base64 = "0.22.1" bincode = "1.3" -bit-vec = "0.9" -bitflags = { version = "2.11", features = ["bytemuck"] } +bit-vec = "0.9.1" +bitflags = { version = "2.13", features = ["bytemuck"] } bytemuck = { version = "1.25", features = ["extern_crate_alloc"] } -byteorder = "1.5.0" -bytes = "1.11.1" -camino = "1.2.2" +byteorder = "1.5" +bytes = "1.12" +camino = "1.2" cargo_metadata = "0.23.1" cargo-run-wasm = "0.4.0" -cdr-encoding = "0.10.2" cfg_aliases = "0.2.1" -cfg-if = "1.0.4" -chrono = { version = "0.4.44", default-features = false } # Needed for datafusion, see `re_datafusion`'s Cargo.toml -clang-format = "0.3" -clap = { version = "4.5.60", features = ["derive"] } +chrono = { version = "0.4.45", default-features = false } # Needed for datafusion, see `re_datafusion`'s Cargo.toml +clang-format = "0.3.0" +clap = { version = "4.6", features = ["derive"] } clean-path = "0.2.1" colored = "2.2" # Old b/c of dify -comfy-table = { version = "7.2.2", default-features = false } +comfy-table = { version = "7.2", default-features = false } console_error_panic_hook = "0.1.7" -const_format = "0.2.35" +const_format = "0.2.36" convert_case = "0.11.0" -criterion = "0.5.1" +criterion = "0.8.2" cros-codecs = "0.0.6" crossbeam = "0.8.4" dae-parser = "0.11.0" -datafusion = { version = "52.5.0", default-features = false, features = [ +datafusion = { version = "53.1", default-features = false, features = [ # NOTE: we enable the same features everywhere # because otherwise we will recompile datafusion all the time based on our current compile target. - # The features here are the same as in https://github.com/lance-format/lance/blob/v3.0.0/Cargo.toml#L116-L123 + # The features here are the same as in https://github.com/rerun-io/lance/blob/release-8.0.0/Cargo.toml#L125 # This is very hacky, and I don't like it. "crypto_expressions", "datetime_expressions", @@ -249,78 +260,98 @@ datafusion = { version = "52.5.0", default-features = false, features = [ "string_expressions", "unicode_expressions", ] } -datafusion-ffi = "52.5.0" +datafusion-ffi = "53.1" directories = "6.0" document-features = "0.2.12" econtext = "0.2.0" # Prints error contexts on crashes ehttp = "0.7.1" -enumset = "1.1.10" -env_filter = { version = "1.0.0", default-features = false } -env_logger = { version = "0.11.9", default-features = false } -ffmpeg-sidecar = { version = "2.4.0", default-features = false } +enumset = "1.1" +env_filter = { version = "1.0", default-features = false } +ffmpeg-sidecar = { version = "2.5", default-features = false } fixed = { version = "1.30", default-features = false } fjadra = "0.2.1" -flatbuffers = "25.12.19" +flatbuffers = "25.12" futures = "0.3.32" futures-util = "0.3.32" getrandom = "0.3.4" getrandom02 = { package = "getrandom", version = "0.2.17" } -glam = { version = "0.30.10", features = ["debug-glam-assert", "serde"] } +gilrs = "0.11.1" +glam = { version = "0.30.10", features = [ + "debug-glam-assert", + "serde", +] } # pinned to 0.30 by `macaw`; 0.33 would duplicate glam. Bump once macaw releases a glam-0.33 version. glob = "0.3.3" gltf = "1.4" h264-reader = "0.8.0" -half = { version = "2.7.1", features = ["bytemuck"] } -hexasphere = "16.0.0" # Update in tandem with glam -hmac = "0.12.1" +half = { version = "2.7", features = ["bytemuck"] } +hdf5-pure = "=0.21.1" # Very young crate (first release 2026-06) — pin exactly, bump deliberately. +hexasphere = "16.0" # Update in tandem with glam +hmac = "0.12.1" # pinned by `jsonwebtoken` (RustCrypto family); bump once jsonwebtoken moves to hmac 0.13/sha2 0.11/signature 3. home = "0.5.12" -http = "1.4.0" -http-body = "1.0.1" +http = "1.4" +http-body = "1.0" image = { version = "0.25.6", default-features = false, features = ["jpeg", "png"] } indent = "0.1.1" -indexmap = { version = "2.13", features = [ +indexmap = { version = "2.14", features = [ # indexmap version chosen to align with other dependencies "std", "serde", ] } indicatif = "0.18.4" # Progress bar -infer = "0.16.0" # infer MIME type by checking the magic number signaturefer MIME type by checking the magic number signature -insta = "1.46" -itertools = "0.14.0" -jiff = { version = "0.2.23", features = ["js"] } +infer = "0.19.0" # infer MIME type by checking the magic number signature +insta = "1.48" +itertools = "0.14.0" # 0.15 would duplicate the 0.14 that `datafusion` (pinned) requires. Bump once datafusion moves to itertools 0.15. +jiff = { version = "0.2.29", features = ["js"] } js-sys = "0.3.94" -jsonwebtoken = { version = "10.3", default-features = false } -lance = { version = "3.0.0", default-features = false } # When you update this, also update the list of features enabled for `datafusion` (~50 lines up) -lance-index = { version = "3.0.0", default-features = false } -lance-linalg = { version = "3.0.0", default-features = false } -libc = "0.2.182" +jsonwebtoken = { version = "10.4", default-features = false } +lance = { version = "8.0", default-features = false } # When you update this, also update the list of features enabled for `datafusion` (~50 lines up) +libc = "0.2.186" linked-hash-map = { version = "0.5.6", default-features = false } -log = "0.4.29" +log = "0.4.33" log-once = "0.4.1" -lz4_flex = "0.13" +lz4_flex = "0.13.0" macaw = "0.30.0" -mcap = "0.24.0" -memmap2 = "0.9.10" +mcap = "0.25.0" +memmap2 = "0.9.11" memory-stats = "1.2" -mimalloc = { version = "0.1.48", features = ["v3"] } +mimalloc = { version = "=0.1.48", features = [ + # A) we want to use v3 because of https://github.com/rerun-io/rerun/pull/11703 + # B) we need to pin to 0.1.48 because of https://github.com/microsoft/mimalloc/issues/1287 + "v3", +] } # Waiting for fix from microsoft/mimalloc/issues/1287 to land in mimalloc crate mime_guess2 = "2.3" # infer MIME type by file extension, and map mime to file extension mint = "0.5.9" -natord = "1.0.9" -ndarray = "0.16.1" +natord = "1.0" +ndarray = "0.16.1" # pinned by `lance-index` (lance family) and `numpy` to 0.16; 0.17 would duplicate. never = "0.1.0" nohash-hasher = "0.2.0" notify = { version = "8.2", features = ["macos_kqueue"] } +# `unicode-segmentation` is off so that match indices are plain `char` indices, +# which is what we index with when highlighting matched characters. +nucleo-matcher = { version = "0.3.1", default-features = false, features = [ + "unicode-casefold", + "unicode-normalization", +] } num-derive = "0.4.2" num-traits = "0.2.19" -numpy = "0.26.0" -opentelemetry = { version = "0.31.0", features = ["metrics"] } -opentelemetry-appender-tracing = "0.31.1" -opentelemetry-http = "0.31.0" -opentelemetry-otlp = { version = "0.31.0", features = ["gzip-tonic"] } -opentelemetry-proto = { version = "0.31.0", default-features = false } -opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio"] } -ordered-float = "5.1.0" +numpy = "0.28.0" +opentelemetry = { version = "0.32.0", features = ["metrics"] } +opentelemetry-appender-tracing = "0.32.0" +opentelemetry-http = "0.32.0" +opentelemetry-otlp = { version = "0.32.0", default-features = false, features = [ + "grpc-tonic", + "gzip-tonic", + "http-proto", + "hyper-client", + "trace", + "metrics", + "logs", +] } +opentelemetry-proto = { version = "0.32.0", default-features = false } +opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio"] } +ordered-float = "5.3" parking_lot = { version = "0.12.5", features = ["serde"] } -parquet = { version = "57.3.0", default-features = false } +parquet = { version = "58.3", default-features = false } paste = "1.0" pathdiff = "0.2.3" percent-encoding = "2.3" @@ -330,77 +361,84 @@ ply-rs-bw = { version = "=3.0.0", default-features = false } # ply-rs-bw has rel poll-promise = "0.3.0" pollster = "0.4.0" prettyplease = "0.2.37" -proc-macro2 = { version = "1.0.106", default-features = false } -profiling = { version = "1.0.17", default-features = false } -prometheus-client = "0.24.0" -prost = "0.14.3" -prost-build = "0.14.3" -prost-reflect = "0.16.3" -prost-types = "0.14.3" +proc-macro-crate = "3.5" +proc-macro2 = { version = "1.0", default-features = false } +profiling = { version = "1.0", default-features = false } +prometheus-client = "0.25.0" +prost = "0.14.4" +prost-build = "0.14.4" +prost-reflect = "0.16.4" +prost-types = "0.14.4" protoc-prebuilt = "0.3.0" -puffin = "0.19.1" -puffin_http = "0.16.1" -pyo3 = "0.26.0" -pyo3-build-config = "0.26.0" -quote = "1.0.45" -rand = { version = "0.9.2", default-features = false, features = [ +puffin = "0.20.0" +puffin_http = "0.17.0" +pyo3 = "0.28.3" +pyo3-build-config = "0.28.3" +quote = "1.0" +rand = { version = "0.9.4", default-features = false, features = [ "small_rng", "std", "thread_rng", ] } rand_distr = { version = "0.5.1", default-features = false, features = ["std"] } raw-window-handle = "0.6.2" -rayon = "1.11" +rayon = "1.12" +re_cdr = "0.1" regex-lite = "0.1.9" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.12.22", default-features = false, features = [ + "rustls-tls", +] } # 0.13 blocked: `walkers` (latest 0.54) and `http-cache-reqwest` still depend on reqwest 0.12. Bump once walkers moves to reqwest 0.13 (also unblocks tower-http 0.7). rexif = "0.7.5" rfd = { version = "0.17.2", default-features = false, features = ["xdg-portal"] } -ron = { version = "0.12.0", features = ["integer128"] } -roxmltree = "0.20.0" +ron = { version = "0.12.1", features = ["integer128"] } +roxmltree = "0.20.0" # 0.21 would duplicate the 0.20 that `resvg` (via egui_extras) requires. Bump once egui_extras/resvg move to roxmltree 0.21. ring = "0.17.14" -rustls = { version = "0.23.37", default-features = false } -saturating_cast = "0.1" +rustls = { version = "0.23.40", default-features = false } +saturating_cast = "0.1.0" scuffle-av1 = "0.1.4" scuffle-bytes-util = "0.1.5" -semver = "1.0.27" +semver = "1.0" seq-macro = "0.3.6" serde = { version = "1.0", features = ["derive"] } serde_bytes = "0.11.19" serde_json = { version = "1.0", default-features = false, features = ["std"] } serde-wasm-bindgen = "0.6.5" -sha2 = "0.10.9" -signature = { version = "2.2", features = ["std"] } -similar-asserts = "1.7.0" -slotmap = { version = "1.1.1", features = ["serde"] } +sha2 = "0.10.9" # pinned by `jsonwebtoken` (RustCrypto family); see hmac note. +signature = { version = "2.2", features = [ + "std", +] } # pinned by `jsonwebtoken` (^2.2); see hmac note. +similar-asserts = "1.7" # 2.0 pulls in `similar` 3.x, duplicating the `similar` 2.x that `insta` needs (cargo-deny ban). Bump once `insta` moves to `similar` 3. +slotmap = { version = "1.1", features = ["serde"] } smallvec = { version = "1.15", features = ["const_generics", "union"] } static_assertions = "1.1" -stl_io = "0.10.0" +stl_io = "0.11.0" strum = { version = "0.26.3", features = ["derive"] } # need to update re_rav1d first strum_macros = "0.26.4" # need to update re_rav1d first -sublime_fuzzy = "0.7.0" syn = "2.0" sysinfo = { version = "0.38.4", default-features = false } -tap = "1.0.1" -tempfile = "3.26" -thiserror = "2.0.18" -tiff = "0.9.1" +tap = "1.0" +tempfile = "3.27" +thiserror = "2.0" +tiff = "0.9.1" # 0.11 duplicates `png` (via the older `image`/`png` stack) and adds a `DecodingResult::F16` variant. Bump once `image` moves to the matching `png`. tiny_http = { version = "0.12.0", default-features = false } tobj = "4.0" -tokio = { version = "1.50.0", default-features = false } +tokio = { version = "1.52", default-features = false } tokio-stream = "0.1.18" tokio-util = { version = "0.7.18", default-features = false } -toml = { version = "1.0.6", default-features = false } -tonic = { version = "0.14.2", default-features = false } -tonic-prost = { version = "0.14.2", default-features = false } -tonic-prost-build = { version = "0.14.2", default-features = false } -tonic-web = "0.14.2" -tonic-web-wasm-client = "0.8.0" +toml = { version = "1.1", default-features = false } +tonic = { version = "0.14.6", default-features = false } +tonic-prost = { version = "0.14.6", default-features = false } +tonic-prost-build = { version = "0.14.6", default-features = false } +tonic-web = "0.14.6" +# tonic-web-wasm-client = "0.8.0" # 0.9 pulls wasm-streams 0.5, duplicating the 0.4 from reqwest 0.12. Update when we update to reqwest 0.13. +tonic-web-wasm-client = { version = "0.8.1", package = "rerun-tonic-web-wasm-client" } # our fork of 0.8.1 tower = "0.5.3" -tower-http = "0.6.8" +tower-http = "0.6.11" # 0.7 duplicates the 0.6 that reqwest 0.12 (via http-cache-reqwest/walkers) requires. Update when we update to reqwest 0.13. tower-service = "0.3.3" tracing = "0.1.44" -tracing-opentelemetry = "0.32.1" -tracing-subscriber = { version = "0.3.22", features = ["tracing-log", "fmt", "env-filter"] } +tracing-log = "0.2.0" # Route `log` through `tracing`. +tracing-opentelemetry = "0.33.0" +tracing-subscriber = { version = "0.3.23", features = ["tracing-log", "fmt", "env-filter"] } tracing-tracy = { version = "0.11.4", default-features = false, features = [ "broadcast", "callstack-inlines", @@ -410,16 +448,17 @@ tracing-tracy = { version = "0.11.4", default-features = false, features = [ "ondemand", # much nicer for a long-lived program "system-tracing", ] } # no sampling, it's very noisy and not that useful +tracing-web = "0.1.3" type-map = "0.5.1" -typenum = "1.19" +typenum = "1.20" unindent = "0.2.4" urdf-rs = "0.9.0" -ureq = "3.3.0" -url = "2.5.8" -uuid = { version = "1.21", features = ["serde", "v4", "js"] } +ureq = { version = "3.3", features = ["json"] } +url = "2.5" +uuid = { version = "1.23", features = ["serde", "v4", "js"] } vec1 = { version = "1.12", features = ["serde", "smallvec-v1"] } walkdir = "2.5" -wildmatch = "2.6.1" +wildmatch = "2.6" # TODO(#8766): `rerun_js/web-viewer/build-wasm.mjs` is HIGHLY sensitive to changes in `wasm-bindgen`. # Whenever updating `wasm-bindgen`, update this and the narrower dependency specifications in # `crates/viewer/re_viewer/Cargo.toml`, and make sure that notebooks still work: @@ -432,17 +471,20 @@ wildmatch = "2.6.1" wasm-bindgen = "0.2.117" # ⚠️ read above notice before touching this! wasm-bindgen-cli-support = "0.2.117" # ⚠️ read above notice before touching this! wasm-bindgen-futures = "0.4.67" +wasm-bindgen-test = "0.3.67" +wayland-client = "0.31.14" +wayland-protocols = { version = "0.32.13", default-features = false } +wayland-sys = "0.31.11" web-sys = "0.3.94" -wayland-sys = "0.31.9" -web-time = "1.1.0" -webbrowser = "1.1" -windows-core = { version = "0.62", default-features = false, features = [ +web-time = "1.1" +webbrowser = "1.2" +windows-core = { version = "0.62.2", default-features = false, features = [ "std", ] } # Ensure `std` is enabled so `windows-result::Error` impls `core::error::Error` (needed by wgpu-hal gles on Windows) winit = { version = "0.30.13", default-features = false } # TODO(andreas): Try to get rid of `fragile-send-sync-non-atomic-wasm`. This requires re_renderer being aware of single-thread restriction on resources. # See also https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer (unsolved part of the Spec as of writing!) -wgpu = { version = "29.0.1", default-features = false, features = [ +wgpu = { version = "29.0", default-features = false, features = [ # Backends (see https://docs.rs/wgpu/latest/wgpu/#feature-flags) "gles", "metal", @@ -460,8 +502,8 @@ wgpu = { version = "29.0.1", default-features = false, features = [ "fragile-send-sync-non-atomic-wasm", ] } xshell = "0.2.7" -xxhash-rust = { version = "0.8", features = ["xxh32", "xxh64"] } -zip = { version = "8.2", default-features = false, features = ["deflate"] } +xxhash-rust = { version = "0.8.15", features = ["xxh32", "xxh64"] } +zip = { version = "8.6", default-features = false, features = ["deflate"] } # --------------------------------------------------------------------------------- [profile] @@ -491,6 +533,7 @@ debug = false "re_build_info".debug = true "re_build_tools".debug = true "re_byte_size".debug = true +"re_byte_size_derive".debug = true "re_capabilities".debug = true "re_case".debug = true "re_chunk_store_ui".debug = true @@ -509,6 +552,7 @@ debug = false "re_entity_db".debug = true "re_error".debug = true "re_format".debug = true +"re_gamepad".debug = true "re_grpc_client".debug = true "re_grpc_server".debug = true "re_integration_test".debug = true @@ -550,7 +594,7 @@ debug = false "re_view_graph".debug = true "re_view_map".debug = true "re_view_spatial".debug = true -"re_view_status".debug = true +"re_view_state_timeline".debug = true "re_view_tensor".debug = true "re_view_text_document".debug = true "re_view_text_log".debug = true @@ -579,7 +623,7 @@ debug-assertions = false ## Release [profile.release] -# debug = true # good for profilers +# debug = true # defaults to false, don't enable unless you really need it, and definitely don't enable for CI builds since it will cause considerable slowdown. panic = "abort" # This leads to better optimizations and smaller binaries (and is the default in Wasm anyways). lto = "thin" # This leads to smaller binaries, but slower compile times. codegen-units = 1 # Smaller binaries (but slower compile time) @@ -682,6 +726,7 @@ cloned_instead_of_copied = "warn" coerce_container_to_any = "warn" dbg_macro = "warn" debug_assert_with_mut_call = "warn" +decimal_bitwise_operands = "warn" default_union_representation = "warn" derive_partial_eq_without_eq = "warn" disallowed_macros = "warn" # See clippy.toml @@ -694,8 +739,9 @@ doc_comment_double_space_linebreaks = "warn" doc_include_without_cfg = "warn" doc_link_with_quotes = "warn" doc_markdown = "warn" +duration_suboptimal_units = "warn" elidable_lifetime_names = "warn" -empty_enum = "warn" +empty_enums = "warn" empty_enum_variants_with_brackets = "warn" empty_line_after_outer_attr = "warn" enum_glob_use = "warn" @@ -747,6 +793,7 @@ lossy_float_literal = "warn" macro_use_imports = "warn" manual_assert = "warn" manual_clamp = "warn" +manual_ilog2 = "warn" manual_instant_elapsed = "warn" manual_is_power_of_two = "warn" manual_is_variant_and = "warn" @@ -773,6 +820,7 @@ needless_continue = "warn" needless_for_each = "warn" needless_pass_by_ref_mut = "warn" needless_pass_by_value = "warn" +needless_type_cast = "warn" negative_feature_names = "warn" non_std_lazy_statics = "warn" non_zero_suggestions = "warn" @@ -796,6 +844,7 @@ ref_option_ref = "warn" rest_pat_in_fully_bound_structs = "warn" return_and_then = "warn" same_functions_in_if_condition = "warn" +same_length_and_capacity = "warn" self_only_used_in_recursion = "warn" semicolon_if_nothing_returned = "warn" set_contains_or_insert = "warn" @@ -829,6 +878,7 @@ unnecessary_safety_doc = "warn" unnecessary_self_imports = "warn" unnecessary_semicolon = "warn" unnecessary_struct_initialization = "warn" +unnecessary_trailing_comma = "warn" unnecessary_wraps = "warn" unnested_or_patterns = "warn" unused_async = "warn" @@ -848,9 +898,11 @@ zero_sized_map_values = "warn" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602 #self_named_module_files = "warn" +# Things we explicitly allow: manual_range_contains = "allow" # this one is just worse imho map_unwrap_or = "allow" # so is this one ref_patterns = "allow" # It's nice to avoid ref pattern, but there are some situations that are hard (impossible?) to express without. +too_many_arguments = "allow" # yes, we should avoid too many arguments, but enforcing it by lint is pointless. # TODO(emilk): enable more of these lints: cast_possible_truncation = "allow" # Moo much noise, sadly @@ -898,13 +950,13 @@ unnecessary_debug_formatting = "allow" # datafusion-physical-plan = { git = "https://github.com/rerun-io/arrow-datafusion.git", branch = "tsaucer/52.3.0-inner-ffi" } # datafusion-sql = { git = "https://github.com/rerun-io/arrow-datafusion.git", branch = "tsaucer/52.3.0-inner-ffi" } -# ecolor = { git = "https://github.com/emilk/egui.git", branch = "main" } -# eframe = { git = "https://github.com/emilk/egui.git", branch = "main" } -# egui = { git = "https://github.com/emilk/egui.git", branch = "main" } -# egui_extras = { git = "https://github.com/emilk/egui.git", branch = "main" } -# egui_kittest = { git = "https://github.com/emilk/egui.git", branch = "main" } -# egui-wgpu = { git = "https://github.com/emilk/egui.git", branch = "main" } -# emath = { git = "https://github.com/emilk/egui.git", branch = "main" } +# ecolor = { git = "https://github.com/emilk/egui", branch = "main" } +# eframe = { git = "https://github.com/emilk/egui", branch = "main" } +# egui = { git = "https://github.com/emilk/egui", branch = "main" } +# egui_extras = { git = "https://github.com/emilk/egui", branch = "main" } +# egui_kittest = { git = "https://github.com/emilk/egui", branch = "main" } +# egui-wgpu = { git = "https://github.com/emilk/egui", branch = "main" } +# emath = { git = "https://github.com/emilk/egui", branch = "main" } # kittest = { git = "https://github.com/rerun-io/kittest.git", rev = 'ce7a2f3b12c36021889b50bdff671cec8016b0fb' } @@ -918,18 +970,18 @@ unnecessary_debug_formatting = "allow" # emath = { path = "../../egui/crates/emath" } # wgpu = { path = "../../wgpu/wgpu" } -# egui_plot = { git = "https://github.com/emilk/egui_plot.git", branch = "legend-id-keying" } +# egui_plot = { git = "https://github.com/lucasmerlin/egui_plot.git", branch = "lucas/update-egui-0.35" } # egui_plot = { path = "../../../egui_plot/egui_plot" } # egui_tiles = { git = "https://github.com/rerun-io/egui_tiles", branch = "main" } # egui_tiles = { path = "../egui_tiles" } -# egui_table = { git = "https://github.com/rerun-io/egui_table", branch = "main" } +# egui_table = { git = "https://github.com/rerun-io/egui_table", branch = "lucas/update-egui" } # egui_table = { path = "../egui_table" } -# egui_dnd = { git = "https://github.com/rerun-io/hello_egui.git", branch = "emilk/egui-0.33.0" } +# egui_dnd = { git = "https://github.com/lucasmerlin/hello_egui.git", branch = "malmal/main" } -# egui_commonmark = { git = "https://github.com/rerun-io/egui_commonmark.git", branch = "lucas/update-egui-main" } +# egui_commonmark = { git = "https://github.com/rerun-io/egui_commonmark.git", branch = "lucas/update-egui-0.35" } # egui_commonmark = { path = "../../forks/egui_commonmark/egui_commonmark" } # walkers = { git = "https://github.com/rerun-io/walkers", branch = "emilk/egui-0.34" } diff --git a/DESIGN.md b/DESIGN.md index 601eb03966a1..d0762af762f3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -17,6 +17,22 @@ Good: `log("File saved")` Bad: `log("file saved.")` +#### Dashes +Use a spaced em dash (` — `) for parenthetical breaks in prose (docs, comments, log messages, UI text). + +Avoid: +- Unspaced em dashes (`word—word`) — add spaces around the em dash. +- En dashes (`–`) used as sentence punctuation — use an em dash instead. + +En dashes are reserved for numeric/range expressions (`2020–2025`, `pp. 10–15`, `~3–4 GB`). + +#### Line breaks in markdown +Write one sentence per line in markdown files (`.md`, docs, READMEs, agent guides). +Markdown joins consecutive non-empty lines into a single paragraph, so this does not affect rendering — but it produces much cleaner diffs. +Each edited sentence shows up as a single changed line, instead of reflowing an entire paragraph. + +Use a blank line between paragraphs as usual. + ### Buttons When a button action requires more input after pressing, suffix it with `…`. diff --git a/README.md b/README.md index 61f9a9043725..e0379bb08c0f 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,39 @@ -

+

- banner + Banner with Rerun logo + -

+ -

+

PyPi crates.io MIT Apache Rerun Discord -

+ + +# The data layer for physical AI -# Time-aware multimodal data stack and visualizations -Rerun is building the multimodal data stack to model, ingest, store, query and view robotics-style data. -It's used in areas like robotics, spatial and embodied AI, generative media, industrial processing, simulation, security, and health. +Log, query, visualize, and stream to training on shared columnar storage built for multimodal data. -Rerun is easy to use! -Use the Rerun SDK (available for C++, Python and Rust) to log data like images, tensors, point clouds, and text. -Logs are streamed to the Rerun Viewer for live visualization or to file for later use. -You can also query the logged data through [our dataframe API](https://rerun.io/docs/howto/query-and-transform/get-data-out). +**What it does:** Rerun ingests multi-rate, multimodal data (images, point clouds, transforms, time series, joint states, video) from many sources and formats (robot logs, human-data rigs, sim, web video; MCAP, rrd, LeRobot). The built-in viewer renders everything in sync, in realtime: scrub episodes, compare sensors side-by-side, watch CV pipelines run live. The same data is queryable with [dataframes](https://rerun.io/docs/howto/query-and-transform/get-data-out) or SQL, and streams directly into training. Built in Rust on column-chunk storage purpose-built for multi-rate physical data. SDKs in Python, Rust, and C++. -[Get started](#getting-started) in minutes – no account needed. +**Quickstart:** `pip install rerun-sdk` — log your first multimodal data and see it in the viewer in under 2 minutes. * [Run the Rerun Viewer in your browser](https://www.rerun.io/viewer) * [Read about what Rerun is and who it is for](https://www.rerun.io/docs/overview/what-is-rerun) +### Use cases +- Ingest robot logs, egocentric/UMI rigs, sim, and web video into one substrate +- Run CV pipelines (SLAM, hand tracking, motion retargeting) as table edits +- Query raw, intermediate, and derived data with dataframes or SQL +- Visualize multi-rate, multimodal sequences across the pipeline +- Stream dataset mixes directly to training — no export jobs, no stale copies + +### Data types +Multi-rate, multimodal, spatial: images, point clouds, time series, tensors, transforms, joint states, video. Preserved end-to-end. + ### A short taste ```py import rerun as rr # pip install rerun-sdk @@ -81,6 +89,18 @@ You should now be able to run `rerun --help` in any terminal. - ⁉️ [Troubleshooting](https://www.rerun.io/docs/overview/installing-rerun/troubleshooting) +### Agent skills +This repo ships a set of agent skills that help coding agents write Rerun code. + +Install them into your agent with the `skills` CLI: + +```sh +npx skills add rerun-io/rerun +``` + +The skills themselves live in [`skills/`](./skills) if you want to read them directly. + + ## Status We are in active development. There are many features we want to add, and the API is still evolving. @@ -138,7 +158,7 @@ You can adjust the size of this buffer to your needs (see [here](https://rerun.i ## Business model Rerun uses an open-core model. Everything in this repository will stay open source and free (both as in beer and as in freedom). -We are also building a commercial data platform. +We are also building Rerun Hub, a scalable catalog for robotic data. Right now that is only available for a few select design partners. [Click here if you're interested](https://rerun.io/pricing). diff --git a/RELEASES.md b/RELEASES.md index 85175f676b3c..fa341e1f6181 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -16,7 +16,8 @@ This document describes the current release and versioning strategy. This strate ## Release cadence -New Rerun versions are released approximately once every month. Sometimes we do out-of-schedule patch releases. +New Rerun versions are released every two weeks. Sometimes we do out-of-schedule patch releases. +We do not block a release on a PR. Incomplete work should be hidden behind a feature flag. ## Library versioning and release cadence @@ -97,8 +98,6 @@ The fastest way to get an overview of all the patch candidate PRs from both repo uv run scripts/fetch_patch_candidates.py ``` -When done, run [`cargo semver-checks`](https://github.com/obi1kenobi/cargo-semver-checks) to check that we haven't introduced any semver breaking changes. - After cherry-picking a commit into the patch, please make sure to remove the `consider-patch` label. ### 4. Update [`CHANGELOG.md`](./CHANGELOG.md) @@ -141,22 +140,25 @@ In the UI: This will create a one-off alpha release. - `rc` if the branch name is `prepare-release-x.y.z`. - This will create a pull request for the release, and publish a release candidate. + This will publish a release candidate. + + - `final` for the final public release. - - `final` for the final public release +In all three cases, the workflow opens (or updates) a release pull request against `main`. ![Image showing the Run workflow UI. It can be found at https://github.com/rerun-io/rerun/actions/workflows/release.yml](https://github.com/rerun-io/rerun/assets/1665677/6cdc8e7e-c0fc-4cf1-99cb-0749957b8328) -### 7. Wait for both workflows to finish +### 7. Wait for the release workflow to finish Once the release workflow is started, it will create a pull request for the release. The pull request description will tell you what to do next. -[The `Release` workflow](https://github.com/rerun-io/rerun/actions/workflows/release.yml) will build artifacts and run PR checks. -Additionally, if the release type is set to `final` or `rc`, it will spawn a second workflow (when the release artifacts have been published to PyPI, crates.io etc.) called [`GitHub Release`](https://github.com/rerun-io/rerun/actions/workflows/on_gh_release.yml). -This workflow is responsible for creating [the GitHub release draft](https://github.com/rerun-io/rerun/releases) and to publish the artifacts to it. -**Make sure this workflow also finishes!**. -Only after it finishes successfully should you un-draft [the GitHub release](https://github.com/rerun-io/rerun/releases). +[The `Release` workflow](https://github.com/rerun-io/rerun/actions/workflows/release.yml) will build artifacts, run PR checks, and publish them to PyPI, crates.io, npm, etc. +For `rc` and `final` releases it also creates a **draft** [GitHub release](https://github.com/rerun-io/rerun/releases) (in the `tag-release` job) and attaches a comment to the release PR pointing at it. + +Once the `Release` workflow has finished successfully and you've sanity-checked the artifacts, edit the GitHub release draft (changelog, header media) and click `Publish release`. +Publishing the release triggers the [`GitHub Release` workflow](https://github.com/rerun-io/rerun/actions/workflows/on_gh_release.yml), which syncs the binary assets from `build.rerun.io` onto the published GitHub release. +**Make sure that workflow also finishes successfully** so the release ends up with all of its assets attached. ### 8. Merge changes to `main` diff --git a/TESTING.md b/TESTING.md index a3d3bcefa506..53916c4895a3 100644 --- a/TESTING.md +++ b/TESTING.md @@ -72,13 +72,13 @@ Creating or updating snapshots is done by adding `--snapshot-update` to the pyte ## Redap tests -Redap stands for "Rerun data protocol." It is the interface between clients such as the Rerun viewer or SDK, and servers such as Rerun OSS or Rerun Cloud. +Redap stands for "Rerun data protocol." It is the interface between clients such as the Rerun viewer or SDK, and servers such as Rerun OSS or Rerun Hub. We have several test harnesses related to redap. ### `re_redap_tests` -This is a Rust-based compliance test suite that builds directly against the server's service handler. It is run both against the OSS server in this repository, and our Rerun's proprietary implementation Rerun Cloud. This test suite does not run through an actual gRPC connection. It directly links to the servers' code. +This is a Rust-based compliance test suite that builds directly against the server's service handler. It is run both against the OSS server in this repository, and our Rerun's proprietary implementation Rerun Hub. This test suite does not run through an actual gRPC connection. It directly links to the servers' code. This test suite is executed by the OSS server tests, so you can run it locally with: diff --git a/ci_docker/Dockerfile b/ci_docker/Dockerfile index 4c5c1f2017a0..7aa3db0b6def 100644 --- a/ci_docker/Dockerfile +++ b/ci_docker/Dockerfile @@ -2,7 +2,7 @@ FROM quay.io/pypa/manylinux_2_28 LABEL maintainer="opensource@rerun.io" # Remember to update the version in publish.sh # TODO(jleibs) use this version in the publish.sh script and below in the CACHE_KEY -LABEL version="0.17.0-x86-64" +LABEL version="0.18.0-x86-64" LABEL description="Docker image used for the CI of https://github.com/rerun-io/rerun" RUN set -eux; \ @@ -31,6 +31,7 @@ RUN set -eux; \ libxkbcommon-devel \ python3-pip \ tar \ + zstd \ jq \ sudo; \ dnf clean all; @@ -43,7 +44,7 @@ ENV RUSTUP_HOME=/usr/local/rustup \ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y # Increment this to invalidate cache -ENV CACHE_KEY=rerun_docker_v0.17.0 +ENV CACHE_KEY=rerun_docker_v0.18.0 # See: https://github.com/actions/runner-images/issues/6775#issuecomment-1410270956 RUN git config --system --add safe.directory '*' diff --git a/ci_docker/publish.sh b/ci_docker/publish.sh index 2069adef24f8..cebb07d0bba4 100755 --- a/ci_docker/publish.sh +++ b/ci_docker/publish.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eux -VERSION=0.17.0 # Bump on each new version. Remember to update the version in the Dockerfile too. +VERSION=0.18.0 # Bump on each new version. Remember to update the version in the Dockerfile too. # The build needs to run from top of repo to access the requirements.txt cd "$(git rev-parse --show-toplevel)/rerun" diff --git a/clippy.toml b/clippy.toml index b44a8e82337a..c72e1d579ecd 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- # Section identical to the main scripts/clippy_wasm/clippy.toml: -msrv = "1.92" +msrv = "1.95" allow-unwrap-in-tests = true @@ -29,6 +29,7 @@ too-many-lines-threshold = 600 # TODO(emilk): decrease this disallowed-macros = [ 'std::dbg', + { path = "cfg_if::cfg_if", reason = "Use the standard library's `cfg_select!` instead" }, { path = "egui::hex_color", reason = "Do not hard-code colors - declare them design_tokens.rs instead, and define in light/dark_theme.json" }, { path = "std::debug_assert", reason = "Use `re_log::debug_assert` instead" }, diff --git a/crates/build/re_build_info/Cargo.toml b/crates/build/re_build_info/Cargo.toml index bbd38124d7f0..19ccdecce850 100644 --- a/crates/build/re_build_info/Cargo.toml +++ b/crates/build/re_build_info/Cargo.toml @@ -22,12 +22,7 @@ all-features = true [features] default = [] -## Enable (de)serialization using serde. -serde = ["dep:serde"] - [dependencies] re_byte_size.workspace = true - -# Optional dependencies: -serde = { workspace = true, optional = true, features = ["derive", "rc"] } +serde = { workspace = true, features = ["derive", "rc"] } diff --git a/crates/build/re_build_info/src/crate_version.rs b/crates/build/re_build_info/src/crate_version.rs index 7c3d9a7be980..5c9f5d37f9e4 100644 --- a/crates/build/re_build_info/src/crate_version.rs +++ b/crates/build/re_build_info/src/crate_version.rs @@ -40,7 +40,7 @@ mod meta { /// - `11NNNNNN` -> `-alpha.N+dev` /// - `01NNNNNN` -> `-rc.N` /// - `00000000` -> none of the above -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, re_byte_size::SizeBytes)] pub struct CrateVersion { pub major: u8, pub minor: u8, @@ -127,8 +127,9 @@ impl CrateVersion { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive( + Clone, Copy, Debug, PartialEq, Eq, re_byte_size::SizeBytes, serde::Deserialize, serde::Serialize, +)] pub enum Meta { Rc(u8), Alpha(u8), @@ -516,18 +517,6 @@ impl std::fmt::Display for CrateVersion { } } -impl re_byte_size::SizeBytes for CrateVersion { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} - #[test] fn test_parse_version() { macro_rules! assert_parse_ok { diff --git a/crates/build/re_dev_tools/src/build_examples/rrd.rs b/crates/build/re_dev_tools/src/build_examples/rrd.rs index e97344f043a8..7c61730b3ed1 100644 --- a/crates/build/re_dev_tools/src/build_examples/rrd.rs +++ b/crates/build/re_dev_tools/src/build_examples/rrd.rs @@ -122,7 +122,7 @@ impl Example { cmd.arg("rrd"); cmd.arg("optimize"); // Small chunks for better streaming: - cmd.arg("--max-bytes").arg((128 * 1024).to_string()); + cmd.arg("--max-size").arg("128KiB"); cmd.arg(&initial_rrd_path); cmd.arg("-o").arg(&final_rrd_path); diff --git a/crates/build/re_dev_tools/src/build_search_index/ingest/python.rs b/crates/build/re_dev_tools/src/build_search_index/ingest/python.rs index 51a5d8aa9a65..ada06a641a6b 100644 --- a/crates/build/re_dev_tools/src/build_search_index/ingest/python.rs +++ b/crates/build/re_dev_tools/src/build_search_index/ingest/python.rs @@ -249,6 +249,7 @@ enum Item { struct Module { name: String, // labels: Vec, + #[serde(default)] members: BTreeMap, docstring: Option, } @@ -281,6 +282,7 @@ struct Docstring { struct Class { name: String, docstring: Option, + #[serde(default)] members: BTreeMap, } diff --git a/crates/build/re_dev_tools/src/build_search_index/repl.rs b/crates/build/re_dev_tools/src/build_search_index/repl.rs index 2ba460597b01..2f659335e3dd 100644 --- a/crates/build/re_dev_tools/src/build_search_index/repl.rs +++ b/crates/build/re_dev_tools/src/build_search_index/repl.rs @@ -72,7 +72,7 @@ impl Repl { .collect::>() .join("\n"); - println!("### {} [{}]", result.title(), result.url(),); + println!("### {} [{}]", result.title(), result.url()); if content.len() > 200 { println!("{}…\n", &content[..200]); } else { diff --git a/crates/build/re_dev_tools/src/build_web_viewer/lib.rs b/crates/build/re_dev_tools/src/build_web_viewer/lib.rs index 7c4314efdff7..a9f5d4b4c3a9 100644 --- a/crates/build/re_dev_tools/src/build_web_viewer/lib.rs +++ b/crates/build/re_dev_tools/src/build_web_viewer/lib.rs @@ -82,6 +82,7 @@ pub fn build( build_dir: &Utf8Path, no_default_features: bool, features: &String, + timings: bool, ) -> anyhow::Result<()> { std::env::set_current_dir(workspace_root())?; @@ -134,6 +135,9 @@ pub fn build( if profile == Profile::WebRelease { cmd.arg("--profile=web-release"); } + if timings { + cmd.arg("--timings"); + } // Note that we can't use RUSTFLAGS here directly since having more than one flag on set via // `cmd.env("RUSTFLAGS", rustflags)` completely messes up the rustc invocation no matter how we quote it. @@ -220,6 +224,14 @@ pub fn build( "--output", wasm_path.as_str(), "--enable-reference-types", + // We compile with `-Ctarget-feature=+simd128,+bulk-memory,+nontrapping-fptoint,+multivalue` + // (see `.cargo/config.toml`). The JS loader feature-detects SIMD + // before loading the .wasm; every other feature here shipped strictly + // earlier in Chrome/Firefox/Safari, so the SIMD check covers them too. + "--enable-simd", + "--enable-bulk-memory", + "--enable-nontrapping-float-to-int", + "--enable-multivalue", "--vacuum", ]; if debug_symbols { diff --git a/crates/build/re_dev_tools/src/build_web_viewer/mod.rs b/crates/build/re_dev_tools/src/build_web_viewer/mod.rs index 8fec588f6cb7..57ba101e8574 100644 --- a/crates/build/re_dev_tools/src/build_web_viewer/mod.rs +++ b/crates/build/re_dev_tools/src/build_web_viewer/mod.rs @@ -41,6 +41,10 @@ pub struct Args { /// whether to exclude default features from `re_viewer` wasm build #[argh(switch, long = "no-default-features")] no_default_features: bool, + + /// generate a cargo build timings report in `/cargo-timings/`. + #[argh(switch)] + timings: bool, } fn default_features() -> String { @@ -67,5 +71,6 @@ pub fn main(args: Args) -> anyhow::Result<()> { &build_dir, args.no_default_features, &args.features, + args.timings, ) } diff --git a/crates/build/re_protos_builder/src/lib.rs b/crates/build/re_protos_builder/src/lib.rs index 6fe5d2754dd6..90ce3ec3817b 100644 --- a/crates/build/re_protos_builder/src/lib.rs +++ b/crates/build/re_protos_builder/src/lib.rs @@ -21,11 +21,6 @@ where ".rerun.log_msg.v1alpha1", ".rerun.manifest_registry.v1alpha1", ]); - prost_config.enum_attribute( - ".rerun.cloud.v1alpha1.VectorDistanceMetric", - "#[derive(serde::Serialize, serde::Deserialize)]", - ); - if let Err(err) = tonic_prost_build::configure() .out_dir(output_dir) .build_client(true) diff --git a/crates/build/re_types_builder/src/codegen/cpp/mod.rs b/crates/build/re_types_builder/src/codegen/cpp/mod.rs index de519b20ebab..c4657bf77921 100644 --- a/crates/build/re_types_builder/src/codegen/cpp/mod.rs +++ b/crates/build/re_types_builder/src/codegen/cpp/mod.rs @@ -1031,23 +1031,25 @@ impl QuotedObject { let tag_typename = format_ident!("{pascal_case_name}Tag"); let data_typename = format_ident!("{pascal_case_name}Data"); - let tag_fields = std::iter::once({ - let comment = quote_doc_comment( - "Having a special empty state makes it possible to implement move-semantics. \ + let tag_fields = std::iter::chain( + std::iter::once({ + let comment = quote_doc_comment( + "Having a special empty state makes it possible to implement move-semantics. \ We need to be able to leave the object in a state which we can run the destructor on."); - let tag_name = format_ident!("None"); - quote! { - #NEWLINE_TOKEN - #comment - #tag_name = 0, - } - }) - .chain(obj.fields.iter().map(|obj_field| { - let ident = field_name_ident(obj_field); - quote! { - #ident, - } - })) + let tag_name = format_ident!("None"); + quote! { + #NEWLINE_TOKEN + #comment + #tag_name = 0, + } + }), + obj.fields.iter().map(|obj_field| { + let ident = field_name_ident(obj_field); + quote! { + #ident, + } + }), + ) .collect_vec(); hpp_includes.insert_system("utility"); // std::move @@ -1164,38 +1166,40 @@ impl QuotedObject { // No destructor needed quote! {} } else { - let destructor_match_arms = std::iter::once({ - let comment = quote_comment("Nothing to destroy"); - quote! { - case detail::#tag_typename::None: { - #NEWLINE_TOKEN - #comment - } break; - } - }) - .chain(obj.fields.iter().map(|obj_field| { - let tag_ident = field_name_ident(obj_field); - let field_ident = format_ident!("{}", obj_field.snake_case_name()); - - if obj_field.typ.has_default_destructor(objects) { - let comment = quote_comment("has a trivial destructor"); + let destructor_match_arms = std::iter::chain( + std::iter::once({ + let comment = quote_comment("Nothing to destroy"); quote! { - case detail::#tag_typename::#tag_ident: { + case detail::#tag_typename::None: { #NEWLINE_TOKEN #comment } break; } - } else { - let typ = quote_field_type(&mut hpp_includes, obj_field); - hpp_includes.insert_system("utility"); // std::move - quote! { - case detail::#tag_typename::#tag_ident: { - using TypeAlias = #typ; - _data.#field_ident.~TypeAlias(); - } break; + }), + obj.fields.iter().map(|obj_field| { + let tag_ident = field_name_ident(obj_field); + let field_ident = format_ident!("{}", obj_field.snake_case_name()); + + if obj_field.typ.has_default_destructor(objects) { + let comment = quote_comment("has a trivial destructor"); + quote! { + case detail::#tag_typename::#tag_ident: { + #NEWLINE_TOKEN + #comment + } break; + } + } else { + let typ = quote_field_type(&mut hpp_includes, obj_field); + hpp_includes.insert_system("utility"); // std::move + quote! { + case detail::#tag_typename::#tag_ident: { + using TypeAlias = #typ; + _data.#field_ident.~TypeAlias(); + } break; + } } - } - })) + }), + ) .collect_vec(); quote! { @@ -2006,7 +2010,7 @@ fn quote_fill_arrow_array_builder( ElementType::Float64 => Some("DoubleBuilder"), ElementType::Binary => Some("BinaryBuilder"), ElementType::String => Some("StringBuilder"), - ElementType::Object{..} => None, + ElementType::Object{..} | ElementType::Array{..} => None, }; if let Some(type_builder_name) = type_builder_name { @@ -2039,7 +2043,7 @@ fn quote_fill_arrow_array_builder( } } else { let variant_accessor = quote!(union_instance.get_union_data()); - quote_append_single_field_to_builder(variant, &variant_builder, &variant_accessor, includes) + quote_append_single_field_to_builder(objects, variant, &variant_builder, &variant_accessor, includes) }; quote! { @@ -2100,19 +2104,59 @@ fn quote_append_field_to_builder( if !field.is_nullable && matches!(field.typ, Type::Array { .. }) && elem_type.has_default_destructor(objects) + && !elem_type + .fqname() + .is_some_and(|fqname| objects[fqname].is_enum()) { // Optimize common case: Trivial batch of transparent fixed size elements. let field_accessor = quote!(elements[0].#field_name); let num_items_per_value = quote_num_items_per_value(&field.typ, &field_accessor); - quote! { + let setup = quote! { auto #value_builder = static_cast(#builder->value_builder()); #NEWLINE_TOKEN #NEWLINE_TOKEN ARROW_RETURN_NOT_OK(#builder->AppendValues(static_cast(num_elements))); static_assert(sizeof(elements[0].#field_name) == sizeof(elements[0])); - ARROW_RETURN_NOT_OK(#value_builder->AppendValues( - #field_accessor.data(), - static_cast(num_elements * #num_items_per_value), nullptr) + }; + if let ElementType::Object { fqname } = elem_type { + // Elements are structs: delegate the contiguous value range to the + // element type's own `fill_arrow_array_builder`. + let fqname = quote_fqname_as_type_path(includes, fqname); + quote! { + #setup + RR_RETURN_NOT_OK(Loggable<#fqname>::fill_arrow_array_builder( + #value_builder, + #field_accessor.data(), + num_elements * #num_items_per_value) + ); + } + } else if matches!(elem_type, ElementType::Array { .. }) { + // Elements are nested fixed-size arrays (e.g. `[[float; 3]; 2]`). + let append_contents = quote_append_nested_array_contents( + objects, + includes, + elem_type, + &value_builder, + "e!(#field_accessor.data()), + "e!(num_elements * #num_items_per_value), ); + quote! { + #setup + #append_contents + } + } else { + // `rerun::half` needs a cast because arrow takes it as `uint16_t`. + let value_ptr_accessor = if *elem_type == ElementType::Float16 { + quote!(reinterpret_cast(#field_accessor.data())) + } else { + quote!(#field_accessor.data()) + }; + quote! { + #setup + ARROW_RETURN_NOT_OK(#value_builder->AppendValues( + #value_ptr_accessor, + static_cast(num_elements * #num_items_per_value), nullptr) + ); + } } } else { let value_reserve_factor = match &field.typ { @@ -2142,6 +2186,7 @@ fn quote_append_field_to_builder( }; let append_value = quote_append_single_value_to_builder( + objects, &field.typ, &value_builder, &value_accessor, @@ -2182,8 +2227,13 @@ fn quote_append_field_to_builder( } } else { let element_accessor = quote!(elements[elem_idx]); - let single_append = - quote_append_single_field_to_builder(field, builder, &element_accessor, includes); + let single_append = quote_append_single_field_to_builder( + objects, + field, + builder, + &element_accessor, + includes, + ); quote! { ARROW_RETURN_NOT_OK(#builder->Reserve(static_cast(num_elements))); for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { @@ -2194,6 +2244,7 @@ fn quote_append_field_to_builder( } fn quote_append_single_field_to_builder( + objects: &Objects, field: &ObjectField, builder: &Ident, element_accessor: &TokenStream, @@ -2207,7 +2258,7 @@ fn quote_append_single_field_to_builder( }; let append_value = - quote_append_single_value_to_builder(&field.typ, builder, &value_access, includes); + quote_append_single_value_to_builder(objects, &field.typ, builder, &value_access, includes); if field.is_nullable { quote! { @@ -2230,6 +2281,7 @@ fn quote_append_single_field_to_builder( /// If the value is an array/vector, it will try to append the batch in one go. /// Note that in that case this does *not* take care of the array/vector builder itself, just the underlying value builder. fn quote_append_single_value_to_builder( + objects: &Objects, typ: &Type, value_builder: &Ident, value_access: &TokenStream, @@ -2321,6 +2373,14 @@ fn quote_append_single_value_to_builder( } } } + ElementType::Array { .. } => quote_append_nested_array_contents( + objects, + includes, + elem_type, + value_builder, + "e!(#value_access.data()), + &num_items_per_element, + ), } } Type::Object { fqname } => { @@ -2330,6 +2390,72 @@ fn quote_append_single_value_to_builder( } } +/// Generates code that appends the contents of (possibly nested) fixed-size-array values +/// to `builder`, the builder for the elements of the outermost array/vector. +/// +/// The intermediate list slots of each nesting level are appended in bulk, then all +/// innermost scalars in one go — the data is contiguous in memory. +/// +/// `elem_type` is the `ElementType::Array` element type of the outermost array/vector, +/// `num_items` the total number of such elements being appended, and `data_ptr` a +/// pointer to the first one. +fn quote_append_nested_array_contents( + objects: &Objects, + includes: &mut Includes, + elem_type: &ElementType, + builder: &Ident, + data_ptr: &TokenStream, + num_items: &TokenStream, +) -> TokenStream { + let mut chain = TokenStream::new(); + let mut parent_builder = builder.clone(); + let mut num_items = num_items.clone(); + let mut elem_type = elem_type; + let mut level: usize = 0; + + while let ElementType::Array { + elem_type: inner, .. + } = elem_type + { + let ElementType::Array { length, .. } = elem_type else { + unreachable!(); + }; + + level += 1; + let child_builder = format_ident!("value_builder_inner{level}"); + let child_builder_type = arrow_array_builder_type(&(**inner).clone().into(), objects); + chain.extend(quote! { + ARROW_RETURN_NOT_OK(#parent_builder->AppendValues(static_cast(#num_items))); + auto #child_builder = static_cast(#parent_builder->value_builder()); + }); + + let length = quote_integer(*length); + num_items = quote!(#num_items * #length); + parent_builder = child_builder; + elem_type = inner; + } + + // The innermost data pointer needs a cast: `data_ptr` points at `std::array`s, + // and `rerun::half`/`bool` are taken as `uint16_t`/`uint8_t` by arrow. + let cast_type = match elem_type { + ElementType::Float16 => quote!(uint16_t), + ElementType::Bool => quote!(uint8_t), + ElementType::Object { .. } | ElementType::String | ElementType::Binary => unimplemented!( + "nested fixed-size arrays over {elem_type:?} are not supported by the C++ codegen" + ), + _ => quote_element_type(includes, elem_type), + }; + + chain.extend(quote! { + ARROW_RETURN_NOT_OK(#parent_builder->AppendValues( + reinterpret_cast(#data_ptr), + static_cast(#num_items), nullptr) + ); + }); + + chain +} + fn quote_num_items_per_value(typ: &Type, value_accessor: &TokenStream) -> TokenStream { match &typ { Type::Array { length, .. } => quote_integer(length), @@ -2533,6 +2659,12 @@ fn quote_element_type(includes: &mut Includes, typ: &ElementType) -> TokenStream quote! { std::string } } ElementType::Object { fqname } => quote_fqname_as_type_path(includes, fqname), + ElementType::Array { elem_type, length } => { + includes.insert_system("array"); + let elem_type = quote_element_type(includes, elem_type); + let length = Literal::usize_unsuffixed(*length); + quote! { std::array<#elem_type, #length> } + } } } diff --git a/crates/build/re_types_builder/src/codegen/docs/snippets_ref.rs b/crates/build/re_types_builder/src/codegen/docs/snippets_ref.rs index fe175603a898..0b84e7241391 100644 --- a/crates/build/re_types_builder/src/codegen/docs/snippets_ref.rs +++ b/crates/build/re_types_builder/src/codegen/docs/snippets_ref.rs @@ -221,7 +221,7 @@ impl SnippetsRefCodeGenerator { let component_opt_outs = &config.snippets_ref.components.opt_out; let snippets_table = |snippets: &BTreeMap<&Object, Vec>>| { - let table = snippets + let rows: Vec = snippets .iter() .flat_map(|(obj, snippets)| { let mut snippets = snippets.clone(); @@ -262,13 +262,13 @@ impl SnippetsRefCodeGenerator { true }) .map(|(obj, snippet)| snippet_row(obj, &snippet)) - .collect::, _>>()? - .join("\n"); + .try_collect()?; + let table = rows.join("\n"); Ok::<_, anyhow::Error>(table) }; - let per_feature_table = snippets + let per_feature_rows: Vec = snippets .per_feature .iter() .flat_map(|(feature, snippets)| { @@ -305,8 +305,8 @@ impl SnippetsRefCodeGenerator { Ok::<_, anyhow::Error>(row) }) - .collect::, _>>()? - .join("\n"); + .try_collect()?; + let per_feature_table = per_feature_rows.join("\n"); let per_archetype_table = snippets_table(&snippets.per_archetype)?; let per_archetype_blueprint_table = snippets_table(&snippets.per_archetype_blueprint)?; @@ -318,7 +318,6 @@ impl SnippetsRefCodeGenerator { ); // NOTE: `C++` is written with a UTF8 zero-width word joiner () in // order to force the markdown renderer to *not* split it into two lines ("C+\n+"). - #[expect(clippy::invisible_characters)] let out = format!( " {autogen_warning} diff --git a/crates/build/re_types_builder/src/codegen/docs/website.rs b/crates/build/re_types_builder/src/codegen/docs/website.rs index 3a04c6d5ddf9..d9bdb79ef592 100644 --- a/crates/build/re_types_builder/src/codegen/docs/website.rs +++ b/crates/build/re_types_builder/src/codegen/docs/website.rs @@ -153,7 +153,14 @@ fn index_page( ) -> String { let mut page = String::new(); - write_frontmatter(&mut page, kind.plural_name(), Some(order)); + // Sort the (long, generated) child type pages alphabetically in the + // side nav rather than requiring an explicit `order` on each one. + write_frontmatter( + &mut page, + kind.plural_name(), + Some(order), + Some("alphabetical"), + ); putln!(page); putln!(page, "{prelude}"); putln!(page); @@ -241,7 +248,7 @@ fn object_page( object.name.clone() }; - write_frontmatter(&mut page, &title, None); + write_frontmatter(&mut page, &title, None, None); putln!(page); if let Some(docline_summary) = object.state.docline_summary() { @@ -375,13 +382,20 @@ fn list_links(page: &mut String, object: &Object) { } } -fn write_frontmatter(o: &mut String, title: &str, order: Option) { +fn write_frontmatter(o: &mut String, title: &str, order: Option, sort_children: Option<&str>) { putln!(o, "---"); putln!(o, "title: {title:?}"); if let Some(order) = order { // The order is used to sort `rerun.io/docs` side navigation putln!(o, "order: {order}"); } + if let Some(sort_children) = sort_children { + // Sorts this page's children in the `rerun.io/docs` side navigation, + // overriding their individual `order`. Used here to keep the long, + // generated type lists alphabetical without stamping an `order` on + // every single page. + putln!(o, "sort_children: {sort_children}"); + } putln!(o, "---"); // Can't put the autogen warning before the frontmatter, stuff breaks down then. putln!(o, "", autogen_warning!()); diff --git a/crates/build/re_types_builder/src/codegen/python/mod.rs b/crates/build/re_types_builder/src/codegen/python/mod.rs index cb75b80360b8..5f0e9f0750a0 100644 --- a/crates/build/re_types_builder/src/codegen/python/mod.rs +++ b/crates/build/re_types_builder/src/codegen/python/mod.rs @@ -613,7 +613,7 @@ impl PythonCodeGenerator { if ext_class.found { code.push_unindented( - format!("from .{} import {}", ext_class.module_name, ext_class.name,), + format!("from .{} import {}", ext_class.module_name, ext_class.name), 1, ); } @@ -634,17 +634,18 @@ impl PythonCodeGenerator { ); } - let import_clauses: HashSet<_> = obj - .fields - .iter() - .filter_map(|field| quote_import_clauses_from_field(obj.scope().as_ref(), field)) - .chain(obj.fields.iter().filter_map(|field| { + let import_clauses: HashSet<_> = std::iter::chain( + obj.fields.iter().filter_map(|field| { + quote_import_clauses_from_field(obj.scope().as_ref(), field) + }), + obj.fields.iter().filter_map(|field| { let fqname = field.typ.fqname()?; objects[fqname].delegate_datatype(objects).map(|delegate| { quote_import_clauses_from_fqname(obj.scope().as_ref(), &delegate.fqname) }) - })) - .collect(); + }), + ) + .collect(); for clause in import_clauses { code.push_indented(0, &clause, 1); } @@ -935,10 +936,10 @@ fn code_for_struct( // and appear at the end of the list, but it currently doesn't. This is unfortunate as // the apparent field order is inconsistent with what the `xxxx_init()` override // accepts. - let fields_in_order = fields - .iter() - .filter(|field| !field.is_nullable) - .chain(fields.iter().filter(|field| field.is_nullable)); + let fields_in_order = std::iter::chain( + fields.iter().filter(|field| !field.is_nullable), + fields.iter().filter(|field| field.is_nullable), + ); for field in fields_in_order { let ObjectField { name, is_nullable, .. @@ -1896,8 +1897,46 @@ fn quote_field_type_from_field( Type::Array { elem_type, length: _, + } => match elem_type { + ElementType::Binary | ElementType::String => unimplemented!( + "fixed-size arrays of {elem_type:?} are not supported by the Python codegen" + ), + _ => quote_plural_field_type_from_element_type(elem_type, unwrap, &mut unwrapped), + }, + Type::Vector { elem_type } => match elem_type { + ElementType::Binary => "list[bytes]".to_owned(), + ElementType::String => "list[str]".to_owned(), + _ => quote_plural_field_type_from_element_type(elem_type, unwrap, &mut unwrapped), + }, + Type::Object { fqname } => quote_type_from_element_type(&ElementType::Object { + fqname: fqname.clone(), + }), + }; + + (typ, unwrapped) +} + +/// The Python type of an array/vector field over the given element type +/// (excluding `Binary`/`String` elements, whose spelling differs between the two). +fn quote_plural_field_type_from_element_type( + elem_type: &ElementType, + unwrap: bool, + unwrapped: &mut bool, +) -> String { + match elem_type { + ElementType::Object { .. } => { + let typ = quote_type_from_element_type(elem_type); + if unwrap { + *unwrapped = true; + typ + } else { + format!("list[{typ}]") + } } - | Type::Vector { elem_type } => match elem_type { + + // Scalars and nested fixed-size arrays map to a (multi-dimensional) + // numpy array of the innermost element type. + _ => match elem_type.innermost_element_type() { ElementType::UInt8 => "npt.NDArray[np.uint8]".to_owned(), ElementType::UInt16 => "npt.NDArray[np.uint16]".to_owned(), ElementType::UInt32 => "npt.NDArray[np.uint32]".to_owned(), @@ -1910,24 +1949,15 @@ fn quote_field_type_from_field( ElementType::Float16 => "npt.NDArray[np.float16]".to_owned(), ElementType::Float32 => "npt.NDArray[np.float32]".to_owned(), ElementType::Float64 => "npt.NDArray[np.float64]".to_owned(), - ElementType::Binary => "list[bytes]".to_owned(), - ElementType::String => "list[str]".to_owned(), - ElementType::Object { .. } => { - let typ = quote_type_from_element_type(elem_type); - if unwrap { - unwrapped = true; - typ - } else { - format!("list[{typ}]") - } + innermost + @ (ElementType::Binary | ElementType::String | ElementType::Object { .. }) => { + unimplemented!( + "nested fixed-size arrays over {innermost:?} are not supported by the Python codegen" + ) } + ElementType::Array { .. } => unreachable!("innermost cannot be an array"), }, - Type::Object { fqname } => quote_type_from_element_type(&ElementType::Object { - fqname: fqname.clone(), - }), - }; - - (typ, unwrapped) + } } /// Returns a default converter function for the given field. @@ -1987,25 +2017,20 @@ fn quote_field_converter_from_field( "str".to_owned() } } - Type::Array { - elem_type, - length: _, + Type::Array { elem_type, length } => { + if let Some(enum_obj) = elem_type.enum_obj(objects) { + quote_enum_array_field_converter(obj, field, enum_obj, Some(*length), &mut function) + } else { + lookup_np_array_conversion_method(elem_type) + } + } + Type::Vector { elem_type } => { + if let Some(enum_obj) = elem_type.enum_obj(objects) { + quote_enum_array_field_converter(obj, field, enum_obj, None, &mut function) + } else { + lookup_np_array_conversion_method(elem_type) + } } - | Type::Vector { elem_type } => match elem_type { - ElementType::UInt8 => "to_np_uint8".to_owned(), - ElementType::UInt16 => "to_np_uint16".to_owned(), - ElementType::UInt32 => "to_np_uint32".to_owned(), - ElementType::UInt64 => "to_np_uint64".to_owned(), - ElementType::Int8 => "to_np_int8".to_owned(), - ElementType::Int16 => "to_np_int16".to_owned(), - ElementType::Int32 => "to_np_int32".to_owned(), - ElementType::Int64 => "to_np_int64".to_owned(), - ElementType::Bool => "to_np_bool".to_owned(), - ElementType::Float16 => "to_np_float16".to_owned(), - ElementType::Float32 => "to_np_float32".to_owned(), - ElementType::Float64 => "to_np_float64".to_owned(), - _ => String::new(), - }, Type::Object { fqname } => { let typ = quote_type_from_element_type(&ElementType::Object { fqname: fqname.clone(), @@ -2067,6 +2092,95 @@ fn quote_field_converter_from_field( (converter, function) } +// Returns the name of the NumPy array conversion method for the given element type or empty string if not applicable. +fn lookup_np_array_conversion_method(elem_type: &ElementType) -> String { + // Nested fixed-size arrays convert like their innermost element type: + // the numpy conversion preserves the multi-dimensional shape. + match elem_type.innermost_element_type() { + ElementType::UInt8 => "to_np_uint8".to_owned(), + ElementType::UInt16 => "to_np_uint16".to_owned(), + ElementType::UInt32 => "to_np_uint32".to_owned(), + ElementType::UInt64 => "to_np_uint64".to_owned(), + ElementType::Int8 => "to_np_int8".to_owned(), + ElementType::Int16 => "to_np_int16".to_owned(), + ElementType::Int32 => "to_np_int32".to_owned(), + ElementType::Int64 => "to_np_int64".to_owned(), + ElementType::Bool => "to_np_bool".to_owned(), + ElementType::Float16 => "to_np_float16".to_owned(), + ElementType::Float32 => "to_np_float32".to_owned(), + ElementType::Float64 => "to_np_float64".to_owned(), + + // No numpy conversion for these; the field keeps its native representation. + ElementType::Binary | ElementType::String | ElementType::Object { .. } => String::new(), + + ElementType::Array { .. } => unreachable!("innermost cannot be an array"), + } +} + +fn quote_enum_array_field_converter( + obj: &Object, + field: &ObjectField, + enum_obj: &Object, + length: Option, + function: &mut String, +) -> String { + let converter_name = format!( + "_{}__{}__special_field_converter_override", + obj.snake_case_name(), + field.name + ); + let enum_name = &enum_obj.name; + // E.g. `datatypes.EnumTest`. This relies on the module (e.g. `datatypes`) being importable + // relative to the generated file, which also works for the test types (where the absolute + // `rerun.testing.datatypes` path does not exist). + let enum_type = fqname_to_type(&enum_obj.fqname); + let enum_module = enum_type.rsplit_once('.').map_or_else( + || panic!("Missing '.' separator in type: {enum_type:?}"), + |(module, _name)| module.to_owned(), + ); + + let length_check = length.map_or_else(String::new, |length| { + format!( + r#" + if len(values) != {length}: + raise ValueError(f"{field_name} must be a {length}-element array. Got: {{len(values)}}") + "#, + field_name = field.name, + ) + }); + + let obj_type = fqname_to_type(&obj.fqname); + + function.push_unindented( + format!( + r#" + def {converter_name}(x: Any) -> list[{enum_type}]: + from .. import {enum_module} + + if isinstance(x, {obj_type}): + return x.{field_name} + + try: + values = list(x) + except TypeError as err: + raise ValueError("{field_name} must be an iterable of {enum_name} values") from err + {length_check} + + def convert_value(value: Any) -> {enum_type}: + if isinstance(value, ({enum_type}, str)): + return {enum_type}.auto(value) + return {enum_type}.auto(int(value)) + + return [convert_value(value) for value in values] + "#, + field_name = field.name, + ), + 1, + ); + + converter_name +} + fn fqname_to_type(fqname: &str) -> String { let fqname = fqname.replace(".testing", ""); @@ -2318,6 +2432,35 @@ fn quote_arrow_serialization( format!("Expected this to have {ATTR_PYTHON_ALIASES} set"), ); } + } else if let Type::Array { + elem_type, + length: _, + } = &obj.fields[0].typ + && let Some(enum_obj) = elem_type.enum_obj(objects) + { + let enum_arrow_datatype = + quote_arrow_datatype(&type_registry.get(&enum_obj.fqname)); + return Ok(unindent(&format!( + r##" + from typing import cast + + if isinstance(data, {name}): + typed_data = [data.{field_name}] + else: + data = cast({name}ArrayLike, data) + try: + typed_data = [{name}(data).{field_name}] # type: ignore[arg-type] + except (AttributeError, TypeError, ValueError): + typed_data = [ + datum.{field_name} if isinstance(datum, {name}) else {name}(datum).{field_name} + for datum in data # type: ignore[union-attr] # ty: ignore[not-iterable] + ] + + flat_data = [axis.value for item in typed_data for axis in item] + return pa.FixedSizeListArray.from_arrays(pa.array(flat_data, type={enum_arrow_datatype}), type=data_type) + "##, + field_name = obj.fields[0].name, + ))); } } diff --git a/crates/build/re_types_builder/src/codegen/python/views.rs b/crates/build/re_types_builder/src/codegen/python/views.rs index f5132504050c..f6fc2fda5f14 100644 --- a/crates/build/re_types_builder/src/codegen/python/views.rs +++ b/crates/build/re_types_builder/src/codegen/python/views.rs @@ -192,7 +192,7 @@ This will be addressed in . let parameter_name = &property.name; let property_type = &objects[property_type_fqname]; let property_name = &property_type.name; - let property_type_name = format!("blueprint_archetypes.{}", &property_type.name); + let property_type_name = format!("blueprint_archetypes.{}", property_type.name); code.push_indented(1, format!("if {parameter_name} is not None:"), 1); code.push_indented( 2, diff --git a/crates/build/re_types_builder/src/codegen/rust/api.rs b/crates/build/re_types_builder/src/codegen/rust/api.rs index 727550bd36c8..b63437f5aabe 100644 --- a/crates/build/re_types_builder/src/codegen/rust/api.rs +++ b/crates/build/re_types_builder/src/codegen/rust/api.rs @@ -159,6 +159,7 @@ fn generate_object_file( code.push_str("#![allow(clippy::allow_attributes)]\n"); code.push_str("#![allow(clippy::clone_on_copy)]\n"); code.push_str("#![allow(clippy::cloned_instead_of_copied)]\n"); + code.push_str("#![allow(clippy::eq_op)]\n"); // `IS_POD` consts of different types resolve to the same trait item, so `SizeBytes` derivations look like equal operands. code.push_str("#![allow(clippy::map_flatten)]\n"); code.push_str("#![allow(clippy::needless_question_mark)]\n"); code.push_str("#![allow(clippy::new_without_default)]\n"); @@ -308,53 +309,14 @@ fn quote_struct( let quoted_builder = quote_builder_from_obj(reporter, objects, obj); - let quoted_heap_size_bytes = { - let heap_size_bytes_impl = if is_tuple_struct_from_obj(obj) { - quote!(self.0.heap_size_bytes()) - } else if obj.fields.is_empty() { - quote!(0) - } else { - let quoted_heap_size_bytes = obj.fields.iter().map(|obj_field| { - let field_name = format_ident!("{}", obj_field.name); - quote!(self.#field_name.heap_size_bytes()) - }); - quote!(#(#quoted_heap_size_bytes)+*) - }; - - let is_pod_impl = if obj.fields.is_empty() { - quote!(true) - } else { - let quoted_is_pods = obj.fields.iter().map(|obj_field| { - let quoted_field_type = quote_field_type_from_object_field(obj, obj_field); - quote!(<#quoted_field_type>::is_pod()) - }); - quote!(#(#quoted_is_pods)&&*) - }; - - let quoted_is_pod = (!obj.is_archetype()).then_some(quote! { - #[inline] - fn is_pod() -> bool { - #is_pod_impl - } - }); - - quote! { - impl ::re_byte_size::SizeBytes for #name { - #[inline] - fn heap_size_bytes(&self) -> u64 { - #heap_size_bytes_impl - } - - #quoted_is_pod - } - } - }; + let quoted_derive_size_bytes = quote!(#[derive(::re_byte_size::SizeBytes)]); let tokens = quote! { #quoted_doc #quoted_derive_clone_debug #quoted_derive_clause #quoted_derive_default_clause + #quoted_derive_size_bytes #quoted_repr_clause #quoted_custom_clause #quoted_deprecation_summary @@ -365,8 +327,6 @@ fn quote_struct( #quoted_from_impl #quoted_builder - - #quoted_heap_size_bytes }; tokens @@ -420,56 +380,13 @@ fn quote_union( let quoted_trait_impls = quote_trait_impls_from_obj(reporter, type_registry, objects, obj); - let quoted_heap_size_bytes = { - let quoted_matches = fields.iter().map(|obj_field| { - let name = format_ident!("{}", re_case::to_pascal_case(&obj_field.name)); - - if obj_field.typ == Type::Unit { - quote!(Self::#name => 0) - } else { - quote!(Self::#name(v) => v.heap_size_bytes()) - } - }); - - let is_pod_impl = { - let quoted_is_pods: Vec<_> = obj - .fields - .iter() - .filter(|obj_field| obj_field.typ != Type::Unit) - .map(|obj_field| { - let quoted_field_type = quote_field_type_from_object_field(obj, obj_field); - quote!(<#quoted_field_type>::is_pod()) - }) - .collect(); - if quoted_is_pods.is_empty() { - quote!(true) - } else { - quote!(#(#quoted_is_pods)&&*) - } - }; - - quote! { - impl ::re_byte_size::SizeBytes for #name { - #[inline] - fn heap_size_bytes(&self) -> u64 { - #![allow(clippy::match_same_arms)] - match self { - #(#quoted_matches),* - } - } - - #[inline] - fn is_pod() -> bool { - #is_pod_impl - } - } - } - }; + let quoted_derive_size_bytes = quote!(#[derive(::re_byte_size::SizeBytes)]); let tokens = quote! { #quoted_doc #quoted_derive_clone_debug #quoted_derive_clause + #quoted_derive_size_bytes #quoted_repr_clause #quoted_custom_clause pub enum #name { @@ -477,8 +394,6 @@ fn quote_union( } #quoted_trait_impls - - #quoted_heap_size_bytes }; tokens @@ -650,6 +565,7 @@ fn quote_enum( let tokens = quote! { #quoted_doc #[derive( #(#derives,)* )] + #[derive(::re_byte_size::SizeBytes)] #quoted_custom_clause #[repr(#repr_type)] pub enum #name { @@ -689,17 +605,6 @@ fn quote_enum( } } - impl ::re_byte_size::SizeBytes for #name { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } - } }; tokens @@ -858,6 +763,10 @@ impl quote::ToTokens for &ElementType { ElementType::Binary => quote!(::arrow::buffer::Buffer), ElementType::String => quote!(::re_types_core::ArrowString), ElementType::Object { fqname } => quote_fqname_as_type_path(fqname), + ElementType::Array { elem_type, length } => { + let elem_type = &**elem_type; + quote!([#elem_type; #length]) + } } .to_tokens(tokens); } @@ -917,7 +826,8 @@ fn quote_trait_impls_for_datatype_or_component( let datatype = type_registry.get(fqname); - let optimize_for_buffer_slice = should_optimize_buffer_slice_deserialize(obj, type_registry); + let optimize_for_buffer_slice = + should_optimize_buffer_slice_deserialize(objects, obj, type_registry); let is_forwarded_type = obj.is_arrow_transparent() && !obj.fields[0].is_nullable @@ -1151,11 +1061,14 @@ fn quote_trait_impls_for_archetype(reporter: &Reporter, obj: &Object) -> TokenSt #(#doc_attrs)* #[inline] pub fn #fn_name() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some(#archetype_name.into()), - component: #component.into(), - component_type: Some(#component_type.into()), - } + static DESCRIPTOR: std::sync::LazyLock = std::sync::LazyLock::new(|| { + ComponentDescriptor { + archetype: Some(#archetype_name.into()), + component: #component.into(), + component_type: Some(#component_type.into()), + } + }); + (*DESCRIPTOR).clone() } } }) @@ -1255,7 +1168,10 @@ fn quote_trait_impls_for_archetype(reporter: &Reporter, obj: &Object) -> TokenSt impl ::re_types_core::Archetype for #name { #[inline] fn name() -> ::re_types_core::ArchetypeName { - #fqname.into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + #fqname + ) } #[inline] @@ -1332,7 +1248,10 @@ fn quote_trait_impls_for_view(reporter: &Reporter, obj: &Object) -> TokenStream impl ::re_types_core::View for #name { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - #identifier .into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + #identifier + ) } } } @@ -1540,7 +1459,7 @@ fn quote_builder_from_obj(reporter: &Reporter, objects: &Objects, obj: &Object) } }; - let with_methods = required.iter().chain(optional.iter()).map(|field| { + let with_methods = std::iter::chain(&required, &optional).map(|field| { // fn with_*() let field_name = format_ident!("{}", field.name); let descr_fn_name = format_ident!("descriptor_{field_name}"); @@ -1596,7 +1515,7 @@ fn quote_builder_from_obj(reporter: &Reporter, objects: &Objects, obj: &Object) quote_doc_line(&format!("Update only some specific fields of a `{name}`.")); let clear_fields_doc = quote_doc_line(&format!("Clear all the fields of a `{name}`.")); - let fields = required.iter().chain(optional.iter()).map(|field| { + let fields = std::iter::chain(&required, &optional).map(|field| { let field_name = format_ident!("{}", field.name); let descr_fn_name = format_ident!("descriptor_{field_name}"); let (typ, _) = quote_field_type_from_typ(&field.typ, true); @@ -1649,15 +1568,15 @@ fn quote_builder_from_obj(reporter: &Reporter, objects: &Objects, obj: &Object) "); let columns_unary_doc = quote_doc_lines(&columns_unary_doc.lines().map(|l| l.to_owned()).collect_vec()); - let fields = required.iter().chain(optional.iter()).map(|field| { + let fields = std::iter::chain(&required, &optional).map(|field| { let field_name = format_ident!("{}", field.name); quote!(self.#field_name.map(|#field_name| #field_name.partitioned(_lengths.clone())).transpose()?) }); - let field_lengths = required.iter().chain(optional.iter()).map(|field| { - format_ident!("len_{}", field.name) - }).collect_vec(); - let unary_fields = required.iter().chain(optional.iter()).map(|field| { + let field_lengths = std::iter::chain(&required, &optional) + .map(|field| format_ident!("len_{}", field.name)) + .collect_vec(); + let unary_fields = std::iter::chain(&required, &optional).map(|field| { let field_name = format_ident!("{}", field.name); let len_field_name = format_ident!("len_{}", field.name); quote!(let #len_field_name = self.#field_name.as_ref().map(|b| b.array.len())) diff --git a/crates/build/re_types_builder/src/codegen/rust/deserializer.rs b/crates/build/re_types_builder/src/codegen/rust/deserializer.rs index e047e0a73037..021f3e6f3ac0 100644 --- a/crates/build/re_types_builder/src/codegen/rust/deserializer.rs +++ b/crates/build/re_types_builder/src/codegen/rust/deserializer.rs @@ -5,7 +5,9 @@ use re_log::debug_assert; use crate::codegen::rust::arrow::{ ArrowDataTypeTokenizer, is_backed_by_scalar_buffer, quote_fqname_as_type_path, }; -use crate::codegen::rust::util::{is_tuple_struct_from_obj, quote_comment}; +use crate::codegen::rust::util::{ + is_tuple_struct_from_obj, quote_comment, quote_default_value_for_datatype, +}; use crate::data_type::{AtomicDataType, DataType, UnionMode}; use crate::{Object, Objects, TypeRegistry}; @@ -237,11 +239,11 @@ pub fn quote_arrow_deserializer( } else { let (#data_src_fields, #data_src_arrays) = (#data_src.fields(), #data_src.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = #data_src_fields - .iter() - .map(|field| field.name().as_str()) - .zip(#data_src_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + #data_src_fields.iter().map(|field| field.name().as_str()), + #data_src_arrays, + ) + .collect(); #(#quoted_field_deserializers;)* @@ -492,10 +494,8 @@ fn quote_arrow_field_deserializer( _ = is_nullable; // not yet used, will be needed very soon // If the inner object is an enum, then dispatch to its deserializer. - if let DataType::Object { fqname, .. } = datatype - && objects.get(fqname).is_some_and(|obj| obj.is_enum()) - { - let fqname_use = quote_fqname_as_type_path(fqname); + if let Some(obj) = datatype.enum_obj(objects) { + let fqname_use = quote_fqname_as_type_path(&obj.fqname); return quote!(#fqname_use::from_arrow_opt(#data_src).with_context(#obj_field_fqname)?.into_iter()); } @@ -662,6 +662,26 @@ fn quote_arrow_field_deserializer( None, ); + let is_enum = inner.data_type().enum_obj(objects).is_some(); + let quoted_enum_missing_check = if is_enum { + quote! { + if data.iter().any(Option::is_none) { + return Err(DeserializationError::missing_data()); + } + } + } else { + quote! {} + }; + // Enums have no `Default` impl, so fall back to their first variant instead. + // (See the note below on why we need to fill in _something_ here.) + let quoted_unwrap = if is_enum { + let quoted_default_value = + quote_default_value_for_datatype(objects, inner.data_type()); + quote!(.map(|opt| opt.unwrap_or_else(|| #quoted_default_value))) + } else { + quote!(.map(Option::unwrap_or_default)) + }; + let comment_note_unwrap = quote_comment("NOTE: Unwrapping cannot fail: the length must be correct."); @@ -674,7 +694,10 @@ fn quote_arrow_field_deserializer( // datastructures for all of our children. Vec::new() } else { - let offsets = (0..).step_by(#length).zip((#length..).step_by(#length).take(#data_src.len())); + let offsets = ::std::iter::zip( + (0..).step_by(#length), + (#length..).step_by(#length).take(#data_src.len()), + ); let #data_src_inner = { let #data_src_inner = &**#data_src.values(); @@ -702,6 +725,8 @@ fn quote_arrow_field_deserializer( #[expect(unsafe_code, clippy::undocumented_unsafe_blocks)] let data = unsafe { #data_src_inner.get_unchecked(start..end) }; + #quoted_enum_missing_check + // NOTE: The call to `Option::unwrap_or_default` is very important here. // // Since we can only get here if the outer entry is marked as @@ -722,7 +747,7 @@ fn quote_arrow_field_deserializer( // is null. // // TODO(#2875): use MaybeUninit rather than requiring a default impl - let data = data.iter().cloned().map(Option::unwrap_or_default); + let data = data.iter().cloned() #quoted_unwrap; // The following would be the correct thing to do, but costs us way // too much performance-wise for something that only applies to // malformed inputs. @@ -949,6 +974,7 @@ fn quote_iterator_transparency( } else { None }; + let inner_is_arrow_transparent = inner_obj.is_some_and(|obj| obj.datatype.is_none()); if inner_is_arrow_transparent { @@ -1122,7 +1148,7 @@ fn quote_arrow_field_deserializer_buffer_slice( } } - DataType::FixedSizeList(inner, length) => { + DataType::FixedSizeList(inner, _) => { let data_src_inner = format_ident!("{data_src}_inner"); let quoted_inner = quote_arrow_field_deserializer_buffer_slice( inner.data_type(), @@ -1141,11 +1167,15 @@ fn quote_arrow_field_deserializer_buffer_slice( ) }; + // Fully spell out the target type of the cast: type inference fails for + // nested fixed-size lists, where only the outermost type is pinned down. + let quoted_elem_type = quote_buffer_slice_element_type(datatype); + quote! {{ let #data_src = #quoted_downcast?; let #data_src_inner = &**#data_src.values(); - bytemuck::cast_slice::<_, [_; #length]>(#quoted_inner) + bytemuck::cast_slice::<_, #quoted_elem_type>(#quoted_inner) }} } @@ -1153,6 +1183,35 @@ fn quote_arrow_field_deserializer_buffer_slice( } } +/// The native Rust type of a buffer-slice-deserialized element, fully spelled out +/// (e.g. `[[half::f16; 3]; 15]`), so that the generated `bytemuck` casts don't have +/// to rely on type inference. +fn quote_buffer_slice_element_type(datatype: &DataType) -> TokenStream { + match datatype.to_logical_type() { + DataType::Atomic(atomic) => match atomic { + AtomicDataType::UInt8 => quote!(u8), + AtomicDataType::UInt16 => quote!(u16), + AtomicDataType::UInt32 => quote!(u32), + AtomicDataType::UInt64 => quote!(u64), + AtomicDataType::Int8 => quote!(i8), + AtomicDataType::Int16 => quote!(i16), + AtomicDataType::Int32 => quote!(i32), + AtomicDataType::Int64 => quote!(i64), + AtomicDataType::Float16 => quote!(half::f16), + AtomicDataType::Float32 => quote!(f32), + AtomicDataType::Float64 => quote!(f64), + AtomicDataType::Null | AtomicDataType::Boolean => { + unimplemented!("{atomic:#?} not supported by the buffer-slice fast path") + } + }, + DataType::FixedSizeList(inner, length) => { + let quoted_inner = quote_buffer_slice_element_type(inner.data_type()); + quote!([#quoted_inner; #length]) + } + _ => unimplemented!("{datatype:#?}"), + } +} + /// Whether or not this object allows for the buffer-slice optimizations. /// /// These optimizations require the outer type to be non-nullable and made up exclusively @@ -1165,6 +1224,7 @@ fn quote_arrow_field_deserializer_buffer_slice( /// /// This should always be checked before using [`quote_arrow_deserializer_buffer_slice`]. pub fn should_optimize_buffer_slice_deserialize( + objects: &Objects, obj: &Object, type_registry: &TypeRegistry, ) -> bool { @@ -1172,23 +1232,27 @@ pub fn should_optimize_buffer_slice_deserialize( if is_arrow_transparent { let typ = type_registry.get(&obj.fqname); let obj_field = &obj.fields[0]; - !obj_field.is_nullable && should_optimize_buffer_slice_deserialize_datatype(&typ) + !obj_field.is_nullable && should_optimize_buffer_slice_deserialize_datatype(objects, &typ) } else { false } } /// Whether or not this datatype allows for the buffer slice optimizations. -fn should_optimize_buffer_slice_deserialize_datatype(typ: &DataType) -> bool { +fn should_optimize_buffer_slice_deserialize_datatype(objects: &Objects, typ: &DataType) -> bool { match typ { DataType::Atomic(atomic) => { !matches!(atomic, AtomicDataType::Null | AtomicDataType::Boolean) } DataType::Object { datatype, .. } => { - should_optimize_buffer_slice_deserialize_datatype(datatype) + typ.enum_obj(objects).is_none() + && should_optimize_buffer_slice_deserialize_datatype(objects, datatype) } DataType::FixedSizeList(field, _) => { - should_optimize_buffer_slice_deserialize_datatype(field.data_type()) + // A wrapper-object element would break the generated `bytemuck` casts, which + // produce bare nested arrays rather than wrapper structs. + !matches!(field.data_type(), DataType::Object { .. }) + && should_optimize_buffer_slice_deserialize_datatype(objects, field.data_type()) } _ => false, } diff --git a/crates/build/re_types_builder/src/codegen/rust/reflection.rs b/crates/build/re_types_builder/src/codegen/rust/reflection.rs index 734f5fa58c0f..4655af38aae2 100644 --- a/crates/build/re_types_builder/src/codegen/rust/reflection.rs +++ b/crates/build/re_types_builder/src/codegen/rust/reflection.rs @@ -117,7 +117,7 @@ fn generate_component_reflection( } else { // Works too let fqname = &obj.fqname; - quote!( ComponentType::new(#fqname) ) + quote!( ComponentType::from(#fqname) ) }; let docstring_md = doc_as_lines( @@ -146,8 +146,8 @@ fn generate_component_reflection( extension_contents_for_fqname .get(&obj.fqname) .is_some_and(|contents| { - contents.contains(&format!("impl Default for {}", &obj.name)) - || contents.contains(&format!("impl Default for super::{}", &obj.name)) + contents.contains(&format!("impl Default for {}", obj.name)) + || contents.contains(&format!("impl Default for super::{}", obj.name)) }); let custom_placeholder = if auto_derive_default || has_custom_default_impl { quote! { Some(#type_name::default().to_arrow()?) } @@ -249,7 +249,7 @@ fn generate_archetype_reflection(reporter: &Reporter, objects: &Objects) -> Toke }); let fqname = &obj.fqname; - let quoted_name = quote!( ArchetypeName::new(#fqname) ); + let quoted_name = quote!( ArchetypeName::from(#fqname) ); let display_name = re_case::to_human_case(&obj.name); if false { // We currently skip the docstring for the archetype itself, diff --git a/crates/build/re_types_builder/src/codegen/rust/serializer.rs b/crates/build/re_types_builder/src/codegen/rust/serializer.rs index 59526a5c18ec..5443f91c4c37 100644 --- a/crates/build/re_types_builder/src/codegen/rust/serializer.rs +++ b/crates/build/re_types_builder/src/codegen/rust/serializer.rs @@ -5,7 +5,7 @@ use super::arrow::{ ArrowFieldTokenizer, is_backed_by_scalar_buffer, quote_fqname_as_type_path, quoted_arrow_primitive_type, }; -use super::util::{is_tuple_struct_from_obj, quote_comment}; +use super::util::{is_tuple_struct_from_obj, quote_comment, quote_default_value_for_datatype}; use crate::data_type::{AtomicDataType, DataType, UnionMode}; use crate::objects::EnumIntegerType; use crate::{Object, Objects, TypeRegistry}; @@ -484,9 +484,7 @@ fn quote_arrow_field_serializer( }; // If the inner object is an enum, then dispatch to its serializer. - if let Some(obj) = inner_obj - && obj.is_enum() - { + if let Some(obj) = datatype.enum_obj(objects) { let fqname_use = quote_fqname_as_type_path(&obj.fqname); let option_wrapper = if elements_are_nullable { quote! {} @@ -782,12 +780,14 @@ fn quote_arrow_field_serializer( } (Some(first_buf), Some(second_buf)) => { // Multiple buffers: single Vec allocation for slices - std::iter::once(first_buf.as_ref() as &[_]) - .chain(std::iter::once(second_buf.as_ref() as &[_])) - .chain(iter.map(|b| b.as_ref() as &[_])) - .collect::>() - .concat() - .into() + ::itertools::chain!( + ::std::iter::once(first_buf.as_ref() as &[_]), + ::std::iter::once(second_buf.as_ref() as &[_]), + iter.map(|b| b.as_ref() as &[_]), + ) + .collect::>() + .concat() + .into() } _ => { // Empty case @@ -812,13 +812,15 @@ fn quote_arrow_field_serializer( InnerRepr::NativeIterable => { if let DataType::FixedSizeList(_, count) = datatype.to_logical_type() { if elements_are_nullable { + let placeholder = + quote_default_value_for_datatype(objects, inner_datatype); quote! { #data_src .into_iter() .flat_map(|v| match v { Some(v) => itertools::Either::Left(v.into_iter()), None => itertools::Either::Right( - std::iter::repeat_n(Default::default(), #count), + std::iter::repeat_n(#placeholder, #count), ), }) } diff --git a/crates/build/re_types_builder/src/codegen/rust/util.rs b/crates/build/re_types_builder/src/codegen/rust/util.rs index 07c2147eb9f8..09d118b3e06d 100644 --- a/crates/build/re_types_builder/src/codegen/rust/util.rs +++ b/crates/build/re_types_builder/src/codegen/rust/util.rs @@ -6,6 +6,8 @@ use quote::quote; use crate::codegen::Target; use crate::codegen::common::{ExampleInfo, collect_snippets_for_api_docs}; +use crate::codegen::rust::arrow::quote_fqname_as_type_path; +use crate::data_type::DataType; use crate::objects::State; use crate::{ATTR_RUST_TUPLE_STRUCT, Docs, Object, ObjectKind, Objects, Reporter}; @@ -27,6 +29,15 @@ pub fn quote_comment(comment: &str) -> TokenStream { quote!(#(#lines)*) } +pub fn quote_default_value_for_datatype(objects: &Objects, datatype: &DataType) -> TokenStream { + if let Some(obj) = datatype.enum_obj(objects) { + let fqname_use = quote_fqname_as_type_path(&obj.fqname); + quote!(<#fqname_use as ::re_types_core::reflection::Enum>::variants()[0]) + } else { + quote!(Default::default()) + } +} + pub fn is_tuple_struct_from_obj(obj: &Object) -> bool { if !obj.is_struct() { return false; diff --git a/crates/build/re_types_builder/src/data_type.rs b/crates/build/re_types_builder/src/data_type.rs index d68daab04b1e..89e75a1c6d2e 100644 --- a/crates/build/re_types_builder/src/data_type.rs +++ b/crates/build/re_types_builder/src/data_type.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use std::sync::Arc; -use crate::TypeRegistry; +use crate::{Object, Objects, TypeRegistry}; /// Mode of [`DataType::Union`] /// @@ -147,6 +147,16 @@ impl DataType { self } } + + /// `Some(Object)` if this is an enum object. + pub fn enum_obj<'a>(&self, objects: &'a Objects) -> Option<&'a Object> { + let Self::Object { fqname, .. } = self else { + return None; + }; + + let obj = &objects[fqname]; + obj.is_enum().then_some(obj) + } } /// Like [`DataType`], but with an extra [`Self::Unresolved`] variant diff --git a/crates/build/re_types_builder/src/lib.rs b/crates/build/re_types_builder/src/lib.rs index ee3c05109d51..e229b1ffb647 100644 --- a/crates/build/re_types_builder/src/lib.rs +++ b/crates/build/re_types_builder/src/lib.rs @@ -315,8 +315,9 @@ fn generate_gitattributes_for_generated_files(files_to_write: &mut GeneratedFile for (dirpath, files) in filepaths_per_folder { let gitattributes_path = dirpath.join(FILENAME); - let generated_files = std::iter::once(FILENAME.to_owned()) // The attributes itself is generated! - .chain(files.iter().map(|filepath| { + let generated_files = std::iter::chain( + std::iter::once(FILENAME.to_owned()), // The attributes itself is generated! + files.iter().map(|filepath| { format_path( filepath .strip_prefix(&dirpath) @@ -325,9 +326,10 @@ fn generate_gitattributes_for_generated_files(files_to_write: &mut GeneratedFile }) .unwrap(), ) - })) - .map(|s| format!("{s} linguist-generated=true")) - .collect::>(); + }), + ) + .map(|s| format!("{s} linguist-generated=true")) + .collect::>(); let content = format!( "# DO NOT EDIT! This file is generated by {}\n\n{}\n", diff --git a/crates/build/re_types_builder/src/objects.rs b/crates/build/re_types_builder/src/objects.rs index 1b8077633291..37722a204dd4 100644 --- a/crates/build/re_types_builder/src/objects.rs +++ b/crates/build/re_types_builder/src/objects.rs @@ -75,7 +75,7 @@ impl Objects { } let mut this = Self { - objects: resolved_enums.into_iter().chain(resolved_objs).collect(), + objects: std::iter::chain(resolved_enums, resolved_objs).collect(), }; // Validate fields types: Archetype consist of components, Views (aka SuperArchetypes) consist of archetypes, everything else consists of datatypes. @@ -182,8 +182,27 @@ impl Objects { .. } = target_obj.fields.pop().unwrap(); - field.typ = typ; - field.datatype = datatype; + match &mut field.typ { + // An array/vector of a transparent object: fold the target's + // type into the element type, e.g. an array of a transparent + // wrapper over `[float: 3]` becomes an array of `[f32; 3]`. + Type::Array { elem_type, .. } | Type::Vector { elem_type } => { + *elem_type = typ.to_element_type().unwrap_or_else(|| { + panic!( + "field '{}' is an array/vector of transparent object '{}' \ + whose inner type {typ:?} cannot be used as an element type", + field.fqname, target_obj.fqname, + ) + }); + field.datatype = None; + } + + // A direct object field: replace the field's type wholesale. + _ => { + field.typ = typ; + field.datatype = datatype; + } + } // TODO(cmc): might want to do something smarter at some point regarding attrs. @@ -1275,6 +1294,10 @@ impl From for Type { ElementType::Binary => Self::Binary, ElementType::String => Self::String, ElementType::Object { fqname } => Self::Object { fqname }, + ElementType::Array { elem_type, length } => Self::Array { + elem_type: *elem_type, + length, + }, } } } @@ -1383,6 +1406,38 @@ impl Type { } } + /// The inverse of `From for Type`: the element type that this type + /// corresponds to when used as an array/vector element. + /// + /// Returns `None` for types that cannot be element types (`Unit`, vectors). + pub fn to_element_type(&self) -> Option { + match self { + Self::UInt8 => Some(ElementType::UInt8), + Self::UInt16 => Some(ElementType::UInt16), + Self::UInt32 => Some(ElementType::UInt32), + Self::UInt64 => Some(ElementType::UInt64), + Self::Int8 => Some(ElementType::Int8), + Self::Int16 => Some(ElementType::Int16), + Self::Int32 => Some(ElementType::Int32), + Self::Int64 => Some(ElementType::Int64), + Self::Bool => Some(ElementType::Bool), + Self::Float16 => Some(ElementType::Float16), + Self::Float32 => Some(ElementType::Float32), + Self::Float64 => Some(ElementType::Float64), + Self::Binary => Some(ElementType::Binary), + Self::String => Some(ElementType::String), + Self::Object { fqname } => Some(ElementType::Object { + fqname: fqname.clone(), + }), + Self::Array { elem_type, length } => Some(ElementType::Array { + elem_type: Box::new(elem_type.clone()), + length: *length, + }), + + Self::Unit | Self::Vector { .. } => None, + } + } + pub fn make_plural(&self) -> Option { match self { Self::Vector { elem_type: _ } @@ -1608,6 +1663,16 @@ pub enum ElementType { Object { fqname: String, }, + + /// A nested fixed-size array. + /// + /// This cannot be expressed directly in the flatbuffers IDL (arrays cannot nest); + /// it is produced by the semantic pass when a `transparent` struct wrapping a + /// fixed-size array is used as the element type of an array/vector. + Array { + elem_type: Box, + length: usize, + }, } impl ElementType { @@ -1681,6 +1746,26 @@ impl ElementType { } } + /// Recursively resolves nested arrays to their innermost element type. + /// + /// Returns `self` for everything but [`Self::Array`]. + pub fn innermost_element_type(&self) -> &Self { + match self { + Self::Array { elem_type, .. } => elem_type.innermost_element_type(), + _ => self, + } + } + + /// `Some(Object)` if this is an enum object. + pub fn enum_obj<'a>(&self, objects: &'a Objects) -> Option<&'a Object> { + let Self::Object { fqname } = self else { + return None; + }; + + let obj = &objects[fqname]; + obj.is_enum().then_some(obj) + } + /// Is the destructor trivial/default (i.e. is this simple data with no allocations)? pub fn has_default_destructor(&self, objects: &Objects) -> bool { match self { @@ -1700,6 +1785,8 @@ impl ElementType { Self::Binary | Self::String => false, Self::Object { fqname } => objects[fqname].has_default_destructor(objects), + + Self::Array { elem_type, .. } => elem_type.has_default_destructor(objects), } } @@ -1719,7 +1806,9 @@ impl ElementType { | Self::Float16 | Self::Float32 | Self::Float64 => true, - Self::Bool | Self::Binary | Self::String | Self::Object { .. } => false, + Self::Bool | Self::Binary | Self::String | Self::Object { .. } | Self::Array { .. } => { + false + } } } diff --git a/crates/build/re_types_builder/src/type_registry.rs b/crates/build/re_types_builder/src/type_registry.rs index f1e1f57d4e69..a7eb15bc630e 100644 --- a/crates/build/re_types_builder/src/type_registry.rs +++ b/crates/build/re_types_builder/src/type_registry.rs @@ -114,22 +114,24 @@ impl TypeRegistry { // NOTE: Inject the null markers' field first and foremost! That way it is // guaranteed to be stable and forward-compatible. - let fields = std::iter::once(LazyField { - name: "_null_markers".into(), - data_type: AtomicDataType::Null.into(), - // NOTE: The spec doesn't allow a `Null` array to be non-nullable. Not that - // we care either way. - is_nullable: true, - metadata: Default::default(), - }) - .chain(obj.fields.iter_mut().map(|field| LazyField { - name: field.name.clone(), - data_type: self.arrow_datatype_from_type(field.typ.clone(), field), - // NOTE: The spec doesn't allow a `Null` array to be non-nullable. - // We map Unit -> Null in enum fields, so this must be nullable. - is_nullable: field.typ == Type::Unit, - metadata: Default::default(), - })) + let fields = std::iter::chain( + std::iter::once(LazyField { + name: "_null_markers".into(), + data_type: AtomicDataType::Null.into(), + // NOTE: The spec doesn't allow a `Null` array to be non-nullable. Not that + // we care either way. + is_nullable: true, + metadata: Default::default(), + }), + obj.fields.iter_mut().map(|field| LazyField { + name: field.name.clone(), + data_type: self.arrow_datatype_from_type(field.typ.clone(), field), + // NOTE: The spec doesn't allow a `Null` array to be non-nullable. + // We map Unit -> Null in enum fields, so this must be nullable. + is_nullable: field.typ == Type::Unit, + metadata: Default::default(), + }), + ) .collect(); LazyDatatype::Object { @@ -166,7 +168,7 @@ impl TypeRegistry { Type::Array { elem_type, length } => LazyDatatype::FixedSizeList( LazyField { name: "item".into(), - data_type: self.arrow_datatype_from_element_type(elem_type), + data_type: Self::arrow_datatype_from_element_type(elem_type), // NOTE: Do _not_ confuse this with the nullability of the field itself! // This would be the nullability of the elements of the list itself, which our IDL // literally is unable to express at the moment, so you can be certain this is @@ -180,7 +182,7 @@ impl TypeRegistry { Type::Vector { elem_type } => LazyDatatype::List( LazyField { name: "item".into(), - data_type: self.arrow_datatype_from_element_type(elem_type), + data_type: Self::arrow_datatype_from_element_type(elem_type), // NOTE: Do _not_ confuse this with the nullability of the field itself! // This would be the nullability of the elements of the list itself, which our IDL // literally is unable to express at the moment, so you can be certain this is @@ -199,8 +201,7 @@ impl TypeRegistry { datatype } - fn arrow_datatype_from_element_type(&self, typ: ElementType) -> LazyDatatype { - _ = self; + fn arrow_datatype_from_element_type(typ: ElementType) -> LazyDatatype { match typ { ElementType::UInt8 => LazyDatatype::Atomic(AtomicDataType::UInt8), ElementType::UInt16 => LazyDatatype::Atomic(AtomicDataType::UInt16), @@ -217,6 +218,16 @@ impl TypeRegistry { ElementType::Binary => LazyDatatype::Binary, ElementType::String => LazyDatatype::Utf8, ElementType::Object { fqname } => LazyDatatype::Unresolved { fqname }, + ElementType::Array { elem_type, length } => LazyDatatype::FixedSizeList( + LazyField { + name: "item".into(), + data_type: Self::arrow_datatype_from_element_type(*elem_type), + is_nullable: false, + metadata: Default::default(), + } + .into(), + length, + ), } } } diff --git a/crates/store/re_chunk/Cargo.toml b/crates/store/re_chunk/Cargo.toml index 433673435a25..638df3384766 100644 --- a/crates/store/re_chunk/Cargo.toml +++ b/crates/store/re_chunk/Cargo.toml @@ -22,9 +22,6 @@ all-features = true [features] default = [] -## Enable (de)serialization using serde. -serde = ["re_log_types/serde", "re_tuid/serde", "re_types_core/serde"] - [dependencies] @@ -39,7 +36,6 @@ re_quota_channel.workspace = true re_sorbet.workspace = true re_span.workspace = true re_tracing.workspace = true -re_tuid.workspace = true re_types_core.workspace = true # External @@ -53,7 +49,6 @@ itertools.workspace = true nohash-hasher.workspace = true rand = { workspace = true, features = ["std_rng"] } thiserror.workspace = true -tracing.workspace = true # Native dependencies: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/store/re_chunk/examples/chunk_latest_at.rs b/crates/store/re_chunk/examples/chunk_latest_at.rs index 699373012aa7..82de2b43bb25 100644 --- a/crates/store/re_chunk/examples/chunk_latest_at.rs +++ b/crates/store/re_chunk/examples/chunk_latest_at.rs @@ -8,7 +8,7 @@ fn main() -> anyhow::Result<()> { eprintln!("Data:\n{chunk}"); - let query = LatestAtQuery::new(TimelineName::new("frame"), 4); + let query = LatestAtQuery::new(TimelineName::from("frame"), 4); // Find all relevant data for a query: let Some(unit) = chunk.latest_at(&query, MyPoints::descriptor_points().component) else { @@ -77,7 +77,7 @@ fn create_chunk() -> anyhow::Result { ) .build()?; - chunk.sort_if_unsorted(); + chunk.sort_by_row_ids_if_needed(); Ok(chunk) } diff --git a/crates/store/re_chunk/examples/chunk_range.rs b/crates/store/re_chunk/examples/chunk_range.rs index d77ac110bb24..3029bdddd0f7 100644 --- a/crates/store/re_chunk/examples/chunk_range.rs +++ b/crates/store/re_chunk/examples/chunk_range.rs @@ -9,7 +9,7 @@ fn main() -> anyhow::Result<()> { eprintln!("Data:\n{chunk}"); - let query = RangeQuery::new(TimelineName::new("frame"), AbsoluteTimeRange::EVERYTHING); + let query = RangeQuery::new(TimelineName::from("frame"), AbsoluteTimeRange::EVERYTHING); // Find all relevant data for a query: let chunk = chunk.range(&query, MyPoints::descriptor_points().component); @@ -75,7 +75,7 @@ fn create_chunk() -> anyhow::Result { ) .build()?; - chunk.sort_if_unsorted(); + chunk.sort_by_row_ids_if_needed(); Ok(chunk) } diff --git a/crates/store/re_chunk/src/batcher.rs b/crates/store/re_chunk/src/batcher.rs index f57f16edaeb2..d33f2a00cfe0 100644 --- a/crates/store/re_chunk/src/batcher.rs +++ b/crates/store/re_chunk/src/batcher.rs @@ -125,7 +125,7 @@ impl std::fmt::Debug for BatcherHooks { /// Defines the different thresholds of the associated [`ChunkBatcher`]. /// /// See [`Self::default`] and [`Self::from_env`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, re_byte_size::SizeBytes)] pub struct ChunkBatcherConfig { /// Duration of the periodic tick. // @@ -166,9 +166,9 @@ impl ChunkBatcherConfig { /// Default configuration, applicable to most use cases. pub const DEFAULT: Self = Self { flush_tick: Duration::from_millis(200), - flush_num_bytes: 1024 * 1024, // 1 MiB + flush_num_bytes: 2 * 1024 * 1024, // 2 MiB flush_num_rows: u64::MAX, - chunk_max_rows_if_unsorted: 256, + chunk_max_rows_if_unsorted: 8192, max_bytes_in_flight: 100 * 1024 * 1024, // Apply backpressure }; @@ -179,7 +179,16 @@ impl ChunkBatcherConfig { }; /// Always flushes ASAP. - pub const ALWAYS: Self = Self { + /// + /// # WARNING: test-only configuration. + /// + /// This produces an unrealistically large number of chunks and is **not** suitable for + /// production workloads. In particular, with a file sink it can drive memory usage through + /// the roof: per-chunk metadata has to be accumulated in memory until the SDK process ends + /// and the file footer can be written. + /// + /// Use [`Self::LOW_LATENCY`] if you actually want fast flushing in real applications. + pub const ALWAYS_TEST_ONLY: Self = Self { flush_tick: Duration::MAX, flush_num_bytes: 0, flush_num_rows: 0, @@ -196,6 +205,16 @@ impl ChunkBatcherConfig { ..Self::DEFAULT }; + /// Returns true if this config flushes after every single row (one chunk per row). + /// + /// This is the case for [`Self::ALWAYS_TEST_ONLY`] and any config where either the row or + /// byte threshold is zero — the batcher flushes whenever pending rows/bytes meet *or exceed* + /// the threshold, so a zero threshold triggers on the first row. + #[inline] + pub fn always_flushes(&self) -> bool { + self.flush_num_rows == 0 || self.flush_num_bytes == 0 + } + /// Environment variable to configure [`Self::flush_tick`]. pub const ENV_FLUSH_TICK: &'static str = "RERUN_FLUSH_TICK_SECS"; @@ -405,26 +424,18 @@ impl Drop for ChunkBatcherInner { } } +#[derive(re_byte_size::SizeBytes)] enum Command { AppendChunk(Chunk), AppendRow(EntityPath, PendingRow), Flush { + #[size_bytes(ignore)] on_done: crossbeam::channel::Sender<()>, }, UpdateConfig(ChunkBatcherConfig), Shutdown, } -impl re_byte_size::SizeBytes for Command { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::AppendChunk(chunk) => chunk.heap_size_bytes(), - Self::AppendRow(_, row) => row.heap_size_bytes(), - Self::Flush { .. } | Self::UpdateConfig(_) | Self::Shutdown => 0, - } - } -} - impl Command { fn flush() -> (Self, crossbeam::channel::Receiver<()>) { let (tx, rx) = crossbeam::channel::bounded(1); // oneshot @@ -767,7 +778,7 @@ fn batching_thread( /// A single row's worth of data (i.e. a single log call). /// /// Send those to the batcher to build up a [`Chunk`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, re_byte_size::SizeBytes)] pub struct PendingRow { /// Auto-generated `TUID`, uniquely identifying this event and keeping track of the client's /// wall-clock. @@ -810,19 +821,6 @@ impl PendingRow { } } -impl re_byte_size::SizeBytes for PendingRow { - #[inline] - fn heap_size_bytes(&self) -> u64 { - let Self { - row_id, - timepoint, - components, - } = self; - - row_id.heap_size_bytes() + timepoint.heap_size_bytes() + components.heap_size_bytes() - } -} - impl PendingRow { /// Turn a single row into a [`Chunk`] of its own. /// diff --git a/crates/store/re_chunk/src/chunk.rs b/crates/store/re_chunk/src/chunk.rs index 1c86ca58608e..89370e9f38de 100644 --- a/crates/store/re_chunk/src/chunk.rs +++ b/crates/store/re_chunk/src/chunk.rs @@ -28,7 +28,7 @@ use crate::{ChunkId, RowId}; /// the use of a [`crate::ChunkBatcher`]. #[derive(thiserror::Error, Debug)] pub enum ChunkError { - #[error("Detected malformed Chunk: {reason}")] + #[error("Detected malformed chunk: {reason}")] Malformed { reason: String }, #[error("Arrow: {0}")] @@ -48,7 +48,7 @@ pub enum ChunkError { Deserialization(#[from] DeserializationError), #[error(transparent)] - UnsupportedTimeType(#[from] re_sorbet::UnsupportedTimeType), + IndexColumn(#[from] re_sorbet::IndexColumnError), #[error(transparent)] WrongDatatypeError(#[from] re_arrow_util::WrongDatatypeError), @@ -58,6 +58,10 @@ pub enum ChunkError { #[error(transparent)] InvalidSorbetSchema(#[from] re_sorbet::SorbetError), + + // Boxed: `DataframeToChunksError` is large (it wraps a `SorbetError`), and this variant is rare. + #[error(transparent)] + DataframeToChunks(#[from] Box), } const _: () = assert!( @@ -340,8 +344,20 @@ impl Chunk { /// /// Useful for tests. pub fn ensure_similar(lhs: &Self, rhs: &Self) -> anyhow::Result<()> { + Self::ensure_similar_ignoring_timelines(lhs, rhs, &[]) + } + + /// Like [`Self::ensure_similar`], but the given timelines are ignored entirely: + /// their presence, absence, and values are not compared. + /// + /// Useful when comparing recordings produced with different default-timeline settings, + /// e.g. `log_tick`, which is opt-in. + pub fn ensure_similar_ignoring_timelines( + lhs: &Self, + rhs: &Self, + ignored_timelines: &[TimelineName], + ) -> anyhow::Result<()> { anyhow::ensure!(lhs.num_rows() == rhs.num_rows()); - anyhow::ensure!(lhs.num_columns() == rhs.num_columns()); let Self { id: _, @@ -353,11 +369,40 @@ impl Chunk { components, } = lhs; + let is_ignored = |timeline: &TimelineName| ignored_timelines.contains(timeline); + anyhow::ensure!(*entity_path == rhs.entity_path); - anyhow::ensure!(timelines.keys().collect_vec() == rhs.timelines.keys().collect_vec()); + // Compare the set of timelines, disregarding any ignored timelines on either side. + // Compared as a sorted set, since the timeline iteration order is not meaningful. + let lhs_timelines = timelines + .keys() + .filter(|t| !is_ignored(t)) + .sorted() + .collect_vec(); + let rhs_timelines = rhs + .timelines + .keys() + .filter(|t| !is_ignored(t)) + .sorted() + .collect_vec(); + anyhow::ensure!( + lhs_timelines == rhs_timelines, + "Timelines differ: {lhs_timelines:?} vs {rhs_timelines:?}" + ); + + // Number of components must match (timelines are already checked above). + anyhow::ensure!( + components.len() == rhs.components.len(), + "Number of components differs: {} vs {}", + components.len(), + rhs.components.len() + ); for (timeline, left_time_col) in timelines { + if is_ignored(timeline) { + continue; + } let right_time_col = rhs .timelines .get(timeline) @@ -527,6 +572,13 @@ impl Chunk { pub fn clone_with_new_id(&self) -> Self { self.clone_with_id(ChunkId::new()) } + + #[inline] + pub fn clone_with_new_entity_path(&self, entity_path: EntityPath) -> Self { + let mut chunk = self.clone_with_id(ChunkId::new()); + chunk.entity_path = entity_path; + chunk + } } impl Chunk { @@ -743,7 +795,7 @@ impl Chunk { .list_arrays() .fold(HashMap::default(), |acc, list_array| { if let Some(validity) = list_array.nulls() { - time_column.times().zip(validity.iter()).fold( + std::iter::zip(time_column.times(), validity.iter()).fold( acc, |mut acc, (time, is_valid)| { *acc.entry(time).or_default() += is_valid as u64; @@ -792,7 +844,7 @@ impl Chunk { let row_ids = self.row_ids().collect_vec(); - if self.is_sorted() { + if self.is_row_ids_sorted() { self.components .iter() .filter_map(|(component, column)| { @@ -840,7 +892,7 @@ impl Chunk { // --- -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, re_byte_size::SizeBytes)] pub struct TimeColumn { pub(crate) timeline: Timeline, @@ -906,7 +958,7 @@ impl Chunk { components, }; - chunk.is_sorted = is_sorted.unwrap_or_else(|| chunk.is_sorted_uncached()); + chunk.is_sorted = is_sorted.unwrap_or_else(|| chunk.is_row_ids_sorted_uncached()); chunk.sanity_check()?; @@ -1395,6 +1447,19 @@ impl Chunk { self.row_ids_slice().iter().copied() } + /// Find the row index of the given [`RowId`] in this chunk, if it is present. + /// + /// Uses a binary search on sorted chunks (the common case), falling back to + /// a linear scan otherwise. + #[inline] + pub fn row_index_of(&self, row_id: RowId) -> Option { + if self.is_row_ids_sorted() { + self.row_ids_slice().binary_search(&row_id).ok() + } else { + self.row_ids_slice().iter().position(|r| *r == row_id) + } + } + /// Returns an iterator over the [`RowId`]s of a [`Chunk`], for a given component. /// /// This is different than [`Self::row_ids`]: it will only yield `RowId`s for rows at which @@ -1435,7 +1500,7 @@ impl Chunk { let row_ids = self.row_ids_slice(); #[expect(clippy::unwrap_used)] // checked above - Some(if self.is_sorted() { + Some(if self.is_row_ids_sorted() { ( row_ids.first().copied().unwrap(), row_ids.last().copied().unwrap(), @@ -1689,26 +1754,46 @@ impl re_byte_size::SizeBytes for Chunk { } } -impl re_byte_size::SizeBytes for TimeColumn { - #[inline] - fn heap_size_bytes(&self) -> u64 { - let Self { - timeline, - times, - is_sorted, - time_range, - } = self; - - timeline.heap_size_bytes() - + times.heap_size_bytes() - + is_sorted.heap_size_bytes() - + time_range.heap_size_bytes() - } -} - // --- Sanity checks --- impl Chunk { + /// Warn if we find out-of-order timelines. + /// + /// If `RERUN_VERY_STRICT` is set, this will instead panic. + /// + /// This assumes the chunk is already sorted by `RowId`, like most chunks. + /// + /// Out-of-order timelines are sometimes unavoidable, + /// but if we can avoid them we absolutely should, + /// because they cause much slower queries + #[track_caller] + pub fn warn_if_out_of_order(&self) { + if !self.is_row_ids_sorted() { + // Big problem! + if re_log::is_rerun_very_strict() { + panic!("Found chunk that wasn't sorted by RowId. This is a bug"); + } else { + re_log::debug_warn_once!("Found chunk that wasn't sorted by RowId. This is a bug"); + } + } + + let unsorted_timelines = self.unsorted_timelines(); + if !unsorted_timelines.is_empty() { + if re_log::is_rerun_very_strict() { + panic!( + "Found out-of-order timelines for entity '{}': {:?}. Out-of-order timelines are sometimes unavoidable, but they may cause performance problems", + self.entity_path, unsorted_timelines + ); + } else { + re_log::debug_warn_once!( + "Found out-of-order timelines for entity '{}': {:?}. Out-of-order timelines are sometimes unavoidable, but they may cause performance problems", + self.entity_path, + unsorted_timelines + ); + } + } + } + /// Returns an error if the Chunk's invariants are not upheld. /// /// Costly checks are only run in debug builds. @@ -1726,6 +1811,8 @@ impl Chunk { components, } = self; + self.warn_if_out_of_order(); + if cfg!(debug_assertions) { let measured = self.heap_size_bytes_inner(); let advertised = heap_size_bytes.load(Ordering::Relaxed); @@ -1755,7 +1842,7 @@ impl Chunk { #[expect(clippy::collapsible_if)] // readability if cfg!(debug_assertions) { - if *is_sorted != self.is_sorted_uncached() { + if *is_sorted != self.is_row_ids_sorted_uncached() { return Err(ChunkError::Malformed { reason: format!( "Chunk is marked as {}sorted but isn't: {row_ids:?}", diff --git a/crates/store/re_chunk/src/iter.rs b/crates/store/re_chunk/src/iter.rs index 1efa3eea279c..5804ae5e8448 100644 --- a/crates/store/re_chunk/src/iter.rs +++ b/crates/store/re_chunk/src/iter.rs @@ -3,11 +3,14 @@ use std::sync::Arc; use arrow::array::{ Array as ArrowArray, ArrayRef as ArrowArrayRef, ArrowPrimitiveType, BinaryArray, BooleanArray as ArrowBooleanArray, FixedSizeListArray as ArrowFixedSizeListArray, - LargeBinaryArray, ListArray as ArrowListArray, PrimitiveArray as ArrowPrimitiveArray, - StringArray as ArrowStringArray, StructArray as ArrowStructArray, + GenericStringArray as ArrowGenericStringArray, LargeBinaryArray, + LargeStringArray as ArrowLargeStringArray, ListArray as ArrowListArray, OffsetSizeTrait, + PrimitiveArray as ArrowPrimitiveArray, StringArray as ArrowStringArray, + StructArray as ArrowStructArray, }; use arrow::buffer::{ - BooleanBuffer as ArrowBooleanBuffer, Buffer, ScalarBuffer as ArrowScalarBuffer, + BooleanBuffer as ArrowBooleanBuffer, Buffer, NullBuffer as ArrowNullBuffer, + ScalarBuffer as ArrowScalarBuffer, }; use arrow::datatypes::ArrowNativeType; use itertools::{Either, Itertools as _, izip}; @@ -133,18 +136,18 @@ impl Chunk { /// * [`Self::iter_component_timepoints`]. #[inline] pub fn iter_timepoints(&self) -> impl Iterator + '_ { - let mut timelines = self + let timelines = self .timelines .values() - .map(|time_column| (time_column.timeline, time_column.times())) + .map(|time_column| (time_column.timeline, time_column.times_raw())) .collect_vec(); - std::iter::from_fn(move || { + (0..self.num_rows()).map(move |row| { let mut timepoint = TimePoint::default(); - for (timeline, times) in &mut timelines { - timepoint.insert(*timeline, times.next()?); + for (timeline, times) in &timelines { + timepoint.insert(*timeline, TimeInt::new_temporal(times[row])); } - Some(timepoint) + timepoint }) } @@ -162,44 +165,25 @@ impl Chunk { return Either::Left(std::iter::empty()); }; - if let Some(validity) = list_array.nulls() { - let mut timelines = self - .timelines - .values() - .map(|time_column| { - ( - time_column.timeline, - time_column - .times() - .enumerate() - .filter(|(i, _)| validity.is_valid(*i)) - .map(|(_, time)| time), - ) - }) - .collect_vec(); + let timelines = self + .timelines + .values() + .map(|time_column| (time_column.timeline, time_column.times_raw())) + .collect_vec(); - Either::Right(Either::Left(std::iter::from_fn(move || { - let mut timepoint = TimePoint::default(); - for (timeline, times) in &mut timelines { - timepoint.insert(*timeline, times.next()?); - } - Some(timepoint) - }))) - } else { - let mut timelines = self - .timelines - .values() - .map(|time_column| (time_column.timeline, time_column.times())) - .collect_vec(); + let validity = list_array.nulls(); - Either::Right(Either::Right(std::iter::from_fn(move || { - let mut timepoint = TimePoint::default(); - for (timeline, times) in &mut timelines { - timepoint.insert(*timeline, times.next()?); - } - Some(timepoint) - }))) - } + Either::Right( + (0..self.num_rows()) + .filter(move |&row| validity.is_none_or(|validity| validity.is_valid(row))) + .map(move |row| { + let mut timepoint = TimePoint::default(); + for (timeline, times) in &timelines { + timepoint.insert(*timeline, TimeInt::new_temporal(times[row])); + } + timepoint + }), + ) } /// Returns an iterator over the offsets & lengths of component arrays within [`Chunk`], for a given @@ -384,6 +368,101 @@ impl_native_type!(arrow::array::types::Float16Type, half::f16); impl_native_type!(arrow::array::types::Float32Type, f32); impl_native_type!(arrow::array::types::Float64Type, f64); +/// Lazily yields `Option` for one component batch (span) of a primitive column: +/// `None` for null slots, `Some(value)` otherwise. +/// +/// Yielded by `slice::>()`, which is like `slice::()` but distinguishes +/// null entries. +pub struct NativeOptSliceIter<'a, T: ArrowNativeType> { + values: &'a [T], + nulls: Option<&'a ArrowNullBuffer>, + range: std::ops::Range, +} + +impl Iterator for NativeOptSliceIter<'_, T> { + type Item = Option; + + #[inline] + fn next(&mut self) -> Option { + let i = self.range.next()?; + Some(if self.nulls.is_some_and(|nulls| !nulls.is_valid(i)) { + None + } else { + Some(self.values[i]) + }) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } +} + +impl ExactSizeIterator for NativeOptSliceIter<'_, T> {} + +/// Like [`slice_as_native`] but yields `None` for null entries. Use this variant instead whenever +/// null entries carry meaning (e.g. a state reset). +/// +/// The [`slice_as_native`] function reads the raw values buffer without consulting the null +/// bitmap, e.g. `[1.5, null, 2.5]` comes back as `[1.5, 0.0, 2.5]`, silently fabricating a value. +fn slice_as_native_opt<'a, P, T>( + component: ComponentIdentifier, + array: &'a dyn ArrowArray, + component_spans: impl Iterator> + 'a, +) -> impl Iterator> + 'a +where + P: ArrowPrimitiveType, + T: ArrowNativeType, +{ + let Some(primitive_array) = array.downcast_array_ref::>() else { + error_on_downcast_failure(component, "ArrowPrimitiveArray", array.data_type()); + return Either::Left(std::iter::empty()); + }; + let values = primitive_array.values().as_ref(); + let nulls = primitive_array.nulls(); + + Either::Right(component_spans.map(move |span| NativeOptSliceIter { + values, + nulls, + range: span.range(), + })) +} + +// We use a macro instead of a blanket impl because this violates orphan rules. +macro_rules! impl_option_native_type { + ($arrow_primitive_type:ty, $native_type:ty) => { + /// Like the plain native slicer but distinguishes null entries: `None` for null, + /// `Some(value)` otherwise. + impl ChunkComponentSlicer for Option<$native_type> { + type Item<'a> = NativeOptSliceIter<'a, $native_type>; + + fn slice<'a>( + component: ComponentIdentifier, + array: &'a dyn ArrowArray, + component_spans: impl Iterator> + 'a, + ) -> impl Iterator> { + slice_as_native_opt::<$arrow_primitive_type, $native_type>( + component, + array, + component_spans, + ) + } + } + }; +} + +impl_option_native_type!(arrow::array::types::UInt8Type, u8); +impl_option_native_type!(arrow::array::types::UInt16Type, u16); +impl_option_native_type!(arrow::array::types::UInt32Type, u32); +impl_option_native_type!(arrow::array::types::UInt64Type, u64); +impl_option_native_type!(arrow::array::types::Int8Type, i8); +impl_option_native_type!(arrow::array::types::Int16Type, i16); +impl_option_native_type!(arrow::array::types::Int32Type, i32); +impl_option_native_type!(arrow::array::types::Int64Type, i64); +impl_option_native_type!(arrow::array::types::Float16Type, half::f16); +impl_option_native_type!(arrow::array::types::Float32Type, f32); +impl_option_native_type!(arrow::array::types::Float64Type, f64); + /// The actual implementation of `impl_array_native_type!`, so that we don't have to work in a macro. fn slice_as_array_native<'a, const N: usize, P, T>( component: ComponentIdentifier, @@ -459,6 +538,93 @@ impl_array_native_type!(arrow::array::types::Float16Type, half::f16); impl_array_native_type!(arrow::array::types::Float32Type, f32); impl_array_native_type!(arrow::array::types::Float64Type, f64); +/// The actual implementation of `impl_array2d_native_type!`, so that we don't have to work in a macro. +/// +/// Slices doubly-nested fixed-size lists, i.e. `FixedSizeList, N>`. +fn slice_as_array2d_native<'a, const N: usize, const M: usize, P, T>( + component: ComponentIdentifier, + array: &'a dyn ArrowArray, + component_spans: impl Iterator> + 'a, +) -> impl Iterator + 'a +where + [[T; M]; N]: bytemuck::Pod, + P: ArrowPrimitiveType, + T: ArrowNativeType + bytemuck::Pod, +{ + let Some(outer_list_array) = array.downcast_array_ref::() else { + error_on_downcast_failure(component, "ArrowFixedSizeListArray", array.data_type()); + return Either::Left(std::iter::empty()); + }; + + let Some(inner_list_array) = outer_list_array + .values() + .downcast_array_ref::() + else { + error_on_downcast_failure( + component, + "ArrowFixedSizeListArray", + outer_list_array.data_type(), + ); + return Either::Left(std::iter::empty()); + }; + + let Some(values) = inner_list_array + .values() + .downcast_array_ref::>() + else { + error_on_downcast_failure( + component, + "ArrowPrimitiveArray

", + inner_list_array.data_type(), + ); + return Either::Left(std::iter::empty()); + }; + + let size = outer_list_array.value_length() as usize * inner_list_array.value_length() as usize; + let values = values.values().as_ref(); + + // NOTE: No need for validity checks here, `component_spans` already takes care of that. + Either::Right( + component_spans.map(move |span| bytemuck::cast_slice(&values[(span * size).range()])), + ) +} + +// We use a macro instead of a blanket impl because this violates orphan rules. +macro_rules! impl_array2d_native_type { + ($arrow_primitive_type:ty, $native_type:ty) => { + impl ChunkComponentSlicer for [[$native_type; M]; N] + where + [[$native_type; M]; N]: bytemuck::Pod, + { + type Item<'a> = &'a [[[$native_type; M]; N]]; + + fn slice<'a>( + component: ComponentIdentifier, + array: &'a dyn ArrowArray, + component_spans: impl Iterator> + 'a, + ) -> impl Iterator> { + slice_as_array2d_native::( + component, + array, + component_spans, + ) + } + } + }; +} + +impl_array2d_native_type!(arrow::array::types::UInt8Type, u8); +impl_array2d_native_type!(arrow::array::types::UInt16Type, u16); +impl_array2d_native_type!(arrow::array::types::UInt32Type, u32); +impl_array2d_native_type!(arrow::array::types::UInt64Type, u64); +impl_array2d_native_type!(arrow::array::types::Int8Type, i8); +impl_array2d_native_type!(arrow::array::types::Int16Type, i16); +impl_array2d_native_type!(arrow::array::types::Int32Type, i32); +impl_array2d_native_type!(arrow::array::types::Int64Type, i64); +impl_array2d_native_type!(arrow::array::types::Float16Type, half::f16); +impl_array2d_native_type!(arrow::array::types::Float32Type, f32); +impl_array2d_native_type!(arrow::array::types::Float64Type, f64); + /// The actual implementation of `impl_buffer_native_type!`, so that we don't have to work in a macro. fn slice_as_buffer_native<'a, P, T>( component: ComponentIdentifier, @@ -724,6 +890,123 @@ impl ChunkComponentSlicer for String { } } +/// Like `slice::()` but distinguishes between null and empty strings: +/// `None` for null entries, `Some("")` for an explicitly-empty string. +/// +/// Prefer this over `slice::()` when null carries meaning (e.g. a state reset): +/// the Arrow spec allows null slots to span arbitrary garbage bytes, so a plain values-buffer +/// slice is not guaranteed to yield an empty string for them. +/// +/// NOTE: If null and `""` are treated the same by every caller, this slicer is removable in +/// practice: known writers (arrow-rs, pyarrow, C++) emit zero-length null slots, so +/// `slice::()` yields `""` for them. But chunks come from the wire, so a writer that +/// exploits the spec's leeway would silently turn resets into phantom garbage values. +impl ChunkComponentSlicer for Option { + type Item<'a> = StringOptSliceIter<'a>; + + fn slice<'a>( + component: ComponentIdentifier, + array: &'a dyn ArrowArray, + component_spans: impl Iterator> + 'a, + ) -> impl Iterator> { + if let Some(utf8_array) = array.downcast_array_ref::() { + Either::Right(Either::Left( + slice_as_opt_string(utf8_array, component_spans) + .map(|batch| StringOptSliceIter(Either::Left(batch))), + )) + } else if let Some(large_utf8_array) = array.downcast_array_ref::() { + Either::Right(Either::Right( + slice_as_opt_string(large_utf8_array, component_spans) + .map(|batch| StringOptSliceIter(Either::Right(batch))), + )) + } else { + error_on_downcast_failure( + component, + "ArrowStringArray or ArrowLargeStringArray", + array.data_type(), + ); + Either::Left(std::iter::empty()) + } + } +} + +/// The shared implementation of `slice::>()` for `Utf8` and `LargeUtf8` arrays. +fn slice_as_opt_string<'a, O: OffsetSizeTrait>( + string_array: &'a ArrowGenericStringArray, + component_spans: impl Iterator> + 'a, +) -> impl Iterator> + 'a { + let values = string_array.values(); + let offsets: &[O] = string_array.offsets(); + let nulls = string_array.nulls(); + + component_spans.map(move |span| GenericStringOptSliceIter { + values, + offsets, + nulls, + range: span.range(), + }) +} + +/// The offset-width-generic implementation behind [`StringOptSliceIter`]. +struct GenericStringOptSliceIter<'a, O: OffsetSizeTrait> { + values: &'a Buffer, + + /// The array's offsets, `len + 1` entries: element `i` spans `offsets[i]..offsets[i + 1]`. + offsets: &'a [O], + nulls: Option<&'a ArrowNullBuffer>, + range: std::ops::Range, +} + +impl Iterator for GenericStringOptSliceIter<'_, O> { + type Item = Option; + + #[inline] + fn next(&mut self) -> Option { + let i = self.range.next()?; + Some(if self.nulls.is_some_and(|nulls| !nulls.is_valid(i)) { + None + } else { + let start = self.offsets[i].as_usize(); + let end = self.offsets[i + 1].as_usize(); + Some(ArrowString::from( + self.values.slice_with_length(start, end - start), + )) + }) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } +} + +impl ExactSizeIterator for GenericStringOptSliceIter<'_, O> {} + +/// Lazily yields `Option` for one component batch (span) of a string column: +/// `None` for null slots, `Some(string)` otherwise (including `Some("")` for explicitly-empty +/// strings). +/// +/// Yielded by `slice::>()`. Wraps both offset widths (`Utf8` and `LargeUtf8`). +pub struct StringOptSliceIter<'a>( + Either, GenericStringOptSliceIter<'a, i64>>, +); + +impl Iterator for StringOptSliceIter<'_> { + type Item = Option; + + #[inline] + fn next(&mut self) -> Option { + self.0.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +impl ExactSizeIterator for StringOptSliceIter<'_> {} + impl ChunkComponentSlicer for bool { type Item<'a> = ArrowBooleanBuffer; @@ -745,6 +1028,64 @@ impl ChunkComponentSlicer for bool { } } +/// Lazily yields `Option` for one component batch (span) of a boolean column: +/// `None` for null slots, `Some(value)` otherwise. +/// +/// Yielded by `slice::>()`, which is like `slice::()` but distinguishes +/// null entries. +/// +/// Note: for booleans, we can't use [`NativeOptSliceIter`] because it reads values through a +/// `&[T]` slice view, but an Arrow `BooleanArray` is bit-packed (8 per byte). +pub struct BoolOptSliceIter<'a> { + values: &'a ArrowBooleanBuffer, + nulls: Option<&'a ArrowNullBuffer>, + range: std::ops::Range, +} + +impl Iterator for BoolOptSliceIter<'_> { + type Item = Option; + + #[inline] + fn next(&mut self) -> Option { + let i = self.range.next()?; + Some(if self.nulls.is_some_and(|nulls| !nulls.is_valid(i)) { + None + } else { + Some(self.values.value(i)) + }) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } +} + +impl ExactSizeIterator for BoolOptSliceIter<'_> {} + +impl ChunkComponentSlicer for Option { + type Item<'a> = BoolOptSliceIter<'a>; + + fn slice<'a>( + component: ComponentIdentifier, + array: &'a dyn ArrowArray, + component_spans: impl Iterator> + 'a, + ) -> impl Iterator> { + let Some(boolean_array) = array.downcast_array_ref::() else { + error_on_downcast_failure(component, "ArrowBooleanArray", array.data_type()); + return Either::Left(std::iter::empty()); + }; + let values = boolean_array.values(); + let nulls = boolean_array.nulls(); + + Either::Right(component_spans.map(move |span| BoolOptSliceIter { + values, + nulls, + range: span.range(), + })) + } +} + // --- pub struct ChunkIndicesIter { @@ -945,12 +1286,35 @@ impl Chunk { mod tests { use std::sync::Arc; + use arrow::array::{BooleanArray, Float64Array, LargeStringArray, StringArray}; use itertools::{Itertools as _, izip}; use re_log_types::example_components::{MyPoint, MyPoints}; use re_log_types::{EntityPath, TimeInt, TimePoint}; + use re_span::Span; + use re_types_core::{ArrowString, ComponentIdentifier}; + use super::ChunkComponentSlicer; use crate::{Chunk, RowId, Timeline}; + /// Builds a chunk with one `MyPoints::points` row per `(timepoint, has_component)` entry. + /// + /// Rows with `has_component == false` leave the component null, making the array nullable. + fn timepoint_chunk(rows: impl IntoIterator) -> Chunk { + let mut builder = Chunk::builder("this/that"); + for (i, (timepoint, has_component)) in rows.into_iter().enumerate() { + let points = [MyPoint::new(i as f32, i as f32)]; + builder = builder.with_sparse_component_batches( + RowId::new(), + timepoint, + [( + MyPoints::descriptor_points(), + has_component.then_some(&points as _), + )], + ); + } + builder.build().expect("valid chunk") + } + #[test] fn iter_indices_temporal() -> anyhow::Result<()> { let entity_path = EntityPath::from("this/that"); @@ -1025,6 +1389,116 @@ mod tests { Ok(()) } + #[test] + fn iter_component_timepoints_temporal() { + let timeline_frame = Timeline::new_sequence("frame"); + let timeline_other = Timeline::new_sequence("other"); + + let timepoint1 = TimePoint::from([(timeline_frame, 10), (timeline_other, 1)]); + let timepoint2 = TimePoint::from([(timeline_frame, 20), (timeline_other, 2)]); + let timepoint3 = TimePoint::from([(timeline_frame, 30), (timeline_other, 3)]); + + let chunk = timepoint_chunk([ + (timepoint1.clone(), true), + (timepoint2.clone(), true), + (timepoint3.clone(), true), + ]); + let expected = vec![timepoint1, timepoint2, timepoint3]; + similar_asserts::assert_eq!( + expected, + chunk + .iter_component_timepoints(MyPoints::descriptor_points().component) + .collect_vec() + ); + } + + #[test] + fn iter_component_timepoints_temporal_sparse() { + let timeline_frame = Timeline::new_sequence("frame"); + let timeline_other = Timeline::new_sequence("other"); + + let timepoint1 = TimePoint::from([(timeline_frame, 10), (timeline_other, 1)]); + let timepoint2 = TimePoint::from([(timeline_frame, 20), (timeline_other, 2)]); + let timepoint3 = TimePoint::from([(timeline_frame, 30), (timeline_other, 3)]); + + let chunk = timepoint_chunk([ + (timepoint1.clone(), true), + (timepoint2, false), + (timepoint3.clone(), true), + ]); + let expected = vec![timepoint1, timepoint3]; + similar_asserts::assert_eq!( + expected, + chunk + .iter_component_timepoints(MyPoints::descriptor_points().component) + .collect_vec() + ); + } + + #[test] + fn iter_component_timepoints_static() { + let chunk = timepoint_chunk((0..3).map(|_| (TimePoint::default(), true))); + assert!(chunk.is_static()); + let expected = vec![TimePoint::default(); 3]; + similar_asserts::assert_eq!( + expected, + chunk + .iter_component_timepoints(MyPoints::descriptor_points().component) + .collect_vec() + ); + } + + #[test] + fn iter_component_timepoints_static_sparse() { + let chunk = timepoint_chunk([ + (TimePoint::default(), true), + (TimePoint::default(), false), + (TimePoint::default(), true), + ]); + assert!(chunk.is_static()); + let expected = vec![TimePoint::default(); 2]; + similar_asserts::assert_eq!( + expected, + chunk + .iter_component_timepoints(MyPoints::descriptor_points().component) + .collect_vec() + ); + } + + #[test] + fn iter_component_timepoints_missing_component() { + let timepoint = TimePoint::from([ + (Timeline::new_sequence("frame"), 10), + (Timeline::new_sequence("other"), 1), + ]); + let chunk = timepoint_chunk([(timepoint, true)]); + let got = chunk + .iter_component_timepoints("non_existing_component".into()) + .collect_vec(); + assert!(got.is_empty()); + } + + #[test] + fn iter_timepoints_temporal() { + let timeline_frame = Timeline::new_sequence("frame"); + let timeline_other = Timeline::new_sequence("other"); + + let timepoint1 = TimePoint::from([(timeline_frame, 10), (timeline_other, 1)]); + let timepoint2 = TimePoint::from([(timeline_frame, 20), (timeline_other, 2)]); + + let chunk = timepoint_chunk([(timepoint1.clone(), true), (timepoint2.clone(), true)]); + let expected = vec![timepoint1, timepoint2]; + similar_asserts::assert_eq!(expected, chunk.iter_timepoints().collect_vec()); + } + + #[test] + fn iter_timepoints_static() { + let chunk = timepoint_chunk((0..3).map(|_| (TimePoint::default(), true))); + assert!(chunk.is_static()); + let expected = vec![TimePoint::default(); 3]; + similar_asserts::assert_eq!(expected, chunk.iter_timepoints().collect_vec()); + } + #[test] fn iter_indices_static() -> anyhow::Result<()> { let entity_path = EntityPath::from("this/that"); @@ -1084,4 +1558,126 @@ mod tests { Ok(()) } + + // The `Option` slicer tests: element-level validity must be preserved — + // including across multi-element spans and on sliced arrays with a nonzero offset. + + /// Materializes each per-span batch into a `Vec` so tests can assert on plain values, + /// regardless of whether the slicer yields lazy iterators or collected containers. + fn slice_all<'a, S: ChunkComponentSlicer>( + array: &'a dyn arrow::array::Array, + spans: impl IntoIterator> + 'a, + ) -> Vec as IntoIterator>::Item>> + where + S::Item<'a>: IntoIterator, + { + S::slice(ComponentIdentifier::from("test"), array, spans.into_iter()) + .map(|batch| batch.into_iter().collect()) + .collect() + } + + #[test] + fn option_f64() { + let array = Float64Array::from(vec![Some(1.0), None, Some(3.0), Some(4.0), None]); + let spans = [Span { start: 0, len: 2 }, Span { start: 2, len: 3 }]; + assert_eq!( + slice_all::>(&array, spans), + vec![vec![Some(1.0), None], vec![Some(3.0), Some(4.0), None],] + ); + + // Nonzero-offset slice: logical elements [None, 3.0, 4.0]. + let sliced = array.slice(1, 3); + assert_eq!( + slice_all::>(&sliced, [Span { start: 0, len: 3 }]), + vec![vec![None, Some(3.0), Some(4.0)]] + ); + } + + #[test] + fn option_bool() { + let array = BooleanArray::from(vec![Some(true), None, Some(false), None]); + let spans = [Span { start: 0, len: 2 }, Span { start: 2, len: 2 }]; + assert_eq!( + slice_all::>(&array, spans), + vec![vec![Some(true), None], vec![Some(false), None]] + ); + + let sliced = array.slice(1, 3); + assert_eq!( + slice_all::>(&sliced, [Span { start: 0, len: 3 }]), + vec![vec![None, Some(false), None]] + ); + } + + #[test] + fn option_string_distinguishes_null_and_empty() { + let array = StringArray::from(vec![Some("a"), None, Some(""), Some("d")]); + let spans = [Span { start: 0, len: 2 }, Span { start: 2, len: 2 }]; + assert_eq!( + slice_all::>(&array, spans), + vec![ + vec![Some(ArrowString::from("a")), None], + vec![Some(ArrowString::from("")), Some(ArrowString::from("d"))], + ] + ); + + // Nonzero-offset slice: logical elements [None, "", "d"]. + let sliced = array.slice(1, 3); + assert_eq!( + slice_all::>(&sliced, [Span { start: 0, len: 3 }]), + vec![vec![ + None, + Some(ArrowString::from("")), + Some(ArrowString::from("d")) + ]] + ); + } + + #[test] + fn option_large_string() { + let array = LargeStringArray::from(vec![Some("a"), None, Some("c")]); + assert_eq!( + slice_all::>(&array, [Span { start: 0, len: 3 }]), + vec![vec![ + Some(ArrowString::from("a")), + None, + Some(ArrowString::from("c")) + ]] + ); + } + + /// Slicing a doubly-nested fixed-size list, i.e. `FixedSizeList, 3>`. + #[test] + #[expect(clippy::cast_possible_wrap, reason = "tiny compile-time constants")] + fn slice_array2d() { + use arrow::array::{FixedSizeListArray, Float32Array}; + use arrow::datatypes::{DataType, Field}; + + use super::{ChunkComponentSlicer as _, Span}; + + const NUM_COMPONENTS: usize = 2; + const N: usize = 3; + const M: usize = 2; + + let values = + Float32Array::from_iter_values((0..(NUM_COMPONENTS * N * M)).map(|i| i as f32)); + let inner_field = Arc::new(Field::new("item", DataType::Float32, false)); + let inner = FixedSizeListArray::new(inner_field.clone(), M as i32, Arc::new(values), None); + let outer_field = Arc::new(Field::new( + "item", + DataType::FixedSizeList(inner_field, M as i32), + false, + )); + let outer = FixedSizeListArray::new(outer_field, N as i32, Arc::new(inner), None); + + let spans = [Span { start: 0, len: 1 }, Span { start: 1, len: 1 }]; + let got = <[[f32; M]; N]>::slice("test_component".into(), &outer, spans.into_iter()) + .collect_vec(); + + let expected: Vec<&[[[f32; M]; N]]> = vec![ + &[[[0.0, 1.0], [2.0, 3.0], [4.0, 5.0]]], + &[[[6.0, 7.0], [8.0, 9.0], [10.0, 11.0]]], + ]; + similar_asserts::assert_eq!(expected, got); + } } diff --git a/crates/store/re_chunk/src/latest_at.rs b/crates/store/re_chunk/src/latest_at.rs index 1199ea180150..ff98325196ab 100644 --- a/crates/store/re_chunk/src/latest_at.rs +++ b/crates/store/re_chunk/src/latest_at.rs @@ -1,5 +1,4 @@ use arrow::array::Array as _; -use re_byte_size::SizeBytes; use re_log_types::{TimeInt, TimelineName}; use re_types_core::ComponentIdentifier; @@ -10,26 +9,25 @@ use crate::{Chunk, RowId, UnitChunkShared}; /// A query at a given time, for a given timeline. /// /// Get the latest version of the data available at this time. -#[derive(Clone, PartialEq, Eq, Hash)] +/// +/// The timeline is `None` for a static-only query (see [`Self::new_static`]), where no timeline +/// is relevant. +#[derive(Clone, PartialEq, Eq, Hash, re_byte_size::SizeBytes)] pub struct LatestAtQuery { - timeline: TimelineName, - at: TimeInt, -} - -impl SizeBytes for LatestAtQuery { - fn heap_size_bytes(&self) -> u64 { - let Self { timeline, at } = self; + timeline: Option, - timeline.heap_size_bytes() + at.heap_size_bytes() - } + /// The time being queried, or [`TimeInt::STATIC`] for a static-only query. + at: TimeInt, } impl std::fmt::Debug for LatestAtQuery { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "", - self.at, self.timeline, - )) + match self.timeline { + Some(timeline) => { + f.write_fmt(format_args!("", self.at, timeline)) + } + None => f.write_fmt(format_args!("", self.at)), + } } } @@ -38,7 +36,7 @@ impl LatestAtQuery { #[inline] pub fn new(timeline: TimelineName, at: impl TryInto) -> Self { Self { - timeline, + timeline: Some(timeline), at: TimeInt::saturated_temporal(at), } } @@ -46,16 +44,27 @@ impl LatestAtQuery { #[inline] pub const fn latest(timeline: TimelineName) -> Self { Self { - timeline, + timeline: Some(timeline), at: TimeInt::MAX, } } + /// A query for static data only, where no timeline is relevant. + #[inline] + pub const fn new_static() -> Self { + Self { + timeline: None, + at: TimeInt::STATIC, + } + } + + /// The timeline being queried, or `None` for a static-only query. #[inline] - pub fn timeline(&self) -> TimelineName { + pub fn timeline(&self) -> Option { self.timeline } + /// The time being queried, or [`TimeInt::STATIC`] for a static-only query. #[inline] pub fn at(&self) -> TimeInt { self.at @@ -95,7 +104,7 @@ impl Chunk { let mut index = None; let is_static = self.is_static(); - let is_sorted_by_row_id = self.is_sorted(); + let is_sorted_by_row_id = self.is_row_ids_sorted(); if is_static { if is_sorted_by_row_id { @@ -128,7 +137,7 @@ impl Chunk { } } } else { - let time_column = self.timelines.get(&query.timeline())?; + let time_column = self.timelines.get(&query.timeline()?)?; let is_sorted_by_time = time_column.is_sorted(); let times = time_column.times_raw(); diff --git a/crates/store/re_chunk/src/lib.rs b/crates/store/re_chunk/src/lib.rs index dd912dd0e016..4f2e83bded7c 100644 --- a/crates/store/re_chunk/src/lib.rs +++ b/crates/store/re_chunk/src/lib.rs @@ -38,7 +38,8 @@ pub use self::chunk::{ Chunk, ChunkComponents, ChunkError, ChunkResult, TimeColumn, TimeColumnError, }; pub use self::iter::{ - ChunkComponentIter, ChunkComponentIterItem, ChunkComponentSlicer, ChunkIndicesIter, + BoolOptSliceIter, ChunkComponentIter, ChunkComponentIterItem, ChunkComponentSlicer, + ChunkIndicesIter, NativeOptSliceIter, StringOptSliceIter, }; pub use self::latest_at::LatestAtQuery; pub use self::range::{RangeQuery, RangeQueryOptions}; diff --git a/crates/store/re_chunk/src/merge.rs b/crates/store/re_chunk/src/merge.rs index 5a64d24c60ed..24ec3c9f6657 100644 --- a/crates/store/re_chunk/src/merge.rs +++ b/crates/store/re_chunk/src/merge.rs @@ -1,6 +1,6 @@ use arrow::array::{Array as _, FixedSizeBinaryArray, ListArray as ArrowListArray}; use arrow::buffer::ScalarBuffer as ArrowScalarBuffer; -use itertools::{Itertools as _, izip}; +use itertools::Itertools as _; use nohash_hasher::IntMap; use re_arrow_util::ArrowArrayDowncastRef as _; use re_types_core::SerializedComponentColumn; @@ -23,7 +23,7 @@ impl Chunk { right.concatenated(left)? }; - compacted.sort_if_unsorted(); + compacted.sort_by_row_ids_if_needed(); // Sanity check that timelines haven't become unsorted. // If they have, we have an unsorted timeline, which is good to know about. @@ -36,7 +36,7 @@ impl Chunk { if left_was_sorted && right_was_sorted { let entity_path = compacted.entity_path(); re_log::debug_warn_once!( - "Timeline '{name}' became unsorted after concatenation for entity '{entity_path}'. This may cause performance issues." + "Timeline '{name}' BECAME unsorted after concatenating overlapping, sorted chunks for entity '{entity_path}'. This may cause performance issues." ); } } @@ -64,9 +64,39 @@ impl Chunk { let cr = rhs; if !cl.concatenable(cr) { - return Err(ChunkError::Malformed { - reason: format!("cannot concatenate incompatible Chunks:\n{cl}\n{cr}"), - }); + // Make sure we provide good errors: + let reason = if cl.entity_path() != cr.entity_path() { + format!( + "cannot concatenate chunks with different entity paths: {:?} != {:?}", + cl.entity_path(), + cr.entity_path() + ) + } else if !cl.same_timelines(cr) { + format!( + "cannot concatenate chunks with different timelines (timelines are dense within a chunk):\n{:?}\n{:?}", + cl.timelines() + .values() + .map(|column| column.timeline()) + .sorted() + .map(|timeline| format!("{}: {}", timeline.name(), timeline.typ())) + .format(", "), + cr.timelines() + .values() + .map(|column| column.timeline()) + .sorted() + .map(|timeline| format!("{}: {}", timeline.name(), timeline.typ())) + .format(", "), + ) + } else if !cl.same_datatypes(cr) { + format!( + "cannot concatenate chunks with different datatypes for shared components:\n{}\n{}", + cl.component_descriptors().format(", "), + cr.component_descriptors().format(", "), + ) + } else { + format!("cannot concatenate incompatible chunks:\n{cl}\n{cr}") + }; + return Err(ChunkError::Malformed { reason }); } let Some((_cl0, cl1)) = cl.row_id_range() else { @@ -90,20 +120,38 @@ impl Chunk { .clone() }; - // NOTE: We know they are the same set, and they are in a btree => we can zip them. - let timelines = { - re_tracing::profile_scope!("timelines"); - izip!(self.timelines.iter(), rhs.timelines.iter()) - .filter_map( - |((lhs_timeline, lhs_time_chunk), (rhs_timeline, rhs_time_chunk))| { - re_log::debug_assert_eq!(lhs_timeline, rhs_timeline); - lhs_time_chunk - .concatenated(rhs_time_chunk) - .map(|time_column| (*lhs_timeline, time_column)) - }, - ) - .collect() - }; + // Pair time columns by name — the maps' iteration orders may differ. + // Both error arms are unreachable behind the `concatenable` check above; hard-error + // rather than silently drop a column if that ever changes. + let timelines: IntMap<_, _> = + { + re_tracing::profile_scope!("timelines"); + cl.timelines + .iter() + .map(|(timeline_name, lhs_time_column)| { + let rhs_time_column = cr.timelines.get(timeline_name).ok_or_else(|| { + ChunkError::Malformed { + reason: format!( + "cannot concatenate chunks: timeline `{timeline_name}` is \ + missing from rhs (concatenability should have been checked \ + before this point)" + ), + } + })?; + let time_column = lhs_time_column + .concatenated(rhs_time_column) + .ok_or_else(|| ChunkError::Malformed { + reason: format!( + "cannot concatenate chunks: timeline `{timeline_name}` differs \ + between chunks: {:?} != {:?}", + lhs_time_column.timeline(), + rhs_time_column.timeline(), + ), + })?; + Ok((*timeline_name, time_column)) + }) + .collect::>()? + }; let lhs_per_component: IntMap<_, _> = cl .components @@ -257,11 +305,18 @@ impl Chunk { self.entity_path() == rhs.entity_path() } - /// Returns `true` if both chunks contains the same set of timelines. + /// Returns `true` if both chunks contain the same set of timelines (both name and type). + /// + /// Compared by key lookup — hash-map iteration order differs between maps and means + /// nothing. Types matter because [`TimeColumn::concatenated`] refuses mismatched types. #[inline] pub fn same_timelines(&self, rhs: &Self) -> bool { self.timelines.len() == rhs.timelines.len() - && self.timelines.keys().collect_vec() == rhs.timelines.keys().collect_vec() + && self.timelines.iter().all(|(name, lhs_column)| { + rhs.timelines + .get(name) + .is_some_and(|rhs_column| lhs_column.timeline() == rhs_column.timeline()) + }) } /// Returns `true` if both chunks share the same datatypes for the components that @@ -309,10 +364,7 @@ impl TimeColumn { let time_range = self.time_range.union(rhs.time_range); - let times = self - .times_raw() - .iter() - .chain(rhs.times_raw()) + let times = std::iter::chain(self.times_raw(), rhs.times_raw()) .copied() .collect_vec(); let times = ArrowScalarBuffer::from(times); @@ -489,8 +541,8 @@ mod tests { ), ); - assert!(got.is_sorted()); - assert!(got.is_time_sorted()); + assert!(got.is_row_ids_sorted()); + assert!(got.all_timelines_sorted()); } { assert!(chunk2.concatenable(&chunk1)); @@ -559,8 +611,8 @@ mod tests { ), ); - assert!(!got.is_sorted()); - assert!(!got.is_time_sorted()); + assert!(!got.is_row_ids_sorted()); + assert!(!got.all_timelines_sorted()); } Ok(()) @@ -722,8 +774,8 @@ mod tests { ), ); - assert!(got.is_sorted()); - assert!(got.is_time_sorted()); + assert!(got.is_row_ids_sorted()); + assert!(got.all_timelines_sorted()); } { assert!(chunk2.concatenable(&chunk1)); @@ -792,8 +844,8 @@ mod tests { ), ); - assert!(!got.is_sorted()); - assert!(!got.is_time_sorted()); + assert!(!got.is_row_ids_sorted()); + assert!(!got.all_timelines_sorted()); } Ok(()) @@ -933,4 +985,117 @@ mod tests { Ok(()) } + + /// Rebuild `chunk`'s timeline map by inserting the timelines in `order`. + /// + /// Insertion order affects iteration order when keys collide, giving equal key sets that + /// iterate differently. + fn with_reinserted_timelines(chunk: &Chunk, order: &[re_log_types::TimelineName]) -> Chunk { + let mut timelines = IntMap::default(); + for name in order { + timelines.insert(*name, chunk.timelines[name].clone()); + } + let mut chunk = chunk.clone(); + chunk.timelines = timelines; + chunk + } + + /// Equal timeline sets must concatenate no matter how the maps iterate, pairing time + /// columns by name. + /// + /// Regression test for order-sensitive `same_timelines` and positional pairing in + /// `concatenated`. + #[test] + fn concatenation_is_insensitive_to_timeline_map_iteration_order() -> anyhow::Result<()> { + use re_log_types::TimelineName; + + // Find names whose map iteration order depends on insertion order. Deterministic + // (fixed-seed hashes); panics if map internals change and nothing diverges anymore. + let iteration_order = |insertion_order: &[TimelineName]| { + let mut set = nohash_hasher::IntSet::::default(); + for name in insertion_order { + set.insert(*name); + } + set.iter().copied().collect_vec() + }; + let (order1, order2) = 'search: { + for i in 0..100 { + let names = ["a", "b", "c"] + .map(|s| TimelineName::try_new(format!("timeline_{s}_{i}")).unwrap()); + let reference = iteration_order(&names); + for perm in names.iter().copied().permutations(names.len()) { + if iteration_order(&perm) != reference { + break 'search (names.to_vec(), perm); + } + } + } + panic!( + "no timeline-name set found whose IntMap iteration order depends on insertion \ + order; did the hasher or hash-map internals change?" + ); + }; + + // Distinct time values per timeline per chunk, so wrongly paired columns can't match by + // accident. + let entity_path = "my/entity"; + let timepoint = |chunk_index: i64, row: i64| -> [(Timeline, i64); 3] { + std::array::from_fn(|k| { + let timeline_index = i64::try_from(k).expect("tiny index"); + ( + Timeline::new_sequence(order1[k]), + 1000 * (timeline_index + 1) + 10 * chunk_index + row, + ) + }) + }; + let points1 = &[MyPoint::new(1.0, 1.0)]; + let points2 = &[MyPoint::new(2.0, 2.0)]; + let build_chunk = |chunk_index: i64, points: &dyn re_types_core::ComponentBatch| { + Chunk::builder(entity_path) + .with_component_batches( + RowId::new(), + timepoint(chunk_index, 0), + [(MyPoints::descriptor_points(), points)], + ) + .with_component_batches( + RowId::new(), + timepoint(chunk_index, 1), + [(MyPoints::descriptor_points(), points)], + ) + .build() + }; + let chunk1 = build_chunk(0, points1 as _)?; + let chunk2 = build_chunk(1, points2 as _)?; + + // Force divergent map layouts; assert the precondition actually holds. + let chunk1 = with_reinserted_timelines(&chunk1, &order1); + let chunk2 = with_reinserted_timelines(&chunk2, &order2); + assert_ne!( + chunk1.timelines.keys().collect_vec(), + chunk2.timelines.keys().collect_vec(), + "test precondition: the two timeline maps must iterate in different orders" + ); + + assert!(chunk1.same_timelines(&chunk2)); + assert!(chunk1.concatenable(&chunk2)); + + let got = chunk1.concatenated(&chunk2)?; + + // Paired by name, not map position. + for name in &order1 { + let expected: Vec = std::iter::chain( + chunk1.timelines[name].times_raw(), + chunk2.timelines[name].times_raw(), + ) + .copied() + .collect(); + assert_eq!( + got.timelines()[name].times_raw(), + expected.as_slice(), + "timeline {name}" + ); + } + got.sanity_check()?; + + Ok(()) + } } diff --git a/crates/store/re_chunk/src/range.rs b/crates/store/re_chunk/src/range.rs index 341dc776f8c1..0bdc9f9e2da6 100644 --- a/crates/store/re_chunk/src/range.rs +++ b/crates/store/re_chunk/src/range.rs @@ -1,4 +1,4 @@ -use re_log_types::{AbsoluteTimeRange, TimeInt, TimelineName}; +use re_log_types::{AbsoluteTimeRange, TimelineName}; use re_types_core::ComponentIdentifier; use crate::Chunk; @@ -112,7 +112,7 @@ impl std::fmt::Debug for RangeQuery { } impl RangeQuery { - /// The returned query is guaranteed to never include [`TimeInt::STATIC`]. + /// The returned query is guaranteed to never include [`TimeInt::STATIC`](re_log_types::TimeInt::STATIC). #[inline] pub const fn new(timeline: TimelineName, range: AbsoluteTimeRange) -> Self { Self { @@ -122,7 +122,7 @@ impl RangeQuery { } } - /// The returned query is guaranteed to never include [`TimeInt::STATIC`]. + /// The returned query is guaranteed to never include [`TimeInt::STATIC`](re_log_types::TimeInt::STATIC). /// /// Keeps all extra timelines and components around. #[inline] @@ -238,10 +238,7 @@ impl Chunk { // NOTE: A given component for a given entity can only have one static entry associated // with it, and this entry overrides everything else, which means it is functionally // equivalent to just running a latest-at query. - if let Some(unit) = chunk.latest_at( - &crate::LatestAtQuery::new(*query.timeline(), TimeInt::MAX), - component, - ) { + if let Some(unit) = chunk.latest_at(&crate::LatestAtQuery::new_static(), component) { std::sync::Arc::unwrap_or_clone(unit.into_chunk()) } else { chunk.emptied() diff --git a/crates/store/re_chunk/src/shuffle.rs b/crates/store/re_chunk/src/shuffle.rs index 4c75bfe1aa7a..70d098f71b76 100644 --- a/crates/store/re_chunk/src/shuffle.rs +++ b/crates/store/re_chunk/src/shuffle.rs @@ -12,16 +12,22 @@ impl Chunk { /// /// This is O(1) (cached). /// - /// See also [`Self::is_sorted_uncached`]. + /// Even if this is true, individual timelines can still be unsorted. + /// + /// See also: + /// * [`Self::is_row_ids_sorted_uncached`] + /// * [`Self::all_timelines_sorted`] + /// * [`Self::is_timeline_sorted`] + /// * [`Self::unsorted_timelines`] #[inline] - pub fn is_sorted(&self) -> bool { + pub fn is_row_ids_sorted(&self) -> bool { self.is_sorted } /// For debugging purposes. #[doc(hidden)] #[inline] - pub fn is_sorted_uncached(&self) -> bool { + pub fn is_row_ids_sorted_uncached(&self) -> bool { re_tracing::profile_function!(); self.row_ids() @@ -31,9 +37,12 @@ impl Chunk { /// Is the chunk ascendingly sorted by time, for all of its timelines? /// + /// This is also known as an "ordered" chunk, + /// and when `false` this is also known as an "out-of-order" chunk. + /// /// This is O(1) (cached). #[inline] - pub fn is_time_sorted(&self) -> bool { + pub fn all_timelines_sorted(&self) -> bool { self.timelines .values() .all(|time_column| time_column.is_sorted()) @@ -61,17 +70,39 @@ impl Chunk { || self .timelines .get(timeline) - .is_some_and(|time_column| time_column.is_sorted_uncached()) + .is_some_and(|time_column| time_column.is_row_ids_sorted_uncached()) } - /// Sort the chunk, if needed. + /// Return all timelines that are not sorted relative to [`crate::RowId`]. + pub fn unsorted_timelines(&self) -> Vec { + self.timelines + .iter() + .filter_map(|(name, time_column)| { + if time_column.is_sorted() { + None + } else { + Some(*name) + } + }) + .collect() + } + + /// Sort the chunk by [`crate::RowId`], if needed. /// /// The underlying arrow data will be copied and shuffled in memory in order to make it contiguous. /// + /// Even after calling this, individual timelines may be unsorted. + /// /// If the chunk changes, it is given a new unique [`ChunkId`]. + /// + /// See also: + /// * [`Self::is_row_ids_sorted_uncached`] + /// * [`Self::all_timelines_sorted`] + /// * [`Self::is_timeline_sorted`] + /// * [`Self::unsorted_timelines`] #[inline] - pub fn sort_if_unsorted(&mut self) { - if self.is_sorted() { + pub fn sort_by_row_ids_if_needed(&mut self) { + if self.is_row_ids_sorted() { return; } @@ -280,7 +311,7 @@ impl Chunk { } } - self.is_sorted = self.is_sorted_uncached(); + self.is_sorted = self.is_row_ids_sorted_uncached(); } } @@ -289,7 +320,7 @@ impl TimeColumn { /// /// This is O(1) (cached). /// - /// See also [`Self::is_sorted_uncached`]. + /// See also [`Self::is_row_ids_sorted_uncached`]. #[inline] pub fn is_sorted(&self) -> bool { self.is_sorted @@ -302,7 +333,7 @@ impl TimeColumn { /// /// See also [`Self::is_sorted`]. #[inline] - pub fn is_sorted_uncached(&self) -> bool { + pub fn is_row_ids_sorted_uncached(&self) -> bool { re_tracing::profile_function!(); self.times_raw() .windows(2) @@ -383,8 +414,8 @@ mod tests { eprintln!("{chunk_sorted}"); - assert!(chunk_sorted.is_sorted()); - assert!(chunk_sorted.is_sorted_uncached()); + assert!(chunk_sorted.is_row_ids_sorted()); + assert!(chunk_sorted.is_row_ids_sorted_uncached()); let chunk_shuffled = { let mut chunk_shuffled = chunk_sorted.clone(); @@ -394,20 +425,20 @@ mod tests { eprintln!("{chunk_shuffled}"); - assert!(!chunk_shuffled.is_sorted()); - assert!(!chunk_shuffled.is_sorted_uncached()); + assert!(!chunk_shuffled.is_row_ids_sorted()); + assert!(!chunk_shuffled.is_row_ids_sorted_uncached()); assert_ne!(chunk_sorted, chunk_shuffled); let chunk_resorted = { let mut chunk_resorted = chunk_shuffled.clone(); - chunk_resorted.sort_if_unsorted(); + chunk_resorted.sort_by_row_ids_if_needed(); chunk_resorted }; eprintln!("{chunk_resorted}"); - assert!(chunk_resorted.is_sorted()); - assert!(chunk_resorted.is_sorted_uncached()); + assert!(chunk_resorted.is_row_ids_sorted()); + assert!(chunk_resorted.is_row_ids_sorted_uncached()); assert_eq!(chunk_sorted, chunk_resorted); } @@ -484,8 +515,8 @@ mod tests { eprintln!("unsorted:\n{chunk_unsorted_timeline2}"); - assert!(chunk_unsorted_timeline2.is_sorted()); - assert!(chunk_unsorted_timeline2.is_sorted_uncached()); + assert!(chunk_unsorted_timeline2.is_row_ids_sorted()); + assert!(chunk_unsorted_timeline2.is_row_ids_sorted_uncached()); assert!( chunk_unsorted_timeline2 @@ -499,7 +530,7 @@ mod tests { .timelines() .get(timeline1.name()) .unwrap() - .is_sorted_uncached() + .is_row_ids_sorted_uncached() ); assert!( @@ -514,7 +545,7 @@ mod tests { .timelines() .get(timeline2.name()) .unwrap() - .is_sorted_uncached() + .is_row_ids_sorted_uncached() ); let chunk_sorted_timeline2 = @@ -522,8 +553,8 @@ mod tests { eprintln!("sorted:\n{chunk_sorted_timeline2}"); - assert!(!chunk_sorted_timeline2.is_sorted()); - assert!(!chunk_sorted_timeline2.is_sorted_uncached()); + assert!(!chunk_sorted_timeline2.is_row_ids_sorted()); + assert!(!chunk_sorted_timeline2.is_row_ids_sorted_uncached()); assert!( !chunk_sorted_timeline2 @@ -537,7 +568,7 @@ mod tests { .timelines() .get(timeline1.name()) .unwrap() - .is_sorted_uncached() + .is_row_ids_sorted_uncached() ); assert!( @@ -552,7 +583,7 @@ mod tests { .timelines() .get(timeline2.name()) .unwrap() - .is_sorted_uncached() + .is_row_ids_sorted_uncached() ); let chunk_sorted_timeline2_expected = @@ -648,8 +679,8 @@ mod tests { eprintln!("{chunk}"); - assert!(chunk.is_sorted()); - assert!(chunk.is_sorted_uncached()); + assert!(chunk.is_row_ids_sorted()); + assert!(chunk.is_row_ids_sorted_uncached()); let alpha = chunk.timelines().get(&"alpha".into()).unwrap(); let beta = chunk.timelines().get(&"beta".into()).unwrap(); diff --git a/crates/store/re_chunk/src/slice.rs b/crates/store/re_chunk/src/slice.rs index ea5da5d1d917..9f007d8446e1 100644 --- a/crates/store/re_chunk/src/slice.rs +++ b/crates/store/re_chunk/src/slice.rs @@ -21,7 +21,7 @@ impl Chunk { pub fn cell(&self, row_id: RowId, component: ComponentIdentifier) -> Option { let list_array = self.components.get_array(component)?; - if self.is_sorted() { + if self.is_row_ids_sorted() { let row_ids = self.row_ids_slice(); let index = row_ids.binary_search(&row_id).ok()?; list_array.is_valid(index).then(|| list_array.value(index)) @@ -176,7 +176,7 @@ impl Chunk { // └──────────────┴───────────────────┴────────────────────────────────────────────┘ // // The original chunk is unsorted, but the new sliced one actually ends up being sorted. - chunk.is_sorted = is_sorted || chunk.is_sorted_uncached(); + chunk.is_sorted = is_sorted || chunk.is_row_ids_sorted_uncached(); chunk } @@ -493,7 +493,7 @@ impl Chunk { // └──────────────┴───────────────────┴────────────────────────────────────────────┘ // // The original chunk is unsorted, but the new filtered one actually ends up being sorted. - chunk.is_sorted = is_sorted || chunk.is_sorted_uncached(); + chunk.is_sorted = is_sorted || chunk.is_row_ids_sorted_uncached(); #[cfg(debug_assertions)] #[expect(clippy::unwrap_used)] // debug-only @@ -738,7 +738,7 @@ impl Chunk { // └──────────────┴───────────────────┴────────────────────────────────────────────┘ // // The original chunk is unsorted, but the new filtered one actually ends up being sorted. - chunk.is_sorted = is_sorted || chunk.is_sorted_uncached(); + chunk.is_sorted = is_sorted || chunk.is_row_ids_sorted_uncached(); #[cfg(debug_assertions)] #[expect(clippy::unwrap_used)] // debug-only @@ -821,7 +821,7 @@ impl Chunk { // └──────────────┴───────────────────┴────────────────────────────────────────────┘ // // The original chunk is unsorted, but the new filtered one actually ends up being sorted. - chunk.is_sorted = is_sorted || chunk.is_sorted_uncached(); + chunk.is_sorted = is_sorted || chunk.is_row_ids_sorted_uncached(); #[cfg(debug_assertions)] #[expect(clippy::unwrap_used)] // debug-only @@ -1089,7 +1089,7 @@ mod tests { (row_id5, mypoints_colors_component, Some(colors5 as _)), ]; - assert!(!chunk.is_sorted()); + assert!(!chunk.is_row_ids_sorted()); for (row_id, component, expected) in expectations { let expected = expected .and_then(|expected| re_types_core::ComponentBatch::to_arrow(expected).ok()); @@ -1097,8 +1097,8 @@ mod tests { similar_asserts::assert_eq!(expected, chunk.cell(*row_id, *component)); } - chunk.sort_if_unsorted(); - assert!(chunk.is_sorted()); + chunk.sort_by_row_ids_if_needed(); + assert!(chunk.is_row_ids_sorted()); for (row_id, component, expected) in expectations { let expected = expected @@ -1208,7 +1208,7 @@ mod tests { eprintln!("chunk:\n{chunk}"); { - let got = chunk.deduped_latest_on_index(&TimelineName::new("frame")); + let got = chunk.deduped_latest_on_index(&TimelineName::from("frame")); eprintln!("got:\n{got}"); assert_eq!(2, got.num_rows()); @@ -1343,7 +1343,7 @@ mod tests { eprintln!("chunk:\n{chunk}"); { - let got = chunk.deduped_latest_on_index(&TimelineName::new("frame")); + let got = chunk.deduped_latest_on_index(&TimelineName::from("frame")); eprintln!("got:\n{got}"); assert_eq!(1, got.num_rows()); diff --git a/crates/store/re_chunk/src/split.rs b/crates/store/re_chunk/src/split.rs index f5647065edb6..61e9c71a977f 100644 --- a/crates/store/re_chunk/src/split.rs +++ b/crates/store/re_chunk/src/split.rs @@ -50,7 +50,7 @@ impl Chunk { let needs_split_rows = chunk_max_rows > 0 && chunk_num_rows > chunk_max_rows; let needs_split_unsorted = chunk_max_rows_if_unsorted > 0 && chunk_num_rows > chunk_max_rows_if_unsorted - && !chunk.is_time_sorted(); + && !chunk.all_timelines_sorted(); if !needs_split_bytes && !needs_split_rows && !needs_split_unsorted { return vec![chunk]; diff --git a/crates/store/re_chunk/src/transport.rs b/crates/store/re_chunk/src/transport.rs index 0386c39fa681..cc1957e945ca 100644 --- a/crates/store/re_chunk/src/transport.rs +++ b/crates/store/re_chunk/src/transport.rs @@ -138,7 +138,11 @@ impl Chunk { )?) } - #[tracing::instrument(level = "trace", skip_all)] + /// Convert a chunk record batch to a chunk. + /// + /// This is for well-formed chunk batches. For generic record-batch-to-chunks conversion, see + /// [`Self::from_dataframe_record_batch`]. + //TODO(RR-4700): rename to `from_chunk_record_batch` pub fn from_record_batch(batch: &ArrowRecordBatch) -> ChunkResult { re_tracing::profile_function!(format!( "num_columns={} num_rows={}", @@ -148,7 +152,27 @@ impl Chunk { Self::from_chunk_batch(&re_sorbet::ChunkBatch::try_from(batch)?) } - #[tracing::instrument(level = "trace", skip_all)] + /// Convert an arbitrary record batch to one or more [`Chunk`]s. + /// + /// See [`re_sorbet::chunk_batches_from_dataframe_record_batch`] for details. + //TODO(RR-4700): rename to `from_record_batch` + pub fn from_dataframe_record_batch( + batch: &ArrowRecordBatch, + index: &re_sorbet::DataframeIndex, + entity_path: Option<&re_log_types::EntityPath>, + ) -> ChunkResult> { + re_tracing::profile_function!(format!( + "num_columns={} num_rows={}", + batch.num_columns(), + batch.num_rows() + )); + re_sorbet::chunk_batches_from_dataframe_record_batch(batch, index, entity_path) + .map_err(Box::new)? + .iter() + .map(Self::from_chunk_batch) + .collect() + } + pub fn from_chunk_batch(batch: &re_sorbet::ChunkBatch) -> ChunkResult { re_tracing::profile_function!(format!( "num_columns={} num_rows={}", @@ -248,6 +272,7 @@ impl Chunk { impl Chunk { #[inline] pub fn from_arrow_msg(msg: &re_log_types::ArrowMsg) -> ChunkResult { + re_tracing::profile_function!(); let re_log_types::ArrowMsg { chunk_id: _, batch, @@ -272,11 +297,16 @@ impl Chunk { #[cfg(test)] mod tests { + use std::sync::Arc; + + use arrow::array::{Float32Array, Int64Array, TimestampMicrosecondArray}; + use arrow::datatypes::{Field as ArrowField, Schema as ArrowSchema}; use nohash_hasher::IntMap; + use similar_asserts::assert_eq; + use re_log_types::example_components::{MyColor, MyPoint, MyPoints}; use re_log_types::{EntityPath, Timeline}; - use re_types_core::{ChunkId, Loggable as _, RowId}; - use similar_asserts::assert_eq; + use re_types_core::{ChunkId, Loggable as _, RowId, TimelineName}; use super::*; @@ -375,4 +405,79 @@ mod tests { Ok(()) } + + fn dataframe_batch(index: arrow::array::ArrayRef) -> ArrowRecordBatch { + let frame = ArrowField::new("frame", index.data_type().clone(), true).with_metadata( + [( + re_sorbet::metadata::RERUN_KIND.to_owned(), + re_sorbet::ColumnKind::Index.to_string(), + )] + .into(), + ); + let values = ArrowField::new("/e:c", arrow::datatypes::DataType::Float32, true) + .with_metadata( + [( + re_sorbet::metadata::SORBET_ENTITY_PATH.to_owned(), + "/e".to_owned(), + )] + .into(), + ); + ArrowRecordBatch::try_new_with_options( + Arc::new(ArrowSchema::new_with_metadata( + vec![frame, values], + Default::default(), + )), + vec![index, Arc::new(Float32Array::from(vec![1.0_f32, 2.0]))], + &arrow::array::RecordBatchOptions::default().with_row_count(Some(2)), + ) + .unwrap() + } + + #[test] + fn from_dataframe_record_batch_temporal() { + let batch = dataframe_batch(Arc::new(Int64Array::from(vec![0_i64, 1]))); + let chunks = + Chunk::from_dataframe_record_batch(&batch, &re_sorbet::DataframeIndex::Auto, None) + .unwrap(); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].entity_path(), &EntityPath::from("/e")); + assert!(!chunks[0].is_static()); + } + + #[test] + fn from_dataframe_record_batch_bad_index_dtype() { + // `timestamp(us)` is not a supported time type; this fails at classification. + let batch = dataframe_batch(Arc::new(TimestampMicrosecondArray::from(vec![0_i64, 1]))); + let err = Chunk::from_dataframe_record_batch( + &batch, + &re_sorbet::DataframeIndex::Columns(vec![TimelineName::from("frame")]), + None, + ) + .unwrap_err(); + assert!(matches!( + err, + ChunkError::DataframeToChunks(ref e) + if matches!(**e, re_sorbet::DataframeToChunksError::Sorbet( + re_sorbet::SorbetError::IndexColumn(_) + )) + )); + } + + #[test] + fn from_dataframe_record_batch_null_index() { + // A null in a promoted index column is rejected eagerly by the re_sorbet conversion. + let index = Arc::new(Int64Array::from(vec![Some(0_i64), None])); + let batch = dataframe_batch(index); + let err = + Chunk::from_dataframe_record_batch(&batch, &re_sorbet::DataframeIndex::Auto, None) + .unwrap_err(); + assert!( + matches!( + err, + ChunkError::DataframeToChunks(ref e) + if matches!(**e, re_sorbet::DataframeToChunksError::NullIndexColumn(_)) + ), + "got {err}" + ); + } } diff --git a/crates/store/re_chunk/src/unit_chunk.rs b/crates/store/re_chunk/src/unit_chunk.rs index efe42000f96a..abcd2f63f142 100644 --- a/crates/store/re_chunk/src/unit_chunk.rs +++ b/crates/store/re_chunk/src/unit_chunk.rs @@ -268,15 +268,17 @@ impl UnitChunkShared { /// Returns the index (`(TimeInt, RowId)` pair) of the single row within, on the given timeline. /// /// Returns the single static index if the chunk is static. + /// + /// A `None` timeline (a static-only query) only ever yields the static index. #[inline] - pub fn index(&self, timeline: &TimelineName) -> Option<(TimeInt, RowId)> { + pub fn index(&self, timeline: Option<&TimelineName>) -> Option<(TimeInt, RowId)> { debug_assert!(self.num_rows() == 1); if self.is_static() { self.row_ids() .next() .map(|row_id| (TimeInt::STATIC, row_id)) } else { - let time_column = self.timelines.get(timeline)?; + let time_column = self.timelines.get(timeline?)?; let time = time_column.times().next()?; self.row_ids().next().map(|row_id| (time, row_id)) } diff --git a/crates/store/re_chunk/tests/latest_at.rs b/crates/store/re_chunk/tests/latest_at.rs index d643eab16d4b..31ed9635d303 100644 --- a/crates/store/re_chunk/tests/latest_at.rs +++ b/crates/store/re_chunk/tests/latest_at.rs @@ -74,7 +74,7 @@ fn temporal_sorted() -> anyhow::Result<()> { .build()?; { - let query = LatestAtQuery::new(TimelineName::new("frame"), 2); + let query = LatestAtQuery::new(TimelineName::from("frame"), 2); let expected = Chunk::builder_with_id(chunk.id(), ENTITY_PATH) .with_sparse_component_batches( @@ -96,7 +96,7 @@ fn temporal_sorted() -> anyhow::Result<()> { query_and_compare((MyPoints::descriptor_labels(), &query), &chunk, &expected); } { - let query = LatestAtQuery::new(TimelineName::new("frame"), 4); + let query = LatestAtQuery::new(TimelineName::from("frame"), 4); let expected = Chunk::builder_with_id(chunk.id(), ENTITY_PATH) .with_sparse_component_batches( @@ -138,7 +138,7 @@ fn temporal_sorted() -> anyhow::Result<()> { query_and_compare((MyPoints::descriptor_labels(), &query), &chunk, &expected); } { - let query = LatestAtQuery::new(TimelineName::new("frame"), 6); + let query = LatestAtQuery::new(TimelineName::from("frame"), 6); let expected = Chunk::builder_with_id(chunk.id(), ENTITY_PATH) .with_sparse_component_batches( @@ -393,7 +393,7 @@ fn static_sorted() -> anyhow::Result<()> { .build()?; for frame_nr in [2, 4, 6] { - let query = LatestAtQuery::new(TimelineName::new("frame"), frame_nr); + let query = LatestAtQuery::new(TimelineName::from("frame"), frame_nr); let expected = Chunk::builder_with_id(chunk.id(), ENTITY_PATH) .with_sparse_component_batches( diff --git a/crates/store/re_chunk/tests/range.rs b/crates/store/re_chunk/tests/range.rs index 86b511bfe21f..186180c442e8 100644 --- a/crates/store/re_chunk/tests/range.rs +++ b/crates/store/re_chunk/tests/range.rs @@ -76,7 +76,7 @@ fn temporal_sorted() -> anyhow::Result<()> { { let query = - RangeQuery::with_extras(TimelineName::new("frame"), AbsoluteTimeRange::EVERYTHING); + RangeQuery::with_extras(TimelineName::from("frame"), AbsoluteTimeRange::EVERYTHING); let expected = Chunk::builder_with_id(chunk.id(), ENTITY_PATH) .with_sparse_component_batches( @@ -354,7 +354,7 @@ fn static_sorted() -> anyhow::Result<()> { .build()?; let queries = [ - RangeQuery::with_extras(TimelineName::new("frame"), AbsoluteTimeRange::EVERYTHING), + RangeQuery::with_extras(TimelineName::from("frame"), AbsoluteTimeRange::EVERYTHING), RangeQuery::with_extras(TimelineName::log_time(), AbsoluteTimeRange::new(1020, 1050)), ]; @@ -447,7 +447,7 @@ fn static_unsorted() -> anyhow::Result<()> { .build()?; let queries = [ - RangeQuery::with_extras(TimelineName::new("frame"), AbsoluteTimeRange::EVERYTHING), + RangeQuery::with_extras(TimelineName::from("frame"), AbsoluteTimeRange::EVERYTHING), RangeQuery::with_extras(TimelineName::log_time(), AbsoluteTimeRange::new(1020, 1050)), ]; diff --git a/crates/store/re_chunk/tests/timeline.rs b/crates/store/re_chunk/tests/timeline.rs index cd26eb8c8cfb..3578d950facb 100644 --- a/crates/store/re_chunk/tests/timeline.rs +++ b/crates/store/re_chunk/tests/timeline.rs @@ -20,7 +20,7 @@ fn out_of_order_timeline() { .build() .unwrap(); - let timeline_frame_nr = TimelineName::new("frame_nr"); + let timeline_frame_nr = TimelineName::from("frame_nr"); let timeline = chunk.timelines().get(&timeline_frame_nr).unwrap(); assert!(!timeline.is_sorted()); assert_eq!(timeline.time_range(), AbsoluteTimeRange::new(10, 30)); @@ -59,7 +59,7 @@ fn in_order_forwards_timeline() { .build() .unwrap(); - let timeline_frame_nr = TimelineName::new("frame_nr"); + let timeline_frame_nr = TimelineName::from("frame_nr"); let timeline = chunk.timelines().get(&timeline_frame_nr).unwrap(); assert!(timeline.is_sorted()); assert_eq!(timeline.time_range(), AbsoluteTimeRange::new(10, 30)); @@ -98,7 +98,7 @@ fn in_order_backwards_timeline() { .build() .unwrap(); - let timeline_frame_nr = TimelineName::new("frame_nr"); + let timeline_frame_nr = TimelineName::from("frame_nr"); let timeline = chunk.timelines().get(&timeline_frame_nr).unwrap(); assert!(!timeline.is_sorted()); assert_eq!(timeline.time_range(), AbsoluteTimeRange::new(10, 30)); diff --git a/crates/store/re_chunk_store/Cargo.toml b/crates/store/re_chunk_store/Cargo.toml index 5c00dc6df88c..8cd4fe246adc 100644 --- a/crates/store/re_chunk_store/Cargo.toml +++ b/crates/store/re_chunk_store/Cargo.toml @@ -45,6 +45,7 @@ ahash.workspace = true anyhow.workspace = true arrow.workspace = true document-features.workspace = true +futures.workspace = true indent.workspace = true itertools.workspace = true nohash-hasher.workspace = true diff --git a/crates/store/re_chunk_store/src/compact.rs b/crates/store/re_chunk_store/src/compact.rs index 761a91ee76ba..7f7b3a08c087 100644 --- a/crates/store/re_chunk_store/src/compact.rs +++ b/crates/store/re_chunk_store/src/compact.rs @@ -45,6 +45,11 @@ pub struct CompactionOptions { /// /// `None` disables the split. pub split_size_ratio: Option, + + /// If true, any user-supplied `VideoStream:is_keyframe` data is dropped and + /// re-derived from codec analysis during video rebatching. This bypasses + /// validation of the user-supplied labels. + pub fix_keyframe: bool, } impl ChunkStore { @@ -54,7 +59,8 @@ impl ChunkStore { /// datatypes, up to the thresholds in the config. Large chunks may be split. /// /// If `is_start_of_gop` is provided, video stream chunks are rebatched to align - /// with GoP boundaries after compaction. + /// with GoP boundaries after compaction, and sparse `is_keyframe` marker chunks + /// are emitted. /// /// If `split_size_ratio` is provided, chunks are split on entry so no two /// archetype groups sharing a chunk differ in byte size by more than that factor. @@ -103,6 +109,7 @@ impl ChunkStore { num_extra_passes, is_start_of_gop, split_size_ratio, + fix_keyframe, } = options; let num_extra_passes = num_extra_passes.unwrap_or(50); @@ -159,13 +166,14 @@ impl ChunkStore { &self, config, is_start_of_gop.as_ref(), + *fix_keyframe, ) { Ok(new_store) => { self = new_store; re_log::info!(time = ?now.elapsed(), "video GoP rebatching completed"); } Err(err) => { - re_log::warn!(%err, "video GoP rebatching failed"); + return Err(ChunkStoreError::VideoRebatch(err)); } } } @@ -194,6 +202,7 @@ mod tests { num_extra_passes: Some(0), is_start_of_gop: None, split_size_ratio: None, + fix_keyframe: false, }; let result = store .finalize_compaction(&options) @@ -257,6 +266,7 @@ mod tests { num_extra_passes: Some(3), is_start_of_gop: None, split_size_ratio: Some(10.0), + fix_keyframe: false, }; let compacted = store.compacted(&options)?; @@ -284,6 +294,69 @@ mod tests { Ok(()) } + /// Regression lock for the `OBJECT_STORE` default: the profile must carry a + /// `split_size_ratio` that actually separates thick columns from thin ones when + /// wired through `compacted`, and no rows may be lost in the process. + #[test] + fn object_store_profile_splits_thick_from_thin() -> anyhow::Result<()> { + re_log::setup_logging(); + + let entity = EntityPath::from("camera"); + let blob_bytes = 128 * 1024; // well above the scalar payload + + let mut store = ChunkStore::new( + StoreId::random(StoreKind::Recording, "test_app"), + ChunkStoreConfig::ALL_DISABLED, + ); + for frame in 0..4 { + store.insert_chunk(&mixed_chunk(&entity, frame, blob_bytes))?; + } + + // Wire exactly as the `rrd optimize` / catalog ingestion paths do: the ratio + // comes from the profile, not a hard-coded literal. If someone resets + // `OBJECT_STORE.split_size_ratio` to `None`, this test fails. + let profile = crate::OptimizationProfile::OBJECT_STORE; + let options = CompactionOptions { + config: profile.to_chunk_store_config(), + num_extra_passes: Some(3), + is_start_of_gop: None, + split_size_ratio: profile.split_size_ratio, + fix_keyframe: false, + }; + let compacted = store.compacted(&options)?; + + // No output chunk may mix the two archetypes. + for chunk in compacted.iter_physical_chunks() { + let archetypes: std::collections::BTreeSet<_> = chunk + .components() + .values() + .map(|c| c.descriptor.archetype) + .collect(); + assert_eq!( + archetypes.len(), + 1, + "OBJECT_STORE profile left a chunk mixing archetypes: {archetypes:?}", + ); + } + + // Every row of every archetype must survive the split: 4 frames each. + let rows_for = |archetype: ArchetypeName| -> u64 { + compacted + .iter_physical_chunks() + .filter(|c| { + c.components() + .values() + .any(|c| c.descriptor.archetype == Some(archetype)) + }) + .map(|c| c.num_rows() as u64) + .sum() + }; + assert_eq!(rows_for(ArchetypeName::from("my.Video")), 4); + assert_eq!(rows_for(ArchetypeName::from("my.Points")), 4); + + Ok(()) + } + #[test] fn compacted_leaves_mixed_chunk_alone_without_ratio() -> anyhow::Result<()> { re_log::setup_logging(); @@ -302,6 +375,7 @@ mod tests { num_extra_passes: Some(3), is_start_of_gop: None, split_size_ratio: None, + fix_keyframe: false, }; let compacted = store.compacted(&options)?; diff --git a/crates/store/re_chunk_store/src/dataframe.rs b/crates/store/re_chunk_store/src/dataframe.rs index ac49ceda175d..56c87aab831f 100644 --- a/crates/store/re_chunk_store/src/dataframe.rs +++ b/crates/store/re_chunk_store/src/dataframe.rs @@ -5,6 +5,7 @@ use std::ops::{Deref, DerefMut}; use arrow::datatypes::DataType as ArrowDatatype; use re_chunk::{ComponentIdentifier, LatestAtQuery, RangeQuery, TimelineName}; +use re_log::ResultExt as _; use re_log_types::{AbsoluteTimeRange, EntityPath, TimeInt, Timeline}; use re_sorbet::{ ChunkColumnDescriptors, ColumnSelector, ComponentColumnDescriptor, ComponentColumnSelector, @@ -342,7 +343,7 @@ impl ChunkStore { component_type: None, entity_path: selector.entity_path.clone(), archetype: None, - component: selector.component.as_str().into(), + component: selector.component_identifier().ok_or_log_error_once()?, is_static: false, is_tombstone: false, is_semantically_empty: false, @@ -353,7 +354,7 @@ impl ChunkStore { .per_column_metadata_for_entity(&selector.entity_path)?; // We perform a scan over all component descriptors in the queried entity path. - let entry = per_identifier.get(&selector.component.as_str().into())?; + let entry = per_identifier.get(&result.component)?; result.store_datatype = entry.datatype.clone(); result.archetype = entry.descriptor.archetype; diff --git a/crates/store/re_chunk_store/src/drop_time_range.rs b/crates/store/re_chunk_store/src/drop_time_range.rs index 8b2e5e6d4423..58021da5b685 100644 --- a/crates/store/re_chunk_store/src/drop_time_range.rs +++ b/crates/store/re_chunk_store/src/drop_time_range.rs @@ -134,7 +134,7 @@ impl ChunkStore { let mut events = self.finalize_events(deletion_diffs); for mut chunk in new_chunks { - chunk.sort_if_unsorted(); + chunk.sort_by_row_ids_if_needed(); #[expect(clippy::unwrap_used)] // The chunk came from the store, so it should be fine events.append(&mut self.insert_chunk(&chunk.into()).unwrap()); } diff --git a/crates/store/re_chunk_store/src/entity_tree.rs b/crates/store/re_chunk_store/src/entity_tree.rs index 1cf7f18ebc10..9390629c6586 100644 --- a/crates/store/re_chunk_store/src/entity_tree.rs +++ b/crates/store/re_chunk_store/src/entity_tree.rs @@ -7,7 +7,7 @@ use re_log_types::{EntityPath, EntityPathPart}; /// A recursive tree structure that maintains the entity hierarchy. /// /// The tree contains a list of subtrees, and so on recursively. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, re_byte_size::SizeBytes)] pub struct EntityTree { /// Full path prefix to the root of this (sub)tree. pub path: EntityPath, @@ -125,13 +125,6 @@ impl EntityTree { } } -impl re_byte_size::SizeBytes for EntityTree { - fn heap_size_bytes(&self) -> u64 { - let Self { path, children } = self; - path.heap_size_bytes() + children.heap_size_bytes() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/store/re_chunk_store/src/events.rs b/crates/store/re_chunk_store/src/events.rs index 44d1cefdeb81..d6f8244a1cbd 100644 --- a/crates/store/re_chunk_store/src/events.rs +++ b/crates/store/re_chunk_store/src/events.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use re_chunk::Chunk; +use re_log::ResultExt as _; use re_log_encoding::RrdManifest; use re_log_types::StoreId; @@ -449,15 +450,16 @@ impl ChunkStoreDiffVirtualAddition { re_sorbet::ColumnKind::try_from(f.as_ref()).ok() == Some(re_sorbet::ColumnKind::Component) }) - .map(|field| { + .filter_map(|field| { let inner_arrow_datatype = match field.data_type() { arrow::datatypes::DataType::List(inner) | arrow::datatypes::DataType::LargeList(inner) => inner.data_type().clone(), other => other.clone(), }; - let descriptor = re_sdk_types::ComponentDescriptor::from((**field).clone()); - ( + let descriptor = re_sdk_types::ComponentDescriptor::try_from((**field).clone()) + .ok_or_log_error_once()?; + Some(( descriptor.component, ChunkComponentMeta { descriptor, @@ -466,7 +468,7 @@ impl ChunkStoreDiffVirtualAddition { has_data: false, is_static: false, }, - ) + )) }) .collect(); diff --git a/crates/store/re_chunk_store/src/gc.rs b/crates/store/re_chunk_store/src/gc.rs index b6ec2373c485..d115ef9cc370 100644 --- a/crates/store/re_chunk_store/src/gc.rs +++ b/crates/store/re_chunk_store/src/gc.rs @@ -1,8 +1,8 @@ +use std::collections::btree_map::Entry as BTreeMapEntry; use std::sync::Arc; use std::time::Duration; use ahash::HashSet; -use itertools::Itertools as _; use nohash_hasher::IntMap; use re_format::format_bytes; use web_time::Instant; @@ -14,6 +14,10 @@ use re_log_types::{AbsoluteTimeRange, TimeInt}; use crate::{ ChunkDeletionReason, ChunkStore, ChunkStoreChunkStats, ChunkStoreDiff, ChunkStoreDiffDeletion, ChunkStoreEvent, ChunkStoreStats, + store::{ + ChunkIdSetPerTimePerComponentPerTimelinePerEntity, ChunkIdSetPerTimePerTimelinePerEntity, + }, + writes::LineageDroppingCtx, }; // Used all over in docstrings. @@ -321,7 +325,7 @@ impl ChunkStore { .time_budget .saturating_sub(mark_start_time.elapsed().min(mark_time_budget)); - { + let dels = { re_tracing::profile_scope!("sweep"); // There is never a good reason not to deeply GC non-root level chunks: they cannot be @@ -359,8 +363,18 @@ impl ChunkStore { ChunkDeletionReason::GarbageCollection, ); - dels1.into_iter().chain(dels2).map(Into::into).collect() - } + std::iter::chain(dels1, dels2).map(Into::into).collect() + }; + + self.chunks_lineage.shrink_to_fit(); + self.leaky_compactions.shrink_to_fit(); + self.split_on_ingest.shrink_to_fit(); + self.dangling_splits.shrink_to_fit(); + + let mut queried_chunk_id_tracker = self.queried_chunk_id_tracker.write(); + queried_chunk_id_tracker.shrink_to_fit(); + + dels } #[must_use] @@ -477,6 +491,117 @@ impl ChunkStore { chunks_to_be_removed } + /// Removes a single temporal chunk from the virtual indices, both the per-component ones and + /// the component-less ones. + /// + /// Returns `true` if the chunk was present in (and removed from) any of those indices. + fn remove_chunk_from_virtual_indices( + temporal_chunk_ids_per_entity_per_component: &mut ChunkIdSetPerTimePerComponentPerTimelinePerEntity, + temporal_chunk_ids_per_entity: &mut ChunkIdSetPerTimePerTimelinePerEntity, + chunk: &Arc, + ) -> bool { + let chunk_id = chunk.id(); + let mut was_removed = false; + + { + re_tracing::profile_scope!("removal (w/ component)"); + + if let Some(temporal_chunk_ids_per_timeline) = + temporal_chunk_ids_per_entity_per_component.get_mut(chunk.entity_path()) + { + for (timeline, time_range_per_component) in chunk.time_range_per_component() { + let Some(temporal_chunk_ids_per_component) = + temporal_chunk_ids_per_timeline.get_mut(&timeline) + else { + continue; + }; + + for (component, time_range) in time_range_per_component { + let Some(temporal_chunk_ids_per_time) = + temporal_chunk_ids_per_component.get_mut(&component) + else { + continue; + }; + + // TODO(cmc): Technically, the optimal thing to do would be to recompute + // `max_interval_length` per time here. + // In practice, this adds a lot of complexity for likely very little + // performance benefit, since we expect the chunks to have similar interval + // lengths on the happy path. + + if let BTreeMapEntry::Occupied(mut entry) = temporal_chunk_ids_per_time + .per_start_time + .entry(time_range.min()) + { + entry.get_mut().remove(&chunk_id); + if entry.get().is_empty() { + entry.remove(); + } + was_removed = true; + } + if let BTreeMapEntry::Occupied(mut entry) = temporal_chunk_ids_per_time + .per_end_time + .entry(time_range.max()) + { + entry.get_mut().remove(&chunk_id); + if entry.get().is_empty() { + entry.remove(); + } + was_removed = true; + } + } + } + } + } + + { + re_tracing::profile_scope!("removal (w/o component)"); + + if let Some(temporal_chunk_ids_per_timeline) = + temporal_chunk_ids_per_entity.get_mut(chunk.entity_path()) + { + for (timeline, time_column) in chunk.timelines() { + let Some(temporal_chunk_ids_per_time) = + temporal_chunk_ids_per_timeline.get_mut(timeline) + else { + continue; + }; + + let time_range = time_column.time_range(); + + // TODO(cmc): Technically, the optimal thing to do would be to recompute + // `max_interval_length` per time here. + // In practice, this adds a lot of complexity for likely very little + // performance benefit, since we expect the chunks to have similar interval + // lengths on the happy path. + + if let BTreeMapEntry::Occupied(mut entry) = temporal_chunk_ids_per_time + .per_start_time + .entry(time_range.min()) + { + entry.get_mut().remove(&chunk_id); + if entry.get().is_empty() { + entry.remove(); + } + was_removed = true; + } + if let BTreeMapEntry::Occupied(mut entry) = temporal_chunk_ids_per_time + .per_end_time + .entry(time_range.max()) + { + entry.get_mut().remove(&chunk_id); + if entry.get().is_empty() { + entry.remove(); + } + was_removed = true; + } + } + } + } + + was_removed + } + /// Surgically removes a set of _temporal_ [`ChunkId`]s from all *physical & virtual* indices. /// /// This only makes sense to use on chunks that resulted from the compaction of other chunks. @@ -501,8 +626,14 @@ impl ChunkStore { // nothing, doesn't mean that a deep one won't. // The deep diff is always a superset of the shallow one (because you can remove physical // chunks while keeping virtual ones, but not vice-versa). + // + // We pass `None` instead of `time_budget` on purpose. The deep loop below clears the + // virtual indexes regardless of the budget, so the shallow pass has to clear the physical + // ones to completion as well. If shallow bailed early on the budget, every chunk past the + // cutoff would keep its physical data but lose its virtual entry, drifting the two indexes + // apart (a physical chunk without a virtual entry) on a later GC pass. let deletions_shallow = - self.remove_chunks_shallow(chunks_to_be_removed.clone(), time_budget, reason); + self.remove_chunks_shallow(chunks_to_be_removed.clone(), None, reason); let Self { id: _, @@ -536,115 +667,27 @@ impl ChunkStore { let mut deletions = Vec::new(); for chunk in chunks_to_be_removed { - let mut was_removed = false; - let chunk_id = chunk.id(); - - { - re_tracing::profile_scope!("removal (w/ component)"); - - if let Some(temporal_chunk_ids_per_timeline) = - temporal_chunk_ids_per_entity_per_component.get_mut(chunk.entity_path()) - { - for (timeline, time_range_per_component) in chunk.time_range_per_component() { - let Some(temporal_chunk_ids_per_component) = - temporal_chunk_ids_per_timeline.get_mut(&timeline) - else { - continue; - }; - - for (component, time_range) in time_range_per_component { - let Some(temporal_chunk_ids_per_time) = - temporal_chunk_ids_per_component.get_mut(&component) - else { - continue; - }; - - // TODO(cmc): Technically, the optimal thing to do would be to recompute - // `max_interval_length` per time here. - // In practice, this adds a lot of complexity for likely very little - // performance benefit, since we expect the chunks to have similar interval - // lengths on the happy path. - - if let Some(set) = temporal_chunk_ids_per_time - .per_start_time - .get_mut(&time_range.min()) - { - set.remove(&chunk_id); - was_removed = true; - } - if let Some(set) = temporal_chunk_ids_per_time - .per_end_time - .get_mut(&time_range.max()) - { - set.remove(&chunk_id); - was_removed = true; - } - } - } - } - } - - { - re_tracing::profile_scope!("insertion (w/o component)"); - - if let Some(temporal_chunk_ids_per_timeline) = - temporal_chunk_ids_per_entity.get_mut(chunk.entity_path()) - { - for (timeline, time_column) in chunk.timelines() { - let Some(temporal_chunk_ids_per_time) = - temporal_chunk_ids_per_timeline.get_mut(timeline) - else { - continue; - }; - - let time_range = time_column.time_range(); - - // TODO(cmc): Technically, the optimal thing to do would be to recompute - // `max_interval_length` per time here. - // In practice, this adds a lot of complexity for likely very little - // performance benefit, since we expect the chunks to have similar interval - // lengths on the happy path. - - if let Some(set) = temporal_chunk_ids_per_time - .per_start_time - .get_mut(&time_range.min()) - { - set.remove(&chunk_id); - was_removed = true; - } - if let Some(set) = temporal_chunk_ids_per_time - .per_end_time - .get_mut(&time_range.max()) - { - set.remove(&chunk_id); - was_removed = true; - } - } - } - } - - if was_removed { + if Self::remove_chunk_from_virtual_indices( + temporal_chunk_ids_per_entity_per_component, + temporal_chunk_ids_per_entity, + &chunk, + ) { deletions.push(ChunkStoreDiffDeletion { chunk, reason }); } } - re_log::debug_assert!( - deletions.len() >= deletions_shallow.len() && { - let del_ids: ahash::HashSet<_> = - deletions.iter().map(|del| del.chunk.id()).collect(); - deletions_shallow - .iter() - .all(|del| del_ids.contains(&del.chunk.id())) - }, - "deep del should always be a superset of the shallow del:\ndeep: [{}]\nshallow: [{}]", - deletions - .iter() - .map(|del| del.chunk.id().to_string()) - .join(", "), + // Volatile (unrecoverable) chunks are fully reclaimed by the shallow pass above: it drops + // their physical data, reclaims their lineage, and -- since an unrecoverable chunk must + // never linger as a ghost virtual entry that a query would wrongly report as missing -- + // purges them from the virtual indices as well. The loop above therefore no longer finds + // those, so fold the shallow deletions back in to keep the deep deletion set a superset of + // the shallow one. + let deep_ids: ahash::HashSet = + deletions.iter().map(|del| del.chunk.id()).collect(); + deletions.extend( deletions_shallow - .iter() - .map(|del| del.chunk.id().to_string()) - .join(", "), + .into_iter() + .filter(|del| !deep_ids.contains(&del.chunk.id())), ); deletions @@ -701,6 +744,27 @@ impl ChunkStore { continue; }; + Self::drop_lineage_reference( + &mut LineageDroppingCtx { + chunks_lineage: &mut self.chunks_lineage, + leaky_compactions: &mut self.leaky_compactions, + split_on_ingest: &mut self.split_on_ingest, + dangling_splits: &mut self.dangling_splits, + }, + &chunk.id(), + ); + + // If dropping the physical reference reclaimed the lineage entirely, this chunk is + // unrecoverable: it must not linger as a ghost entry in the virtual indices, or queries + // would keep reporting it as a (re-fetchable) missing chunk forever. + if !self.chunks_lineage.contains_key(&chunk.id()) { + Self::remove_chunk_from_virtual_indices( + &mut self.temporal_chunk_ids_per_entity_per_component, + &mut self.temporal_chunk_ids_per_entity, + &chunk, + ); + } + // TODO(cmc): Technically, the optimal thing to do would be to recompute // `max_interval_length` per time here. // In practice, this adds a lot of complexity for likely very little diff --git a/crates/store/re_chunk_store/src/lazy_rrd_store.rs b/crates/store/re_chunk_store/src/lazy_rrd_store.rs deleted file mode 100644 index c1c83283de20..000000000000 --- a/crates/store/re_chunk_store/src/lazy_rrd_store.rs +++ /dev/null @@ -1,532 +0,0 @@ -use std::fs::File; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use ahash::{HashMap, HashMapExt as _}; -use nohash_hasher::{IntMap, IntSet}; -use parking_lot::Mutex; - -use re_chunk::{Chunk, ChunkId}; -use re_log_encoding::{CodecResult, RawRrdManifest, RrdManifest}; -use re_log_types::{AbsoluteTimeRange, EntityPath, StoreId, Timeline}; - -use crate::{ - ChunkStore, ChunkStoreConfig, ChunkStoreHandle, ChunkStoreResult, ChunkTrackingMode, - EntityTree, ExtractPropertiesError, LatestAtQuery, QueryResults, RangeQuery, StoreSchema, -}; - -/// A [`ChunkStore`] backed by an RRD file, with index loaded but chunks loaded on demand. -/// -/// Constructed from a single store's [`RrdManifest`]. Store selection (which manifest to extract -/// from the `RrdFooter`) is the caller's responsibility. -/// -/// On construction, the `ChunkStore`'s virtual index is populated via `insert_rrd_manifest()`. -/// No physical chunk data is loaded until [`Self::load_chunks`] or [`Self::load_all_chunks`] -/// is called. -/// -/// Holds the RRD file open for the lifetime of the store, so that lazy chunk reads succeed -/// even if the file is deleted from the filesystem after construction. -//TODO(RR-4341): this abstraction is very primitive. We need a more general `ChunkProvider`-style -// abstraction to cover for the many larger-than-RAM use cases. -pub struct LazyRrdStore { - store: ChunkStoreHandle, - file: Mutex, - rrd_path: PathBuf, - raw_manifest: Arc, - manifest: Arc, - - /// Precomputed map from `ChunkId` to manifest row index. - chunk_id_to_index: HashMap, - - /// Precomputed per-chunk timeline ranges. - timeline_ranges: HashMap>, -} - -impl LazyRrdStore { - /// Create a new lazy store from a manifest and an open file handle. - /// Populates the virtual index (no data loaded). - /// - /// The caller is responsible for reading the `RrdFooter` from the file and selecting - /// the appropriate manifest (e.g. filtering by `StoreKind::Recording`). This keeps - /// store-selection policy out of `re_chunk_store`. The manifest **must** come from - /// the same file — byte offsets in the manifest are meaningless otherwise. - /// - /// `rrd_path` is kept for diagnostic messages only; all I/O goes through `file`. - pub fn try_new( - file: File, - rrd_path: PathBuf, - raw_manifest: Arc, - ) -> CodecResult { - let manifest = Arc::new(RrdManifest::try_new(&raw_manifest)?); - - // IMPORTANT: `ALL_DISABLED` here is load-bearing, since the `ChunkStore` is essentially - // acting as a cache for the underlying RRD. Any compaction, etc. would lead to unexpected - // consequences. - let mut store = - ChunkStore::new(manifest.store_id().clone(), ChunkStoreConfig::ALL_DISABLED); - - #[expect(clippy::let_underscore_must_use)] - let _ = store.insert_rrd_manifest(Arc::clone(&manifest)); - - let chunk_id_to_index: HashMap = manifest - .col_chunk_ids() - .iter() - .enumerate() - .map(|(i, &id)| (id, i)) - .collect(); - - let timeline_ranges = Self::build_timeline_ranges(&manifest); - - Ok(Self { - store: ChunkStoreHandle::new(store), - file: Mutex::new(file), - rrd_path, - raw_manifest, - manifest, - chunk_id_to_index, - timeline_ranges, - }) - } - - fn build_timeline_ranges( - manifest: &RrdManifest, - ) -> HashMap> { - let mut result: HashMap> = HashMap::new(); - for per_entity in manifest.temporal_map().values() { - for (timeline, per_component) in per_entity { - for per_chunk in per_component.values() { - for (&chunk_id, entry) in per_chunk { - let e = result.entry(chunk_id).or_default(); - e.entry(*timeline) - .and_modify(|existing| { - *existing = existing.union(entry.time_range); - }) - .or_insert(entry.time_range); - } - } - } - } - result - } - - /// Load specific chunks from disk into the store. - /// - /// Chunks that are already physically loaded are skipped. - /// Returns an error if any chunk ID is not in the manifest. - /// All I/O happens without holding any store lock. - pub fn load_chunks(&self, chunk_ids: &[ChunkId]) -> ChunkStoreResult>> { - // 1. Filter out chunks that are already physical. - let to_load: Vec = { - let guard = self.store.read(); - chunk_ids - .iter() - .filter(|id| guard.physical_chunk(id).is_none()) - .copied() - .collect() - }; - - if to_load.is_empty() { - return Ok(Vec::new()); - } - - // 2. Read from disk — NO store lock held. - // Returns `CodecError::ChunkNotInManifest` if any ID is unknown. - let loaded = { - let mut file = self.file.lock(); - re_log_encoding::read_chunks(&mut file, &self.manifest, &to_load)? - }; - - // 3. Insert into store. - let mut store = self.store.write(); - for chunk in &loaded { - // insert_chunk on an already-present ChunkId is a no-op. - store.insert_chunk(chunk)?; - } - - Ok(loaded) - } - - /// Load all chunks from the RRD file into the store. - pub fn load_all_chunks(&self) -> ChunkStoreResult<()> { - self.load_chunks(self.manifest.col_chunk_ids())?; - Ok(()) - } - - /// The store's schema, populated from the manifest (available without loading chunks). - #[inline] - pub fn schema(&self) -> StoreSchema { - self.store.read().schema().clone() - } - - /// The entity tree, populated from the manifest (available without loading chunks). - pub fn entity_tree(&self) -> EntityTree { - self.store.read().entity_tree().clone() - } - - /// The number of chunks described by the manifest (physical + virtual). - pub fn num_chunks(&self) -> usize { - self.manifest.num_chunks() - } - - /// The number of chunks currently loaded in memory. - pub fn num_physical_chunks(&self) -> usize { - self.store.read().num_physical_chunks() - } - - /// Whether a specific chunk is currently loaded in memory. - pub fn has_physical_chunk(&self, chunk_id: &ChunkId) -> bool { - self.store.read().physical_chunk(chunk_id).is_some() - } - - /// Load all chunks, then return a compacted copy of the store. - pub fn compacted(&self, options: &crate::CompactionOptions) -> ChunkStoreResult { - self.load_all_chunks()?; - self.store.read().compacted(options) - } - - /// Load all chunks and return them. - pub fn collect_physical_chunks(&self) -> ChunkStoreResult>> { - self.load_all_chunks()?; - Ok(self.store.read().iter_physical_chunks().cloned().collect()) - } - - /// Path to the source RRD file. - pub fn rrd_path(&self) -> &Path { - &self.rrd_path - } - - /// The parsed manifest for this store. - pub fn manifest(&self) -> &Arc { - &self.manifest - } - - /// The raw manifest as-parsed from the RRD footer, before validation/extraction. - /// - /// Kept around so the server can synthesize `GetRrdManifest` responses without materializing - /// chunks: the footer already contains everything a client needs to pick which chunks to fetch. - pub fn raw_manifest(&self) -> &Arc { - &self.raw_manifest - } - - /// Look up the manifest row index for a given chunk ID. - pub fn chunk_row_index(&self, chunk_id: &ChunkId) -> Option { - self.chunk_id_to_index.get(chunk_id).copied() - } - - /// Per-chunk timeline ranges. - pub fn timeline_ranges(&self) -> &HashMap> { - &self.timeline_ranges - } - - /// The store ID (from the manifest, no store lock needed). - pub fn store_id(&self) -> &StoreId { - self.manifest.store_id() - } - - /// All entity paths known to this store (populated from the virtual index). - pub fn all_entities(&self) -> IntSet { - self.store.read().all_entities() - } - - /// Get a physical chunk by ID if it's already loaded. Returns `None` for - /// virtual-only chunks — use [`Self::load_chunks`] to materialize them first. - pub fn physical_chunk(&self, id: &ChunkId) -> Option> { - self.store.read().physical_chunk(id).cloned() - } - - /// Extract properties, automatically loading the required property chunks - /// on demand if they are still virtual. - //TODO(RR-4458): currently takes one disk round-trip per property entity with virtual - // chunks because `ChunkStore::extract_properties` short-circuits on the first missing - // entity. Once it reports the full union of missing chunks, this will converge in a - // single retry. - pub fn extract_properties(&self) -> Result { - self.with_autoload(|store| store.extract_properties()) - } - - /// Run an operation against the inner [`ChunkStore`], auto-loading any chunks the - /// operation reports as missing and retrying until it succeeds or returns a different - /// error. - /// - /// The closure receives `&ChunkStore` rather than `&self`, which structurally prevents - /// the read guard from escaping a single iteration — [`Self::load_chunks`] needs the - /// write lock, and holding a read guard across that call would deadlock. - /// - /// A generous fixed attempt cap guards against a bug downstream (e.g. `load_chunks` - /// silently no-ops while `MissingData` keeps being reported): exceeding it surfaces - /// as an `Internal` error instead of spinning forever. In practice this loop converges - /// in a handful of iterations; the cap is a paranoia valve, not a tight bound. - fn with_autoload(&self, mut op: F) -> Result - where - F: FnMut(&ChunkStore) -> Result, - { - const MAX_AUTOLOAD_ATTEMPTS: usize = 1024; - for _ in 0..MAX_AUTOLOAD_ATTEMPTS { - // IMPORTANT: bind to a local first so the read-guard temporary from - // `self.store.read()` is dropped at this statement's semicolon. Matching on - // `op(&self.store.read())` directly would extend the scrutinee's temporaries - // through the arms and `self.load_chunks` (write lock) would deadlock. - let result = op(&self.store.read()); - match result { - Err(ExtractPropertiesError::MissingData(missing_ids)) => { - self.load_chunks(&missing_ids) - .map_err(|err| ExtractPropertiesError::Internal(err.to_string()))?; - } - other => return other, - } - } - Err(ExtractPropertiesError::Internal(format!( - "autoload did not converge after {MAX_AUTOLOAD_ATTEMPTS} attempts" - ))) - } - - /// Run a latest-at query against the virtual index. - /// - /// Returns [`QueryResults`] with physical chunks in `chunks` and - /// not-yet-loaded chunk IDs in `missing_virtual`. - pub fn latest_at_relevant_chunks_for_all_components( - &self, - report_mode: ChunkTrackingMode, - query: &LatestAtQuery, - entity_path: &EntityPath, - include_static: bool, - ) -> QueryResults { - self.store - .read() - .latest_at_relevant_chunks_for_all_components( - report_mode, - query, - entity_path, - include_static, - ) - } - - /// Run a range query against the virtual index. - /// - /// Returns [`QueryResults`] with physical chunks in `chunks` and - /// not-yet-loaded chunk IDs in `missing_virtual`. - pub fn range_relevant_chunks_for_all_components( - &self, - report_mode: ChunkTrackingMode, - query: &RangeQuery, - entity_path: &EntityPath, - include_static: bool, - ) -> QueryResults { - self.store.read().range_relevant_chunks_for_all_components( - report_mode, - query, - entity_path, - include_static, - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use re_chunk::{RowId, TimePoint, Timeline}; - use re_log_encoding::EncodingOptions; - use re_log_types::{ - EntityPath, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind, StoreSource, - example_components::{MyPoint, MyPoints}, - }; - - /// Helper: create test chunks and encode to RRD file. - /// Returns `(path, open file handle, store_id, chunks)`. - fn create_test_rrd( - dir: &Path, - num_entities: usize, - num_frames: usize, - ) -> (PathBuf, File, StoreId, Vec>) { - let path = dir.join("test.rrd"); - let store_id = StoreId::random(StoreKind::Recording, "test"); - let store_info = StoreInfo::new(store_id.clone(), StoreSource::Unknown); - let timeline = Timeline::new_sequence("frame"); - - let mut chunks = Vec::new(); - for entity_idx in 0..num_entities { - for frame_idx in 0..num_frames { - let entity_path = EntityPath::from(format!("/entity_{entity_idx}")); - let row_id = RowId::new(); - let points = MyPoint::from_iter(frame_idx as u32..frame_idx as u32 + 1); - let chunk = Chunk::builder(entity_path) - .with_sparse_component_batches( - row_id, - #[expect(clippy::cast_possible_wrap)] - TimePoint::default().with(timeline, frame_idx as i64), - [(MyPoints::descriptor_points(), Some(&points as _))], - ) - .build() - .unwrap(); - chunks.push(Arc::new(chunk)); - } - } - - // Encode to file. - let set_store_info = LogMsg::SetStoreInfo(SetStoreInfo { - row_id: *RowId::ZERO, - info: store_info, - }); - let mut file = std::fs::File::create(&path).unwrap(); - let mut encoder = re_log_encoding::Encoder::new_eager( - re_log_encoding::CrateVersion::LOCAL, - EncodingOptions::PROTOBUF_COMPRESSED, - &mut file, - ) - .unwrap(); - encoder.append(&set_store_info).unwrap(); - for chunk in &chunks { - let arrow_msg = chunk.to_arrow_msg().unwrap(); - let msg = LogMsg::ArrowMsg(store_id.clone(), arrow_msg); - encoder.append(&msg).unwrap(); - } - encoder.finish().unwrap(); - - // Re-open for reading. - let file = File::open(&path).unwrap(); - (path, file, store_id, chunks) - } - - fn read_raw_manifest(file: &mut File, store_id: &StoreId) -> Arc { - let footer = re_log_encoding::read_rrd_footer(file).unwrap().unwrap(); - Arc::new(footer.manifests[store_id].clone()) - } - - #[test] - fn test_lazy_store_no_physical_chunks() { - let dir = tempfile::tempdir().unwrap(); - let (path, mut file, store_id, chunks) = create_test_rrd(dir.path(), 2, 3); - let raw = read_raw_manifest(&mut file, &store_id); - - let lazy = LazyRrdStore::try_new(file, path, raw).unwrap(); - - assert_eq!(lazy.num_physical_chunks(), 0); - assert_eq!( - lazy.manifest().col_chunk_ids().len(), - chunks.len(), - "All chunk IDs should be in manifest" - ); - } - - #[test] - fn test_lazy_store_entities_visible() { - let dir = tempfile::tempdir().unwrap(); - let (path, mut file, store_id, _) = create_test_rrd(dir.path(), 3, 2); - let raw = read_raw_manifest(&mut file, &store_id); - - let lazy = LazyRrdStore::try_new(file, path, raw).unwrap(); - let entity_tree = lazy.entity_tree(); - - let mut entities = Vec::new(); - entity_tree.visit_children_recursively(|path| { - if !path.is_root() { - entities.push(path.clone()); - } - }); - // 3 entities + intermediate paths - assert!(entities.len() >= 3, "Should have at least 3 leaf entities"); - } - - #[test] - fn test_lazy_store_load_all() { - let dir = tempfile::tempdir().unwrap(); - let (path, mut file, store_id, chunks) = create_test_rrd(dir.path(), 2, 3); - let raw = read_raw_manifest(&mut file, &store_id); - - let lazy = LazyRrdStore::try_new(file, path, raw).unwrap(); - let loaded = lazy.collect_physical_chunks().unwrap(); - assert_eq!(loaded.len(), chunks.len()); - } - - #[test] - fn test_lazy_store_load_single_chunk() { - let dir = tempfile::tempdir().unwrap(); - let (path, mut file, store_id, chunks) = create_test_rrd(dir.path(), 2, 3); - let raw = read_raw_manifest(&mut file, &store_id); - - let lazy = LazyRrdStore::try_new(file, path, raw).unwrap(); - let first_chunk_id = lazy.manifest().col_chunk_ids()[0]; - let loaded = lazy.load_chunks(&[first_chunk_id]).unwrap(); - - assert_eq!(loaded.len(), 1); - assert_eq!(lazy.num_physical_chunks(), 1); - assert!(lazy.has_physical_chunk(&first_chunk_id)); - - // Other chunks are still virtual. - let total_chunks = chunks.len(); - assert!(total_chunks > 1); - } - - #[test] - fn test_lazy_store_load_idempotent() { - let dir = tempfile::tempdir().unwrap(); - let (path, mut file, store_id, _) = create_test_rrd(dir.path(), 1, 3); - let raw = read_raw_manifest(&mut file, &store_id); - - let lazy = LazyRrdStore::try_new(file, path, raw).unwrap(); - lazy.load_all_chunks().unwrap(); - - let count_before = lazy.num_physical_chunks(); - - // Loading again should be a no-op. - let loaded = lazy.load_chunks(lazy.manifest().col_chunk_ids()).unwrap(); - assert!(loaded.is_empty(), "Already-loaded chunks should be skipped"); - - let count_after = lazy.num_physical_chunks(); - assert_eq!(count_before, count_after); - } - - #[test] - fn test_lazy_store_schema() { - let dir = tempfile::tempdir().unwrap(); - let (path, mut file, store_id, _) = create_test_rrd(dir.path(), 2, 3); - let raw = read_raw_manifest(&mut file, &store_id); - - let lazy = LazyRrdStore::try_new(file, path, raw).unwrap(); - let schema = lazy.schema(); - - // Schema should be non-empty even without physical chunks. - let columns = schema.chunk_column_descriptors(); - assert!( - !columns.components.is_empty() || !columns.indices.is_empty(), - "Schema should be populated from manifest" - ); - } - - #[test] - fn test_lazy_vs_eager_equivalence() { - let dir = tempfile::tempdir().unwrap(); - let (path, mut file, store_id, _) = create_test_rrd(dir.path(), 2, 3); - let raw = read_raw_manifest(&mut file, &store_id); - - // Lazy path: create lazy store, load all chunks. - let lazy = LazyRrdStore::try_new(file, path.clone(), raw).unwrap(); - lazy.load_all_chunks().unwrap(); - - // Eager path: load the same file fully. - let eager_stores = - ChunkStore::from_rrd_filepath(&ChunkStoreConfig::ALL_DISABLED, &path).unwrap(); - let eager_store = eager_stores.into_values().next().unwrap(); - - let collect_entities = |tree: &crate::EntityTree| { - let mut entities = Vec::new(); - tree.visit_children_recursively(|path| { - if !path.is_root() { - entities.push(path.clone()); - } - }); - entities.sort(); - entities - }; - let lazy_entities = collect_entities(&lazy.entity_tree()); - let eager_entities = collect_entities(eager_store.entity_tree()); - - assert_eq!(lazy_entities, eager_entities, "Same entities"); - assert_eq!( - lazy.num_physical_chunks(), - eager_store.num_physical_chunks(), - "Same number of physical chunks" - ); - } -} diff --git a/crates/store/re_chunk_store/src/lazy_store.rs b/crates/store/re_chunk_store/src/lazy_store.rs new file mode 100644 index 000000000000..482e2b2f8cf3 --- /dev/null +++ b/crates/store/re_chunk_store/src/lazy_store.rs @@ -0,0 +1,585 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use ahash::{HashMap, HashMapExt as _}; +use nohash_hasher::{IntMap, IntSet}; + +use re_chunk::{Chunk, ChunkId}; +use re_log_encoding::{ChunkProvider, RawRrdManifest, RrdManifest}; +use re_log_types::{AbsoluteTimeRange, EntityPath, StoreId, TimelineName}; + +use crate::{ + ChunkStore, ChunkStoreConfig, ChunkStoreHandle, ChunkStoreResult, ChunkTrackingMode, + EntityTree, ExtractPropertiesError, LatestAtQuery, QueryResults, RangeQuery, StoreSchema, + extract_properties_from_chunks, +}; + +/// A [`ChunkStore`] backed by a [`ChunkProvider`], with index loaded but chunks loaded on demand. +/// +/// Constructed from a [`ChunkProvider`]; store selection (which manifest to extract from the +/// `RrdFooter`, etc.) is the provider's concern. +/// +/// On construction, the `ChunkStore`'s virtual index is populated via `insert_rrd_manifest()`. +/// Physical chunks are **never retained** in the inner store — [`Self::load_chunks`] forwards to +/// the provider and returns the `Vec>` to the caller, who is responsible for the +/// resulting memory. This is deliberately cache-free to keep the OSS server from `OOMing` on +/// large RRDs. +//TODO(RR-4503): caching support. +pub struct LazyStore { + store: ChunkStoreHandle, + provider: Arc, + + /// Precomputed map from `ChunkId` to manifest row index. + chunk_id_to_index: HashMap, + + /// Precomputed per-chunk timeline ranges. + timeline_ranges: HashMap>, + + /// Monotonic count of chunks physically materialized through [`Self::load_chunks`]. + /// Used for test purposes. + chunks_loaded: AtomicU64, +} + +impl LazyStore { + /// Build a lazy store from any chunk provider. + /// + /// The provider's manifest is used to populate the inner [`ChunkStore`]'s virtual index; the + /// provider's `load_chunks` serves on-demand reads. + /// + /// Infallible: every fallible step (manifest parsing, file open, etc.) happens during the + /// provider's own construction. + pub fn new(provider: Arc) -> Self { + let manifest = Arc::clone(provider.manifest()); + + // `ALL_DISABLED` here is irrelevant, this store will never see chunks. + let mut store = + ChunkStore::new(manifest.store_id().clone(), ChunkStoreConfig::ALL_DISABLED); + + #[expect(clippy::let_underscore_must_use)] + let _ = store.insert_rrd_manifest(Arc::clone(&manifest)); + + let chunk_id_to_index: HashMap = manifest + .col_chunk_ids() + .iter() + .enumerate() + .map(|(i, &id)| (id, i)) + .collect(); + + let timeline_ranges = Self::build_timeline_ranges(&manifest); + + Self { + store: ChunkStoreHandle::new(store), + provider, + chunk_id_to_index, + timeline_ranges, + chunks_loaded: AtomicU64::new(0), + } + } + + fn build_timeline_ranges( + manifest: &RrdManifest, + ) -> HashMap> { + let mut result: HashMap> = HashMap::new(); + for per_entity in manifest.temporal_map().values() { + for (timeline, per_component) in per_entity { + for per_chunk in per_component.values() { + for (&chunk_id, entry) in per_chunk { + let e = result.entry(chunk_id).or_default(); + e.entry(*timeline.name()) + .and_modify(|existing| { + *existing = existing.union(entry.time_range); + }) + .or_insert(entry.time_range); + } + } + } + } + result + } + + /// Load specific chunks via the underlying provider. + /// + /// The inner [`ChunkStore`] is **not** mutated — it stays purely virtual for the lifetime of + /// this store. The caller owns the returned `Vec>`; dropping it frees the memory. + /// Returns an error if any chunk ID is not in the manifest. + pub async fn load_chunks(&self, chunk_ids: &[ChunkId]) -> ChunkStoreResult>> { + let chunks = self.provider.load_chunks(chunk_ids).await?; + self.chunks_loaded + .fetch_add(chunks.len() as u64, Ordering::Relaxed); + Ok(chunks) + } + + /// Monotonic count of chunks physically materialized through [`Self::load_chunks`] since + /// this store was constructed. Intended for test-side validation that pushdown / lazy + /// loading is engaged; not a performance metric. + pub fn chunks_loaded(&self) -> u64 { + self.chunks_loaded.load(Ordering::Relaxed) + } + + /// Load every chunk in the manifest and return them in a single [`Vec`]. + /// + /// Memory cost scales with the full RRD — consider streaming for large stores. + pub async fn load_all_chunks(&self) -> ChunkStoreResult>> { + self.load_chunks(self.manifest().col_chunk_ids()).await + } + + /// The store's schema, populated from the manifest (available without loading chunks). + #[inline] + pub fn schema(&self) -> StoreSchema { + self.store.read().schema().clone() + } + + /// The entity tree, populated from the manifest (available without loading chunks). + pub fn entity_tree(&self) -> EntityTree { + self.store.read().entity_tree().clone() + } + + /// The number of chunks described by the manifest (physical + virtual). + pub fn num_chunks(&self) -> usize { + self.manifest().num_chunks() + } + + /// The parsed manifest for this store. + pub fn manifest(&self) -> &Arc { + self.provider.manifest() + } + + /// Human-readable source identifier of the underlying provider, for diagnostics. + pub fn source(&self) -> String { + self.provider.source() + } + + /// The raw manifest as-parsed from the RRD footer, before validation/extraction. + /// + /// Kept around so the server can synthesize `GetRrdManifest` responses without materializing + /// chunks: the footer already contains everything a client needs to pick which chunks to fetch. + pub fn raw_manifest(&self) -> &Arc { + self.provider.raw_manifest() + } + + /// The underlying chunk provider. + pub fn provider(&self) -> &Arc { + &self.provider + } + + /// Look up the manifest row index for a given chunk ID. + pub fn chunk_row_index(&self, chunk_id: &ChunkId) -> Option { + self.chunk_id_to_index.get(chunk_id).copied() + } + + /// Per-chunk timeline ranges. + pub fn timeline_ranges(&self) -> &HashMap> { + &self.timeline_ranges + } + + /// The store ID (from the manifest, no store lock needed). + pub fn store_id(&self) -> &StoreId { + self.manifest().store_id() + } + + /// All entity paths known to this store (populated from the virtual index). + pub fn all_entities(&self) -> IntSet { + self.store.read().all_entities() + } + + /// Extract properties in a single pass: query the virtual index for required property + /// chunks, load them from the provider, and run the extraction. + pub async fn extract_properties( + &self, + ) -> Result { + let per_entity = self.store.read().property_entities_query_results(); + + let ids: Vec = per_entity + .iter() + .flat_map(|(_, qr)| { + std::iter::chain( + qr.chunks.iter().map(|c| c.id()), + qr.missing_virtual.iter().copied(), + ) + }) + .collect(); + + let chunks = self + .load_chunks(&ids) + .await + .map_err(|err| ExtractPropertiesError::Internal(err.to_string()))?; + + extract_properties_from_chunks(&per_entity, &chunks) + } + + /// Run a latest-at query against the virtual index. + /// + /// Returns [`QueryResults`] with physical chunks in `chunks` and + /// not-yet-loaded chunk IDs in `missing_virtual`. + pub fn latest_at_relevant_chunks_for_all_components( + &self, + report_mode: ChunkTrackingMode, + query: &LatestAtQuery, + entity_path: &EntityPath, + include_static: bool, + ) -> QueryResults { + self.store + .read() + .latest_at_relevant_chunks_for_all_components( + report_mode, + query, + entity_path, + include_static, + ) + } + + /// Run a range query against the virtual index. + /// + /// Returns [`QueryResults`] with physical chunks in `chunks` and + /// not-yet-loaded chunk IDs in `missing_virtual`. + pub fn range_relevant_chunks_for_all_components( + &self, + report_mode: ChunkTrackingMode, + query: &RangeQuery, + entity_path: &EntityPath, + include_static: bool, + ) -> QueryResults { + self.store.read().range_relevant_chunks_for_all_components( + report_mode, + query, + entity_path, + include_static, + ) + } +} + +#[cfg(test)] +mod tests { + use std::fs::File; + use std::path::Path; + + use super::*; + + use re_chunk::{RowId, TimePoint, Timeline}; + use re_log_encoding::EncodingOptions; + use re_log_types::{ + EntityPath, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind, StoreSource, + example_components::{MyPoint, MyPoints}, + }; + + /// Helper: create test chunks and encode to RRD file at `path`. + /// Returns `(open file handle, store_id, chunks)`. + fn create_test_rrd( + path: &Path, + num_entities: usize, + num_frames: usize, + ) -> (futures::io::AllowStdIo, StoreId, Vec>) { + let store_id = StoreId::random(StoreKind::Recording, "test"); + let store_info = StoreInfo::new(store_id.clone(), StoreSource::Unknown); + let timeline = Timeline::new_sequence("frame"); + + let mut chunks = Vec::new(); + for entity_idx in 0..num_entities { + for frame_idx in 0..num_frames { + let entity_path = EntityPath::from(format!("/entity_{entity_idx}")); + let row_id = RowId::new(); + let points = MyPoint::from_iter(frame_idx as u32..frame_idx as u32 + 1); + let chunk = Chunk::builder(entity_path) + .with_sparse_component_batches( + row_id, + #[expect(clippy::cast_possible_wrap)] + TimePoint::default().with(timeline, frame_idx as i64), + [(MyPoints::descriptor_points(), Some(&points as _))], + ) + .build() + .unwrap(); + chunks.push(Arc::new(chunk)); + } + } + + // Encode to file. + let set_store_info = LogMsg::SetStoreInfo(SetStoreInfo { + row_id: *RowId::ZERO, + info: store_info, + }); + let mut file = std::fs::File::create(path).unwrap(); + let mut encoder = re_log_encoding::Encoder::new_eager( + re_log_encoding::CrateVersion::LOCAL, + EncodingOptions::PROTOBUF_COMPRESSED, + &mut file, + ) + .unwrap(); + encoder.append(&set_store_info).unwrap(); + for chunk in &chunks { + let arrow_msg = chunk.to_arrow_msg().unwrap(); + let msg = LogMsg::ArrowMsg(store_id.clone(), arrow_msg); + encoder.append(&msg).unwrap(); + } + encoder.finish().unwrap(); + + // Re-open for reading. + let file = File::open(path).unwrap(); + (futures::io::AllowStdIo::new(file), store_id, chunks) + } + + async fn read_raw_manifest( + file: &mut futures::io::AllowStdIo, + store_id: &StoreId, + ) -> Arc { + let footer = re_log_encoding::read_rrd_footer(file) + .await + .unwrap() + .unwrap(); + Arc::new(footer.manifests[store_id].clone()) + } + + /// Construct a `LazyStore` from an open RRD file, via `RrdChunkProvider`. + fn build_test_lazy_store( + path: &Path, + file: futures::io::AllowStdIo, + raw_manifest: Arc, + ) -> LazyStore { + let provider = Arc::new( + re_log_encoding::RrdChunkProvider::from_reader( + file, + path.display().to_string(), + raw_manifest, + ) + .expect("test rrd provider"), + ); + LazyStore::new(provider) + } + + #[test] + fn test_lazy_store_no_physical_chunks() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let (mut file, store_id, chunks) = create_test_rrd(&path, 2, 3); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + let lazy = build_test_lazy_store(&path, file, raw); + + assert_eq!(lazy.store.read().num_physical_chunks(), 0); + assert_eq!( + lazy.manifest().col_chunk_ids().len(), + chunks.len(), + "All chunk IDs should be in manifest" + ); + } + + #[test] + fn test_lazy_store_entities_visible() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let (mut file, store_id, _) = create_test_rrd(&path, 3, 2); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + let lazy = build_test_lazy_store(&path, file, raw); + let entity_tree = lazy.entity_tree(); + + let mut entities = Vec::new(); + entity_tree.visit_children_recursively(|path| { + if !path.is_root() { + entities.push(path.clone()); + } + }); + // 3 entities + intermediate paths + assert!(entities.len() >= 3, "Should have at least 3 leaf entities"); + } + + #[test] + fn test_lazy_store_load_all() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let (mut file, store_id, chunks) = create_test_rrd(&path, 2, 3); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + let lazy = build_test_lazy_store(&path, file, raw); + let loaded = futures::executor::block_on(lazy.load_all_chunks()).unwrap(); + assert_eq!(loaded.len(), chunks.len()); + } + + #[test] + fn test_lazy_store_load_single_chunk() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let (mut file, store_id, chunks) = create_test_rrd(&path, 2, 3); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + let lazy = build_test_lazy_store(&path, file, raw); + let first_chunk_id = lazy.manifest().col_chunk_ids()[0]; + let loaded = futures::executor::block_on(lazy.load_chunks(&[first_chunk_id])).unwrap(); + + assert_eq!(loaded.len(), 1); + assert_eq!(loaded[0].id(), first_chunk_id); + + // Other chunks are still virtual. + let total_chunks = chunks.len(); + assert!(total_chunks > 1); + } + + #[test] + fn test_lazy_store_load_idempotent() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let (mut file, store_id, _) = create_test_rrd(&path, 1, 3); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + let lazy = build_test_lazy_store(&path, file, raw); + + // Calling `load_chunks` twice with the same IDs yields equivalent results, and neither + // call retains chunks in the inner store — guard against anyone sneaking a cache back in. + let ids = lazy.manifest().col_chunk_ids(); + let first = futures::executor::block_on(lazy.load_chunks(ids)).unwrap(); + let second = futures::executor::block_on(lazy.load_chunks(ids)).unwrap(); + + assert_eq!(first.len(), second.len()); + let mut first_ids: Vec<_> = first.iter().map(|c| c.id()).collect(); + let mut second_ids: Vec<_> = second.iter().map(|c| c.id()).collect(); + first_ids.sort(); + second_ids.sort(); + assert_eq!(first_ids, second_ids); + + assert_eq!( + lazy.store.read().num_physical_chunks(), + 0, + "no-cache: inner store must stay empty across loads" + ); + } + + #[test] + fn test_lazy_store_load_does_not_retain() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let (mut file, store_id, _) = create_test_rrd(&path, 2, 3); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + let lazy = build_test_lazy_store(&path, file, raw); + let first_chunk_id = lazy.manifest().col_chunk_ids()[0]; + let loaded = futures::executor::block_on(lazy.load_chunks(&[first_chunk_id])).unwrap(); + assert_eq!(loaded.len(), 1); + + drop(loaded); + + assert_eq!( + lazy.store.read().num_physical_chunks(), + 0, + "dropping the returned Vec must free the chunk; inner store is not a cache" + ); + } + + #[test] + fn test_lazy_store_extract_properties() { + // Build an RRD with a single property entity, extract properties, and assert that the + // inner store remains empty afterwards. + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("props.rrd"); + let store_id = StoreId::random(StoreKind::Recording, "props"); + let store_info = StoreInfo::new(store_id.clone(), StoreSource::Unknown); + + let property_entity = EntityPath::from("/__properties/my_prop"); + let row_id = RowId::new(); + let points = MyPoint::from_iter(0..1); + let chunk = Chunk::builder(property_entity) + .with_sparse_component_batches( + row_id, + TimePoint::default(), + [(MyPoints::descriptor_points(), Some(&points as _))], + ) + .build() + .unwrap(); + let chunk = Arc::new(chunk); + + let mut file = std::fs::File::create(&path).unwrap(); + let mut encoder = re_log_encoding::Encoder::new_eager( + re_log_encoding::CrateVersion::LOCAL, + EncodingOptions::PROTOBUF_COMPRESSED, + &mut file, + ) + .unwrap(); + encoder + .append(&LogMsg::SetStoreInfo(SetStoreInfo { + row_id: *RowId::ZERO, + info: store_info, + })) + .unwrap(); + let arrow_msg = chunk.to_arrow_msg().unwrap(); + encoder + .append(&LogMsg::ArrowMsg(store_id.clone(), arrow_msg)) + .unwrap(); + encoder.finish().unwrap(); + + let mut file = futures::io::AllowStdIo::new(File::open(&path).unwrap()); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + let lazy = build_test_lazy_store(&path, file, raw); + let batch = futures::executor::block_on(lazy.extract_properties()).unwrap(); + assert!( + batch.num_columns() > 0, + "properties record batch should contain the property column" + ); + assert_eq!( + lazy.store.read().num_physical_chunks(), + 0, + "extract_properties must not retain any chunks" + ); + } + + #[test] + fn test_lazy_store_schema() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let (mut file, store_id, _) = create_test_rrd(&path, 2, 3); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + let lazy = build_test_lazy_store(&path, file, raw); + let schema = lazy.schema(); + + // Schema should be non-empty even without physical chunks. + let columns = schema.chunk_column_descriptors(); + assert!( + !columns.components.is_empty() || !columns.indices.is_empty(), + "Schema should be populated from manifest" + ); + } + + #[test] + fn arc_lazy_store_is_send_sync() { + fn assert_send_sync() {} + assert_send_sync::>(); + } + + #[test] + fn test_lazy_vs_eager_equivalence() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let (mut file, store_id, _) = create_test_rrd(&path, 2, 3); + let raw = futures::executor::block_on(read_raw_manifest(&mut file, &store_id)); + + // Lazy path: create lazy store, load all chunks via the no-cache API. + let lazy = build_test_lazy_store(&path, file, raw); + let lazy_chunks = futures::executor::block_on(lazy.load_all_chunks()).unwrap(); + + // Eager path: load the same file fully. + let mut eager_file = File::open(&path).unwrap(); + let eager_stores = + ChunkStore::from_rrd_reader(&ChunkStoreConfig::ALL_DISABLED, &mut eager_file).unwrap(); + let eager_store = eager_stores.into_values().next().unwrap(); + + let collect_entities = |tree: &crate::EntityTree| { + let mut entities = Vec::new(); + tree.visit_children_recursively(|path| { + if !path.is_root() { + entities.push(path.clone()); + } + }); + entities.sort(); + entities + }; + let lazy_entities = collect_entities(&lazy.entity_tree()); + let eager_entities = collect_entities(eager_store.entity_tree()); + + assert_eq!(lazy_entities, eager_entities, "Same entities"); + + let mut lazy_ids: Vec<_> = lazy_chunks.iter().map(|c| c.id()).collect(); + let mut eager_ids: Vec<_> = eager_store.iter_physical_chunks().map(|c| c.id()).collect(); + lazy_ids.sort(); + eager_ids.sort(); + assert_eq!(lazy_ids, eager_ids, "Same set of chunks"); + } +} diff --git a/crates/store/re_chunk_store/src/lib.rs b/crates/store/re_chunk_store/src/lib.rs index 487cba093837..fd8b49e5a119 100644 --- a/crates/store/re_chunk_store/src/lib.rs +++ b/crates/store/re_chunk_store/src/lib.rs @@ -21,10 +21,10 @@ mod drop_time_range; pub mod entity_tree; mod events; mod gc; -#[cfg(not(target_arch = "wasm32"))] -mod lazy_rrd_store; +mod lazy_store; mod lineage; mod missing_chunk_reporter; +mod profile; mod properties; mod query; mod rebatch_videos; @@ -60,7 +60,8 @@ pub use self::events::{ pub use self::gc::{GarbageCollectionOptions, GarbageCollectionTarget}; pub use self::lineage::{ChunkDirectLineage, ChunkDirectLineageReport}; pub use self::missing_chunk_reporter::MissingChunkReporter; -pub use self::properties::ExtractPropertiesError; +pub use self::profile::OptimizationProfile; +pub use self::properties::{ExtractPropertiesError, extract_properties_from_chunks}; pub use self::query::QueryResults; pub use self::stats::{ChunkStoreChunkStats, ChunkStoreStats}; pub use self::store::{ @@ -72,8 +73,7 @@ pub use self::subscribers::{ ChunkStoreSubscriber, ChunkStoreSubscriberHandle, PerStoreChunkSubscriber, }; -#[cfg(not(target_arch = "wasm32"))] -pub use self::lazy_rrd_store::LazyRrdStore; +pub use self::lazy_store::LazyStore; pub(crate) use self::store::ColumnMetadataState; @@ -94,6 +94,9 @@ pub enum ChunkStoreError { #[error("Failed to load data, parsing error: {0:#}")] Codec(#[from] re_log_encoding::CodecError), + #[error(transparent)] + Provider(#[from] re_log_encoding::ChunkProviderError), + #[error("Failed to load data, semantic error: {0:#}")] Sorbet(#[from] re_sorbet::SorbetError), @@ -104,19 +107,36 @@ pub enum ChunkStoreError { value: String, err: Box, }, + + #[error("{0:#}")] + VideoRebatch(anyhow::Error), } pub type ChunkStoreResult = ::std::result::Result; /// What to do when a virtual chunk is missing from the store. +/// +/// The default for queries should be [`ChunkTrackingMode::Report`], which signals +/// that the chunk should be downloaded/protected from GC. And similar chunks (same +/// entity & components) should be prefetched. +/// +/// When a query is driven by something short-lived, [`ChunkTrackingMode::ReportTransient`] +/// should be used. That does download/protect the given chunk, but ignores similar chunks. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ChunkTrackingMode { /// Ignore missing & used chunks, and return partial results. Ignore, /// Remember the missing & used chunk ID in [`ChunkStore::take_tracked_chunk_ids`]. + /// + /// This signals to the prefetcher that this, and similar chunks should be fetched. Report, + /// Like [`ChunkTrackingMode::Report`], but doesn't speculate by prefetching similar chunks. + /// + /// This should be used for short-lived things like queries on-hover ui. + ReportTransient, + /// Panic when a chunk is missing. /// /// Only use this in tests, or contexts where there really can't be diff --git a/crates/store/re_chunk_store/src/lineage.rs b/crates/store/re_chunk_store/src/lineage.rs index e49d59c793dd..c102293ac924 100644 --- a/crates/store/re_chunk_store/src/lineage.rs +++ b/crates/store/re_chunk_store/src/lineage.rs @@ -1,7 +1,7 @@ -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use std::sync::Arc; -use itertools::Itertools as _; +use itertools::{Either, Itertools as _}; use re_chunk::{Chunk, ChunkId}; @@ -9,6 +9,34 @@ use crate::ChunkStore; // --- +#[derive(Clone, Debug, re_byte_size::SizeBytes)] +pub(crate) struct TrackedDirectChunkLineage { + pub(crate) lineage: ChunkDirectLineage, + + /// How many other [`TrackedDirectChunkLineage`] or physical chunks that + /// reference this lineage. + pub(crate) ref_count: u32, + pub(crate) descends_from_manifest: bool, +} + +impl std::ops::Deref for TrackedDirectChunkLineage { + type Target = ChunkDirectLineage; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.lineage + } +} + +impl std::ops::DerefMut for TrackedDirectChunkLineage { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.lineage + } +} + +// --- + /// How a chunk relates its direct ancestor(s). /// /// These ancestors can be other chunk(s) or, at the top of the lineage tree, the origin of where @@ -18,7 +46,7 @@ use crate::ChunkStore; /// This makes it usable in virtual contexts where lineage information alone should never force the /// underlying data to remain in local memory, such as the store's virtual indexes. /// Use [`ChunkDirectLineage::to_report`] to generate a [`ChunkDirectLineageReport`] instead. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, re_byte_size::SizeBytes)] pub enum ChunkDirectLineage { /// This chunk resulted from the splitting of that other chunk. It must have siblings, somewhere. /// @@ -35,7 +63,7 @@ pub enum ChunkDirectLineage { /// chunk at depth=1. /// /// Value: `(parent_id, sibling_ids)`. - SplitFrom(ChunkId, Vec), + SplitFrom(ChunkId, Box<[ChunkId]>), /// This chunk resulted from the compaction of these other chunks. /// @@ -50,8 +78,8 @@ pub enum ChunkDirectLineage { /// /// If a chunk descends from a split, it can never take part in a compaction event again. /// - /// Value: `(parent, siblings)`. - CompactedFrom(BTreeSet), + /// Value: `parents`. + CompactedFrom(Box<[ChunkId]>), /// This chunk's data was originally fetched from an RRD manifest. /// @@ -65,18 +93,6 @@ pub enum ChunkDirectLineage { Volatile, } -impl re_byte_size::SizeBytes for ChunkDirectLineage { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::SplitFrom(chunk_id, chunk_ids) => { - chunk_id.heap_size_bytes() + chunk_ids.heap_size_bytes() - } - Self::CompactedFrom(btree_set) => btree_set.heap_size_bytes(), - Self::RootFromManifest { .. } | Self::Volatile => 0, - } - } -} - impl std::fmt::Debug for ChunkDirectLineage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -169,6 +185,16 @@ impl ChunkDirectLineage { Self::Volatile => Some(ChunkDirectLineageReport::Volatile), } } + + pub fn iter_referenced_chunks(&self) -> impl Iterator { + match self { + Self::SplitFrom(chunk_id, _) => Some(Either::Left(std::iter::once(chunk_id))), + Self::CompactedFrom(chunks) => Some(Either::Right(chunks.iter())), + Self::RootFromManifest { .. } | Self::Volatile => None, + } + .into_iter() + .flatten() + } } /// How a chunk relates its direct ancestor(s). @@ -263,7 +289,7 @@ impl ChunkStore { // whether it is static or not from the lineage info. for root_id in store.find_root_manifest_chunks(chunk_id) { if let Some(ChunkDirectLineage::RootFromManifest { is_static }) = - store.chunks_lineage.get(&root_id) + store.chunks_lineage.get(&root_id).map(|l| &l.lineage) { return if *is_static { "yes" } else { "no" }; } @@ -277,7 +303,7 @@ impl ChunkStore { fn recurse(store: &ChunkStore, chunk_id: &ChunkId, depth: usize) -> String { let chunk = store.physical_chunks_per_chunk_id.get(chunk_id); - let lineage = store.chunks_lineage.get(chunk_id); + let lineage = store.chunks_lineage.get(chunk_id).map(|l| &l.lineage); let status = if chunk.is_some() { "loaded" } else { @@ -287,7 +313,7 @@ impl ChunkStore { let width = (depth + 1) * 4; let sibling_ids = match lineage { - Some(ChunkDirectLineage::SplitFrom(_, sibling_ids)) => sibling_ids.as_slice(), + Some(ChunkDirectLineage::SplitFrom(_, sibling_ids)) => &**sibling_ids, _ => &[], }; @@ -342,7 +368,7 @@ impl ChunkStore { return true; }; matches!( - lineage, + lineage.lineage, ChunkDirectLineage::RootFromManifest { .. } | ChunkDirectLineage::Volatile ) } @@ -362,7 +388,7 @@ impl ChunkStore { /// See [`Self::find_root_chunks`]. pub fn collect_root_ids(&self, chunk_id: &ChunkId, roots: &mut Vec) { - let lineage = self.chunks_lineage.get(chunk_id); + let lineage = self.chunks_lineage.get(chunk_id).map(|l| &l.lineage); match lineage { Some(ChunkDirectLineage::SplitFrom(chunk_id, _sibling_ids)) => { self.collect_root_ids(chunk_id, roots); @@ -399,7 +425,7 @@ impl ChunkStore { /// See [`Self::find_root_manifest_chunks`]. fn collect_root_manifest_chunks(&self, chunk_id: &ChunkId, roots: &mut Vec) { - let lineage = self.chunks_lineage.get(chunk_id); + let lineage = self.chunks_lineage.get(chunk_id).map(|l| &l.lineage); match lineage { Some(ChunkDirectLineage::SplitFrom(chunk_id, _sibling_ids)) => { self.collect_root_manifest_chunks(chunk_id, roots); @@ -454,12 +480,19 @@ impl ChunkStore { } } + /// Returns true if either the specified chunk or one of its ancestors is from a manifest. + pub fn descends_from_manifest(&self, chunk: &ChunkId) -> bool { + self.chunks_lineage + .get(chunk) + .is_some_and(|l| l.descends_from_manifest) + } + /// Returns true if either the specified chunk or one of its ancestors resulted from a split. pub fn descends_from_a_split(&self, chunk_id: &ChunkId) -> bool { if cfg!(debug_assertions) { // Do a bit more expensive recursion as a form of sanity checking: fn recurse(store: &ChunkStore, chunk_id: &ChunkId, compaction_found: bool) -> bool { - let lineage = store.chunks_lineage.get(chunk_id); + let lineage = store.chunks_lineage.get(chunk_id).map(|l| &l.lineage); match lineage { Some(ChunkDirectLineage::SplitFrom(_chunk_id, _sibling_ids)) => { re_log::debug_assert!( @@ -488,7 +521,7 @@ impl ChunkStore { // We never mix splits and compactions in the same lineage tree, // so no need to recurse: matches!( - self.chunks_lineage.get(chunk_id), + self.chunks_lineage.get(chunk_id).map(|l| &l.lineage), Some(ChunkDirectLineage::SplitFrom { .. }) ) } @@ -497,7 +530,7 @@ impl ChunkStore { /// Returns true if either the specified chunk or one of its ancestors resulted from a compaction. pub fn descends_from_a_compaction(&self, chunk_id: &ChunkId) -> bool { fn recurse(store: &ChunkStore, chunk_id: &ChunkId, split_found: bool) -> bool { - let lineage = store.chunks_lineage.get(chunk_id); + let lineage = store.chunks_lineage.get(chunk_id).map(|l| &l.lineage); match lineage { Some(ChunkDirectLineage::SplitFrom(chunk_id, _sibling_ids)) => { recurse(store, chunk_id, true) @@ -523,7 +556,7 @@ impl ChunkStore { /// Returns the direct lineage of a chunk. pub fn direct_lineage(&self, chunk_id: &ChunkId) -> Option<&ChunkDirectLineage> { - self.chunks_lineage.get(chunk_id) + self.chunks_lineage.get(chunk_id).map(|l| &l.lineage) } } @@ -1058,8 +1091,14 @@ mod tests { let lineage: ChunkDirectLineage = event.direct_lineage.clone().into(); if let Some(prev_chunk) = prev_chunk.take() { + // `CompactedFrom` keeps its parents sorted by `ChunkId` (it is built from the + // report's `BTreeMap`), so canonicalize the expected ids the same way. let expected = ChunkDirectLineage::CompactedFrom( - [chunk.id(), prev_chunk.id()].into_iter().collect(), + [chunk.id(), prev_chunk.id()] + .into_iter() + .collect::>() + .into_iter() + .collect(), ); assert_eq!(expected, lineage); assert_eq!( @@ -1133,6 +1172,337 @@ mod tests { ); } + // --- ref-counting --- + + #[test] + fn ref_count_single_volatile_chunk() { + let mut store = temporal_store(10); + let mut make_chunk = chunk_factory(); + + let chunk = make_chunk(1); + store.insert_chunk(&chunk).unwrap(); + + { + let lineage = store + .chunks_lineage + .get(&chunk.id()) + .expect("a physical chunk must always be tracked"); + assert_eq!(lineage.ref_count, 1, "a single physical reference"); + assert_eq!(lineage.descends_from_manifest, false); + assert!(matches!(lineage.lineage, ChunkDirectLineage::Volatile)); + } + assert_eq!(true, store.is_root_chunk(&chunk.id())); + assert_store_invariants(&store); + + // A volatile root has no manifest to fall back on, so once nothing references it anymore + // its lineage is dropped rather than kept around forever. + store.gc(&crate::GarbageCollectionOptions::gc_everything()); + assert_eq!(0, store.num_physical_chunks()); + assert!( + !store.chunks_lineage.contains_key(&chunk.id()), + "an unreferenced volatile lineage must not linger after GC", + ); + } + + #[test] + fn ref_count_compaction_protects_sources() { + let mut store = temporal_store(10); + let mut make_chunk = chunk_factory(); + + let chunk_a = make_chunk(1); + let chunk_b = make_chunk(1); + + store.insert_chunk(&chunk_a).unwrap(); + // The two tiny chunks fold into a single compacted chunk. + let events = store.insert_chunk(&chunk_b).unwrap(); + + let compacted = events + .iter() + .find_map(|event| event.to_addition()) + .expect("the compaction must emit an addition") + .chunk_after_processing + .clone(); + let compacted_id = compacted.id(); + + assert_eq!(1, store.num_physical_chunks()); + assert_eq!( + vec![compacted_id], + store + .physical_chunks_per_chunk_id + .keys() + .copied() + .collect_vec(), + ); + + // The compacted chunk holds the only physical reference and points at both sources. + { + let lineage = &store.chunks_lineage[&compacted_id]; + assert_eq!(lineage.ref_count, 1, "a single physical reference"); + let referenced: ahash::HashSet = + lineage.iter_referenced_chunks().copied().collect(); + assert_eq!( + referenced, + [chunk_a.id(), chunk_b.id()].into_iter().collect(), + ); + assert_eq!(true, store.descends_from_a_compaction(&compacted_id)); + assert_eq!(false, store.is_root_chunk(&compacted_id)); + } + + // Both sources are physically gone, but their lineage is kept alive by the compacted chunk + // that now carries their data. + for src in [&chunk_a, &chunk_b] { + assert_eq!( + false, + store.physical_chunks_per_chunk_id.contains_key(&src.id()), + ); + let lineage = &store.chunks_lineage[&src.id()]; + assert_eq!(lineage.ref_count, 1, "kept alive by the compacted chunk"); + assert_eq!(true, store.is_root_chunk(&src.id())); + } + + // Both sources resolve to the compacted result, so re-inserting the same data is a no-op. + assert_eq!( + Some(&compacted_id), + store.leaky_compactions.get(&chunk_a.id()), + ); + assert_eq!( + Some(&compacted_id), + store.leaky_compactions.get(&chunk_b.id()), + ); + + assert_store_invariants(&store); + } + + #[test] + fn compacted_lineage_fully_reclaimed_on_gc() { + let mut store = temporal_store(10); + let mut make_chunk = chunk_factory(); + + // A handful of tiny chunks fold into one compacted chunk, leaving a whole lineage tree + // behind it. + for _ in 0..4 { + store.insert_chunk(&make_chunk(1)).unwrap(); + } + assert_eq!(1, store.num_physical_chunks()); + assert!( + store.chunks_lineage.len() > 1, + "the compacted sources should still be tracked", + ); + assert!(!store.leaky_compactions.is_empty()); + assert_store_invariants(&store); + + // Dropping the one physical chunk must cascade through the whole tree and reclaim it all, + // otherwise the bookkeeping maps grow without bound over a long session. + store.gc(&crate::GarbageCollectionOptions::gc_everything()); + assert_eq!(0, store.num_physical_chunks()); + assert!( + store.chunks_lineage.is_empty(), + "compacted lineage tree leaked: {:?}", + store.chunks_lineage, + ); + assert!( + store.leaky_compactions.is_empty(), + "leaky-compaction tracker leaked: {:?}", + store.leaky_compactions, + ); + assert_store_invariants(&store); + } + + #[test] + fn split_parent_ref_count_and_reclamation() { + let mut store = temporal_store(1); // force every row into its own chunk + let mut make_chunk = chunk_factory(); + + let parent = make_chunk(4); + store.insert_chunk(&parent).unwrap(); + + // We end up with four split children, and the parent itself is never physically stored. + assert_eq!(4, store.num_physical_chunks()); + assert_eq!( + false, + store + .physical_chunks_per_chunk_id + .contains_key(&parent.id()), + ); + + // The parent's lineage is kept alive by one reference per child. + { + let lineage = store + .chunks_lineage + .get(&parent.id()) + .expect("the split parent must stay tracked while its children live"); + assert_eq!(lineage.ref_count, 4); + } + assert_eq!(true, store.split_on_ingest.contains(&parent.id())); + assert_eq!( + Some(4), + store.dangling_splits.get(&parent.id()).map(|s| s.len()), + ); + + for child in store.iter_physical_chunks() { + let lineage = &store.chunks_lineage[&child.id()]; + assert_eq!( + lineage.ref_count, 1, + "each child holds one physical reference" + ); + assert_eq!(true, store.descends_from_a_split(&child.id())); + } + assert_store_invariants(&store); + + // Reclaiming the children must take the parent's bookkeeping down with them. + store.gc(&crate::GarbageCollectionOptions::gc_everything()); + assert_eq!(0, store.num_physical_chunks()); + assert!( + !store.chunks_lineage.contains_key(&parent.id()), + "split parent lineage leaked", + ); + assert_eq!(false, store.split_on_ingest.contains(&parent.id())); + assert_eq!(false, store.dangling_splits.contains_key(&parent.id())); + assert_store_invariants(&store); + } + + #[test] + fn manifest_lineage_survives_gc() { + let store_id = StoreId::recording("app_id", "rec_id"); + let mut make_chunk = chunk_factory(); + let chunk = make_chunk(1); + + let rrd_manifest = + RrdManifest::build_in_memory_from_chunks(store_id.clone(), std::iter::once(&*chunk)) + .unwrap(); + let mut store = ChunkStore::new(store_id, temporal_config(10)); + + // Load it virtually: the chunk is tracked and flagged as descending from a manifest, but + // it isn't physically present yet. + let _ignored_events = store.insert_rrd_manifest(rrd_manifest); + { + let lineage = store + .chunks_lineage + .get(&chunk.id()) + .expect("a manifest chunk must be tracked"); + assert_eq!(lineage.ref_count, 0, "no physical reference yet"); + assert_eq!(lineage.descends_from_manifest, true); + assert!(matches!( + lineage.lineage, + ChunkDirectLineage::RootFromManifest { .. } + )); + } + assert_eq!( + false, + store.physical_chunks_per_chunk_id.contains_key(&chunk.id()), + ); + assert_eq!(true, store.descends_from_manifest(&chunk.id())); + + // Load it physically. The manifest lineage must not get clobbered. + store.insert_chunk(&chunk).unwrap(); + { + let lineage = &store.chunks_lineage[&chunk.id()]; + assert_eq!(lineage.ref_count, 1, "now physically referenced"); + assert_eq!(lineage.descends_from_manifest, true); + assert!( + matches!(lineage.lineage, ChunkDirectLineage::RootFromManifest { .. }), + "physical insertion must keep the manifest lineage", + ); + } + assert_store_invariants(&store); + + // GC the physical data away. Because the chunk descends from a manifest, its lineage must + // stick around so the data stays re-fetchable, unlike a volatile chunk. + store.gc(&crate::GarbageCollectionOptions::gc_everything()); + assert_eq!(0, store.num_physical_chunks()); + let lineage = store + .chunks_lineage + .get(&chunk.id()) + .expect("a manifest lineage must survive GC"); + assert_eq!(lineage.descends_from_manifest, true); + assert_eq!(lineage.ref_count, 0); + } + + #[test] + fn physical_chunks_keep_live_lineage_through_mixed_workload() { + let mut store = temporal_store(4); + let mut make_chunk = chunk_factory(); + + assert_store_invariants(&store); + + for round in 0..6 { + // A few tiny chunks that the compactor will happily merge together. + for _ in 0..5 { + store.insert_chunk(&make_chunk(1)).unwrap(); + assert_store_invariants(&store); + } + + // A fat chunk that gets split into several smaller ones. + store.insert_chunk(&make_chunk(8)).unwrap(); + assert_store_invariants(&store); + + // Drop part of the store, alternating between shallow-friendly and deep deletions. + store.gc(&crate::GarbageCollectionOptions { + target: crate::GarbageCollectionTarget::DropAtLeastFraction(0.5), + time_budget: std::time::Duration::MAX, + protect_latest: 0, + protected_time_ranges: Default::default(), + protected_chunks: Default::default(), + furthest_from: None, + perform_deep_deletions: round % 2 == 0, + }); + assert_store_invariants(&store); + } + + // Finally drop everything and make sure the bookkeeping maps don't keep growing: every + // non-manifest lineage and leaky-compaction entry must be reclaimed. + store.gc(&crate::GarbageCollectionOptions::gc_everything()); + assert_store_invariants(&store); + assert_eq!(0, store.num_physical_chunks()); + assert!( + store.chunks_lineage.is_empty(), + "no volatile lineage should linger once the store is empty, got {} entries", + store.chunks_lineage.len(), + ); + assert!( + store.leaky_compactions.is_empty(), + "the leaky-compaction tracker should be empty once the store is empty, got {} entries", + store.leaky_compactions.len(), + ); + assert!( + store.dangling_splits.is_empty(), + "no dangling-split bookkeeping should linger, got {} entries", + store.dangling_splits.len(), + ); + assert!( + store.split_on_ingest.is_empty(), + "no split-on-ingest bookkeeping should linger, got {} entries", + store.split_on_ingest.len(), + ); + } + + // Test that `ChunkIdSetPerTimePerComponentPerTimelinePerEntity` and `ChunkIdSetPerTimePerTimelinePerEntity` + // are correctly tracked even under tight time budget. + #[test] + fn deep_removal_under_tight_budget_keeps_indices_consistent() { + let mut store = temporal_store(1); // force every row into its own non-root split chunk + let mut make_chunk = chunk_factory(); + + store.insert_chunk(&make_chunk(8)).unwrap(); + let chunks = store.iter_physical_chunks().cloned().collect_vec(); + assert!( + chunks.len() >= 2, + "we need several chunks to exercise an early bail", + ); + assert_store_invariants(&store); + + // A near-zero budget makes the shallow pass bail after the first chunk, while the deep pass + // clears every virtual entry regardless of the budget. The two indices must not drift + // apart, otherwise a later GC pass trips the deep-superset-of-shallow assertion. + store.remove_chunks_deep( + chunks, + Some(std::time::Duration::ZERO), + crate::ChunkDeletionReason::GarbageCollection, + ); + + assert_store_invariants(&store); + } + // --- fn next_chunk_id_generator(prefix: u64) -> impl FnMut() -> re_chunk::ChunkId { @@ -1165,4 +1535,81 @@ mod tests { lineage_report } + + fn temporal_config(chunk_max_rows: u64) -> ChunkStoreConfig { + ChunkStoreConfig { + enable_changelog: false, // irrelevant + chunk_max_bytes: u64::MAX, + chunk_max_rows, + chunk_max_rows_if_unsorted: chunk_max_rows, + } + } + + fn temporal_store(chunk_max_rows: u64) -> ChunkStore { + ChunkStore::new( + StoreId::recording("app_id", "rec_id"), + temporal_config(chunk_max_rows), + ) + } + + /// Builds chunks with deterministic ids, all on the same entity and timeline. + fn chunk_factory() -> impl FnMut(usize) -> Arc { + let mut next_chunk_id = next_chunk_id_generator(1); + let entity_path = EntityPath::from("this/that"); + let timepoint = [(Timeline::new_sequence("frame"), 1)]; + let points = [MyPoint::new(1.0, 1.0)]; + + move |num_rows: usize| { + let mut builder = Chunk::builder_with_id(next_chunk_id(), entity_path.clone()); + for _ in 0..num_rows { + builder = builder.with_component_batches( + RowId::new(), + timepoint, + [(MyPoints::descriptor_points(), &points as _)], + ); + } + Arc::new(builder.build().unwrap()) + } + } + + /// All temporal chunk ids that currently live in the virtual indices. + fn virtual_chunk_ids(store: &ChunkStore) -> ahash::HashSet { + let mut ids = ahash::HashSet::default(); + for per_timeline in store.temporal_chunk_ids_per_entity.values() { + for set_per_time in per_timeline.values() { + ids.extend(set_per_time.per_start_time.values().flatten().copied()); + ids.extend(set_per_time.per_end_time.values().flatten().copied()); + } + } + ids + } + + /// Checks the two invariants that the ref-counted lineage tracking must uphold. + /// + /// Every physical chunk must keep a lineage entry with a non-zero ref count, otherwise + /// `is_root_chunk` silently treats it as a root and reroutes its GC deletion. + /// Every physical temporal chunk must also live in the virtual indices, which is exactly the + /// `deep ⊇ shallow` invariant that `remove_chunks_deep` asserts on. + fn assert_store_invariants(store: &ChunkStore) { + let virtual_ids = virtual_chunk_ids(store); + + for chunk in store.physical_chunks_per_chunk_id.values() { + let chunk_id = chunk.id(); + + let lineage = store.chunks_lineage.get(&chunk_id); + assert!( + lineage.is_some_and(|l| l.ref_count >= 1), + "physical chunk {chunk_id} lost its live lineage entry, so is_root_chunk would \ + wrongly treat it as a root. lineage: {lineage:?}", + ); + + if !chunk.is_static() { + assert!( + virtual_ids.contains(&chunk_id), + "physical chunk {chunk_id} is missing from the virtual indices, which breaks \ + the deep-superset-of-shallow GC invariant", + ); + } + } + } } diff --git a/crates/store/re_chunk_store/src/profile.rs b/crates/store/re_chunk_store/src/profile.rs new file mode 100644 index 000000000000..7a68ccbeca2d --- /dev/null +++ b/crates/store/re_chunk_store/src/profile.rs @@ -0,0 +1,136 @@ +use crate::ChunkStoreConfig; + +/// Named optimization profile combining chunk-size thresholds with +/// post-processing knobs (extra passes, GoP rebatching, thick/thin split). +/// +/// Two presets are provided: [`Self::LIVE`] (small chunks tuned for the live +/// Viewer workflow) and [`Self::OBJECT_STORE`] (large chunks tuned for +/// object-store-backed query and streaming). +/// +/// A profile does not consult environment variables. Callers that need env-var +/// layering must call [`ChunkStoreConfig::apply_env`] themselves on the result +/// of [`Self::to_chunk_store_config`]. +#[derive(Debug, Clone, PartialEq)] +pub struct OptimizationProfile { + /// Maximum byte size of a single chunk. + /// + /// Two chunks are only merged if their combined size stays within this limit. + /// Incoming chunks that exceed this size are recursively split. + /// Setting this to `0` disables merging entirely. + pub chunk_max_bytes: u64, + + /// Maximum row count for time-sorted chunks. + /// + /// Applied as both a compaction ceiling and a splitting trigger, but only when + /// all timelines in the chunk are sorted. See also [`Self::chunk_max_rows_if_unsorted`]. + pub chunk_max_rows: u64, + + /// Maximum row count for chunks that contain at least one unsorted timeline. + /// + /// Kept lower than [`Self::chunk_max_rows`] because unsorted chunks have higher + /// query costs — they require a full scan to resolve time ranges. + pub chunk_max_rows_if_unsorted: u64, + + /// How many additional compaction passes to run after initial ingestion. + /// + /// Each pass walks all chunks and merges neighboring pairs that fit within the + /// size thresholds. Passes stop early if a pass produces no merges. + pub num_extra_passes: u32, + + /// Whether to rebatch video stream chunks along Group-of-Pictures (GoP) boundaries. + /// + /// When enabled, chunks containing video frames are reorganized so each chunk + /// begins at a keyframe. A single GoP that exceeds `chunk_max_bytes` is kept + /// intact (oversized chunks are permitted). Aligning to GoP boundaries lets + /// random-access reads load at most one chunk per frame. + pub gop_batching: bool, + + /// If set, split chunks so no two archetype groups within a chunk differ in + /// byte size by more than this ratio. + /// + /// This separates "thick" columns (images, blobs) from "thin" columns (scalars, + /// transforms). A value of `1.0` forces each archetype into its own chunk. + /// Components belonging to the same archetype are never split across chunks. + /// `None` disables the thick/thin split entirely. + pub split_size_ratio: Option, +} + +impl OptimizationProfile { + /// Optimized for the live Viewer workflow: small chunks for low-latency + /// rendering and fine-grained time-panel precision. + /// + /// Threshold values intentionally mirror [`ChunkStoreConfig::DEFAULT`]. + /// If you change one, change the other (see the unit test in this module). + pub const LIVE: Self = Self { + chunk_max_bytes: 12 * 8 * 4096, + chunk_max_rows: 4096, + chunk_max_rows_if_unsorted: 1024, + num_extra_passes: 50, + gop_batching: true, + split_size_ratio: None, + }; + + /// Optimized for object-store-backed storage (e.g. a catalog server): + /// larger chunks tuned for query throughput and streaming over the network. + pub const OBJECT_STORE: Self = Self { + chunk_max_bytes: 2 * 1024 * 1024, + chunk_max_rows: 65_536, + chunk_max_rows_if_unsorted: 8_192, + num_extra_passes: 50, + gop_batching: true, + // Separate thick columns (images, blobs) from thin columns (scalars, transforms) so + // that viewers and query engines can fetch lightweight metadata without downloading + // the full image payload. 10× is the recommended starting point. + split_size_ratio: Some(10.0), + }; + + /// Build a [`ChunkStoreConfig`] from this profile, with `enable_changelog` + /// at its `ChunkStoreConfig::DEFAULT` value. + /// + /// Headless callers (the CLI, the Python `LazyChunkStream.collect` + /// binding) want the changelog off and must set it explicitly on the + /// returned config. + pub fn to_chunk_store_config(&self) -> ChunkStoreConfig { + ChunkStoreConfig { + enable_changelog: ChunkStoreConfig::DEFAULT.enable_changelog, + chunk_max_bytes: self.chunk_max_bytes, + chunk_max_rows: self.chunk_max_rows, + chunk_max_rows_if_unsorted: self.chunk_max_rows_if_unsorted, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Regression detector: if someone changes `ChunkStoreConfig::DEFAULT` + /// thresholds without updating `LIVE`, this fails. + #[test] + fn live_thresholds_track_default() { + assert_eq!( + OptimizationProfile::LIVE.chunk_max_bytes, + ChunkStoreConfig::DEFAULT.chunk_max_bytes, + ); + assert_eq!( + OptimizationProfile::LIVE.chunk_max_rows, + ChunkStoreConfig::DEFAULT.chunk_max_rows, + ); + assert_eq!( + OptimizationProfile::LIVE.chunk_max_rows_if_unsorted, + ChunkStoreConfig::DEFAULT.chunk_max_rows_if_unsorted, + ); + } + + #[test] + fn to_chunk_store_config_carries_thresholds() { + let cfg = OptimizationProfile::OBJECT_STORE.to_chunk_store_config(); + assert_eq!(cfg.chunk_max_bytes, 2 * 1024 * 1024); + assert_eq!(cfg.chunk_max_rows, 65_536); + assert_eq!(cfg.chunk_max_rows_if_unsorted, 8_192); + assert_eq!( + cfg.enable_changelog, + ChunkStoreConfig::DEFAULT.enable_changelog + ); + } +} diff --git a/crates/store/re_chunk_store/src/properties.rs b/crates/store/re_chunk_store/src/properties.rs index 461bad7e515a..ccb5b5f2df62 100644 --- a/crates/store/re_chunk_store/src/properties.rs +++ b/crates/store/re_chunk_store/src/properties.rs @@ -7,7 +7,7 @@ use arrow::datatypes::{Field, Schema}; use arrow::error::ArrowError; use itertools::Itertools as _; use re_arrow_util::ArrowArrayDowncastRef as _; -use re_chunk::LatestAtQuery; +use re_chunk::{Chunk, ChunkId, LatestAtQuery}; use re_log_types::{EntityPath, TimeInt, TimelineName}; use re_sorbet::ComponentColumnDescriptor; use re_types_core::ComponentDescriptor; @@ -38,15 +38,33 @@ impl ChunkStore { /// // TODO(ab): move these ^ to a better place. pub fn extract_properties(&self) -> Result { - let mut fields = vec![]; - let mut data = vec![]; + let per_entity = self.property_entities_query_results(); + let all_missing: Vec<_> = per_entity + .iter() + .flat_map(|(_, qr)| qr.missing_virtual.iter().copied()) + .collect(); + if !all_missing.is_empty() { + return Err(ExtractPropertiesError::MissingData(all_missing)); + } + + let per_entity_chunks: Vec<(EntityPath, Vec>)> = per_entity + .into_iter() + .map(|(entity, qr)| (entity, qr.chunks)) + .collect(); + + build_properties_record_batch(&per_entity_chunks) + } + + /// Run the property-entity latest-at queries used by both the pure-`ChunkStore` path and the + /// split `property_entities_query_results` / [`extract_properties_from_chunks`] path used by + /// lazy stores. + pub fn property_entities_query_results(&self) -> Vec<(EntityPath, QueryResults)> { // Sweep all property entities first and collect the union of missing virtual chunks - // across all of them. This way callers that auto-load (e.g. `LazyRrdStore::extract_properties`) + // across all of them. This way callers that auto-load (e.g. `LazyStore::extract_properties`) // see the full batch in one shot and converge in a single retry instead of one disk // round-trip per entity. - let per_entity: Vec<(EntityPath, QueryResults)> = self - .all_entities() + self.all_entities() .into_iter() .filter(EntityPath::is_property) .map(|entity| { @@ -63,145 +81,184 @@ impl ChunkStore { ); (entity, results) }) - .collect(); + .collect() + } +} - let all_missing: Vec<_> = per_entity - .iter() - .flat_map(|(_, qr)| qr.missing_virtual.iter().copied()) +/// Build a one-row properties [`RecordBatch`] from a pre-materialized slice of chunks plus the +/// per-entity query results that produced the chunk-id list. +/// +/// `chunks` must contain every chunk referenced by `per_entity_results` (both the already-resolved +/// `chunks` and the `missing_virtual` ids). Returns [`ExtractPropertiesError::MissingData`] listing +/// any ids that aren't present in `chunks`. +pub fn extract_properties_from_chunks( + per_entity_results: &[(EntityPath, QueryResults)], + chunks: &[Arc], +) -> Result { + use ahash::HashMap; + + let chunks_by_id: HashMap> = chunks.iter().map(|c| (c.id(), c)).collect(); + + let mut missing: Vec = Vec::new(); + let per_entity_chunks: Vec<(EntityPath, Vec>)> = per_entity_results + .iter() + .map(|(entity, qr)| { + let materialized: Vec> = std::iter::chain( + qr.chunks.iter().map(|c| c.id()), + qr.missing_virtual.iter().copied(), + ) + .filter_map(|id| { + if let Some(c) = chunks_by_id.get(&id) { + Some(Arc::clone(c)) + } else { + missing.push(id); + None + } + }) .collect(); - if !all_missing.is_empty() { - return Err(ExtractPropertiesError::MissingData(all_missing)); - } + (entity.clone(), materialized) + }) + .collect(); - for (entity, QueryResults { chunks, .. }) in per_entity { - for chunk in chunks { - for component_desc in chunk.component_descriptors() { - let component = component_desc.component; - - // it's possible to have multiple values for the same component, hence we take the latest value - let Some(chunk_comp_latest) = chunk.latest_at( - /* same as above, timeline is irrelevant as these are static chunks */ - &LatestAtQuery::new(TimelineName::log_tick(), TimeInt::MIN), - component, - ) else { - continue; - }; - let (_, column) = chunk_comp_latest - .components() - .iter() - .find(|(c, _)| **c == component) - .ok_or({ - // this should never happen really - ExtractPropertiesError::Internal(format!( - "failed to find component in chunk: {component:?}" - )) - })?; - - let store_datatype = column.list_array.data_type().clone(); - let mut list_array = ListArray::from(column.list_array.clone()); - - // we strip __properties from the entity path, see - // - // NOTE: we need to handle both `/__properties` AND `/__properties/$FOO` here - let name = property_column_name(&entity, component_desc); - - let column_descriptor = ComponentColumnDescriptor { - component_type: component_desc.component_type, - entity_path: entity.clone(), - archetype: component_desc.archetype, - component: component_desc.component, - store_datatype, - is_semantically_empty: false, - is_static: false, - is_tombstone: false, - }; - - let metadata = column_descriptor - .to_arrow_field(re_sorbet::BatchType::Dataframe) - .metadata() - .clone(); - - let nullable = true; // we can have partitions that don't have properties like other partitions - let mut new_field = - Field::new(name.clone(), list_array.data_type().clone(), nullable); - - // TODO(rerun-io/dataplatform#567) it seems we're hitting https://github.com/lance-format/lance/issues/2304. So what happens is that - // we store a properties with a FixedSizeList and Lance stores it as nullable = true, regardless of the input - // field. If we then try to register another partition with the same property, but with nullable = false, we'll - // get a "Cannot change field type for field" error. Hence, we have to make field nullable in case of FixedSizeList - // Also see `register_one_partition_then_another_with_same_property` test. - // let list_array: &dyn arrow::array::Array = &list_array; - let list_array_values = (&list_array as &dyn arrow::array::Array) - .try_downcast_array_ref::()? - .values(); - - if let arrow::datatypes::DataType::FixedSizeList( - fixed_size_list_inner, - length, - ) = list_array_values.data_type() - { - let inner_field = Arc::new( - (**fixed_size_list_inner) - .clone() - .with_nullable(true /* now nullable */), - ); - - let fixed_size_list_field = Arc::new(Field::new( - "item", - arrow::datatypes::DataType::FixedSizeList(inner_field.clone(), *length), - true, /* nullable */ - )); - - let array = - list_array_values.try_downcast_array_ref::()?; - let values = array.values(); - let nulls = array.nulls(); - - // we have to recreate the FixedSizeListArray with the new field's nullable field definition - let new_fixed_size_list = FixedSizeListArray::try_new( - inner_field, - *length, - values.clone(), - nulls.cloned(), - )?; - - let array = ListArray::try_new( - fixed_size_list_field.clone(), - list_array.offsets().clone(), - Arc::new(new_fixed_size_list) as ArrayRef, - list_array.nulls().cloned(), - )?; - - let field_nullable = Field::new( - name, - arrow::datatypes::DataType::List(fixed_size_list_field), - nullable, - ); - - new_field = field_nullable; - list_array = array; - } - - let new_field = new_field.with_metadata(metadata); - - fields.push(new_field); - data.push(re_arrow_util::into_arrow_ref(list_array)); - } + if !missing.is_empty() { + return Err(ExtractPropertiesError::MissingData(missing)); + } + + build_properties_record_batch(&per_entity_chunks) +} + +fn build_properties_record_batch( + per_entity_chunks: &[(EntityPath, Vec>)], +) -> Result { + let mut fields = vec![]; + let mut data = vec![]; + + for (entity, chunks) in per_entity_chunks { + for chunk in chunks { + for component_desc in chunk.component_descriptors() { + let component = component_desc.component; + + // it's possible to have multiple values for the same component, hence we take the latest value + let Some(chunk_comp_latest) = chunk.latest_at( + /* same as above, timeline is irrelevant as these are static chunks */ + &LatestAtQuery::new(TimelineName::log_tick(), TimeInt::MIN), + component, + ) else { + continue; + }; + let (_, column) = chunk_comp_latest + .components() + .iter() + .find(|(c, _)| **c == component) + .ok_or({ + // this should never happen really + ExtractPropertiesError::Internal(format!( + "failed to find component in chunk: {component:?}" + )) + })?; + + let store_datatype = column.list_array.data_type().clone(); + let list_array = ListArray::from(column.list_array.clone()); + + // we strip __properties from the entity path, see + // + // NOTE: we need to handle both `/__properties` AND `/__properties/$FOO` here + let name = property_column_name(entity, component_desc); + + let column_descriptor = ComponentColumnDescriptor { + component_type: component_desc.component_type, + entity_path: entity.clone(), + archetype: component_desc.archetype, + component: component_desc.component, + store_datatype, + is_semantically_empty: false, + is_static: false, + is_tombstone: false, + }; + + let metadata = column_descriptor + .to_arrow_field(re_sorbet::BatchType::Dataframe) + .metadata() + .clone(); + + let nullable = true; // we can have partitions that don't have properties like other partitions + + let (new_field, list_array) = + relax_fixed_size_list_nullability(list_array, &name, nullable)?; + + let new_field = new_field.with_metadata(metadata); + + fields.push(new_field); + data.push(re_arrow_util::into_arrow_ref(list_array)); } } - - let (fields, data): (Vec<_>, Vec<_>) = fields - .into_iter() - .zip(data) - .sorted_by(|(field1, _), (field2, _)| field1.name().cmp(field2.name())) - .unzip(); - - Ok(RecordBatch::try_new_with_options( - Arc::new(Schema::new_with_metadata(fields, Default::default())), - data, - &RecordBatchOptions::default().with_row_count(Some(1)), - )?) } + + let (fields, data): (Vec<_>, Vec<_>) = std::iter::zip(fields, data) + .sorted_by(|(field1, _), (field2, _)| field1.name().cmp(field2.name())) + .unzip(); + + Ok(RecordBatch::try_new_with_options( + Arc::new(Schema::new_with_metadata(fields, Default::default())), + data, + &RecordBatchOptions::default().with_row_count(Some(1)), + )?) +} + +/// Make the inner `FixedSizeList` field nullable, working around +/// [lance-format/lance#2304](https://github.com/lance-format/lance/issues/2304). +/// +/// Lance stores `FixedSizeList` properties as `nullable = true` regardless of the input field, so +/// re-registering the same property as `nullable = false` later fails with "Cannot change field +/// type for field". We force-relax it on our side to avoid that. See also +/// `register_one_partition_then_another_with_same_property` and `rerun-io/dataplatform#567`. +/// +/// Returns `(field, list_array)` unchanged when the inner type isn't a `FixedSizeList`. +//TODO(RR-2041): clean this when the upstream issue is resolved +fn relax_fixed_size_list_nullability( + list_array: ListArray, + name: &str, + outer_nullable: bool, +) -> Result<(Field, ListArray), ExtractPropertiesError> { + let field = Field::new(name, list_array.data_type().clone(), outer_nullable); + + let arrow::datatypes::DataType::FixedSizeList(fixed_size_list_inner, length) = + list_array.values().data_type() + else { + return Ok((field, list_array)); + }; + + let inner_field = Arc::new((**fixed_size_list_inner).clone().with_nullable(true)); + let fixed_size_list_field = Arc::new(Field::new( + "item", + arrow::datatypes::DataType::FixedSizeList(inner_field.clone(), *length), + true, /* nullable */ + )); + + let inner_fixed = list_array + .values() + .try_downcast_array_ref::()?; + let new_fixed_size_list = FixedSizeListArray::try_new( + inner_field, + *length, + inner_fixed.values().clone(), + inner_fixed.nulls().cloned(), + )?; + + let new_list_array = ListArray::try_new( + fixed_size_list_field.clone(), + list_array.offsets().clone(), + Arc::new(new_fixed_size_list) as ArrayRef, + list_array.nulls().cloned(), + )?; + + let new_field = Field::new( + name, + arrow::datatypes::DataType::List(fixed_size_list_field), + outer_nullable, + ); + + Ok((new_field, new_list_array)) } fn property_column_name(entity_path: &EntityPath, component_desc: &ComponentDescriptor) -> String { diff --git a/crates/store/re_chunk_store/src/query.rs b/crates/store/re_chunk_store/src/query.rs index e58b6b768da7..db7ced84eea1 100644 --- a/crates/store/re_chunk_store/src/query.rs +++ b/crates/store/re_chunk_store/src/query.rs @@ -30,11 +30,11 @@ impl ChunkStore { /// Retrieve all [`EntityPath`]s in the store. #[inline] pub fn all_entities(&self) -> IntSet { - self.static_chunk_ids_per_entity - .keys() - .cloned() - .chain(self.temporal_chunk_ids_per_entity.keys().cloned()) - .collect() + std::iter::chain( + self.static_chunk_ids_per_entity.keys().cloned(), + self.temporal_chunk_ids_per_entity.keys().cloned(), + ) + .collect() } /// Returns a vector with all the chunks in this store, sorted in descending order relative to @@ -110,55 +110,51 @@ impl ChunkStore { /// Retrieve all [`EntityPath`]s in the store. #[inline] pub fn all_entities_sorted(&self) -> BTreeSet { - self.static_chunk_ids_per_entity - .keys() - .cloned() - .chain(self.temporal_chunk_ids_per_entity.keys().cloned()) - .collect() + std::iter::chain( + self.static_chunk_ids_per_entity.keys().cloned(), + self.temporal_chunk_ids_per_entity.keys().cloned(), + ) + .collect() } /// Retrieve all [`ComponentIdentifier`]s in the store. /// /// See also [`Self::all_components_sorted`]. pub fn all_components(&self) -> UnorderedComponentSet { - self.static_chunk_ids_per_entity - .values() - .flat_map(|static_chunks_per_component| static_chunks_per_component.keys()) - .chain( - self.temporal_chunk_ids_per_entity_per_component - .values() - .flat_map(|temporal_chunk_ids_per_timeline| { - temporal_chunk_ids_per_timeline.values().flat_map( - |temporal_chunk_ids_per_component| { - temporal_chunk_ids_per_component.keys() - }, - ) - }), - ) - .copied() - .collect() + std::iter::chain( + self.static_chunk_ids_per_entity + .values() + .flat_map(|static_chunks_per_component| static_chunks_per_component.keys()), + self.temporal_chunk_ids_per_entity_per_component + .values() + .flat_map(|temporal_chunk_ids_per_timeline| { + temporal_chunk_ids_per_timeline.values().flat_map( + |temporal_chunk_ids_per_component| temporal_chunk_ids_per_component.keys(), + ) + }), + ) + .copied() + .collect() } /// Retrieve all [`ComponentIdentifier`]s in the store. /// /// See also [`Self::all_components`]. pub fn all_components_sorted(&self) -> ComponentSet { - self.static_chunk_ids_per_entity - .values() - .flat_map(|static_chunks_per_component| static_chunks_per_component.keys()) - .chain( - self.temporal_chunk_ids_per_entity_per_component - .values() - .flat_map(|temporal_chunk_ids_per_timeline| { - temporal_chunk_ids_per_timeline.values().flat_map( - |temporal_chunk_ids_per_component| { - temporal_chunk_ids_per_component.keys() - }, - ) - }), - ) - .copied() - .collect() + std::iter::chain( + self.static_chunk_ids_per_entity + .values() + .flat_map(|static_chunks_per_component| static_chunks_per_component.keys()), + self.temporal_chunk_ids_per_entity_per_component + .values() + .flat_map(|temporal_chunk_ids_per_timeline| { + temporal_chunk_ids_per_timeline.values().flat_map( + |temporal_chunk_ids_per_component| temporal_chunk_ids_per_component.keys(), + ) + }), + ) + .copied() + .collect() } /// Retrieve all the [`ComponentIdentifier`]s that have been written to for a given [`EntityPath`] on @@ -166,10 +162,12 @@ impl ChunkStore { /// /// Static components are always included in the results. /// + /// A `None` timeline (a static-only query) yields only the static components. + /// /// Returns `None` if the entity doesn't exist at all on this `timeline`. pub fn all_components_on_timeline( &self, - timeline: &TimelineName, + timeline: Option<&TimelineName>, entity_path: &EntityPath, ) -> Option { re_tracing::profile_function!(); @@ -189,8 +187,8 @@ impl ChunkStore { .temporal_chunk_ids_per_entity_per_component .get(entity_path) .map(|temporal_chunk_ids_per_timeline| { - temporal_chunk_ids_per_timeline - .get(timeline) + timeline + .and_then(|timeline| temporal_chunk_ids_per_timeline.get(timeline)) .map(|temporal_chunk_ids_per_component| { temporal_chunk_ids_per_component .keys() @@ -205,7 +203,7 @@ impl ChunkStore { (None, None) => None, (None, Some(comps)) | (Some(comps), None) => Some(comps), (Some(static_comps), Some(temporal_comps)) => { - Some(static_comps.into_iter().chain(temporal_comps).collect()) + Some(std::iter::chain(static_comps, temporal_comps).collect()) } } } @@ -254,7 +252,7 @@ impl ChunkStore { (None, None) => None, (None, Some(comps)) | (Some(comps), None) => Some(comps), (Some(static_comps), Some(temporal_comps)) => { - Some(static_comps.into_iter().chain(temporal_comps).collect()) + Some(std::iter::chain(static_comps, temporal_comps).collect()) } } } @@ -265,14 +263,16 @@ impl ChunkStore { #[inline] pub fn entity_has_component_on_timeline( &self, - timeline: &TimelineName, + timeline: Option<&TimelineName>, entity_path: &EntityPath, component: ComponentIdentifier, ) -> bool { // re_tracing::profile_function!(); // This function is too fast; profiling will only add overhead self.entity_has_static_component(entity_path, component) - || self.entity_has_temporal_component_on_timeline(timeline, entity_path, component) + || timeline.is_some_and(|timeline| { + self.entity_has_temporal_component_on_timeline(timeline, entity_path, component) + }) } /// Check whether an entity has a static component or a temporal component on any timeline. @@ -747,7 +747,7 @@ impl QueryResults { } else { match report_mode { ChunkTrackingMode::Ignore => {} - ChunkTrackingMode::Report => { + ChunkTrackingMode::Report | ChunkTrackingMode::ReportTransient => { this.missing_virtual.push(chunk_id); } ChunkTrackingMode::PanicOnMissing => { @@ -757,7 +757,9 @@ impl QueryResults { } } - if report_mode == ChunkTrackingMode::Report { + if report_mode == ChunkTrackingMode::Report + || report_mode == ChunkTrackingMode::ReportTransient + { let mut tracker = store.queried_chunk_id_tracker.write(); for chunk_id in &this.missing_virtual { @@ -777,21 +779,31 @@ impl QueryResults { } } - tracker - .missing_virtual - .extend(this.missing_virtual.iter().copied()); + if report_mode == ChunkTrackingMode::Report { + tracker + .missing_virtual + .extend(this.missing_virtual.iter().copied()); - tracker - .used_physical - .extend(this.chunks.iter().map(|c| c.id())); + tracker + .used_physical + .extend(this.chunks.iter().map(|c| c.id())); + } else { + tracker + .transient_missing_virtual + .extend(this.missing_virtual.iter().copied()); + + tracker + .transient_used_physical + .extend(this.chunks.iter().map(|c| c.id())); + } } debug_assert!( - this.chunks - .iter() - .map(|chunk| chunk.id()) - .chain(this.missing_virtual.iter().copied()) - .all_unique() + std::iter::chain( + this.chunks.iter().map(|chunk| chunk.id()), + this.missing_virtual.iter().copied(), + ) + .all_unique() ); this @@ -908,7 +920,8 @@ impl ChunkStore { .temporal_chunk_ids_per_entity_per_component .get(entity_path) .and_then(|temporal_chunk_ids_per_timeline| { - temporal_chunk_ids_per_timeline.get(&query.timeline()) + let timeline = query.timeline()?; + temporal_chunk_ids_per_timeline.get(&timeline) }) .and_then(|temporal_chunk_ids_per_component| { temporal_chunk_ids_per_component.get(&component) @@ -959,7 +972,8 @@ impl ChunkStore { .temporal_chunk_ids_per_entity_per_component .get(entity_path) .and_then(|temporal_chunk_ids_per_timeline_per_component| { - temporal_chunk_ids_per_timeline_per_component.get(&query.timeline()) + let timeline = query.timeline()?; + temporal_chunk_ids_per_timeline_per_component.get(&timeline) }) .map(|temporal_chunk_ids_per_component| { temporal_chunk_ids_per_component @@ -976,11 +990,10 @@ impl ChunkStore { }) .flatten(); - static_chunk_ids - .chain(temporal_chunk_ids) - // Deduplicate before passing it along. - // Both temporal and static chunk "sets" here may have duplicates in them, - // so we de-duplicate them together to reduce the number of allocations. + // Deduplicate before passing it along. + // Both temporal and static chunk "sets" here may have duplicates in them, + // so we de-duplicate them together to reduce the number of allocations. + std::iter::chain(static_chunk_ids, temporal_chunk_ids) .unique() .collect_vec() } else { @@ -988,7 +1001,8 @@ impl ChunkStore { self.temporal_chunk_ids_per_entity .get(entity_path) .and_then(|temporal_chunk_ids_per_timeline| { - temporal_chunk_ids_per_timeline.get(&query.timeline()) + let timeline = query.timeline()?; + temporal_chunk_ids_per_timeline.get(&timeline) }) .and_then(|temporal_chunk_ids_per_time| { Self::latest_at(query, temporal_chunk_ids_per_time) @@ -1169,14 +1183,10 @@ impl ChunkStore { ) .into_iter(); - Either::Left( - static_chunk_ids - .chain(temporal_chunk_ids) - // Deduplicate before passing it along. - // Both temporal and static chunk "sets" here may have duplicates in them, - // so we de-duplicate them together to reduce the number of allocations. - .unique(), - ) + // Deduplicate before passing it along. + // Both temporal and static chunk "sets" here may have duplicates in them, + // so we de-duplicate them together to reduce the number of allocations. + Either::Left(std::iter::chain(static_chunk_ids, temporal_chunk_ids).unique()) } else { // This cannot yield duplicates by definition. Either::Right(Self::range( @@ -1306,10 +1316,9 @@ mod tests { // Make sure queries yield partial results when we expect them to. #[test] fn partial_data_basics() { - let mut store = ChunkStore::new( - re_log_types::StoreId::random(re_log_types::StoreKind::Recording, "test_app"), - crate::ChunkStoreConfig::ALL_DISABLED, - ); + let store_id = + re_log_types::StoreId::random(re_log_types::StoreKind::Recording, "test_app"); + let mut store = ChunkStore::new(store_id.clone(), crate::ChunkStoreConfig::ALL_DISABLED); let entity_path: EntityPath = "some_entity".into(); @@ -1364,6 +1373,16 @@ mod tests { assert!(store.take_tracked_chunk_ids().missing_virtual.is_empty()); } + // Back the chunks with an RRD manifest. That way, once they get garbage collected, they + // stay recoverable and keep being reported as missing (partial results) instead of + // vanishing from the virtual indices entirely. + let rrd_manifest = re_log_encoding::RrdManifest::build_in_memory_from_chunks( + store_id, + [&*chunk1, &*chunk2, &*chunk3].into_iter(), + ) + .unwrap(); + _ = store.insert_rrd_manifest(rrd_manifest); + store.insert_chunk(&chunk1).unwrap(); store.insert_chunk(&chunk2).unwrap(); store.insert_chunk(&chunk3).unwrap(); diff --git a/crates/store/re_chunk_store/src/rebatch_videos.rs b/crates/store/re_chunk_store/src/rebatch_videos.rs index cfc4a212cb7e..f5ce981427c2 100644 --- a/crates/store/re_chunk_store/src/rebatch_videos.rs +++ b/crates/store/re_chunk_store/src/rebatch_videos.rs @@ -1,14 +1,17 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; use ahash::{HashMap, HashSet}; +use arrow::array::{ArrayRef, BooleanArray, ListArray as ArrowListArray}; +use arrow::buffer::{OffsetBuffer, ScalarBuffer}; +use arrow::datatypes::Field; use itertools::izip; use re_byte_size::SizeBytes as _; -use re_chunk::{Chunk, ChunkId, ChunkShared, EntityPath, Timeline, TimelineName}; +use re_chunk::{Chunk, ChunkId, ChunkShared, EntityPath, TimeColumn, Timeline, TimelineName}; use re_format::format_bytes; use re_log_types::TimeInt; use re_sdk_types::archetypes::VideoStream; -use re_sdk_types::components::{VideoCodec, VideoSample}; +use re_sdk_types::components::{IsKeyframe, VideoCodec, VideoSample}; use crate::{ChunkStore, ChunkStoreConfig, ChunkTrackingMode}; @@ -44,20 +47,36 @@ pub fn rebatch_video_chunks_to_gops( store: &ChunkStore, config: &ChunkStoreConfig, is_start_of_gop: &dyn Fn(&[u8], VideoCodec) -> anyhow::Result, + fix_keyframe: bool, ) -> anyhow::Result { re_tracing::profile_function!(); let sample_component = VideoStream::descriptor_sample().component; + let keyframe_component = VideoStream::descriptor_is_keyframe().component; // Collect all temporal chunks that contain video samples, grouped by entity. + // Also collect dedicated `is_keyframe` chunks (no sample column) separately — + // we need to read user labels from them during validation. let mut sample_chunks_per_entity: HashMap> = Default::default(); + let mut dedicated_keyframe_chunks_per_entity: HashMap< + EntityPath, + HashMap, + > = Default::default(); for chunk in store.iter_physical_chunks() { - if !chunk.is_static() && chunk.components().contains_component(sample_component) { + if chunk.is_static() { + continue; + } + if chunk.components().contains_component(sample_component) { sample_chunks_per_entity .entry(chunk.entity_path().clone()) .or_default() .insert(chunk.id(), chunk.clone()); + } else if chunk.components().contains_component(keyframe_component) { + dedicated_keyframe_chunks_per_entity + .entry(chunk.entity_path().clone()) + .or_default() + .insert(chunk.id(), chunk.clone()); } } @@ -67,6 +86,7 @@ pub fn rebatch_video_chunks_to_gops( let mut replaced_chunk_ids: HashSet = HashSet::default(); let mut new_chunks: Vec = Vec::new(); + let mut keyframe_chunks: Vec = Vec::new(); re_log::info!( num_video_entities = sample_chunks_per_entity.len(), @@ -74,13 +94,50 @@ pub fn rebatch_video_chunks_to_gops( ); for (entity_path, sample_chunks) in &sample_chunks_per_entity { - match rebatch_video_entity(store, config, is_start_of_gop, entity_path, sample_chunks) { - Ok(new_entity_chunks) => { + let dedicated_keyframe_chunks = dedicated_keyframe_chunks_per_entity.get(entity_path); + match rebatch_video_entity( + store, + config, + is_start_of_gop, + entity_path, + sample_chunks, + dedicated_keyframe_chunks, + fix_keyframe, + ) { + Ok(EntityRebatch::Rebuild { + rebatched, + new_keyframe_chunk, + }) => { replaced_chunk_ids.extend(sample_chunks.keys().copied()); - new_chunks.extend(new_entity_chunks); + if let Some(stale) = dedicated_keyframe_chunks { + replaced_chunk_ids.extend(stale.keys().copied()); + } + new_chunks.extend(rebatched); + if let Some(kf) = new_keyframe_chunk { + keyframe_chunks.push(*kf); + } + } + Ok(EntityRebatch::KeepDedicatedKeyframeChunks { rebatched }) => { + // Existing dedicated keyframe chunks are canonical — leave + // them alone. Sample chunks still get GoP-rebatched. + replaced_chunk_ids.extend(sample_chunks.keys().copied()); + new_chunks.extend(rebatched); + } + Ok(EntityRebatch::Skip) => { + // Entity can't be safely rebatched (e.g. unsorted timelines). + // Leave its chunks alone; a warning was already logged. } Err(err) => { - re_log::warn!(entity = %entity_path, %err, "failed to rebatch video entity, skipping"); + // Don't abort the whole optimize because a single entity can't be + // rebatched — leave its chunks untouched and carry on. Causes range + // from mechanical (different chunks carry different timelines, so + // their GoPs can't be concatenated) to bad user data (the error + // explains how to fix it, e.g. setting `fix_keyframe`). + re_log::warn!( + entity = %entity_path, + error = %err, + "skipping GoP rebatching of video entity, leaving it un-optimized" + ); } } } @@ -104,6 +161,10 @@ pub fn rebatch_video_chunks_to_gops( new_store.insert_chunk(&Arc::new(chunk))?; } + for chunk in keyframe_chunks { + new_store.insert_chunk(&Arc::new(chunk))?; + } + /// Warn once per compaction if any rebatched chunk exceeds this size. /// /// GoP rebatching never splits a GoP across chunks, so streams with long @@ -122,38 +183,70 @@ pub fn rebatch_video_chunks_to_gops( Ok(new_store) } -/// Rebatch a single video entity's chunks along GoP boundaries. +/// Per-entity rebatch result. +enum EntityRebatch { + /// Rebuild both sample chunks and the keyframe marker chunk. Existing + /// dedicated `is_keyframe` chunks for this entity are dropped. + Rebuild { + /// Chunks that replace the original `VideoSample` chunks for this entity. + rebatched: Vec, + + /// Marker chunk holding sparse `is_keyframe` rows for this entity, if any. + /// Boxed to keep the `Rebuild` variant small enough to avoid a + /// `clippy::large_enum_variant` warning vs. the unit `Skip` variant. + new_keyframe_chunk: Option>, + }, + + /// Replace sample chunks with `rebatched`, but leave the existing + /// dedicated `is_keyframe` chunks alone — they're already canonical. + KeepDedicatedKeyframeChunks { rebatched: Vec }, + + /// Don't touch this entity's chunks. Used when rebatching can't be done + /// safely (e.g. the sample chunks have unsorted timelines). + Skip, +} + +/// Rebatch a single video entity's sample chunks along GoP boundaries, and +/// emit a sparse dedicated `is_keyframe` marker chunk derived by parsing the +/// encoded samples. /// -/// Returns the new chunks that replace the old ones. +/// When the user has supplied their own `is_keyframe` data (and `fix_keyframe` +/// is not set), validate it against the encoded samples: +/// - Canonical (correct labels in a pure dedicated chunk, no `false` rows): +/// the user's chunk is preserved verbatim. +/// - Correct but co-located with other components: rebuild a clean dedicated +/// chunk with the same content. +/// - Mismatched against the encoded samples, or any `false` row present: error. fn rebatch_video_entity( store: &ChunkStore, config: &ChunkStoreConfig, is_start_of_gop: &dyn Fn(&[u8], VideoCodec) -> anyhow::Result, entity_path: &EntityPath, sample_chunks: &HashMap, -) -> anyhow::Result> { + dedicated_keyframe_chunks: Option<&HashMap>, + fix_keyframe: bool, +) -> anyhow::Result { re_tracing::profile_function!(); for chunk in sample_chunks.values() { - let unsorted_timelines: Vec<_> = chunk - .timelines() - .iter() - .filter(|(_, tc)| !tc.is_sorted()) - .map(|(name, _)| name) - .collect(); + let unsorted_timelines = chunk.unsorted_timelines(); if !unsorted_timelines.is_empty() { // We could try pick one of the timelines _are_ sorted (w/ relation to RowId), - // but let's be better safe than sorry for now. - anyhow::bail!( - "chunk {} for entity '{entity_path}' has unsorted timelines: {:?} (compared to RowId). Video playback on these timelines may already be broken, and rebatching may make things worse", - chunk.id(), - unsorted_timelines + // but let's be better safe than sorry for now. Video playback on these + // timelines may already be broken, and rebatching could make things worse. + re_log::warn!( + entity = %entity_path, + chunk = %chunk.id(), + ?unsorted_timelines, + "skipping GoP rebatching: chunk has unsorted timelines (compared to RowId)" ); + return Ok(EntityRebatch::Skip); } } - let timeline_name = - choose_timeline(sample_chunks).ok_or_else(|| anyhow::anyhow!("no timeline found"))?; + let timeline_name = *choose_timeline(sample_chunks) + .ok_or_else(|| anyhow::anyhow!("no timeline found"))? + .name(); let codec = extract_codec(store, entity_path, timeline_name) .ok_or_else(|| anyhow::anyhow!("couldn't resolve video codec"))?; @@ -162,26 +255,261 @@ fn rebatch_video_entity( anyhow::ensure!(!sample_index.is_empty(), "no video samples found"); + // GoP-rebatch the sample chunks. This happens regardless of the keyframe + // column's state — even when the keyframe column is already canonical, the + // sample chunks might still need to be aligned to GoP boundaries. let gop_groups = split_into_gop_groups(entity_path, &sample_index); - - // Materialize each GoP into its own chunk: let gop_chunks: Vec = gop_groups .iter() .map(|group| chunk_from_gop(group, sample_chunks)) .collect::>()?; - log_gop_stats(entity_path, &gop_chunks); - - // Merge consecutive GoP chunks as long as the total stays within chunk_max_bytes. let merged = merge_chunks(config, gop_chunks)?; - log_entity_chunk_stats(entity_path, timeline_name, codec, &merged); - Ok(merged) + // Decide what to do with the `is_keyframe` column. + if !fix_keyframe { + let user = + collect_user_keyframe_labels(sample_chunks, dedicated_keyframe_chunks, timeline_name); + + if user.has_any_label { + let codec_true: BTreeSet = sample_index + .iter() + .filter(|s| s.is_start_of_gop) + .map(|s| s.time) + .collect(); + + let mismatched = user.true_times != codec_true; + let has_false = !user.false_times.is_empty(); + if mismatched || has_false { + return Err(build_keyframe_validation_error( + &user.true_times, + &codec_true, + &user.false_times, + )); + } + + // Labels match the codec and no `false` rows are present. If every + // is_keyframe row lives in a pure dedicated chunk, those chunks are + // already what we'd emit — keep them. + if !user.shares_chunk { + return Ok(EntityRebatch::KeepDedicatedKeyframeChunks { rebatched: merged }); + } + // Else fall through: move the `is_keyframe` column into a + // dedicated chunk. + } + } + + let new_keyframe_chunk = + build_keyframe_chunk(entity_path, sample_chunks, timeline_name, &sample_index)?; + + Ok(EntityRebatch::Rebuild { + rebatched: merged, + new_keyframe_chunk: new_keyframe_chunk.map(Box::new), + }) +} + +/// Summary of user-supplied `VideoStream:is_keyframe` data for one entity. +#[derive(Default)] +struct UserKeyframeLabels { + /// Times at which the user logged `is_keyframe=true`, on the chosen timeline. + true_times: BTreeSet, + + /// Times at which the user logged `is_keyframe=false`. Optimize refuses to + /// run unless this is empty — no `false` should remain in the output. + false_times: BTreeSet, + + /// True if at least one `is_keyframe` row sits in a chunk that also carries + /// any other component column (sample data, scalars, anything). The + /// canonical layout is a dedicated chunk holding only `is_keyframe`. + shares_chunk: bool, + + /// True if any `is_keyframe` value was logged at all. + has_any_label: bool, +} + +/// Walk every chunk for this entity and aggregate its user-supplied +/// `VideoStream:is_keyframe` labels into a [`UserKeyframeLabels`]. +fn collect_user_keyframe_labels( + sample_chunks: &HashMap, + dedicated_keyframe_chunks: Option<&HashMap>, + timeline_name: TimelineName, +) -> UserKeyframeLabels { + let keyframe_component = VideoStream::descriptor_is_keyframe().component; + + let mut labels = UserKeyframeLabels::default(); + + let mut process_chunk = |chunk: &ChunkShared| { + if !chunk.components().contains_component(keyframe_component) { + return; + } + if !chunk.timelines().contains_key(&timeline_name) { + return; + } + let is_pure_keyframe_chunk = chunk + .components() + .values() + .all(|c| c.descriptor.component == keyframe_component); + for ((time, _row_id), value) in izip!( + chunk.iter_component_indices(timeline_name, keyframe_component), + chunk.iter_component::(keyframe_component), + ) { + let Some(kf) = value.as_slice().first() else { + continue; + }; + labels.has_any_label = true; + if !is_pure_keyframe_chunk { + labels.shares_chunk = true; + } + if bool::from(kf.0) { + labels.true_times.insert(time); + } else { + labels.false_times.insert(time); + } + } + }; + + for chunk in sample_chunks.values() { + process_chunk(chunk); + } + if let Some(kf_chunks) = dedicated_keyframe_chunks { + for chunk in kf_chunks.values() { + process_chunk(chunk); + } + } + + labels +} + +/// Build an actionable error describing how the user's `is_keyframe` labels +/// disagree with the codec, or that `false` rows are present. +fn build_keyframe_validation_error( + user_true: &BTreeSet, + codec_true: &BTreeSet, + false_times: &BTreeSet, +) -> anyhow::Error { + const MAX_EXAMPLES: usize = 3; + + let missing: Vec<_> = codec_true.difference(user_true).copied().collect(); + let extra: Vec<_> = user_true.difference(codec_true).copied().collect(); + let false_examples: Vec<_> = false_times.iter().copied().collect(); + + let fmt_examples = |times: &[TimeInt]| -> String { + let head: Vec<_> = times + .iter() + .take(MAX_EXAMPLES) + .map(|t| t.as_i64().to_string()) + .collect(); + if times.len() > MAX_EXAMPLES { + format!("{} (+{} more)", head.join(", "), times.len() - MAX_EXAMPLES) + } else { + head.join(", ") + } + }; + + let mut parts = Vec::new(); + if !missing.is_empty() { + parts.push(format!( + "{} codec keyframe(s) missing an `is_keyframe=true` label (e.g. at times {})", + missing.len(), + fmt_examples(&missing), + )); + } + if !extra.is_empty() { + parts.push(format!( + "{} sample(s) labeled `is_keyframe=true` that are not codec keyframes (e.g. at times {})", + extra.len(), + fmt_examples(&extra), + )); + } + if !false_examples.is_empty() { + parts.push(format!( + "{} `is_keyframe=false` row(s) (e.g. at times {}); `is_keyframe` is a sparse marker, only `true` should be logged", + false_examples.len(), + fmt_examples(&false_examples), + )); + } + + anyhow::anyhow!( + "user-supplied `is_keyframe` data is incorrect: {}; \ + pass `--fix-keyframe` (Python: `fix_keyframe=True`) to drop the existing labels and re-derive them from the encoded samples", + parts.join("; "), + ) +} + +/// Build a sparse `is_keyframe` marker chunk for this entity. +/// +/// Emits one row per keyframe sample, all with value `true`, on the +/// [`VideoStream::descriptor_is_keyframe`] descriptor. The result carries every +/// timeline present in the source sample chunks so that keyframe queries work +/// on any timeline the user logged on. Returns `None` if no sample in +/// `sample_index` was detected as a keyframe. +fn build_keyframe_chunk( + entity_path: &EntityPath, + sample_chunks: &HashMap, + chosen_timeline: TimelineName, + sample_index: &[SampleInfo], +) -> anyhow::Result> { + re_tracing::profile_function!(); + + let keyframes: Vec<&SampleInfo> = sample_index.iter().filter(|s| s.is_start_of_gop).collect(); + let num_keyframes = keyframes.len(); + if num_keyframes == 0 { + return Ok(None); + } + + // All sample chunks for one entity share the same timeline set. + let reference_chunk = sample_chunks + .values() + .next() + .ok_or_else(|| anyhow::anyhow!("no sample chunks"))?; + + let mut time_columns: Vec = Vec::with_capacity(reference_chunk.timelines().len()); + for (timeline_name, ref_tc) in reference_chunk.timelines() { + let timeline = *ref_tc.timeline(); + let mut times: Vec = Vec::with_capacity(num_keyframes); + for s in &keyframes { + let src = &sample_chunks[&s.chunk_id]; + let src_tc = src.timelines().get(timeline_name).ok_or_else(|| { + anyhow::anyhow!( + "chunk {} missing timeline {timeline_name} \ + (sample chunks must share the same timeline set)", + s.chunk_id, + ) + })?; + times.push(src_tc.times_raw()[s.row_index]); + } + // `build_sample_index` sorts by `chosen_timeline`, so the subset + // on that timeline is monotonic. For other timelines, cross-chunk + // interleaving may not preserve sort order — auto-detect. + let is_sorted = (*timeline_name == chosen_timeline).then_some(true); + time_columns.push(TimeColumn::new( + is_sorted, + timeline, + ScalarBuffer::from(times), + )); + } + + // Build the component column as a single ListArray: `num_keyframes` rows, + // each holding a one-element boolean batch with value `true`. + let values: ArrayRef = Arc::new(BooleanArray::from(vec![true; num_keyframes])); + let offsets = OffsetBuffer::from_lengths(std::iter::repeat_n(1, num_keyframes)); + let field = Field::new("item", values.data_type().clone(), true); + let list_array = ArrowListArray::try_new(field.into(), offsets, values, None) + .map_err(|err| anyhow::anyhow!("failed to build keyframe list array: {err}"))?; + + let chunk = Chunk::from_columns( + entity_path.clone(), + time_columns, + [(VideoStream::descriptor_is_keyframe(), list_array)], + ) + .map_err(|err| anyhow::anyhow!("failed to build keyframe chunk: {err}"))?; + + Ok(Some(chunk)) } /// Pick the best timeline for sorting video samples. -fn choose_timeline(sample_chunks: &HashMap) -> Option { +fn choose_timeline(sample_chunks: &HashMap) -> Option { let mut counts: HashMap = Default::default(); for chunk in sample_chunks.values() { for tc in chunk.timelines().values() { @@ -194,8 +522,9 @@ fn choose_timeline(sample_chunks: &HashMap) -> Option

/// -/// -/// -/// -/// -/// +/// +/// +/// +/// +/// /// ///
-#[derive(Clone, Debug, PartialEq, Default)] -pub struct Status { - /// The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array. - pub status: Option, +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] +pub struct StateChange { + /// The new state values; each instance gets its own lane in the state timeline view. + /// + /// A reset ends the previous state and shows a gap in the state timeline view until the + /// next state. An empty string, a null array entry, and an empty state array (e.g. from + /// clearing the field) all act as resets. + /// + /// The length of the state array should not change over time. + pub state: Option, } -impl Status { - /// Returns the [`ComponentDescriptor`] for [`Self::status`]. +impl StateChange { + /// Returns the [`ComponentDescriptor`] for [`Self::state`]. /// /// The corresponding component is [`crate::components::Text`]. #[inline] - pub fn descriptor_status() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Status".into()), - component: "Status:status".into(), - component_type: Some("rerun.components.Text".into()), - } + pub fn descriptor_state() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.StateChange".into()), + component: "StateChange:state".into(), + component_type: Some("rerun.components.Text".into()), + }); + (*DESCRIPTOR).clone() } } static REQUIRED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 1usize]> = - std::sync::LazyLock::new(|| [Status::descriptor_status()]); + std::sync::LazyLock::new(|| [StateChange::descriptor_state()]); static RECOMMENDED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = std::sync::LazyLock::new(|| []); @@ -89,22 +97,25 @@ static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = std::sync::LazyLock::new(|| []); static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 1usize]> = - std::sync::LazyLock::new(|| [Status::descriptor_status()]); + std::sync::LazyLock::new(|| [StateChange::descriptor_state()]); -impl Status { +impl StateChange { /// The total number of components in the archetype: 1 required, 0 recommended, 0 optional pub const NUM_COMPONENTS: usize = 1usize; } -impl ::re_types_core::Archetype for Status { +impl ::re_types_core::Archetype for StateChange { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.Status".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.StateChange" + ) } #[inline] fn display_name() -> &'static str { - "Status" + "State change" } #[inline] @@ -134,51 +145,51 @@ impl ::re_types_core::Archetype for Status { re_tracing::profile_function!(); use ::re_types_core::{Loggable as _, ResultExt as _}; let arrays_by_descr: ::nohash_hasher::IntMap<_, _> = arrow_data.into_iter().collect(); - let status = arrays_by_descr - .get(&Self::descriptor_status()) - .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_status())); - Ok(Self { status }) + let state = arrays_by_descr + .get(&Self::descriptor_state()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_state())); + Ok(Self { state }) } } -impl ::re_types_core::AsComponents for Status { +impl ::re_types_core::AsComponents for StateChange { #[inline] fn as_serialized_batches(&self) -> Vec { use ::re_types_core::Archetype as _; - std::iter::once(self.status.clone()).flatten().collect() + std::iter::once(self.state.clone()).flatten().collect() } } -impl ::re_types_core::ArchetypeReflectionMarker for Status {} +impl ::re_types_core::ArchetypeReflectionMarker for StateChange {} -impl crate::VisualizableArchetype for Status { +impl crate::VisualizableArchetype for StateChange { #[inline] fn visualizer(&self) -> crate::Visualizer { - crate::Visualizer::new("StatusVisualizer").with_overrides(self) + crate::Visualizer::new("StateVisualizer").with_overrides(self) } } -impl Status { - /// Create a new `Status`. +impl StateChange { + /// Create a new `StateChange`. #[inline] pub fn new() -> Self { - Self { status: None } + Self { state: None } } - /// Update only some specific fields of a `Status`. + /// Update only some specific fields of a `StateChange`. #[inline] pub fn update_fields() -> Self { Self::default() } - /// Clear all the fields of a `Status`. + /// Clear all the fields of a `StateChange`. #[inline] pub fn clear_fields() -> Self { use ::re_types_core::Loggable as _; Self { - status: Some(SerializedComponentBatch::new( + state: Some(SerializedComponentBatch::new( crate::components::Text::arrow_empty(), - Self::descriptor_status(), + Self::descriptor_state(), )), } } @@ -202,8 +213,8 @@ impl Status { I: IntoIterator + Clone, { let columns = [self - .status - .map(|status| status.partitioned(_lengths.clone())) + .state + .map(|state| state.partitioned(_lengths.clone())) .transpose()?]; Ok(columns.into_iter().flatten()) } @@ -216,35 +227,24 @@ impl Status { pub fn columns_of_unit_batches( self, ) -> SerializationResult> { - let len_status = self.status.as_ref().map(|b| b.array.len()); - let len = None.or(len_status).unwrap_or(0); + let len_state = self.state.as_ref().map(|b| b.array.len()); + let len = None.or(len_state).unwrap_or(0); self.columns(std::iter::repeat_n(1, len)) } - /// The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array. - #[inline] - pub fn with_status(mut self, status: impl Into) -> Self { - self.status = try_serialize_field(Self::descriptor_status(), [status]); - self - } - - /// This method makes it possible to pack multiple [`crate::components::Text`] in a single component batch. + /// The new state values; each instance gets its own lane in the state timeline view. + /// + /// A reset ends the previous state and shows a gap in the state timeline view until the + /// next state. An empty string, a null array entry, and an empty state array (e.g. from + /// clearing the field) all act as resets. /// - /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_status`] should - /// be used when logging a single row's worth of data. + /// The length of the state array should not change over time. #[inline] - pub fn with_many_status( + pub fn with_state( mut self, - status: impl IntoIterator>, + state: impl IntoIterator>, ) -> Self { - self.status = try_serialize_field(Self::descriptor_status(), status); + self.state = try_serialize_field(Self::descriptor_state(), state); self } } - -impl ::re_byte_size::SizeBytes for Status { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.status.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/state_change_ext.rs b/crates/store/re_sdk_types/src/archetypes/state_change_ext.rs new file mode 100644 index 000000000000..feffce4eec34 --- /dev/null +++ b/crates/store/re_sdk_types/src/archetypes/state_change_ext.rs @@ -0,0 +1,54 @@ +use re_types_core::{Loggable as _, SerializedComponentBatch}; + +use crate::components::Text; + +impl super::StateChange { + /// Constructor for a single state value (one lane). + pub fn single(state: impl Into) -> Self { + Self::new().with_state([state]) + } + + /// Set the state array from optional values. + /// + /// A `None` entry resets that instance's state, showing a gap in its lane — something + /// [`Self::with_state`] can't express, since it only takes present values. + pub fn with_state_opt( + mut self, + state: impl IntoIterator>>, + ) -> Self { + let res = Text::to_arrow_opt( + state + .into_iter() + .map(|v| v.map(|v| std::borrow::Cow::Owned(v.into()))), + ); + + match res { + Ok(array) => { + self.state = Some(SerializedComponentBatch::new( + array, + Self::descriptor_state(), + )); + } + + #[cfg(debug_assertions)] + Err(err) => { + panic!( + "failed to serialize data for {}: {}", + Self::descriptor_state(), + re_error::format_ref(&err) + ) + } + + #[cfg(not(debug_assertions))] + Err(err) => { + re_log::error!( + descriptor = %Self::descriptor_state(), + "failed to serialize data: {}", + re_error::format_ref(&err) + ); + } + } + + self + } +} diff --git a/crates/store/re_sdk_types/src/archetypes/state_configuration.rs b/crates/store/re_sdk_types/src/archetypes/state_configuration.rs new file mode 100644 index 000000000000..a2af214d525d --- /dev/null +++ b/crates/store/re_sdk_types/src/archetypes/state_configuration.rs @@ -0,0 +1,422 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/state_configuration.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: Define the style and mapping for state values in a state timeline view. +/// +/// This archetype provides configuration for how state values are displayed. +/// It maps raw state values to display labels, colors, and visibility. +/// +/// `values`, `labels`, `colors`, and `visible` are parallel arrays: the entry +/// at index `i` of each describes the same state value, and only the +/// per-index pairing is meaningful. The four arrays should have matching +/// length; any secondary array (`labels`, `colors`, `visible`) that is shorter +/// than `values` falls back to defaults for the missing entries. +/// +/// It's generally recommended to log this type as static. +/// +/// The underlying data needs to be logged to the same entity path using [`archetypes::StateChange`][crate::archetypes::StateChange]. +/// +/// ## Example +/// +/// ### State changes with a custom style +/// ```ignore +/// fn main() -> Result<(), Box> { +/// let rec = +/// rerun::RecordingStreamBuilder::new("rerun_example_state_configuration") +/// .spawn()?; +/// +/// // Configure how each raw state value is displayed (label, color, visibility). +/// rec.log_static( +/// "door", +/// &rerun::StateConfiguration::new() +/// .with_values(["open", "closed"]) +/// .with_labels(["Open", "Closed"]) +/// .with_colors([0x4CAF50FFu32, 0xEF5350FFu32]), +/// )?; +/// +/// rec.set_time_sequence("step", 0); +/// rec.log("door", &rerun::StateChange::single("open"))?; +/// +/// rec.set_time_sequence("step", 1); +/// rec.log("door", &rerun::StateChange::single("closed"))?; +/// +/// rec.set_time_sequence("step", 2); +/// rec.log("door", &rerun::StateChange::single("open"))?; +/// +/// Ok(()) +/// } +/// ``` +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] +pub struct StateConfiguration { + /// The raw state values that this configuration applies to. + /// + /// Each entry defines a known state value. The order determines the mapping to + /// `labels`, `colors`, and `visible` (by index). + pub values: Option, + + /// Display labels for each state value. + /// + /// If provided, the label at index `i` is shown instead of the raw value at index `i`. + /// If not provided or shorter than `values`, the raw value is used as the label. + pub labels: Option, + + /// Colors for each state value. + /// + /// If provided, the color at index `i` is used for the state at index `i`. + /// If not provided, colors are assigned automatically from a built-in palette. + pub colors: Option, + + /// Visibility for each state value. + /// + /// If provided, the visibility at index `i` controls whether the state at index `i` is shown. + /// If not provided, all state values are visible. + pub visible: Option, +} + +impl StateConfiguration { + /// Returns the [`ComponentDescriptor`] for [`Self::values`]. + /// + /// The corresponding component is [`crate::components::Text`]. + #[inline] + pub fn descriptor_values() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.StateConfiguration".into()), + component: "StateConfiguration:values".into(), + component_type: Some("rerun.components.Text".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::labels`]. + /// + /// The corresponding component is [`crate::components::Text`]. + #[inline] + pub fn descriptor_labels() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.StateConfiguration".into()), + component: "StateConfiguration:labels".into(), + component_type: Some("rerun.components.Text".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::colors`]. + /// + /// The corresponding component is [`crate::components::Color`]. + #[inline] + pub fn descriptor_colors() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.StateConfiguration".into()), + component: "StateConfiguration:colors".into(), + component_type: Some("rerun.components.Color".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::visible`]. + /// + /// The corresponding component is [`crate::components::Visible`]. + #[inline] + pub fn descriptor_visible() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.StateConfiguration".into()), + component: "StateConfiguration:visible".into(), + component_type: Some("rerun.components.Visible".into()), + }); + (*DESCRIPTOR).clone() + } +} + +static REQUIRED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static RECOMMENDED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 4usize]> = + std::sync::LazyLock::new(|| { + [ + StateConfiguration::descriptor_values(), + StateConfiguration::descriptor_labels(), + StateConfiguration::descriptor_colors(), + StateConfiguration::descriptor_visible(), + ] + }); + +static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 4usize]> = + std::sync::LazyLock::new(|| { + [ + StateConfiguration::descriptor_values(), + StateConfiguration::descriptor_labels(), + StateConfiguration::descriptor_colors(), + StateConfiguration::descriptor_visible(), + ] + }); + +impl StateConfiguration { + /// The total number of components in the archetype: 0 required, 0 recommended, 4 optional + pub const NUM_COMPONENTS: usize = 4usize; +} + +impl ::re_types_core::Archetype for StateConfiguration { + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.StateConfiguration" + ) + } + + #[inline] + fn display_name() -> &'static str { + "State configuration" + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow_components( + arrow_data: impl IntoIterator, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_descr: ::nohash_hasher::IntMap<_, _> = arrow_data.into_iter().collect(); + let values = arrays_by_descr + .get(&Self::descriptor_values()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_values())); + let labels = arrays_by_descr + .get(&Self::descriptor_labels()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_labels())); + let colors = arrays_by_descr + .get(&Self::descriptor_colors()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_colors())); + let visible = arrays_by_descr + .get(&Self::descriptor_visible()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_visible())); + Ok(Self { + values, + labels, + colors, + visible, + }) + } +} + +impl ::re_types_core::AsComponents for StateConfiguration { + #[inline] + fn as_serialized_batches(&self) -> Vec { + use ::re_types_core::Archetype as _; + [ + self.values.clone(), + self.labels.clone(), + self.colors.clone(), + self.visible.clone(), + ] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for StateConfiguration {} + +impl crate::VisualizableArchetype for StateConfiguration { + #[inline] + fn visualizer(&self) -> crate::Visualizer { + crate::Visualizer::new("StateVisualizer").with_overrides(self) + } +} + +impl StateConfiguration { + /// Create a new `StateConfiguration`. + #[inline] + pub fn new() -> Self { + Self { + values: None, + labels: None, + colors: None, + visible: None, + } + } + + /// Update only some specific fields of a `StateConfiguration`. + #[inline] + pub fn update_fields() -> Self { + Self::default() + } + + /// Clear all the fields of a `StateConfiguration`. + #[inline] + pub fn clear_fields() -> Self { + use ::re_types_core::Loggable as _; + Self { + values: Some(SerializedComponentBatch::new( + crate::components::Text::arrow_empty(), + Self::descriptor_values(), + )), + labels: Some(SerializedComponentBatch::new( + crate::components::Text::arrow_empty(), + Self::descriptor_labels(), + )), + colors: Some(SerializedComponentBatch::new( + crate::components::Color::arrow_empty(), + Self::descriptor_colors(), + )), + visible: Some(SerializedComponentBatch::new( + crate::components::Visible::arrow_empty(), + Self::descriptor_visible(), + )), + } + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing [`SerializedComponentBatch`]es data into [`SerializedComponentColumn`]s + /// instead, via [`SerializedComponentBatch::partitioned`]. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + /// + /// [`SerializedComponentColumn`]: [::re_types_core::SerializedComponentColumn] + #[inline] + pub fn columns( + self, + _lengths: I, + ) -> SerializationResult> + where + I: IntoIterator + Clone, + { + let columns = [ + self.values + .map(|values| values.partitioned(_lengths.clone())) + .transpose()?, + self.labels + .map(|labels| labels.partitioned(_lengths.clone())) + .transpose()?, + self.colors + .map(|colors| colors.partitioned(_lengths.clone())) + .transpose()?, + self.visible + .map(|visible| visible.partitioned(_lengths.clone())) + .transpose()?, + ]; + Ok(columns.into_iter().flatten()) + } + + /// Helper to partition the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling [`Self::columns`] with `std::iter::take(1).repeat(n)`, + /// where `n` is automatically guessed. + #[inline] + pub fn columns_of_unit_batches( + self, + ) -> SerializationResult> { + let len_values = self.values.as_ref().map(|b| b.array.len()); + let len_labels = self.labels.as_ref().map(|b| b.array.len()); + let len_colors = self.colors.as_ref().map(|b| b.array.len()); + let len_visible = self.visible.as_ref().map(|b| b.array.len()); + let len = None + .or(len_values) + .or(len_labels) + .or(len_colors) + .or(len_visible) + .unwrap_or(0); + self.columns(std::iter::repeat_n(1, len)) + } + + /// The raw state values that this configuration applies to. + /// + /// Each entry defines a known state value. The order determines the mapping to + /// `labels`, `colors`, and `visible` (by index). + #[inline] + pub fn with_values( + mut self, + values: impl IntoIterator>, + ) -> Self { + self.values = try_serialize_field(Self::descriptor_values(), values); + self + } + + /// Display labels for each state value. + /// + /// If provided, the label at index `i` is shown instead of the raw value at index `i`. + /// If not provided or shorter than `values`, the raw value is used as the label. + #[inline] + pub fn with_labels( + mut self, + labels: impl IntoIterator>, + ) -> Self { + self.labels = try_serialize_field(Self::descriptor_labels(), labels); + self + } + + /// Colors for each state value. + /// + /// If provided, the color at index `i` is used for the state at index `i`. + /// If not provided, colors are assigned automatically from a built-in palette. + #[inline] + pub fn with_colors( + mut self, + colors: impl IntoIterator>, + ) -> Self { + self.colors = try_serialize_field(Self::descriptor_colors(), colors); + self + } + + /// Visibility for each state value. + /// + /// If provided, the visibility at index `i` controls whether the state at index `i` is shown. + /// If not provided, all state values are visible. + #[inline] + pub fn with_visible( + mut self, + visible: impl IntoIterator>, + ) -> Self { + self.visible = try_serialize_field(Self::descriptor_visible(), visible); + self + } +} diff --git a/crates/store/re_sdk_types/src/archetypes/tensor.rs b/crates/store/re_sdk_types/src/archetypes/tensor.rs index 0120dcdc1687..5feff0414ca8 100644 --- a/crates/store/re_sdk_types/src/archetypes/tensor.rs +++ b/crates/store/re_sdk_types/src/archetypes/tensor.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -31,14 +32,15 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// use rand::prelude::*; /// /// fn main() -> Result<(), Box> { -/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_tensor").spawn()?; +/// let rec = +/// rerun::RecordingStreamBuilder::new("rerun_example_tensor").spawn()?; /// /// let mut data = Array::::default((8, 6, 3, 5).f()); /// let mut rng = rand::rngs::SmallRng::seed_from_u64(42); /// data.map_inplace(|x| *x = rng.random()); /// -/// let tensor = -/// rerun::Tensor::try_from(data)?.with_dim_names(["width", "height", "channel", "batch"]); +/// let tensor = rerun::Tensor::try_from(data)? +/// .with_dim_names(["width", "height", "channel", "batch"]); /// rec.log("tensor", &tensor)?; /// /// Ok(()) @@ -53,7 +55,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// /// -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct Tensor { /// The tensor data pub data: Option, @@ -78,11 +80,13 @@ impl Tensor { /// The corresponding component is [`crate::components::TensorData`]. #[inline] pub fn descriptor_data() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Tensor".into()), - component: "Tensor:data".into(), - component_type: Some("rerun.components.TensorData".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Tensor".into()), + component: "Tensor:data".into(), + component_type: Some("rerun.components.TensorData".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::value_range`]. @@ -90,11 +94,13 @@ impl Tensor { /// The corresponding component is [`crate::components::ValueRange`]. #[inline] pub fn descriptor_value_range() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Tensor".into()), - component: "Tensor:value_range".into(), - component_type: Some("rerun.components.ValueRange".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Tensor".into()), + component: "Tensor:value_range".into(), + component_type: Some("rerun.components.ValueRange".into()), + }); + (*DESCRIPTOR).clone() } } @@ -118,7 +124,10 @@ impl Tensor { impl ::re_types_core::Archetype for Tensor { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.Tensor".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.Tensor" + ) } #[inline] @@ -313,10 +322,3 @@ impl Tensor { self } } - -impl ::re_byte_size::SizeBytes for Tensor { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.data.heap_size_bytes() + self.value_range.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/text_document.rs b/crates/store/re_sdk_types/src/archetypes/text_document.rs index 5fdaa2fa2ab2..43c20ac2c49a 100644 --- a/crates/store/re_sdk_types/src/archetypes/text_document.rs +++ b/crates/store/re_sdk_types/src/archetypes/text_document.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,8 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// ### Markdown text document /// ```ignore /// fn main() -> Result<(), Box> { -/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_text_document").spawn()?; +/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_text_document") +/// .spawn()?; /// /// rec.log( /// "text_document", @@ -90,7 +92,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// /// -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct TextDocument { /// Contents of the text document. pub text: Option, @@ -111,11 +113,13 @@ impl TextDocument { /// The corresponding component is [`crate::components::Text`]. #[inline] pub fn descriptor_text() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.TextDocument".into()), - component: "TextDocument:text".into(), - component_type: Some("rerun.components.Text".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.TextDocument".into()), + component: "TextDocument:text".into(), + component_type: Some("rerun.components.Text".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::media_type`]. @@ -123,11 +127,13 @@ impl TextDocument { /// The corresponding component is [`crate::components::MediaType`]. #[inline] pub fn descriptor_media_type() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.TextDocument".into()), - component: "TextDocument:media_type".into(), - component_type: Some("rerun.components.MediaType".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.TextDocument".into()), + component: "TextDocument:media_type".into(), + component_type: Some("rerun.components.MediaType".into()), + }); + (*DESCRIPTOR).clone() } } @@ -156,7 +162,10 @@ impl TextDocument { impl ::re_types_core::Archetype for TextDocument { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.TextDocument".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.TextDocument" + ) } #[inline] @@ -344,10 +353,3 @@ impl TextDocument { self } } - -impl ::re_byte_size::SizeBytes for TextDocument { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.text.heap_size_bytes() + self.media_type.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/text_log.rs b/crates/store/re_sdk_types/src/archetypes/text_log.rs index c3469f2b16f3..f788a8b42527 100644 --- a/crates/store/re_sdk_types/src/archetypes/text_log.rs +++ b/crates/store/re_sdk_types/src/archetypes/text_log.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,10 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// use rerun::external::log; /// /// fn main() -> Result<(), Box> { -/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_text_log_integration").spawn()?; +/// let rec = rerun::RecordingStreamBuilder::new( +/// "rerun_example_text_log_integration", +/// ) +/// .spawn()?; /// /// // Log a text entry directly: /// rec.log( @@ -45,7 +49,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// // You can also use the standard `RUST_LOG` environment variable! /// .with_filter(rerun::default_log_filter()) /// .init()?; -/// log::info!("This INFO log got added through the standard logging interface"); +/// log::info!( +/// "This INFO log got added through the standard logging interface" +/// ); /// /// log::logger().flush(); /// @@ -61,7 +67,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// /// -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct TextLog { /// The body of the message. pub text: Option, @@ -81,11 +87,13 @@ impl TextLog { /// The corresponding component is [`crate::components::Text`]. #[inline] pub fn descriptor_text() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.TextLog".into()), - component: "TextLog:text".into(), - component_type: Some("rerun.components.Text".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.TextLog".into()), + component: "TextLog:text".into(), + component_type: Some("rerun.components.Text".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::level`]. @@ -93,11 +101,13 @@ impl TextLog { /// The corresponding component is [`crate::components::TextLogLevel`]. #[inline] pub fn descriptor_level() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.TextLog".into()), - component: "TextLog:level".into(), - component_type: Some("rerun.components.TextLogLevel".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.TextLog".into()), + component: "TextLog:level".into(), + component_type: Some("rerun.components.TextLogLevel".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::color`]. @@ -105,11 +115,13 @@ impl TextLog { /// The corresponding component is [`crate::components::Color`]. #[inline] pub fn descriptor_color() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.TextLog".into()), - component: "TextLog:color".into(), - component_type: Some("rerun.components.Color".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.TextLog".into()), + component: "TextLog:color".into(), + component_type: Some("rerun.components.Color".into()), + }); + (*DESCRIPTOR).clone() } } @@ -139,7 +151,10 @@ impl TextLog { impl ::re_types_core::Archetype for TextLog { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.TextLog".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.TextLog" + ) } #[inline] @@ -353,10 +368,3 @@ impl TextLog { self } } - -impl ::re_byte_size::SizeBytes for TextLog { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.text.heap_size_bytes() + self.level.heap_size_bytes() + self.color.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/transform3d.rs b/crates/store/re_sdk_types/src/archetypes/transform3d.rs index 2b7371947516..397d8b53d41a 100644 --- a/crates/store/re_sdk_types/src/archetypes/transform3d.rs +++ b/crates/store/re_sdk_types/src/archetypes/transform3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -42,9 +43,11 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// use std::f32::consts::TAU; /// /// fn main() -> Result<(), Box> { -/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_transform3d").spawn()?; +/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_transform3d") +/// .spawn()?; /// -/// let arrow = rerun::Arrows3D::from_vectors([(0.0, 1.0, 0.0)]).with_origins([(0.0, 0.0, 0.0)]); +/// let arrow = rerun::Arrows3D::from_vectors([(0.0, 1.0, 0.0)]) +/// .with_origins([(0.0, 0.0, 0.0)]); /// /// rec.log("base", &arrow)?; /// @@ -58,7 +61,10 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// rec.log( /// "base/rotated_scaled", /// &rerun::Transform3D::from_rotation_scale( -/// rerun::RotationAxisAngle::new([0.0, 0.0, 1.0], rerun::Angle::from_radians(TAU / 8.0)), +/// rerun::RotationAxisAngle::new( +/// [0.0, 0.0, 1.0], +/// rerun::Angle::from_radians(TAU / 8.0), +/// ), /// rerun::Scale3D::from(2.0), /// ), /// )?; @@ -81,15 +87,18 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// ### Update a transform over time /// ```ignore /// fn main() -> Result<(), Box> { -/// let rec = -/// rerun::RecordingStreamBuilder::new("rerun_example_transform3d_row_updates").spawn()?; +/// let rec = rerun::RecordingStreamBuilder::new( +/// "rerun_example_transform3d_row_updates", +/// ) +/// .spawn()?; /// /// rec.set_time_sequence("tick", 0); /// rec.log( /// "box", /// &[ /// &rerun::Boxes3D::from_half_sizes([(4.0, 2.0, 1.0)]) -/// .with_fill_mode(rerun::FillMode::Solid) as &dyn rerun::AsComponents, +/// .with_fill_mode(rerun::FillMode::Solid) +/// as &dyn rerun::AsComponents, /// &rerun::TransformAxes3D::new(10.0), /// ], /// )?; @@ -102,7 +111,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// .with_translation([0.0, 0.0, t as f32 / 10.0]) /// .with_rotation(rerun::RotationAxisAngle::new( /// [0.0, 1.0, 0.0], -/// rerun::Angle::from_radians(truncated_radians((t * 4) as f32)), +/// rerun::Angle::from_radians(truncated_radians( +/// (t * 4) as f32, +/// )), /// )), /// )?; /// } @@ -127,23 +138,32 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// ### Update a transform over time, in a single operation /// ```ignore /// fn main() -> Result<(), Box> { -/// let rec = -/// rerun::RecordingStreamBuilder::new("rerun_example_transform3d_column_updates").spawn()?; +/// let rec = rerun::RecordingStreamBuilder::new( +/// "rerun_example_transform3d_column_updates", +/// ) +/// .spawn()?; /// /// rec.set_time_sequence("tick", 0); /// rec.log( /// "box", /// &[ /// &rerun::Boxes3D::from_half_sizes([(4.0, 2.0, 1.0)]) -/// .with_fill_mode(rerun::FillMode::Solid) as &dyn rerun::AsComponents, +/// .with_fill_mode(rerun::FillMode::Solid) +/// as &dyn rerun::AsComponents, /// &rerun::TransformAxes3D::new(10.0), /// ], /// )?; /// /// let translations = (0..100).map(|t| [0.0, 0.0, t as f32 / 10.0]); -/// let rotations = (0..100) -/// .map(|t| truncated_radians((t * 4) as f32)) -/// .map(|rad| rerun::RotationAxisAngle::new([0.0, 1.0, 0.0], rerun::Angle::from_radians(rad))); +/// let rotations = +/// (0..100) +/// .map(|t| truncated_radians((t * 4) as f32)) +/// .map(|rad| { +/// rerun::RotationAxisAngle::new( +/// [0.0, 1.0, 0.0], +/// rerun::Angle::from_radians(rad), +/// ) +/// }); /// /// let ticks = rerun::TimeColumn::new_sequence("tick", 1..101); /// rec.send_columns( @@ -177,16 +197,17 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// use rerun::AsComponents; /// /// fn main() -> Result<(), Box> { -/// let rec = -/// rerun::RecordingStreamBuilder::new("rerun_example_transform3d_partial_updates").spawn()?; +/// let rec = rerun::RecordingStreamBuilder::new( +/// "rerun_example_transform3d_partial_updates", +/// ) +/// .spawn()?; /// /// // Set up a 3D box. /// rec.log( /// "box", -/// &[ -/// &rerun::Boxes3D::from_half_sizes([(4.0, 2.0, 1.0)]) -/// .with_fill_mode(rerun::FillMode::Solid) as &dyn AsComponents, -/// ], +/// &[&rerun::Boxes3D::from_half_sizes([(4.0, 2.0, 1.0)]) +/// .with_fill_mode(rerun::FillMode::Solid) +/// as &dyn AsComponents], /// )?; /// /// // Update only the rotation of the box. @@ -194,10 +215,12 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// let rad = truncated_radians((deg * 4) as f32); /// rec.log( /// "box", -/// &rerun::Transform3D::new().with_rotation(rerun::RotationAxisAngle::new( -/// [0.0, 1.0, 0.0], -/// rerun::Angle::from_radians(rad), -/// )), +/// &rerun::Transform3D::new().with_rotation( +/// rerun::RotationAxisAngle::new( +/// [0.0, 1.0, 0.0], +/// rerun::Angle::from_radians(rad), +/// ), +/// ), /// )?; /// } /// @@ -205,7 +228,11 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// for t in 0..=50 { /// rec.log( /// "box", -/// &rerun::Transform3D::new().with_translation([0.0, 0.0, t as f32 / 10.0]), +/// &rerun::Transform3D::new().with_translation([ +/// 0.0, +/// 0.0, +/// t as f32 / 10.0, +/// ]), /// )?; /// } /// @@ -214,10 +241,12 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// let rad = truncated_radians(((deg + 45) * 4) as f32); /// rec.log( /// "box", -/// &rerun::Transform3D::new().with_rotation(rerun::RotationAxisAngle::new( -/// [0.0, 1.0, 0.0], -/// rerun::Angle::from_radians(rad), -/// )), +/// &rerun::Transform3D::new().with_rotation( +/// rerun::RotationAxisAngle::new( +/// [0.0, 1.0, 0.0], +/// rerun::Angle::from_radians(rad), +/// ), +/// ), /// )?; /// } /// @@ -240,7 +269,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// /// -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct Transform3D { /// Translation vector. /// @@ -303,11 +332,13 @@ impl Transform3D { /// The corresponding component is [`crate::components::Translation3D`]. #[inline] pub fn descriptor_translation() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Transform3D".into()), - component: "Transform3D:translation".into(), - component_type: Some("rerun.components.Translation3D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Transform3D".into()), + component: "Transform3D:translation".into(), + component_type: Some("rerun.components.Translation3D".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::rotation_axis_angle`]. @@ -315,11 +346,13 @@ impl Transform3D { /// The corresponding component is [`crate::components::RotationAxisAngle`]. #[inline] pub fn descriptor_rotation_axis_angle() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Transform3D".into()), - component: "Transform3D:rotation_axis_angle".into(), - component_type: Some("rerun.components.RotationAxisAngle".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Transform3D".into()), + component: "Transform3D:rotation_axis_angle".into(), + component_type: Some("rerun.components.RotationAxisAngle".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::quaternion`]. @@ -327,11 +360,13 @@ impl Transform3D { /// The corresponding component is [`crate::components::RotationQuat`]. #[inline] pub fn descriptor_quaternion() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Transform3D".into()), - component: "Transform3D:quaternion".into(), - component_type: Some("rerun.components.RotationQuat".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Transform3D".into()), + component: "Transform3D:quaternion".into(), + component_type: Some("rerun.components.RotationQuat".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::scale`]. @@ -339,11 +374,13 @@ impl Transform3D { /// The corresponding component is [`crate::components::Scale3D`]. #[inline] pub fn descriptor_scale() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Transform3D".into()), - component: "Transform3D:scale".into(), - component_type: Some("rerun.components.Scale3D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Transform3D".into()), + component: "Transform3D:scale".into(), + component_type: Some("rerun.components.Scale3D".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::mat3x3`]. @@ -351,11 +388,13 @@ impl Transform3D { /// The corresponding component is [`crate::components::TransformMat3x3`]. #[inline] pub fn descriptor_mat3x3() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Transform3D".into()), - component: "Transform3D:mat3x3".into(), - component_type: Some("rerun.components.TransformMat3x3".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Transform3D".into()), + component: "Transform3D:mat3x3".into(), + component_type: Some("rerun.components.TransformMat3x3".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::relation`]. @@ -363,11 +402,13 @@ impl Transform3D { /// The corresponding component is [`crate::components::TransformRelation`]. #[inline] pub fn descriptor_relation() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Transform3D".into()), - component: "Transform3D:relation".into(), - component_type: Some("rerun.components.TransformRelation".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Transform3D".into()), + component: "Transform3D:relation".into(), + component_type: Some("rerun.components.TransformRelation".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::child_frame`]. @@ -375,11 +416,13 @@ impl Transform3D { /// The corresponding component is [`crate::components::TransformFrameId`]. #[inline] pub fn descriptor_child_frame() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Transform3D".into()), - component: "Transform3D:child_frame".into(), - component_type: Some("rerun.components.TransformFrameId".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Transform3D".into()), + component: "Transform3D:child_frame".into(), + component_type: Some("rerun.components.TransformFrameId".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::parent_frame`]. @@ -387,11 +430,13 @@ impl Transform3D { /// The corresponding component is [`crate::components::TransformFrameId`]. #[inline] pub fn descriptor_parent_frame() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Transform3D".into()), - component: "Transform3D:parent_frame".into(), - component_type: Some("rerun.components.TransformFrameId".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Transform3D".into()), + component: "Transform3D:parent_frame".into(), + component_type: Some("rerun.components.TransformFrameId".into()), + }); + (*DESCRIPTOR).clone() } } @@ -437,7 +482,10 @@ impl Transform3D { impl ::re_types_core::Archetype for Transform3D { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.Transform3D".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.Transform3D" + ) } #[inline] @@ -891,17 +939,3 @@ impl Transform3D { self } } - -impl ::re_byte_size::SizeBytes for Transform3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.translation.heap_size_bytes() - + self.rotation_axis_angle.heap_size_bytes() - + self.quaternion.heap_size_bytes() - + self.scale.heap_size_bytes() - + self.mat3x3.heap_size_bytes() - + self.relation.heap_size_bytes() - + self.child_frame.heap_size_bytes() - + self.parent_frame.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/transform_axes3d.rs b/crates/store/re_sdk_types/src/archetypes/transform_axes3d.rs index 2260c4d55a13..e4a8ac2144f1 100644 --- a/crates/store/re_sdk_types/src/archetypes/transform_axes3d.rs +++ b/crates/store/re_sdk_types/src/archetypes/transform_axes3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// use rerun::AsComponents; /// /// fn main() -> Result<(), Box> { -/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_transform3d_axes").spawn()?; +/// let rec = +/// rerun::RecordingStreamBuilder::new("rerun_example_transform3d_axes") +/// .spawn()?; /// /// rec.set_time_sequence("step", 0); /// @@ -47,17 +50,20 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// rec.log( /// "base/rotated", /// &[ -/// &rerun::Transform3D::new().with_rotation(rerun::RotationAxisAngle::new( -/// [1.0, 1.0, 1.0], -/// rerun::Angle::from_degrees(deg as f32), -/// )) as &dyn AsComponents, +/// &rerun::Transform3D::new().with_rotation( +/// rerun::RotationAxisAngle::new( +/// [1.0, 1.0, 1.0], +/// rerun::Angle::from_degrees(deg as f32), +/// ), +/// ) as &dyn AsComponents, /// &rerun::TransformAxes3D::new(0.5), /// ], /// )?; /// rec.log( /// "base/rotated/translated", /// &[ -/// &rerun::Transform3D::new().with_translation([2.0, 0.0, 0.0]) as &dyn AsComponents, +/// &rerun::Transform3D::new().with_translation([2.0, 0.0, 0.0]) +/// as &dyn AsComponents, /// &rerun::TransformAxes3D::new(0.5), /// ], /// )?; @@ -75,7 +81,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// /// -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct TransformAxes3D { /// Visual length of the 3 axes. /// @@ -93,11 +99,13 @@ impl TransformAxes3D { /// The corresponding component is [`crate::components::AxisLength`]. #[inline] pub fn descriptor_axis_length() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.TransformAxes3D".into()), - component: "TransformAxes3D:axis_length".into(), - component_type: Some("rerun.components.AxisLength".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.TransformAxes3D".into()), + component: "TransformAxes3D:axis_length".into(), + component_type: Some("rerun.components.AxisLength".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::show_frame`]. @@ -105,11 +113,13 @@ impl TransformAxes3D { /// The corresponding component is [`crate::components::ShowLabels`]. #[inline] pub fn descriptor_show_frame() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.TransformAxes3D".into()), - component: "TransformAxes3D:show_frame".into(), - component_type: Some("rerun.components.ShowLabels".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.TransformAxes3D".into()), + component: "TransformAxes3D:show_frame".into(), + component_type: Some("rerun.components.ShowLabels".into()), + }); + (*DESCRIPTOR).clone() } } @@ -138,7 +148,10 @@ impl TransformAxes3D { impl ::re_types_core::Archetype for TransformAxes3D { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.TransformAxes3D".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.TransformAxes3D" + ) } #[inline] @@ -331,10 +344,3 @@ impl TransformAxes3D { self } } - -impl ::re_byte_size::SizeBytes for TransformAxes3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.axis_length.heap_size_bytes() + self.show_frame.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/video_frame_reference.rs b/crates/store/re_sdk_types/src/archetypes/video_frame_reference.rs index 712d672083c1..14f21ca59a01 100644 --- a/crates/store/re_sdk_types/src/archetypes/video_frame_reference.rs +++ b/crates/store/re_sdk_types/src/archetypes/video_frame_reference.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -43,8 +44,10 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// anyhow::bail!("Usage: {} ", args[0]); /// }; /// -/// let rec = -/// rerun::RecordingStreamBuilder::new("rerun_example_asset_video_auto_frames").spawn()?; +/// let rec = rerun::RecordingStreamBuilder::new( +/// "rerun_example_asset_video_auto_frames", +/// ) +/// .spawn()?; /// /// // Log video asset which is referred to by frame references. /// let video_asset = rerun::AssetVideo::from_file_path(path)?; @@ -95,8 +98,10 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// anyhow::bail!("Usage: {} ", args[0]); /// }; /// -/// let rec = -/// rerun::RecordingStreamBuilder::new("rerun_example_asset_video_manual_frames").spawn()?; +/// let rec = rerun::RecordingStreamBuilder::new( +/// "rerun_example_asset_video_manual_frames", +/// ) +/// .spawn()?; /// /// // Log video asset which is referred to by frame references. /// rec.log_static("video_asset", &rerun::AssetVideo::from_file_path(path)?)?; @@ -104,13 +109,17 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// // Create two entities, showing the same video frozen at different times. /// rec.log( /// "frame_1s", -/// &rerun::VideoFrameReference::new(rerun::components::VideoTimestamp::from_secs(1.0)) -/// .with_video_reference("video_asset"), +/// &rerun::VideoFrameReference::new( +/// rerun::components::VideoTimestamp::from_secs(1.0), +/// ) +/// .with_video_reference("video_asset"), /// )?; /// rec.log( /// "frame_2s", -/// &rerun::VideoFrameReference::new(rerun::components::VideoTimestamp::from_secs(2.0)) -/// .with_video_reference("video_asset"), +/// &rerun::VideoFrameReference::new( +/// rerun::components::VideoTimestamp::from_secs(2.0), +/// ) +/// .with_video_reference("video_asset"), /// )?; /// /// // TODO(#5520): log blueprint once supported @@ -126,7 +135,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// /// -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct VideoFrameReference { /// References the closest video frame to this timestamp. /// @@ -167,11 +176,13 @@ impl VideoFrameReference { /// The corresponding component is [`crate::components::VideoTimestamp`]. #[inline] pub fn descriptor_timestamp() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.VideoFrameReference".into()), - component: "VideoFrameReference:timestamp".into(), - component_type: Some("rerun.components.VideoTimestamp".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoFrameReference".into()), + component: "VideoFrameReference:timestamp".into(), + component_type: Some("rerun.components.VideoTimestamp".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::video_reference`]. @@ -179,11 +190,13 @@ impl VideoFrameReference { /// The corresponding component is [`crate::components::EntityPath`]. #[inline] pub fn descriptor_video_reference() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.VideoFrameReference".into()), - component: "VideoFrameReference:video_reference".into(), - component_type: Some("rerun.components.EntityPath".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoFrameReference".into()), + component: "VideoFrameReference:video_reference".into(), + component_type: Some("rerun.components.EntityPath".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::opacity`]. @@ -191,11 +204,13 @@ impl VideoFrameReference { /// The corresponding component is [`crate::components::Opacity`]. #[inline] pub fn descriptor_opacity() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.VideoFrameReference".into()), - component: "VideoFrameReference:opacity".into(), - component_type: Some("rerun.components.Opacity".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoFrameReference".into()), + component: "VideoFrameReference:opacity".into(), + component_type: Some("rerun.components.Opacity".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::draw_order`]. @@ -203,11 +218,13 @@ impl VideoFrameReference { /// The corresponding component is [`crate::components::DrawOrder`]. #[inline] pub fn descriptor_draw_order() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.VideoFrameReference".into()), - component: "VideoFrameReference:draw_order".into(), - component_type: Some("rerun.components.DrawOrder".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoFrameReference".into()), + component: "VideoFrameReference:draw_order".into(), + component_type: Some("rerun.components.DrawOrder".into()), + }); + (*DESCRIPTOR).clone() } } @@ -244,7 +261,10 @@ impl VideoFrameReference { impl ::re_types_core::Archetype for VideoFrameReference { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.VideoFrameReference".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.VideoFrameReference" + ) } #[inline] @@ -537,13 +557,3 @@ impl VideoFrameReference { self } } - -impl ::re_byte_size::SizeBytes for VideoFrameReference { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.timestamp.heap_size_bytes() - + self.video_reference.heap_size_bytes() - + self.opacity.heap_size_bytes() - + self.draw_order.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/video_stream.rs b/crates/store/re_sdk_types/src/archetypes/video_stream.rs index c0faa4692bb7..3b7abacfeb58 100644 --- a/crates/store/re_sdk_types/src/archetypes/video_stream.rs +++ b/crates/store/re_sdk_types/src/archetypes/video_stream.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -32,7 +33,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// TODO(#10422): [`archetypes::VideoFrameReference`][crate::archetypes::VideoFrameReference] does not yet work with [`archetypes::VideoStream`][crate::archetypes::VideoStream]. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct VideoStream { /// The codec used to encode the video chunks. /// @@ -64,6 +65,17 @@ pub struct VideoStream { /// See [`components::VideoCodec`][crate::components::VideoCodec] for codec specific requirements. pub sample: Option, + /// Whether the corresponding [`components::VideoSample`][crate::components::VideoSample] contains a keyframe. + /// + /// A keyframe (also known as a sync sample or IDR) is a frame from which a decoder can + /// start decoding the stream with no prior decoder state. See [`components::IsKeyframe`][crate::components::IsKeyframe] + /// and [`components::VideoCodec`][crate::components::VideoCodec] for the codec-specific definition. + /// + /// This field is optional. It does not change how the stream itself is decoded: it is + /// metadata that travels with the sample and can be inspected when querying the data + /// back, for example to locate sync points or build a frame index. + pub is_keyframe: Option, + /// Opacity of the video stream, useful for layering several media. /// /// Defaults to 1.0 (fully opaque). @@ -82,11 +94,13 @@ impl VideoStream { /// The corresponding component is [`crate::components::VideoCodec`]. #[inline] pub fn descriptor_codec() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.VideoStream".into()), - component: "VideoStream:codec".into(), - component_type: Some("rerun.components.VideoCodec".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoStream".into()), + component: "VideoStream:codec".into(), + component_type: Some("rerun.components.VideoCodec".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::sample`]. @@ -94,11 +108,27 @@ impl VideoStream { /// The corresponding component is [`crate::components::VideoSample`]. #[inline] pub fn descriptor_sample() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.VideoStream".into()), - component: "VideoStream:sample".into(), - component_type: Some("rerun.components.VideoSample".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoStream".into()), + component: "VideoStream:sample".into(), + component_type: Some("rerun.components.VideoSample".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::is_keyframe`]. + /// + /// The corresponding component is [`crate::components::IsKeyframe`]. + #[inline] + pub fn descriptor_is_keyframe() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoStream".into()), + component: "VideoStream:is_keyframe".into(), + component_type: Some("rerun.components.IsKeyframe".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::opacity`]. @@ -106,11 +136,13 @@ impl VideoStream { /// The corresponding component is [`crate::components::Opacity`]. #[inline] pub fn descriptor_opacity() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.VideoStream".into()), - component: "VideoStream:opacity".into(), - component_type: Some("rerun.components.Opacity".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoStream".into()), + component: "VideoStream:opacity".into(), + component_type: Some("rerun.components.Opacity".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::draw_order`]. @@ -118,11 +150,13 @@ impl VideoStream { /// The corresponding component is [`crate::components::DrawOrder`]. #[inline] pub fn descriptor_draw_order() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.VideoStream".into()), - component: "VideoStream:draw_order".into(), - component_type: Some("rerun.components.DrawOrder".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VideoStream".into()), + component: "VideoStream:draw_order".into(), + component_type: Some("rerun.components.DrawOrder".into()), + }); + (*DESCRIPTOR).clone() } } @@ -132,33 +166,38 @@ static REQUIRED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 1usize]> = static RECOMMENDED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 1usize]> = std::sync::LazyLock::new(|| [VideoStream::descriptor_sample()]); -static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 2usize]> = +static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 3usize]> = std::sync::LazyLock::new(|| { [ + VideoStream::descriptor_is_keyframe(), VideoStream::descriptor_opacity(), VideoStream::descriptor_draw_order(), ] }); -static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 4usize]> = +static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 5usize]> = std::sync::LazyLock::new(|| { [ VideoStream::descriptor_codec(), VideoStream::descriptor_sample(), + VideoStream::descriptor_is_keyframe(), VideoStream::descriptor_opacity(), VideoStream::descriptor_draw_order(), ] }); impl VideoStream { - /// The total number of components in the archetype: 1 required, 1 recommended, 2 optional - pub const NUM_COMPONENTS: usize = 4usize; + /// The total number of components in the archetype: 1 required, 1 recommended, 3 optional + pub const NUM_COMPONENTS: usize = 5usize; } impl ::re_types_core::Archetype for VideoStream { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.VideoStream".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.VideoStream" + ) } #[inline] @@ -199,6 +238,11 @@ impl ::re_types_core::Archetype for VideoStream { let sample = arrays_by_descr .get(&Self::descriptor_sample()) .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_sample())); + let is_keyframe = arrays_by_descr + .get(&Self::descriptor_is_keyframe()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_is_keyframe()) + }); let opacity = arrays_by_descr .get(&Self::descriptor_opacity()) .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_opacity())); @@ -210,6 +254,7 @@ impl ::re_types_core::Archetype for VideoStream { Ok(Self { codec, sample, + is_keyframe, opacity, draw_order, }) @@ -223,6 +268,7 @@ impl ::re_types_core::AsComponents for VideoStream { [ self.codec.clone(), self.sample.clone(), + self.is_keyframe.clone(), self.opacity.clone(), self.draw_order.clone(), ] @@ -248,6 +294,7 @@ impl VideoStream { Self { codec: try_serialize_field(Self::descriptor_codec(), [codec]), sample: None, + is_keyframe: None, opacity: None, draw_order: None, } @@ -272,6 +319,10 @@ impl VideoStream { crate::components::VideoSample::arrow_empty(), Self::descriptor_sample(), )), + is_keyframe: Some(SerializedComponentBatch::new( + crate::components::IsKeyframe::arrow_empty(), + Self::descriptor_is_keyframe(), + )), opacity: Some(SerializedComponentBatch::new( crate::components::Opacity::arrow_empty(), Self::descriptor_opacity(), @@ -308,6 +359,9 @@ impl VideoStream { self.sample .map(|sample| sample.partitioned(_lengths.clone())) .transpose()?, + self.is_keyframe + .map(|is_keyframe| is_keyframe.partitioned(_lengths.clone())) + .transpose()?, self.opacity .map(|opacity| opacity.partitioned(_lengths.clone())) .transpose()?, @@ -328,11 +382,13 @@ impl VideoStream { ) -> SerializationResult> { let len_codec = self.codec.as_ref().map(|b| b.array.len()); let len_sample = self.sample.as_ref().map(|b| b.array.len()); + let len_is_keyframe = self.is_keyframe.as_ref().map(|b| b.array.len()); let len_opacity = self.opacity.as_ref().map(|b| b.array.len()); let len_draw_order = self.draw_order.as_ref().map(|b| b.array.len()); let len = None .or(len_codec) .or(len_sample) + .or(len_is_keyframe) .or(len_opacity) .or(len_draw_order) .unwrap_or(0); @@ -403,6 +459,37 @@ impl VideoStream { self } + /// Whether the corresponding [`components::VideoSample`][crate::components::VideoSample] contains a keyframe. + /// + /// A keyframe (also known as a sync sample or IDR) is a frame from which a decoder can + /// start decoding the stream with no prior decoder state. See [`components::IsKeyframe`][crate::components::IsKeyframe] + /// and [`components::VideoCodec`][crate::components::VideoCodec] for the codec-specific definition. + /// + /// This field is optional. It does not change how the stream itself is decoded: it is + /// metadata that travels with the sample and can be inspected when querying the data + /// back, for example to locate sync points or build a frame index. + #[inline] + pub fn with_is_keyframe( + mut self, + is_keyframe: impl Into, + ) -> Self { + self.is_keyframe = try_serialize_field(Self::descriptor_is_keyframe(), [is_keyframe]); + self + } + + /// This method makes it possible to pack multiple [`crate::components::IsKeyframe`] in a single component batch. + /// + /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_is_keyframe`] should + /// be used when logging a single row's worth of data. + #[inline] + pub fn with_many_is_keyframe( + mut self, + is_keyframe: impl IntoIterator>, + ) -> Self { + self.is_keyframe = try_serialize_field(Self::descriptor_is_keyframe(), is_keyframe); + self + } + /// Opacity of the video stream, useful for layering several media. /// /// Defaults to 1.0 (fully opaque). @@ -448,13 +535,3 @@ impl VideoStream { self } } - -impl ::re_byte_size::SizeBytes for VideoStream { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.codec.heap_size_bytes() - + self.sample.heap_size_bytes() - + self.opacity.heap_size_bytes() - + self.draw_order.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/view_coordinates.rs b/crates/store/re_sdk_types/src/archetypes/view_coordinates.rs index acf1af18317b..2486a15fd57d 100644 --- a/crates/store/re_sdk_types/src/archetypes/view_coordinates.rs +++ b/crates/store/re_sdk_types/src/archetypes/view_coordinates.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -41,7 +42,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// ### View coordinates for adjusting the eye camera /// ```ignore /// fn main() -> Result<(), Box> { -/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_view_coordinates").spawn()?; +/// let rec = +/// rerun::RecordingStreamBuilder::new("rerun_example_view_coordinates") +/// .spawn()?; /// /// rec.log_static("world", &rerun::ViewCoordinates::RIGHT_HAND_Z_UP())?; // Set an up-axis /// rec.log( @@ -64,7 +67,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// /// -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ViewCoordinates { /// The directions of the [x, y, z] axes. @@ -77,11 +80,13 @@ impl ViewCoordinates { /// The corresponding component is [`crate::components::ViewCoordinates`]. #[inline] pub fn descriptor_xyz() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.ViewCoordinates".into()), - component: "ViewCoordinates:xyz".into(), - component_type: Some("rerun.components.ViewCoordinates".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.ViewCoordinates".into()), + component: "ViewCoordinates:xyz".into(), + component_type: Some("rerun.components.ViewCoordinates".into()), + }); + (*DESCRIPTOR).clone() } } @@ -105,7 +110,10 @@ impl ViewCoordinates { impl ::re_types_core::Archetype for ViewCoordinates { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.archetypes.ViewCoordinates".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.ViewCoordinates" + ) } #[inline] @@ -242,10 +250,3 @@ impl ViewCoordinates { self } } - -impl ::re_byte_size::SizeBytes for ViewCoordinates { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.xyz.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/archetypes/voxel_grid_map.rs b/crates/store/re_sdk_types/src/archetypes/voxel_grid_map.rs new file mode 100644 index 000000000000..f3718f236256 --- /dev/null +++ b/crates/store/re_sdk_types/src/archetypes/voxel_grid_map.rs @@ -0,0 +1,819 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/voxel_grid_map.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: A sparse 3D voxel grid map with grid indices and voxel dimensions. +/// +/// This archetype is intended for 3D occupancy maps and other volumetric data +/// represented as a sparse grid of voxels with scene-unit dimensions along the local X/Y/Z axes. +/// +/// The minimum corner of the voxel with `[0, 0, 0]` index is located at the origin of the entity's coordinate frame +/// and can have an additional offset from there through the optional translation and rotation fields. +/// +/// A voxel center is at `(index + 0.5) * voxel_size` in local grid coordinates (i.e. relative to the minimum corner). +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +/// +/// ## Example +/// +/// ### Simple sparse voxel grid map +/// ```ignore +/// fn main() -> Result<(), Box> { +/// let rec = rerun::RecordingStreamBuilder::new( +/// "rerun_example_voxel_grid_map_simple", +/// ) +/// .spawn()?; +/// +/// let voxel_indices = [ +/// (-1, 0, 0), +/// (1, 0, 0), +/// (1, 1, 0), +/// (3, 0, 0), +/// (3, 0, 1), +/// (4, 0, 1), +/// ]; +/// let values = [0.0_f32, 0.2, 0.4, 0.6, 0.8, 1.0]; +/// +/// rec.log( +/// "world/voxels", +/// &rerun::VoxelGridMap::new(voxel_indices, [0.25, 0.25, 0.25]) +/// .with_values(values) +/// .with_value_range([0.0, 1.0]) +/// .with_colormap(rerun::components::Colormap::Turbo) +/// .with_translation([-0.5, -0.5, 0.0]), +/// )?; +/// +/// Ok(()) +/// } +/// ``` +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] +pub struct VoxelGridMap { + /// Indices of the voxels within the grid volume. + pub voxel_indices: Option, + + /// The scene-unit dimensions of a single voxel cell. + /// + /// This defines the voxel size along the local grid X/Y/Z axes. + /// Each dimension must be finite and positive. + pub voxel_size: Option, + + /// Optional scalar occupancy or value data for each voxel. + /// + /// If explicit colors are not provided, values are mapped through `colormap` and `value_range`. + pub values: Option, + + /// Optional colors for each voxel. + /// + /// If set, these colors take precedence over color-mapped scalar values. + pub colors: Option, + + /// Translation of the minimum corner of voxel `[0, 0, 0]`. + /// + /// Together with [`components::RotationAxisAngle`][crate::components::RotationAxisAngle] or [`components::RotationQuat`][crate::components::RotationQuat], this defines the pose of the + /// grid relative to the map's parent coordinate frame. + /// + /// If not set, the minimum corner is placed at the origin of the map's parent coordinate frame. + pub translation: Option, + + /// Rotation of the grid via axis + angle. + /// + /// Together with [`components::Translation3D`][crate::components::Translation3D], this defines the pose of the grid relative to the + /// map's parent coordinate frame. + /// + /// Note: either this or [`components::RotationQuat`][crate::components::RotationQuat] can be set to specify the grid's rotation, but not both. + /// If both this and [`components::RotationQuat`][crate::components::RotationQuat] are set, this is ignored in favor of the quaternion. + pub rotation_axis_angle: Option, + + /// Rotation of the grid via quaternion. + /// + /// Together with [`components::Translation3D`][crate::components::Translation3D], this defines the pose of the grid relative to the + /// map's parent coordinate frame. + pub quaternion: Option, + + /// Opacity of the voxels after color or colormap application. + /// + /// Defaults to 1.0 (fully opaque). + pub opacity: Option, + + /// Scalar value range for color-mapping. + /// + /// Defaults to `[0.0, 1.0]`. + pub value_range: Option, + + /// Colormap to use when `values` are present and explicit `colors` are not provided. + /// + /// Defaults to Turbo. + pub colormap: Option, +} + +impl VoxelGridMap { + /// Returns the [`ComponentDescriptor`] for [`Self::voxel_indices`]. + /// + /// The corresponding component is [`crate::components::VoxelIndex`]. + #[inline] + pub fn descriptor_voxel_indices() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:voxel_indices".into(), + component_type: Some("rerun.components.VoxelIndex".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::voxel_size`]. + /// + /// The corresponding component is [`crate::components::VoxelSize`]. + #[inline] + pub fn descriptor_voxel_size() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:voxel_size".into(), + component_type: Some("rerun.components.VoxelSize".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::values`]. + /// + /// The corresponding component is [`crate::components::VoxelValue`]. + #[inline] + pub fn descriptor_values() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:values".into(), + component_type: Some("rerun.components.VoxelValue".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::colors`]. + /// + /// The corresponding component is [`crate::components::Color`]. + #[inline] + pub fn descriptor_colors() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:colors".into(), + component_type: Some("rerun.components.Color".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::translation`]. + /// + /// The corresponding component is [`crate::components::Translation3D`]. + #[inline] + pub fn descriptor_translation() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:translation".into(), + component_type: Some("rerun.components.Translation3D".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::rotation_axis_angle`]. + /// + /// The corresponding component is [`crate::components::RotationAxisAngle`]. + #[inline] + pub fn descriptor_rotation_axis_angle() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:rotation_axis_angle".into(), + component_type: Some("rerun.components.RotationAxisAngle".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::quaternion`]. + /// + /// The corresponding component is [`crate::components::RotationQuat`]. + #[inline] + pub fn descriptor_quaternion() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:quaternion".into(), + component_type: Some("rerun.components.RotationQuat".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::opacity`]. + /// + /// The corresponding component is [`crate::components::Opacity`]. + #[inline] + pub fn descriptor_opacity() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:opacity".into(), + component_type: Some("rerun.components.Opacity".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::value_range`]. + /// + /// The corresponding component is [`crate::components::ValueRange`]. + #[inline] + pub fn descriptor_value_range() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:value_range".into(), + component_type: Some("rerun.components.ValueRange".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::colormap`]. + /// + /// The corresponding component is [`crate::components::Colormap`]. + #[inline] + pub fn descriptor_colormap() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.VoxelGridMap".into()), + component: "VoxelGridMap:colormap".into(), + component_type: Some("rerun.components.Colormap".into()), + }); + (*DESCRIPTOR).clone() + } +} + +static REQUIRED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 2usize]> = + std::sync::LazyLock::new(|| { + [ + VoxelGridMap::descriptor_voxel_indices(), + VoxelGridMap::descriptor_voxel_size(), + ] + }); + +static RECOMMENDED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 8usize]> = + std::sync::LazyLock::new(|| { + [ + VoxelGridMap::descriptor_values(), + VoxelGridMap::descriptor_colors(), + VoxelGridMap::descriptor_translation(), + VoxelGridMap::descriptor_rotation_axis_angle(), + VoxelGridMap::descriptor_quaternion(), + VoxelGridMap::descriptor_opacity(), + VoxelGridMap::descriptor_value_range(), + VoxelGridMap::descriptor_colormap(), + ] + }); + +static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 10usize]> = + std::sync::LazyLock::new(|| { + [ + VoxelGridMap::descriptor_voxel_indices(), + VoxelGridMap::descriptor_voxel_size(), + VoxelGridMap::descriptor_values(), + VoxelGridMap::descriptor_colors(), + VoxelGridMap::descriptor_translation(), + VoxelGridMap::descriptor_rotation_axis_angle(), + VoxelGridMap::descriptor_quaternion(), + VoxelGridMap::descriptor_opacity(), + VoxelGridMap::descriptor_value_range(), + VoxelGridMap::descriptor_colormap(), + ] + }); + +impl VoxelGridMap { + /// The total number of components in the archetype: 2 required, 0 recommended, 8 optional + pub const NUM_COMPONENTS: usize = 10usize; +} + +impl ::re_types_core::Archetype for VoxelGridMap { + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.archetypes.VoxelGridMap" + ) + } + + #[inline] + fn display_name() -> &'static str { + "Voxel grid map" + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow_components( + arrow_data: impl IntoIterator, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_descr: ::nohash_hasher::IntMap<_, _> = arrow_data.into_iter().collect(); + let voxel_indices = arrays_by_descr + .get(&Self::descriptor_voxel_indices()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_voxel_indices()) + }); + let voxel_size = arrays_by_descr + .get(&Self::descriptor_voxel_size()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_voxel_size()) + }); + let values = arrays_by_descr + .get(&Self::descriptor_values()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_values())); + let colors = arrays_by_descr + .get(&Self::descriptor_colors()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_colors())); + let translation = arrays_by_descr + .get(&Self::descriptor_translation()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_translation()) + }); + let rotation_axis_angle = arrays_by_descr + .get(&Self::descriptor_rotation_axis_angle()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_rotation_axis_angle()) + }); + let quaternion = arrays_by_descr + .get(&Self::descriptor_quaternion()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_quaternion()) + }); + let opacity = arrays_by_descr + .get(&Self::descriptor_opacity()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_opacity())); + let value_range = arrays_by_descr + .get(&Self::descriptor_value_range()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_value_range()) + }); + let colormap = arrays_by_descr + .get(&Self::descriptor_colormap()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_colormap())); + Ok(Self { + voxel_indices, + voxel_size, + values, + colors, + translation, + rotation_axis_angle, + quaternion, + opacity, + value_range, + colormap, + }) + } +} + +impl ::re_types_core::AsComponents for VoxelGridMap { + #[inline] + fn as_serialized_batches(&self) -> Vec { + use ::re_types_core::Archetype as _; + [ + self.voxel_indices.clone(), + self.voxel_size.clone(), + self.values.clone(), + self.colors.clone(), + self.translation.clone(), + self.rotation_axis_angle.clone(), + self.quaternion.clone(), + self.opacity.clone(), + self.value_range.clone(), + self.colormap.clone(), + ] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for VoxelGridMap {} + +impl crate::VisualizableArchetype for VoxelGridMap { + #[inline] + fn visualizer(&self) -> crate::Visualizer { + crate::Visualizer::new("VoxelGridMap").with_overrides(self) + } +} + +impl VoxelGridMap { + /// Create a new `VoxelGridMap`. + #[inline] + pub fn new( + voxel_indices: impl IntoIterator>, + voxel_size: impl Into, + ) -> Self { + Self { + voxel_indices: try_serialize_field(Self::descriptor_voxel_indices(), voxel_indices), + voxel_size: try_serialize_field(Self::descriptor_voxel_size(), [voxel_size]), + values: None, + colors: None, + translation: None, + rotation_axis_angle: None, + quaternion: None, + opacity: None, + value_range: None, + colormap: None, + } + } + + /// Update only some specific fields of a `VoxelGridMap`. + #[inline] + pub fn update_fields() -> Self { + Self::default() + } + + /// Clear all the fields of a `VoxelGridMap`. + #[inline] + pub fn clear_fields() -> Self { + use ::re_types_core::Loggable as _; + Self { + voxel_indices: Some(SerializedComponentBatch::new( + crate::components::VoxelIndex::arrow_empty(), + Self::descriptor_voxel_indices(), + )), + voxel_size: Some(SerializedComponentBatch::new( + crate::components::VoxelSize::arrow_empty(), + Self::descriptor_voxel_size(), + )), + values: Some(SerializedComponentBatch::new( + crate::components::VoxelValue::arrow_empty(), + Self::descriptor_values(), + )), + colors: Some(SerializedComponentBatch::new( + crate::components::Color::arrow_empty(), + Self::descriptor_colors(), + )), + translation: Some(SerializedComponentBatch::new( + crate::components::Translation3D::arrow_empty(), + Self::descriptor_translation(), + )), + rotation_axis_angle: Some(SerializedComponentBatch::new( + crate::components::RotationAxisAngle::arrow_empty(), + Self::descriptor_rotation_axis_angle(), + )), + quaternion: Some(SerializedComponentBatch::new( + crate::components::RotationQuat::arrow_empty(), + Self::descriptor_quaternion(), + )), + opacity: Some(SerializedComponentBatch::new( + crate::components::Opacity::arrow_empty(), + Self::descriptor_opacity(), + )), + value_range: Some(SerializedComponentBatch::new( + crate::components::ValueRange::arrow_empty(), + Self::descriptor_value_range(), + )), + colormap: Some(SerializedComponentBatch::new( + crate::components::Colormap::arrow_empty(), + Self::descriptor_colormap(), + )), + } + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing [`SerializedComponentBatch`]es data into [`SerializedComponentColumn`]s + /// instead, via [`SerializedComponentBatch::partitioned`]. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + /// + /// [`SerializedComponentColumn`]: [::re_types_core::SerializedComponentColumn] + #[inline] + pub fn columns( + self, + _lengths: I, + ) -> SerializationResult> + where + I: IntoIterator + Clone, + { + let columns = [ + self.voxel_indices + .map(|voxel_indices| voxel_indices.partitioned(_lengths.clone())) + .transpose()?, + self.voxel_size + .map(|voxel_size| voxel_size.partitioned(_lengths.clone())) + .transpose()?, + self.values + .map(|values| values.partitioned(_lengths.clone())) + .transpose()?, + self.colors + .map(|colors| colors.partitioned(_lengths.clone())) + .transpose()?, + self.translation + .map(|translation| translation.partitioned(_lengths.clone())) + .transpose()?, + self.rotation_axis_angle + .map(|rotation_axis_angle| rotation_axis_angle.partitioned(_lengths.clone())) + .transpose()?, + self.quaternion + .map(|quaternion| quaternion.partitioned(_lengths.clone())) + .transpose()?, + self.opacity + .map(|opacity| opacity.partitioned(_lengths.clone())) + .transpose()?, + self.value_range + .map(|value_range| value_range.partitioned(_lengths.clone())) + .transpose()?, + self.colormap + .map(|colormap| colormap.partitioned(_lengths.clone())) + .transpose()?, + ]; + Ok(columns.into_iter().flatten()) + } + + /// Helper to partition the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling [`Self::columns`] with `std::iter::take(1).repeat(n)`, + /// where `n` is automatically guessed. + #[inline] + pub fn columns_of_unit_batches( + self, + ) -> SerializationResult> { + let len_voxel_indices = self.voxel_indices.as_ref().map(|b| b.array.len()); + let len_voxel_size = self.voxel_size.as_ref().map(|b| b.array.len()); + let len_values = self.values.as_ref().map(|b| b.array.len()); + let len_colors = self.colors.as_ref().map(|b| b.array.len()); + let len_translation = self.translation.as_ref().map(|b| b.array.len()); + let len_rotation_axis_angle = self.rotation_axis_angle.as_ref().map(|b| b.array.len()); + let len_quaternion = self.quaternion.as_ref().map(|b| b.array.len()); + let len_opacity = self.opacity.as_ref().map(|b| b.array.len()); + let len_value_range = self.value_range.as_ref().map(|b| b.array.len()); + let len_colormap = self.colormap.as_ref().map(|b| b.array.len()); + let len = None + .or(len_voxel_indices) + .or(len_voxel_size) + .or(len_values) + .or(len_colors) + .or(len_translation) + .or(len_rotation_axis_angle) + .or(len_quaternion) + .or(len_opacity) + .or(len_value_range) + .or(len_colormap) + .unwrap_or(0); + self.columns(std::iter::repeat_n(1, len)) + } + + /// Indices of the voxels within the grid volume. + #[inline] + pub fn with_voxel_indices( + mut self, + voxel_indices: impl IntoIterator>, + ) -> Self { + self.voxel_indices = try_serialize_field(Self::descriptor_voxel_indices(), voxel_indices); + self + } + + /// The scene-unit dimensions of a single voxel cell. + /// + /// This defines the voxel size along the local grid X/Y/Z axes. + /// Each dimension must be finite and positive. + #[inline] + pub fn with_voxel_size(mut self, voxel_size: impl Into) -> Self { + self.voxel_size = try_serialize_field(Self::descriptor_voxel_size(), [voxel_size]); + self + } + + /// This method makes it possible to pack multiple [`crate::components::VoxelSize`] in a single component batch. + /// + /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_voxel_size`] should + /// be used when logging a single row's worth of data. + #[inline] + pub fn with_many_voxel_size( + mut self, + voxel_size: impl IntoIterator>, + ) -> Self { + self.voxel_size = try_serialize_field(Self::descriptor_voxel_size(), voxel_size); + self + } + + /// Optional scalar occupancy or value data for each voxel. + /// + /// If explicit colors are not provided, values are mapped through `colormap` and `value_range`. + #[inline] + pub fn with_values( + mut self, + values: impl IntoIterator>, + ) -> Self { + self.values = try_serialize_field(Self::descriptor_values(), values); + self + } + + /// Optional colors for each voxel. + /// + /// If set, these colors take precedence over color-mapped scalar values. + #[inline] + pub fn with_colors( + mut self, + colors: impl IntoIterator>, + ) -> Self { + self.colors = try_serialize_field(Self::descriptor_colors(), colors); + self + } + + /// Translation of the minimum corner of voxel `[0, 0, 0]`. + /// + /// Together with [`components::RotationAxisAngle`][crate::components::RotationAxisAngle] or [`components::RotationQuat`][crate::components::RotationQuat], this defines the pose of the + /// grid relative to the map's parent coordinate frame. + /// + /// If not set, the minimum corner is placed at the origin of the map's parent coordinate frame. + #[inline] + pub fn with_translation( + mut self, + translation: impl Into, + ) -> Self { + self.translation = try_serialize_field(Self::descriptor_translation(), [translation]); + self + } + + /// This method makes it possible to pack multiple [`crate::components::Translation3D`] in a single component batch. + /// + /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_translation`] should + /// be used when logging a single row's worth of data. + #[inline] + pub fn with_many_translation( + mut self, + translation: impl IntoIterator>, + ) -> Self { + self.translation = try_serialize_field(Self::descriptor_translation(), translation); + self + } + + /// Rotation of the grid via axis + angle. + /// + /// Together with [`components::Translation3D`][crate::components::Translation3D], this defines the pose of the grid relative to the + /// map's parent coordinate frame. + /// + /// Note: either this or [`components::RotationQuat`][crate::components::RotationQuat] can be set to specify the grid's rotation, but not both. + /// If both this and [`components::RotationQuat`][crate::components::RotationQuat] are set, this is ignored in favor of the quaternion. + #[inline] + pub fn with_rotation_axis_angle( + mut self, + rotation_axis_angle: impl Into, + ) -> Self { + self.rotation_axis_angle = try_serialize_field( + Self::descriptor_rotation_axis_angle(), + [rotation_axis_angle], + ); + self + } + + /// This method makes it possible to pack multiple [`crate::components::RotationAxisAngle`] in a single component batch. + /// + /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_rotation_axis_angle`] should + /// be used when logging a single row's worth of data. + #[inline] + pub fn with_many_rotation_axis_angle( + mut self, + rotation_axis_angle: impl IntoIterator>, + ) -> Self { + self.rotation_axis_angle = + try_serialize_field(Self::descriptor_rotation_axis_angle(), rotation_axis_angle); + self + } + + /// Rotation of the grid via quaternion. + /// + /// Together with [`components::Translation3D`][crate::components::Translation3D], this defines the pose of the grid relative to the + /// map's parent coordinate frame. + #[inline] + pub fn with_quaternion( + mut self, + quaternion: impl Into, + ) -> Self { + self.quaternion = try_serialize_field(Self::descriptor_quaternion(), [quaternion]); + self + } + + /// This method makes it possible to pack multiple [`crate::components::RotationQuat`] in a single component batch. + /// + /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_quaternion`] should + /// be used when logging a single row's worth of data. + #[inline] + pub fn with_many_quaternion( + mut self, + quaternion: impl IntoIterator>, + ) -> Self { + self.quaternion = try_serialize_field(Self::descriptor_quaternion(), quaternion); + self + } + + /// Opacity of the voxels after color or colormap application. + /// + /// Defaults to 1.0 (fully opaque). + #[inline] + pub fn with_opacity(mut self, opacity: impl Into) -> Self { + self.opacity = try_serialize_field(Self::descriptor_opacity(), [opacity]); + self + } + + /// This method makes it possible to pack multiple [`crate::components::Opacity`] in a single component batch. + /// + /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_opacity`] should + /// be used when logging a single row's worth of data. + #[inline] + pub fn with_many_opacity( + mut self, + opacity: impl IntoIterator>, + ) -> Self { + self.opacity = try_serialize_field(Self::descriptor_opacity(), opacity); + self + } + + /// Scalar value range for color-mapping. + /// + /// Defaults to `[0.0, 1.0]`. + #[inline] + pub fn with_value_range( + mut self, + value_range: impl Into, + ) -> Self { + self.value_range = try_serialize_field(Self::descriptor_value_range(), [value_range]); + self + } + + /// This method makes it possible to pack multiple [`crate::components::ValueRange`] in a single component batch. + /// + /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_value_range`] should + /// be used when logging a single row's worth of data. + #[inline] + pub fn with_many_value_range( + mut self, + value_range: impl IntoIterator>, + ) -> Self { + self.value_range = try_serialize_field(Self::descriptor_value_range(), value_range); + self + } + + /// Colormap to use when `values` are present and explicit `colors` are not provided. + /// + /// Defaults to Turbo. + #[inline] + pub fn with_colormap(mut self, colormap: impl Into) -> Self { + self.colormap = try_serialize_field(Self::descriptor_colormap(), [colormap]); + self + } + + /// This method makes it possible to pack multiple [`crate::components::Colormap`] in a single component batch. + /// + /// This only makes sense when used in conjunction with [`Self::columns`]. [`Self::with_colormap`] should + /// be used when logging a single row's worth of data. + #[inline] + pub fn with_many_colormap( + mut self, + colormap: impl IntoIterator>, + ) -> Self { + self.colormap = try_serialize_field(Self::descriptor_colormap(), colormap); + self + } +} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/.gitattributes b/crates/store/re_sdk_types/src/blueprint/archetypes/.gitattributes index 4d0325f9d507..7d2a018ec13d 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/.gitattributes +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/.gitattributes @@ -23,9 +23,11 @@ plot_background.rs linguist-generated=true plot_legend.rs linguist-generated=true scalar_axis.rs linguist-generated=true spatial_information.rs linguist-generated=true +table_blueprint.rs linguist-generated=true tensor_scalar_mapping.rs linguist-generated=true tensor_slice_selection.rs linguist-generated=true tensor_view_fit.rs linguist-generated=true +text_document_format.rs linguist-generated=true text_log_columns.rs linguist-generated=true text_log_format.rs linguist-generated=true text_log_rows.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/active_visualizers.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/active_visualizers.rs index 8901351fe53a..0e67786b0e89 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/active_visualizers.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/active_visualizers.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -31,7 +32,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// in a regular entity. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ActiveVisualizers { /// Id's of the visualizers that should be active. pub instruction_ids: Option, @@ -43,11 +44,13 @@ impl ActiveVisualizers { /// The corresponding component is [`crate::blueprint::components::VisualizerInstructionId`]. #[inline] pub fn descriptor_instruction_ids() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ActiveVisualizers".into()), - component: "ActiveVisualizers:instruction_ids".into(), - component_type: Some("rerun.blueprint.components.VisualizerInstructionId".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ActiveVisualizers".into()), + component: "ActiveVisualizers:instruction_ids".into(), + component_type: Some("rerun.blueprint.components.VisualizerInstructionId".into()), + }); + (*DESCRIPTOR).clone() } } @@ -71,7 +74,10 @@ impl ActiveVisualizers { impl ::re_types_core::Archetype for ActiveVisualizers { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ActiveVisualizers".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ActiveVisualizers" + ) } #[inline] @@ -174,10 +180,3 @@ impl ActiveVisualizers { self } } - -impl ::re_byte_size::SizeBytes for ActiveVisualizers { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.instruction_ids.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/background.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/background.rs index 505f16c533ea..05d27ccee5d4 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/background.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/background.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration for the background of a spatial view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct Background { /// The type of the background. pub kind: Option, @@ -39,11 +40,13 @@ impl Background { /// The corresponding component is [`crate::blueprint::components::BackgroundKind`]. #[inline] pub fn descriptor_kind() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.Background".into()), - component: "Background:kind".into(), - component_type: Some("rerun.blueprint.components.BackgroundKind".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.Background".into()), + component: "Background:kind".into(), + component_type: Some("rerun.blueprint.components.BackgroundKind".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::color`]. @@ -51,11 +54,13 @@ impl Background { /// The corresponding component is [`crate::components::Color`]. #[inline] pub fn descriptor_color() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.Background".into()), - component: "Background:color".into(), - component_type: Some("rerun.components.Color".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.Background".into()), + component: "Background:color".into(), + component_type: Some("rerun.components.Color".into()), + }); + (*DESCRIPTOR).clone() } } @@ -84,7 +89,10 @@ impl Background { impl ::re_types_core::Archetype for Background { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.Background".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.Background" + ) } #[inline] @@ -191,10 +199,3 @@ impl Background { self } } - -impl ::re_byte_size::SizeBytes for Background { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.kind.heap_size_bytes() + self.color.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/container_blueprint.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/container_blueprint.rs index 8ca14399930c..9665513a7ca8 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/container_blueprint.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/container_blueprint.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: The description of a container. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ContainerBlueprint { /// The class of the view. pub container_kind: Option, @@ -73,11 +74,13 @@ impl ContainerBlueprint { /// The corresponding component is [`crate::blueprint::components::ContainerKind`]. #[inline] pub fn descriptor_container_kind() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), - component: "ContainerBlueprint:container_kind".into(), - component_type: Some("rerun.blueprint.components.ContainerKind".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), + component: "ContainerBlueprint:container_kind".into(), + component_type: Some("rerun.blueprint.components.ContainerKind".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::display_name`]. @@ -85,11 +88,13 @@ impl ContainerBlueprint { /// The corresponding component is [`crate::components::Name`]. #[inline] pub fn descriptor_display_name() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), - component: "ContainerBlueprint:display_name".into(), - component_type: Some("rerun.components.Name".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), + component: "ContainerBlueprint:display_name".into(), + component_type: Some("rerun.components.Name".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::contents`]. @@ -97,11 +102,13 @@ impl ContainerBlueprint { /// The corresponding component is [`crate::blueprint::components::IncludedContent`]. #[inline] pub fn descriptor_contents() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), - component: "ContainerBlueprint:contents".into(), - component_type: Some("rerun.blueprint.components.IncludedContent".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), + component: "ContainerBlueprint:contents".into(), + component_type: Some("rerun.blueprint.components.IncludedContent".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::col_shares`]. @@ -109,11 +116,13 @@ impl ContainerBlueprint { /// The corresponding component is [`crate::blueprint::components::ColumnShare`]. #[inline] pub fn descriptor_col_shares() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), - component: "ContainerBlueprint:col_shares".into(), - component_type: Some("rerun.blueprint.components.ColumnShare".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), + component: "ContainerBlueprint:col_shares".into(), + component_type: Some("rerun.blueprint.components.ColumnShare".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::row_shares`]. @@ -121,11 +130,13 @@ impl ContainerBlueprint { /// The corresponding component is [`crate::blueprint::components::RowShare`]. #[inline] pub fn descriptor_row_shares() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), - component: "ContainerBlueprint:row_shares".into(), - component_type: Some("rerun.blueprint.components.RowShare".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), + component: "ContainerBlueprint:row_shares".into(), + component_type: Some("rerun.blueprint.components.RowShare".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::active_tab`]. @@ -133,11 +144,13 @@ impl ContainerBlueprint { /// The corresponding component is [`crate::blueprint::components::ActiveTab`]. #[inline] pub fn descriptor_active_tab() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), - component: "ContainerBlueprint:active_tab".into(), - component_type: Some("rerun.blueprint.components.ActiveTab".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), + component: "ContainerBlueprint:active_tab".into(), + component_type: Some("rerun.blueprint.components.ActiveTab".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::visible`]. @@ -145,11 +158,13 @@ impl ContainerBlueprint { /// The corresponding component is [`crate::components::Visible`]. #[inline] pub fn descriptor_visible() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), - component: "ContainerBlueprint:visible".into(), - component_type: Some("rerun.components.Visible".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), + component: "ContainerBlueprint:visible".into(), + component_type: Some("rerun.components.Visible".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::grid_columns`]. @@ -157,11 +172,13 @@ impl ContainerBlueprint { /// The corresponding component is [`crate::blueprint::components::GridColumns`]. #[inline] pub fn descriptor_grid_columns() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), - component: "ContainerBlueprint:grid_columns".into(), - component_type: Some("rerun.blueprint.components.GridColumns".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ContainerBlueprint".into()), + component: "ContainerBlueprint:grid_columns".into(), + component_type: Some("rerun.blueprint.components.GridColumns".into()), + }); + (*DESCRIPTOR).clone() } } @@ -206,7 +223,10 @@ impl ContainerBlueprint { impl ::re_types_core::Archetype for ContainerBlueprint { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ContainerBlueprint".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ContainerBlueprint" + ) } #[inline] @@ -468,17 +488,3 @@ impl ContainerBlueprint { self } } - -impl ::re_byte_size::SizeBytes for ContainerBlueprint { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.container_kind.heap_size_bytes() - + self.display_name.heap_size_bytes() - + self.contents.heap_size_bytes() - + self.col_shares.heap_size_bytes() - + self.row_shares.heap_size_bytes() - + self.active_tab.heap_size_bytes() - + self.visible.heap_size_bytes() - + self.grid_columns.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/dataframe_query.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/dataframe_query.rs index 34eb75cdcafe..85cd831affa9 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/dataframe_query.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/dataframe_query.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: The query for the dataframe view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct DataframeQuery { /// The timeline for this query. /// @@ -66,11 +67,13 @@ impl DataframeQuery { /// The corresponding component is [`crate::blueprint::components::TimelineName`]. #[inline] pub fn descriptor_timeline() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), - component: "DataframeQuery:timeline".into(), - component_type: Some("rerun.blueprint.components.TimelineName".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), + component: "DataframeQuery:timeline".into(), + component_type: Some("rerun.blueprint.components.TimelineName".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::filter_by_range`]. @@ -78,11 +81,13 @@ impl DataframeQuery { /// The corresponding component is [`crate::blueprint::components::FilterByRange`]. #[inline] pub fn descriptor_filter_by_range() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), - component: "DataframeQuery:filter_by_range".into(), - component_type: Some("rerun.blueprint.components.FilterByRange".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), + component: "DataframeQuery:filter_by_range".into(), + component_type: Some("rerun.blueprint.components.FilterByRange".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::filter_is_not_null`]. @@ -90,11 +95,13 @@ impl DataframeQuery { /// The corresponding component is [`crate::blueprint::components::FilterIsNotNull`]. #[inline] pub fn descriptor_filter_is_not_null() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), - component: "DataframeQuery:filter_is_not_null".into(), - component_type: Some("rerun.blueprint.components.FilterIsNotNull".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), + component: "DataframeQuery:filter_is_not_null".into(), + component_type: Some("rerun.blueprint.components.FilterIsNotNull".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::apply_latest_at`]. @@ -102,11 +109,13 @@ impl DataframeQuery { /// The corresponding component is [`crate::blueprint::components::ApplyLatestAt`]. #[inline] pub fn descriptor_apply_latest_at() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), - component: "DataframeQuery:apply_latest_at".into(), - component_type: Some("rerun.blueprint.components.ApplyLatestAt".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), + component: "DataframeQuery:apply_latest_at".into(), + component_type: Some("rerun.blueprint.components.ApplyLatestAt".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::select`]. @@ -114,11 +123,13 @@ impl DataframeQuery { /// The corresponding component is [`crate::blueprint::components::SelectedColumns`]. #[inline] pub fn descriptor_select() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), - component: "DataframeQuery:select".into(), - component_type: Some("rerun.blueprint.components.SelectedColumns".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), + component: "DataframeQuery:select".into(), + component_type: Some("rerun.blueprint.components.SelectedColumns".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::entity_order`]. @@ -126,11 +137,13 @@ impl DataframeQuery { /// The corresponding component is [`crate::blueprint::components::ColumnOrder`]. #[inline] pub fn descriptor_entity_order() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), - component: "DataframeQuery:entity_order".into(), - component_type: Some("rerun.blueprint.components.ColumnOrder".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), + component: "DataframeQuery:entity_order".into(), + component_type: Some("rerun.blueprint.components.ColumnOrder".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::auto_scroll`]. @@ -138,11 +151,13 @@ impl DataframeQuery { /// The corresponding component is [`crate::blueprint::components::AutoScroll`]. #[inline] pub fn descriptor_auto_scroll() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), - component: "DataframeQuery:auto_scroll".into(), - component_type: Some("rerun.blueprint.components.AutoScroll".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.DataframeQuery".into()), + component: "DataframeQuery:auto_scroll".into(), + component_type: Some("rerun.blueprint.components.AutoScroll".into()), + }); + (*DESCRIPTOR).clone() } } @@ -186,7 +201,10 @@ impl DataframeQuery { impl ::re_types_core::Archetype for DataframeQuery { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.DataframeQuery".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.DataframeQuery" + ) } #[inline] @@ -427,16 +445,3 @@ impl DataframeQuery { self } } - -impl ::re_byte_size::SizeBytes for DataframeQuery { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.timeline.heap_size_bytes() - + self.filter_by_range.heap_size_bytes() - + self.filter_is_not_null.heap_size_bytes() - + self.apply_latest_at.heap_size_bytes() - + self.select.heap_size_bytes() - + self.entity_order.heap_size_bytes() - + self.auto_scroll.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/entity_behavior.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/entity_behavior.rs index a5ae9d70512c..ecb2c495f69f 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/entity_behavior.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/entity_behavior.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: General visualization behavior of an entity. /// /// TODO(#6541): Fields of this archetype currently only have an effect when logged in the blueprint store. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct EntityBehavior { /// Whether the entity can be interacted with. /// @@ -49,11 +50,13 @@ impl EntityBehavior { /// The corresponding component is [`crate::components::Interactive`]. #[inline] pub fn descriptor_interactive() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EntityBehavior".into()), - component: "EntityBehavior:interactive".into(), - component_type: Some("rerun.components.Interactive".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EntityBehavior".into()), + component: "EntityBehavior:interactive".into(), + component_type: Some("rerun.components.Interactive".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::visible`]. @@ -61,11 +64,13 @@ impl EntityBehavior { /// The corresponding component is [`crate::components::Visible`]. #[inline] pub fn descriptor_visible() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EntityBehavior".into()), - component: "EntityBehavior:visible".into(), - component_type: Some("rerun.components.Visible".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EntityBehavior".into()), + component: "EntityBehavior:visible".into(), + component_type: Some("rerun.components.Visible".into()), + }); + (*DESCRIPTOR).clone() } } @@ -99,7 +104,10 @@ impl EntityBehavior { impl ::re_types_core::Archetype for EntityBehavior { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.EntityBehavior".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.EntityBehavior" + ) } #[inline] @@ -221,10 +229,3 @@ impl EntityBehavior { self } } - -impl ::re_byte_size::SizeBytes for EntityBehavior { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.interactive.heap_size_bytes() + self.visible.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/eye_controls3d.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/eye_controls3d.rs index a184ba3d6e40..c2ae1402b8e7 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/eye_controls3d.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/eye_controls3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// This configures the camera through which the 3D scene is viewed. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct EyeControls3D { /// The kind of the eye for the spatial 3D view. /// @@ -74,11 +75,13 @@ impl EyeControls3D { /// The corresponding component is [`crate::blueprint::components::Eye3DKind`]. #[inline] pub fn descriptor_kind() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), - component: "EyeControls3D:kind".into(), - component_type: Some("rerun.blueprint.components.Eye3DKind".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), + component: "EyeControls3D:kind".into(), + component_type: Some("rerun.blueprint.components.Eye3DKind".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::position`]. @@ -86,11 +89,13 @@ impl EyeControls3D { /// The corresponding component is [`crate::components::Position3D`]. #[inline] pub fn descriptor_position() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), - component: "EyeControls3D:position".into(), - component_type: Some("rerun.components.Position3D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), + component: "EyeControls3D:position".into(), + component_type: Some("rerun.components.Position3D".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::look_target`]. @@ -98,11 +103,13 @@ impl EyeControls3D { /// The corresponding component is [`crate::components::Position3D`]. #[inline] pub fn descriptor_look_target() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), - component: "EyeControls3D:look_target".into(), - component_type: Some("rerun.components.Position3D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), + component: "EyeControls3D:look_target".into(), + component_type: Some("rerun.components.Position3D".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::eye_up`]. @@ -110,11 +117,13 @@ impl EyeControls3D { /// The corresponding component is [`crate::components::Vector3D`]. #[inline] pub fn descriptor_eye_up() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), - component: "EyeControls3D:eye_up".into(), - component_type: Some("rerun.components.Vector3D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), + component: "EyeControls3D:eye_up".into(), + component_type: Some("rerun.components.Vector3D".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::speed`]. @@ -122,11 +131,13 @@ impl EyeControls3D { /// The corresponding component is [`crate::components::LinearSpeed`]. #[inline] pub fn descriptor_speed() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), - component: "EyeControls3D:speed".into(), - component_type: Some("rerun.components.LinearSpeed".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), + component: "EyeControls3D:speed".into(), + component_type: Some("rerun.components.LinearSpeed".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::tracking_entity`]. @@ -134,11 +145,13 @@ impl EyeControls3D { /// The corresponding component is [`crate::components::EntityPath`]. #[inline] pub fn descriptor_tracking_entity() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), - component: "EyeControls3D:tracking_entity".into(), - component_type: Some("rerun.components.EntityPath".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), + component: "EyeControls3D:tracking_entity".into(), + component_type: Some("rerun.components.EntityPath".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::spin_speed`]. @@ -146,11 +159,13 @@ impl EyeControls3D { /// The corresponding component is [`crate::blueprint::components::AngularSpeed`]. #[inline] pub fn descriptor_spin_speed() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), - component: "EyeControls3D:spin_speed".into(), - component_type: Some("rerun.blueprint.components.AngularSpeed".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.EyeControls3D".into()), + component: "EyeControls3D:spin_speed".into(), + component_type: Some("rerun.blueprint.components.AngularSpeed".into()), + }); + (*DESCRIPTOR).clone() } } @@ -194,7 +209,10 @@ impl EyeControls3D { impl ::re_types_core::Archetype for EyeControls3D { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.EyeControls3D".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.EyeControls3D" + ) } #[inline] @@ -423,16 +441,3 @@ impl EyeControls3D { self } } - -impl ::re_byte_size::SizeBytes for EyeControls3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.kind.heap_size_bytes() - + self.position.heap_size_bytes() - + self.look_target.heap_size_bytes() - + self.eye_up.heap_size_bytes() - + self.speed.heap_size_bytes() - + self.tracking_entity.heap_size_bytes() - + self.spin_speed.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/force_center.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/force_center.rs index c9a5393dcc04..81da61d32b6a 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/force_center.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/force_center.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Tries to move the center of mass of the graph to the origin. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ForceCenter { /// Whether the center force is enabled. /// @@ -41,11 +42,13 @@ impl ForceCenter { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_enabled() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceCenter".into()), - component: "ForceCenter:enabled".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceCenter".into()), + component: "ForceCenter:enabled".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::strength`]. @@ -53,11 +56,13 @@ impl ForceCenter { /// The corresponding component is [`crate::blueprint::components::ForceStrength`]. #[inline] pub fn descriptor_strength() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceCenter".into()), - component: "ForceCenter:strength".into(), - component_type: Some("rerun.blueprint.components.ForceStrength".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceCenter".into()), + component: "ForceCenter:strength".into(), + component_type: Some("rerun.blueprint.components.ForceStrength".into()), + }); + (*DESCRIPTOR).clone() } } @@ -91,7 +96,10 @@ impl ForceCenter { impl ::re_types_core::Archetype for ForceCenter { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ForceCenter".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ForceCenter" + ) } #[inline] @@ -203,10 +211,3 @@ impl ForceCenter { self } } - -impl ::re_byte_size::SizeBytes for ForceCenter { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.enabled.heap_size_bytes() + self.strength.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/force_collision_radius.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/force_collision_radius.rs index 2d0ac9a79755..940d99b66caf 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/force_collision_radius.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/force_collision_radius.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Resolves collisions between the bounding circles, according to the radius of the nodes. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ForceCollisionRadius { /// Whether the collision force is enabled. /// @@ -46,11 +47,13 @@ impl ForceCollisionRadius { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_enabled() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceCollisionRadius".into()), - component: "ForceCollisionRadius:enabled".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceCollisionRadius".into()), + component: "ForceCollisionRadius:enabled".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::strength`]. @@ -58,11 +61,13 @@ impl ForceCollisionRadius { /// The corresponding component is [`crate::blueprint::components::ForceStrength`]. #[inline] pub fn descriptor_strength() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceCollisionRadius".into()), - component: "ForceCollisionRadius:strength".into(), - component_type: Some("rerun.blueprint.components.ForceStrength".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceCollisionRadius".into()), + component: "ForceCollisionRadius:strength".into(), + component_type: Some("rerun.blueprint.components.ForceStrength".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::iterations`]. @@ -70,11 +75,13 @@ impl ForceCollisionRadius { /// The corresponding component is [`crate::blueprint::components::ForceIterations`]. #[inline] pub fn descriptor_iterations() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceCollisionRadius".into()), - component: "ForceCollisionRadius:iterations".into(), - component_type: Some("rerun.blueprint.components.ForceIterations".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceCollisionRadius".into()), + component: "ForceCollisionRadius:iterations".into(), + component_type: Some("rerun.blueprint.components.ForceIterations".into()), + }); + (*DESCRIPTOR).clone() } } @@ -110,7 +117,10 @@ impl ForceCollisionRadius { impl ::re_types_core::Archetype for ForceCollisionRadius { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ForceCollisionRadius".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ForceCollisionRadius" + ) } #[inline] @@ -252,12 +262,3 @@ impl ForceCollisionRadius { self } } - -impl ::re_byte_size::SizeBytes for ForceCollisionRadius { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.enabled.heap_size_bytes() - + self.strength.heap_size_bytes() - + self.iterations.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/force_link.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/force_link.rs index 8de2f4f16fa8..b86ef8c33c8a 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/force_link.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/force_link.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Aims to achieve a target distance between two nodes that are connected by an edge. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ForceLink { /// Whether the link force is enabled. /// @@ -46,11 +47,13 @@ impl ForceLink { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_enabled() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceLink".into()), - component: "ForceLink:enabled".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceLink".into()), + component: "ForceLink:enabled".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::distance`]. @@ -58,11 +61,13 @@ impl ForceLink { /// The corresponding component is [`crate::blueprint::components::ForceDistance`]. #[inline] pub fn descriptor_distance() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceLink".into()), - component: "ForceLink:distance".into(), - component_type: Some("rerun.blueprint.components.ForceDistance".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceLink".into()), + component: "ForceLink:distance".into(), + component_type: Some("rerun.blueprint.components.ForceDistance".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::iterations`]. @@ -70,11 +75,13 @@ impl ForceLink { /// The corresponding component is [`crate::blueprint::components::ForceIterations`]. #[inline] pub fn descriptor_iterations() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceLink".into()), - component: "ForceLink:iterations".into(), - component_type: Some("rerun.blueprint.components.ForceIterations".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceLink".into()), + component: "ForceLink:iterations".into(), + component_type: Some("rerun.blueprint.components.ForceIterations".into()), + }); + (*DESCRIPTOR).clone() } } @@ -110,7 +117,10 @@ impl ForceLink { impl ::re_types_core::Archetype for ForceLink { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ForceLink".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ForceLink" + ) } #[inline] @@ -252,12 +262,3 @@ impl ForceLink { self } } - -impl ::re_byte_size::SizeBytes for ForceLink { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.enabled.heap_size_bytes() - + self.distance.heap_size_bytes() - + self.iterations.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/force_many_body.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/force_many_body.rs index 129c131bc3e6..0f8e8b13f28a 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/force_many_body.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/force_many_body.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// If `strength` is smaller than 0, it pushes nodes apart, if it is larger than 0 it pulls them together. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ForceManyBody { /// Whether the many body force is enabled. /// @@ -46,11 +47,13 @@ impl ForceManyBody { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_enabled() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceManyBody".into()), - component: "ForceManyBody:enabled".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceManyBody".into()), + component: "ForceManyBody:enabled".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::strength`]. @@ -58,11 +61,13 @@ impl ForceManyBody { /// The corresponding component is [`crate::blueprint::components::ForceStrength`]. #[inline] pub fn descriptor_strength() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForceManyBody".into()), - component: "ForceManyBody:strength".into(), - component_type: Some("rerun.blueprint.components.ForceStrength".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForceManyBody".into()), + component: "ForceManyBody:strength".into(), + component_type: Some("rerun.blueprint.components.ForceStrength".into()), + }); + (*DESCRIPTOR).clone() } } @@ -96,7 +101,10 @@ impl ForceManyBody { impl ::re_types_core::Archetype for ForceManyBody { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ForceManyBody".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ForceManyBody" + ) } #[inline] @@ -211,10 +219,3 @@ impl ForceManyBody { self } } - -impl ::re_byte_size::SizeBytes for ForceManyBody { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.enabled.heap_size_bytes() + self.strength.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/force_position.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/force_position.rs index b5320fa8f59e..8a37b88310f1 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/force_position.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/force_position.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Similar to gravity, this force pulls nodes towards a specific position. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ForcePosition { /// Whether the position force is enabled. /// @@ -44,11 +45,13 @@ impl ForcePosition { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_enabled() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForcePosition".into()), - component: "ForcePosition:enabled".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForcePosition".into()), + component: "ForcePosition:enabled".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::strength`]. @@ -56,11 +59,13 @@ impl ForcePosition { /// The corresponding component is [`crate::blueprint::components::ForceStrength`]. #[inline] pub fn descriptor_strength() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForcePosition".into()), - component: "ForcePosition:strength".into(), - component_type: Some("rerun.blueprint.components.ForceStrength".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForcePosition".into()), + component: "ForcePosition:strength".into(), + component_type: Some("rerun.blueprint.components.ForceStrength".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::position`]. @@ -68,11 +73,13 @@ impl ForcePosition { /// The corresponding component is [`crate::components::Position2D`]. #[inline] pub fn descriptor_position() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ForcePosition".into()), - component: "ForcePosition:position".into(), - component_type: Some("rerun.components.Position2D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ForcePosition".into()), + component: "ForcePosition:position".into(), + component_type: Some("rerun.components.Position2D".into()), + }); + (*DESCRIPTOR).clone() } } @@ -108,7 +115,10 @@ impl ForcePosition { impl ::re_types_core::Archetype for ForcePosition { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ForcePosition".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ForcePosition" + ) } #[inline] @@ -243,12 +253,3 @@ impl ForcePosition { self } } - -impl ::re_byte_size::SizeBytes for ForcePosition { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.enabled.heap_size_bytes() - + self.strength.heap_size_bytes() - + self.position.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/graph_background.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/graph_background.rs index 293b02fef944..3b5fa54ff188 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/graph_background.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/graph_background.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration of a background in a graph view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct GraphBackground { /// Color used for the background. pub color: Option, @@ -36,11 +37,13 @@ impl GraphBackground { /// The corresponding component is [`crate::components::Color`]. #[inline] pub fn descriptor_color() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.GraphBackground".into()), - component: "GraphBackground:color".into(), - component_type: Some("rerun.components.Color".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.GraphBackground".into()), + component: "GraphBackground:color".into(), + component_type: Some("rerun.components.Color".into()), + }); + (*DESCRIPTOR).clone() } } @@ -64,7 +67,10 @@ impl GraphBackground { impl ::re_types_core::Archetype for GraphBackground { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.GraphBackground".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.GraphBackground" + ) } #[inline] @@ -148,10 +154,3 @@ impl GraphBackground { self } } - -impl ::re_byte_size::SizeBytes for GraphBackground { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.color.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/line_grid3d.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/line_grid3d.rs index 9d83b360b4c8..2eab4e32c973 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/line_grid3d.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/line_grid3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration for the 3D line grid. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct LineGrid3D { /// Whether the grid is visible. /// @@ -60,11 +61,13 @@ impl LineGrid3D { /// The corresponding component is [`crate::components::Visible`]. #[inline] pub fn descriptor_visible() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), - component: "LineGrid3D:visible".into(), - component_type: Some("rerun.components.Visible".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), + component: "LineGrid3D:visible".into(), + component_type: Some("rerun.components.Visible".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::spacing`]. @@ -72,11 +75,13 @@ impl LineGrid3D { /// The corresponding component is [`crate::blueprint::components::GridSpacing`]. #[inline] pub fn descriptor_spacing() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), - component: "LineGrid3D:spacing".into(), - component_type: Some("rerun.blueprint.components.GridSpacing".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), + component: "LineGrid3D:spacing".into(), + component_type: Some("rerun.blueprint.components.GridSpacing".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::plane`]. @@ -84,11 +89,13 @@ impl LineGrid3D { /// The corresponding component is [`crate::components::Plane3D`]. #[inline] pub fn descriptor_plane() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), - component: "LineGrid3D:plane".into(), - component_type: Some("rerun.components.Plane3D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), + component: "LineGrid3D:plane".into(), + component_type: Some("rerun.components.Plane3D".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::stroke_width`]. @@ -96,11 +103,13 @@ impl LineGrid3D { /// The corresponding component is [`crate::components::StrokeWidth`]. #[inline] pub fn descriptor_stroke_width() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), - component: "LineGrid3D:stroke_width".into(), - component_type: Some("rerun.components.StrokeWidth".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), + component: "LineGrid3D:stroke_width".into(), + component_type: Some("rerun.components.StrokeWidth".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::color`]. @@ -108,11 +117,13 @@ impl LineGrid3D { /// The corresponding component is [`crate::components::Color`]. #[inline] pub fn descriptor_color() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), - component: "LineGrid3D:color".into(), - component_type: Some("rerun.components.Color".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.LineGrid3D".into()), + component: "LineGrid3D:color".into(), + component_type: Some("rerun.components.Color".into()), + }); + (*DESCRIPTOR).clone() } } @@ -152,7 +163,10 @@ impl LineGrid3D { impl ::re_types_core::Archetype for LineGrid3D { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.LineGrid3D".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.LineGrid3D" + ) } #[inline] @@ -333,14 +347,3 @@ impl LineGrid3D { self } } - -impl ::re_byte_size::SizeBytes for LineGrid3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.visible.heap_size_bytes() - + self.spacing.heap_size_bytes() - + self.plane.heap_size_bytes() - + self.stroke_width.heap_size_bytes() - + self.color.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/map_background.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/map_background.rs index 911c6951b7fc..a58885b2fdfc 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/map_background.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/map_background.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration for the background map of the map view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct MapBackground { /// Map provider and style to use. /// @@ -38,11 +39,13 @@ impl MapBackground { /// The corresponding component is [`crate::blueprint::components::MapProvider`]. #[inline] pub fn descriptor_provider() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.MapBackground".into()), - component: "MapBackground:provider".into(), - component_type: Some("rerun.blueprint.components.MapProvider".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.MapBackground".into()), + component: "MapBackground:provider".into(), + component_type: Some("rerun.blueprint.components.MapProvider".into()), + }); + (*DESCRIPTOR).clone() } } @@ -66,7 +69,10 @@ impl MapBackground { impl ::re_types_core::Archetype for MapBackground { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.MapBackground".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.MapBackground" + ) } #[inline] @@ -157,10 +163,3 @@ impl MapBackground { self } } - -impl ::re_byte_size::SizeBytes for MapBackground { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.provider.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/map_zoom.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/map_zoom.rs index 00e8cb6262b3..65519e0e0dac 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/map_zoom.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/map_zoom.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration of the map view zoom level. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct MapZoom { /// Zoom level for the map. /// @@ -38,11 +39,13 @@ impl MapZoom { /// The corresponding component is [`crate::blueprint::components::ZoomLevel`]. #[inline] pub fn descriptor_zoom() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.MapZoom".into()), - component: "MapZoom:zoom".into(), - component_type: Some("rerun.blueprint.components.ZoomLevel".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.MapZoom".into()), + component: "MapZoom:zoom".into(), + component_type: Some("rerun.blueprint.components.ZoomLevel".into()), + }); + (*DESCRIPTOR).clone() } } @@ -66,7 +69,10 @@ impl MapZoom { impl ::re_types_core::Archetype for MapZoom { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.MapZoom".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.MapZoom" + ) } #[inline] @@ -154,10 +160,3 @@ impl MapZoom { self } } - -impl ::re_byte_size::SizeBytes for MapZoom { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.zoom.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/mod.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/mod.rs index c683ae55c04b..3440b06698a6 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/mod.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/mod.rs @@ -21,9 +21,11 @@ mod plot_background; mod plot_legend; mod scalar_axis; mod spatial_information; +mod table_blueprint; mod tensor_scalar_mapping; mod tensor_slice_selection; mod tensor_view_fit; +mod text_document_format; mod text_log_columns; mod text_log_format; mod text_log_rows; @@ -58,9 +60,11 @@ pub use self::plot_background::PlotBackground; pub use self::plot_legend::PlotLegend; pub use self::scalar_axis::ScalarAxis; pub use self::spatial_information::SpatialInformation; +pub use self::table_blueprint::TableBlueprint; pub use self::tensor_scalar_mapping::TensorScalarMapping; pub use self::tensor_slice_selection::TensorSliceSelection; pub use self::tensor_view_fit::TensorViewFit; +pub use self::text_document_format::TextDocumentFormat; pub use self::text_log_columns::TextLogColumns; pub use self::text_log_format::TextLogFormat; pub use self::text_log_rows::TextLogRows; diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/near_clip_plane.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/near_clip_plane.rs index d670bd3afd7c..948fe6fff5ef 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/near_clip_plane.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/near_clip_plane.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Controls the distance to the near clip plane in 3D scene units. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct NearClipPlane { /// Controls the distance to the near clip plane in 3D scene units. /// @@ -38,11 +39,13 @@ impl NearClipPlane { /// The corresponding component is [`crate::blueprint::components::NearClipPlane`]. #[inline] pub fn descriptor_near_clip_plane() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.NearClipPlane".into()), - component: "NearClipPlane:near_clip_plane".into(), - component_type: Some("rerun.blueprint.components.NearClipPlane".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.NearClipPlane".into()), + component: "NearClipPlane:near_clip_plane".into(), + component_type: Some("rerun.blueprint.components.NearClipPlane".into()), + }); + (*DESCRIPTOR).clone() } } @@ -66,7 +69,10 @@ impl NearClipPlane { impl ::re_types_core::Archetype for NearClipPlane { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.NearClipPlane".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.NearClipPlane" + ) } #[inline] @@ -165,10 +171,3 @@ impl NearClipPlane { self } } - -impl ::re_byte_size::SizeBytes for NearClipPlane { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.near_clip_plane.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/panel_blueprint.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/panel_blueprint.rs index 3729d669ed01..6565fd5e10e2 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/panel_blueprint.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/panel_blueprint.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Shared state for the 3 collapsible panels. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct PanelBlueprint { /// Current state of the panel. pub state: Option, @@ -36,11 +37,13 @@ impl PanelBlueprint { /// The corresponding component is [`crate::blueprint::components::PanelState`]. #[inline] pub fn descriptor_state() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.PanelBlueprint".into()), - component: "PanelBlueprint:state".into(), - component_type: Some("rerun.blueprint.components.PanelState".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.PanelBlueprint".into()), + component: "PanelBlueprint:state".into(), + component_type: Some("rerun.blueprint.components.PanelState".into()), + }); + (*DESCRIPTOR).clone() } } @@ -64,7 +67,10 @@ impl PanelBlueprint { impl ::re_types_core::Archetype for PanelBlueprint { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.PanelBlueprint".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.PanelBlueprint" + ) } #[inline] @@ -151,10 +157,3 @@ impl PanelBlueprint { self } } - -impl ::re_byte_size::SizeBytes for PanelBlueprint { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.state.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/plot_background.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/plot_background.rs index 070a50e507b9..60019c78b583 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/plot_background.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/plot_background.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration of a background in a plot view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct PlotBackground { /// Color used for the background. pub color: Option, @@ -39,11 +40,13 @@ impl PlotBackground { /// The corresponding component is [`crate::components::Color`]. #[inline] pub fn descriptor_color() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.PlotBackground".into()), - component: "PlotBackground:color".into(), - component_type: Some("rerun.components.Color".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.PlotBackground".into()), + component: "PlotBackground:color".into(), + component_type: Some("rerun.components.Color".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::show_grid`]. @@ -51,11 +54,13 @@ impl PlotBackground { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_show_grid() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.PlotBackground".into()), - component: "PlotBackground:show_grid".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.PlotBackground".into()), + component: "PlotBackground:show_grid".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } } @@ -89,7 +94,10 @@ impl PlotBackground { impl ::re_types_core::Archetype for PlotBackground { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.PlotBackground".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.PlotBackground" + ) } #[inline] @@ -198,10 +206,3 @@ impl PlotBackground { self } } - -impl ::re_byte_size::SizeBytes for PlotBackground { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.color.heap_size_bytes() + self.show_grid.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/plot_legend.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/plot_legend.rs index 716e9a57bd2c..2f64eb61716c 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/plot_legend.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/plot_legend.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration for the legend of a plot. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct PlotLegend { /// To what corner the legend is aligned. /// @@ -43,11 +44,13 @@ impl PlotLegend { /// The corresponding component is [`crate::blueprint::components::Corner2D`]. #[inline] pub fn descriptor_corner() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.PlotLegend".into()), - component: "PlotLegend:corner".into(), - component_type: Some("rerun.blueprint.components.Corner2D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.PlotLegend".into()), + component: "PlotLegend:corner".into(), + component_type: Some("rerun.blueprint.components.Corner2D".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::visible`]. @@ -55,11 +58,13 @@ impl PlotLegend { /// The corresponding component is [`crate::components::Visible`]. #[inline] pub fn descriptor_visible() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.PlotLegend".into()), - component: "PlotLegend:visible".into(), - component_type: Some("rerun.components.Visible".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.PlotLegend".into()), + component: "PlotLegend:visible".into(), + component_type: Some("rerun.components.Visible".into()), + }); + (*DESCRIPTOR).clone() } } @@ -93,7 +98,10 @@ impl PlotLegend { impl ::re_types_core::Archetype for PlotLegend { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.PlotLegend".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.PlotLegend" + ) } #[inline] @@ -204,10 +212,3 @@ impl PlotLegend { self } } - -impl ::re_byte_size::SizeBytes for PlotLegend { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.corner.heap_size_bytes() + self.visible.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/scalar_axis.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/scalar_axis.rs index 6800d834396a..b24ea7504104 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/scalar_axis.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/scalar_axis.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration for the scalar (Y) axis of a plot. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ScalarAxis { /// The range of the axis. /// @@ -41,11 +42,13 @@ impl ScalarAxis { /// The corresponding component is [`crate::components::Range1D`]. #[inline] pub fn descriptor_range() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ScalarAxis".into()), - component: "ScalarAxis:range".into(), - component_type: Some("rerun.components.Range1D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ScalarAxis".into()), + component: "ScalarAxis:range".into(), + component_type: Some("rerun.components.Range1D".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::zoom_lock`]. @@ -53,11 +56,13 @@ impl ScalarAxis { /// The corresponding component is [`crate::blueprint::components::LockRangeDuringZoom`]. #[inline] pub fn descriptor_zoom_lock() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ScalarAxis".into()), - component: "ScalarAxis:zoom_lock".into(), - component_type: Some("rerun.blueprint.components.LockRangeDuringZoom".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ScalarAxis".into()), + component: "ScalarAxis:zoom_lock".into(), + component_type: Some("rerun.blueprint.components.LockRangeDuringZoom".into()), + }); + (*DESCRIPTOR).clone() } } @@ -91,7 +96,10 @@ impl ScalarAxis { impl ::re_types_core::Archetype for ScalarAxis { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ScalarAxis".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ScalarAxis" + ) } #[inline] @@ -202,10 +210,3 @@ impl ScalarAxis { self } } - -impl ::re_byte_size::SizeBytes for ScalarAxis { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.range.heap_size_bytes() + self.zoom_lock.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/spatial_information.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/spatial_information.rs index 7040356b5dc6..5d857c33f279 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/spatial_information.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/spatial_information.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: This configures extra drawing config for the 3D view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct SpatialInformation { /// The target reference frame for all transformations. /// @@ -44,11 +45,13 @@ impl SpatialInformation { /// The corresponding component is [`crate::components::TransformFrameId`]. #[inline] pub fn descriptor_target_frame() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.SpatialInformation".into()), - component: "SpatialInformation:target_frame".into(), - component_type: Some("rerun.components.TransformFrameId".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.SpatialInformation".into()), + component: "SpatialInformation:target_frame".into(), + component_type: Some("rerun.components.TransformFrameId".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::show_axes`]. @@ -56,11 +59,13 @@ impl SpatialInformation { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_show_axes() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.SpatialInformation".into()), - component: "SpatialInformation:show_axes".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.SpatialInformation".into()), + component: "SpatialInformation:show_axes".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::show_bounding_box`]. @@ -68,11 +73,13 @@ impl SpatialInformation { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_show_bounding_box() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.SpatialInformation".into()), - component: "SpatialInformation:show_bounding_box".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.SpatialInformation".into()), + component: "SpatialInformation:show_bounding_box".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } } @@ -108,7 +115,10 @@ impl SpatialInformation { impl ::re_types_core::Archetype for SpatialInformation { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.SpatialInformation".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.SpatialInformation" + ) } #[inline] @@ -253,12 +263,3 @@ impl SpatialInformation { self } } - -impl ::re_byte_size::SizeBytes for SpatialInformation { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.target_frame.heap_size_bytes() - + self.show_axes.heap_size_bytes() - + self.show_bounding_box.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/table_blueprint.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/table_blueprint.rs new file mode 100644 index 000000000000..7644bf1f67d6 --- /dev/null +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/table_blueprint.rs @@ -0,0 +1,349 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/table_blueprint.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: Blueprint for configuring the styling of a table. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] +pub struct TableBlueprint { + /// The name of the column that contains recording URIs for segment previews. + /// + /// Every row can at most preview a single segment. + /// + /// For the preview, the rest of the blueprint data is read it as it would be with regular recording blueprints, + /// meaning that the regular structure of [`archetypes::ViewportBlueprint`][crate::blueprint::archetypes::ViewportBlueprint], and [`archetypes::ViewBlueprint`][crate::blueprint::archetypes::ViewBlueprint] structure applies. + /// However, this mostly ignores layout container types as well as automatic spawning. + /// + /// If unset, defaults to the first URL column in the table that points to the same Rerun server + pub segment_preview_column: Option, + + /// The name of the boolean column used for flag/annotation toggles. + /// + /// Must be set for flagging to be available. The named column must exist in the + /// table and be of boolean type. + /// Additionally, the table must be remote and have another column with + /// `rerun:is_table_index` metadata since flag changes are persisted to the server + /// via upsert. + pub flag_column: Option, + + /// The name of the column to use as the card title in grid view. + /// + /// If unset, the first visible string column is used as the title. + pub grid_view_card_title: Option, + + /// The name of the column containing URLs to open when a card is clicked in grid view. + /// + /// If unset, defaults to the segment preview column. + pub url_column: Option, +} + +impl TableBlueprint { + /// Returns the [`ComponentDescriptor`] for [`Self::segment_preview_column`]. + /// + /// The corresponding component is [`crate::blueprint::components::ColumnName`]. + #[inline] + pub fn descriptor_segment_preview_column() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TableBlueprint".into()), + component: "TableBlueprint:segment_preview_column".into(), + component_type: Some("rerun.blueprint.components.ColumnName".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::flag_column`]. + /// + /// The corresponding component is [`crate::blueprint::components::ColumnName`]. + #[inline] + pub fn descriptor_flag_column() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TableBlueprint".into()), + component: "TableBlueprint:flag_column".into(), + component_type: Some("rerun.blueprint.components.ColumnName".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::grid_view_card_title`]. + /// + /// The corresponding component is [`crate::blueprint::components::ColumnName`]. + #[inline] + pub fn descriptor_grid_view_card_title() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TableBlueprint".into()), + component: "TableBlueprint:grid_view_card_title".into(), + component_type: Some("rerun.blueprint.components.ColumnName".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::url_column`]. + /// + /// The corresponding component is [`crate::blueprint::components::ColumnName`]. + #[inline] + pub fn descriptor_url_column() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TableBlueprint".into()), + component: "TableBlueprint:url_column".into(), + component_type: Some("rerun.blueprint.components.ColumnName".into()), + }); + (*DESCRIPTOR).clone() + } +} + +static REQUIRED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static RECOMMENDED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 4usize]> = + std::sync::LazyLock::new(|| { + [ + TableBlueprint::descriptor_segment_preview_column(), + TableBlueprint::descriptor_flag_column(), + TableBlueprint::descriptor_grid_view_card_title(), + TableBlueprint::descriptor_url_column(), + ] + }); + +static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 4usize]> = + std::sync::LazyLock::new(|| { + [ + TableBlueprint::descriptor_segment_preview_column(), + TableBlueprint::descriptor_flag_column(), + TableBlueprint::descriptor_grid_view_card_title(), + TableBlueprint::descriptor_url_column(), + ] + }); + +impl TableBlueprint { + /// The total number of components in the archetype: 0 required, 0 recommended, 4 optional + pub const NUM_COMPONENTS: usize = 4usize; +} + +impl ::re_types_core::Archetype for TableBlueprint { + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TableBlueprint" + ) + } + + #[inline] + fn display_name() -> &'static str { + "Table blueprint" + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow_components( + arrow_data: impl IntoIterator, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_descr: ::nohash_hasher::IntMap<_, _> = arrow_data.into_iter().collect(); + let segment_preview_column = arrays_by_descr + .get(&Self::descriptor_segment_preview_column()) + .map(|array| { + SerializedComponentBatch::new( + array.clone(), + Self::descriptor_segment_preview_column(), + ) + }); + let flag_column = arrays_by_descr + .get(&Self::descriptor_flag_column()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_flag_column()) + }); + let grid_view_card_title = arrays_by_descr + .get(&Self::descriptor_grid_view_card_title()) + .map(|array| { + SerializedComponentBatch::new( + array.clone(), + Self::descriptor_grid_view_card_title(), + ) + }); + let url_column = arrays_by_descr + .get(&Self::descriptor_url_column()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_url_column()) + }); + Ok(Self { + segment_preview_column, + flag_column, + grid_view_card_title, + url_column, + }) + } +} + +impl ::re_types_core::AsComponents for TableBlueprint { + #[inline] + fn as_serialized_batches(&self) -> Vec { + use ::re_types_core::Archetype as _; + [ + self.segment_preview_column.clone(), + self.flag_column.clone(), + self.grid_view_card_title.clone(), + self.url_column.clone(), + ] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for TableBlueprint {} + +impl TableBlueprint { + /// Create a new `TableBlueprint`. + #[inline] + pub fn new() -> Self { + Self { + segment_preview_column: None, + flag_column: None, + grid_view_card_title: None, + url_column: None, + } + } + + /// Update only some specific fields of a `TableBlueprint`. + #[inline] + pub fn update_fields() -> Self { + Self::default() + } + + /// Clear all the fields of a `TableBlueprint`. + #[inline] + pub fn clear_fields() -> Self { + use ::re_types_core::Loggable as _; + Self { + segment_preview_column: Some(SerializedComponentBatch::new( + crate::blueprint::components::ColumnName::arrow_empty(), + Self::descriptor_segment_preview_column(), + )), + flag_column: Some(SerializedComponentBatch::new( + crate::blueprint::components::ColumnName::arrow_empty(), + Self::descriptor_flag_column(), + )), + grid_view_card_title: Some(SerializedComponentBatch::new( + crate::blueprint::components::ColumnName::arrow_empty(), + Self::descriptor_grid_view_card_title(), + )), + url_column: Some(SerializedComponentBatch::new( + crate::blueprint::components::ColumnName::arrow_empty(), + Self::descriptor_url_column(), + )), + } + } + + /// The name of the column that contains recording URIs for segment previews. + /// + /// Every row can at most preview a single segment. + /// + /// For the preview, the rest of the blueprint data is read it as it would be with regular recording blueprints, + /// meaning that the regular structure of [`archetypes::ViewportBlueprint`][crate::blueprint::archetypes::ViewportBlueprint], and [`archetypes::ViewBlueprint`][crate::blueprint::archetypes::ViewBlueprint] structure applies. + /// However, this mostly ignores layout container types as well as automatic spawning. + /// + /// If unset, defaults to the first URL column in the table that points to the same Rerun server + #[inline] + pub fn with_segment_preview_column( + mut self, + segment_preview_column: impl Into, + ) -> Self { + self.segment_preview_column = try_serialize_field( + Self::descriptor_segment_preview_column(), + [segment_preview_column], + ); + self + } + + /// The name of the boolean column used for flag/annotation toggles. + /// + /// Must be set for flagging to be available. The named column must exist in the + /// table and be of boolean type. + /// Additionally, the table must be remote and have another column with + /// `rerun:is_table_index` metadata since flag changes are persisted to the server + /// via upsert. + #[inline] + pub fn with_flag_column( + mut self, + flag_column: impl Into, + ) -> Self { + self.flag_column = try_serialize_field(Self::descriptor_flag_column(), [flag_column]); + self + } + + /// The name of the column to use as the card title in grid view. + /// + /// If unset, the first visible string column is used as the title. + #[inline] + pub fn with_grid_view_card_title( + mut self, + grid_view_card_title: impl Into, + ) -> Self { + self.grid_view_card_title = try_serialize_field( + Self::descriptor_grid_view_card_title(), + [grid_view_card_title], + ); + self + } + + /// The name of the column containing URLs to open when a card is clicked in grid view. + /// + /// If unset, defaults to the segment preview column. + #[inline] + pub fn with_url_column( + mut self, + url_column: impl Into, + ) -> Self { + self.url_column = try_serialize_field(Self::descriptor_url_column(), [url_column]); + self + } +} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_scalar_mapping.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_scalar_mapping.rs index 9f5171bc5d77..d6851d50c191 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_scalar_mapping.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_scalar_mapping.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configures how tensor scalars are mapped to color. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct TensorScalarMapping { /// Filter used when zooming in on the tensor. /// @@ -50,11 +51,13 @@ impl TensorScalarMapping { /// The corresponding component is [`crate::components::MagnificationFilter`]. #[inline] pub fn descriptor_mag_filter() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TensorScalarMapping".into()), - component: "TensorScalarMapping:mag_filter".into(), - component_type: Some("rerun.components.MagnificationFilter".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TensorScalarMapping".into()), + component: "TensorScalarMapping:mag_filter".into(), + component_type: Some("rerun.components.MagnificationFilter".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::colormap`]. @@ -62,11 +65,13 @@ impl TensorScalarMapping { /// The corresponding component is [`crate::components::Colormap`]. #[inline] pub fn descriptor_colormap() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TensorScalarMapping".into()), - component: "TensorScalarMapping:colormap".into(), - component_type: Some("rerun.components.Colormap".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TensorScalarMapping".into()), + component: "TensorScalarMapping:colormap".into(), + component_type: Some("rerun.components.Colormap".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::gamma`]. @@ -74,11 +79,13 @@ impl TensorScalarMapping { /// The corresponding component is [`crate::components::GammaCorrection`]. #[inline] pub fn descriptor_gamma() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TensorScalarMapping".into()), - component: "TensorScalarMapping:gamma".into(), - component_type: Some("rerun.components.GammaCorrection".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TensorScalarMapping".into()), + component: "TensorScalarMapping:gamma".into(), + component_type: Some("rerun.components.GammaCorrection".into()), + }); + (*DESCRIPTOR).clone() } } @@ -114,7 +121,10 @@ impl TensorScalarMapping { impl ::re_types_core::Archetype for TensorScalarMapping { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.TensorScalarMapping".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TensorScalarMapping" + ) } #[inline] @@ -254,12 +264,3 @@ impl TensorScalarMapping { self } } - -impl ::re_byte_size::SizeBytes for TensorScalarMapping { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.mag_filter.heap_size_bytes() - + self.colormap.heap_size_bytes() - + self.gamma.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_slice_selection.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_slice_selection.rs index 80d05622d126..380aac08e848 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_slice_selection.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_slice_selection.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Specifies a 2D slice of a tensor. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct TensorSliceSelection { /// Which dimension to map to width. /// @@ -55,11 +56,13 @@ impl TensorSliceSelection { /// The corresponding component is [`crate::components::TensorWidthDimension`]. #[inline] pub fn descriptor_width() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TensorSliceSelection".into()), - component: "TensorSliceSelection:width".into(), - component_type: Some("rerun.components.TensorWidthDimension".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TensorSliceSelection".into()), + component: "TensorSliceSelection:width".into(), + component_type: Some("rerun.components.TensorWidthDimension".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::height`]. @@ -67,11 +70,13 @@ impl TensorSliceSelection { /// The corresponding component is [`crate::components::TensorHeightDimension`]. #[inline] pub fn descriptor_height() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TensorSliceSelection".into()), - component: "TensorSliceSelection:height".into(), - component_type: Some("rerun.components.TensorHeightDimension".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TensorSliceSelection".into()), + component: "TensorSliceSelection:height".into(), + component_type: Some("rerun.components.TensorHeightDimension".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::indices`]. @@ -79,11 +84,13 @@ impl TensorSliceSelection { /// The corresponding component is [`crate::components::TensorDimensionIndexSelection`]. #[inline] pub fn descriptor_indices() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TensorSliceSelection".into()), - component: "TensorSliceSelection:indices".into(), - component_type: Some("rerun.components.TensorDimensionIndexSelection".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TensorSliceSelection".into()), + component: "TensorSliceSelection:indices".into(), + component_type: Some("rerun.components.TensorDimensionIndexSelection".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::slider`]. @@ -91,11 +98,15 @@ impl TensorSliceSelection { /// The corresponding component is [`crate::blueprint::components::TensorDimensionIndexSlider`]. #[inline] pub fn descriptor_slider() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TensorSliceSelection".into()), - component: "TensorSliceSelection:slider".into(), - component_type: Some("rerun.blueprint.components.TensorDimensionIndexSlider".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TensorSliceSelection".into()), + component: "TensorSliceSelection:slider".into(), + component_type: Some( + "rerun.blueprint.components.TensorDimensionIndexSlider".into(), + ), + }); + (*DESCRIPTOR).clone() } } @@ -133,7 +144,10 @@ impl TensorSliceSelection { impl ::re_types_core::Archetype for TensorSliceSelection { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.TensorSliceSelection".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TensorSliceSelection" + ) } #[inline] @@ -298,13 +312,3 @@ impl TensorSliceSelection { self } } - -impl ::re_byte_size::SizeBytes for TensorSliceSelection { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.width.heap_size_bytes() - + self.height.heap_size_bytes() - + self.indices.heap_size_bytes() - + self.slider.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_view_fit.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_view_fit.rs index 6437b73c8ddf..11fe42b35a37 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_view_fit.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/tensor_view_fit.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configures how a selected tensor slice is shown on screen. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct TensorViewFit { /// How the image is scaled to fit the view. pub scaling: Option, @@ -36,11 +37,13 @@ impl TensorViewFit { /// The corresponding component is [`crate::blueprint::components::ViewFit`]. #[inline] pub fn descriptor_scaling() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TensorViewFit".into()), - component: "TensorViewFit:scaling".into(), - component_type: Some("rerun.blueprint.components.ViewFit".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TensorViewFit".into()), + component: "TensorViewFit:scaling".into(), + component_type: Some("rerun.blueprint.components.ViewFit".into()), + }); + (*DESCRIPTOR).clone() } } @@ -64,7 +67,10 @@ impl TensorViewFit { impl ::re_types_core::Archetype for TensorViewFit { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.TensorViewFit".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TensorViewFit" + ) } #[inline] @@ -151,10 +157,3 @@ impl TensorViewFit { self } } - -impl ::re_byte_size::SizeBytes for TensorViewFit { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.scaling.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/text_document_format.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/text_document_format.rs new file mode 100644 index 000000000000..726aaa0f4132 --- /dev/null +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/text_document_format.rs @@ -0,0 +1,226 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/text_document_format.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: Formatting options for the text document view. +/// +/// These options only apply to plain text documents and have no effect on Markdown documents. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] +pub struct TextDocumentFormat { + /// Whether to use a monospace font for the document body. + /// + /// Defaults to disabled. + pub monospace: Option, + + /// Whether to wrap long lines in the document body. + /// + /// Defaults to enabled. + pub word_wrap: Option, +} + +impl TextDocumentFormat { + /// Returns the [`ComponentDescriptor`] for [`Self::monospace`]. + /// + /// The corresponding component is [`crate::blueprint::components::Enabled`]. + #[inline] + pub fn descriptor_monospace() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TextDocumentFormat".into()), + component: "TextDocumentFormat:monospace".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() + } + + /// Returns the [`ComponentDescriptor`] for [`Self::word_wrap`]. + /// + /// The corresponding component is [`crate::blueprint::components::Enabled`]. + #[inline] + pub fn descriptor_word_wrap() -> ComponentDescriptor { + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TextDocumentFormat".into()), + component: "TextDocumentFormat:word_wrap".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() + } +} + +static REQUIRED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static RECOMMENDED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 2usize]> = + std::sync::LazyLock::new(|| { + [ + TextDocumentFormat::descriptor_monospace(), + TextDocumentFormat::descriptor_word_wrap(), + ] + }); + +static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 2usize]> = + std::sync::LazyLock::new(|| { + [ + TextDocumentFormat::descriptor_monospace(), + TextDocumentFormat::descriptor_word_wrap(), + ] + }); + +impl TextDocumentFormat { + /// The total number of components in the archetype: 0 required, 0 recommended, 2 optional + pub const NUM_COMPONENTS: usize = 2usize; +} + +impl ::re_types_core::Archetype for TextDocumentFormat { + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TextDocumentFormat" + ) + } + + #[inline] + fn display_name() -> &'static str { + "Text document format" + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow_components( + arrow_data: impl IntoIterator, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_descr: ::nohash_hasher::IntMap<_, _> = arrow_data.into_iter().collect(); + let monospace = arrays_by_descr + .get(&Self::descriptor_monospace()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_monospace()) + }); + let word_wrap = arrays_by_descr + .get(&Self::descriptor_word_wrap()) + .map(|array| { + SerializedComponentBatch::new(array.clone(), Self::descriptor_word_wrap()) + }); + Ok(Self { + monospace, + word_wrap, + }) + } +} + +impl ::re_types_core::AsComponents for TextDocumentFormat { + #[inline] + fn as_serialized_batches(&self) -> Vec { + use ::re_types_core::Archetype as _; + [self.monospace.clone(), self.word_wrap.clone()] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for TextDocumentFormat {} + +impl TextDocumentFormat { + /// Create a new `TextDocumentFormat`. + #[inline] + pub fn new() -> Self { + Self { + monospace: None, + word_wrap: None, + } + } + + /// Update only some specific fields of a `TextDocumentFormat`. + #[inline] + pub fn update_fields() -> Self { + Self::default() + } + + /// Clear all the fields of a `TextDocumentFormat`. + #[inline] + pub fn clear_fields() -> Self { + use ::re_types_core::Loggable as _; + Self { + monospace: Some(SerializedComponentBatch::new( + crate::blueprint::components::Enabled::arrow_empty(), + Self::descriptor_monospace(), + )), + word_wrap: Some(SerializedComponentBatch::new( + crate::blueprint::components::Enabled::arrow_empty(), + Self::descriptor_word_wrap(), + )), + } + } + + /// Whether to use a monospace font for the document body. + /// + /// Defaults to disabled. + #[inline] + pub fn with_monospace( + mut self, + monospace: impl Into, + ) -> Self { + self.monospace = try_serialize_field(Self::descriptor_monospace(), [monospace]); + self + } + + /// Whether to wrap long lines in the document body. + /// + /// Defaults to enabled. + #[inline] + pub fn with_word_wrap( + mut self, + word_wrap: impl Into, + ) -> Self { + self.word_wrap = try_serialize_field(Self::descriptor_word_wrap(), [word_wrap]); + self + } +} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_columns.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_columns.rs index 26e96b58205b..24e70756a717 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_columns.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_columns.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration of the text log columns. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct TextLogColumns { /// What timeline columns to show. /// @@ -43,11 +44,13 @@ impl TextLogColumns { /// The corresponding component is [`crate::blueprint::components::TimelineColumn`]. #[inline] pub fn descriptor_timeline_columns() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TextLogColumns".into()), - component: "TextLogColumns:timeline_columns".into(), - component_type: Some("rerun.blueprint.components.TimelineColumn".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TextLogColumns".into()), + component: "TextLogColumns:timeline_columns".into(), + component_type: Some("rerun.blueprint.components.TimelineColumn".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::text_log_columns`]. @@ -55,11 +58,13 @@ impl TextLogColumns { /// The corresponding component is [`crate::blueprint::components::TextLogColumn`]. #[inline] pub fn descriptor_text_log_columns() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TextLogColumns".into()), - component: "TextLogColumns:text_log_columns".into(), - component_type: Some("rerun.blueprint.components.TextLogColumn".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TextLogColumns".into()), + component: "TextLogColumns:text_log_columns".into(), + component_type: Some("rerun.blueprint.components.TextLogColumn".into()), + }); + (*DESCRIPTOR).clone() } } @@ -93,7 +98,10 @@ impl TextLogColumns { impl ::re_types_core::Archetype for TextLogColumns { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.TextLogColumns".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TextLogColumns" + ) } #[inline] @@ -220,10 +228,3 @@ impl TextLogColumns { self } } - -impl ::re_byte_size::SizeBytes for TextLogColumns { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.timeline_columns.heap_size_bytes() + self.text_log_columns.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_format.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_format.rs index 9bd48110c06a..a31673823496 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_format.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_format.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration of the text log rows. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct TextLogFormat { /// Whether to use a monospace font for the log message body. /// @@ -38,11 +39,13 @@ impl TextLogFormat { /// The corresponding component is [`crate::blueprint::components::Enabled`]. #[inline] pub fn descriptor_monospace_body() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TextLogFormat".into()), - component: "TextLogFormat:monospace_body".into(), - component_type: Some("rerun.blueprint.components.Enabled".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TextLogFormat".into()), + component: "TextLogFormat:monospace_body".into(), + component_type: Some("rerun.blueprint.components.Enabled".into()), + }); + (*DESCRIPTOR).clone() } } @@ -66,7 +69,10 @@ impl TextLogFormat { impl ::re_types_core::Archetype for TextLogFormat { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.TextLogFormat".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TextLogFormat" + ) } #[inline] @@ -162,10 +168,3 @@ impl TextLogFormat { self } } - -impl ::re_byte_size::SizeBytes for TextLogFormat { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.monospace_body.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_rows.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_rows.rs index 75f77dbeaced..3216649f60fb 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_rows.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/text_log_rows.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration of the text log rows. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct TextLogRows { /// Log levels to display. /// @@ -38,11 +39,13 @@ impl TextLogRows { /// The corresponding component is [`crate::components::TextLogLevel`]. #[inline] pub fn descriptor_filter_by_log_level() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TextLogRows".into()), - component: "TextLogRows:filter_by_log_level".into(), - component_type: Some("rerun.components.TextLogLevel".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TextLogRows".into()), + component: "TextLogRows:filter_by_log_level".into(), + component_type: Some("rerun.components.TextLogLevel".into()), + }); + (*DESCRIPTOR).clone() } } @@ -66,7 +69,10 @@ impl TextLogRows { impl ::re_types_core::Archetype for TextLogRows { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.TextLogRows".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TextLogRows" + ) } #[inline] @@ -164,10 +170,3 @@ impl TextLogRows { self } } - -impl ::re_byte_size::SizeBytes for TextLogRows { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.filter_by_log_level.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/time_axis.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/time_axis.rs index 28b31eb76034..61f9e5ea5ec3 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/time_axis.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/time_axis.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Configuration for the time (X) axis of a plot. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct TimeAxis { /// How should the horizontal/X/time axis be linked across multiple plots? /// @@ -44,11 +45,13 @@ impl TimeAxis { /// The corresponding component is [`crate::blueprint::components::LinkAxis`]. #[inline] pub fn descriptor_link() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimeAxis".into()), - component: "TimeAxis:link".into(), - component_type: Some("rerun.blueprint.components.LinkAxis".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimeAxis".into()), + component: "TimeAxis:link".into(), + component_type: Some("rerun.blueprint.components.LinkAxis".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::view_range`]. @@ -56,11 +59,13 @@ impl TimeAxis { /// The corresponding component is [`crate::blueprint::components::TimeRange`]. #[inline] pub fn descriptor_view_range() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimeAxis".into()), - component: "TimeAxis:view_range".into(), - component_type: Some("rerun.blueprint.components.TimeRange".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimeAxis".into()), + component: "TimeAxis:view_range".into(), + component_type: Some("rerun.blueprint.components.TimeRange".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::zoom_lock`]. @@ -68,11 +73,13 @@ impl TimeAxis { /// The corresponding component is [`crate::blueprint::components::LockRangeDuringZoom`]. #[inline] pub fn descriptor_zoom_lock() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimeAxis".into()), - component: "TimeAxis:zoom_lock".into(), - component_type: Some("rerun.blueprint.components.LockRangeDuringZoom".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimeAxis".into()), + component: "TimeAxis:zoom_lock".into(), + component_type: Some("rerun.blueprint.components.LockRangeDuringZoom".into()), + }); + (*DESCRIPTOR).clone() } } @@ -108,7 +115,10 @@ impl TimeAxis { impl ::re_types_core::Archetype for TimeAxis { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.TimeAxis".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TimeAxis" + ) } #[inline] @@ -247,12 +257,3 @@ impl TimeAxis { self } } - -impl ::re_byte_size::SizeBytes for TimeAxis { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.link.heap_size_bytes() - + self.view_range.heap_size_bytes() - + self.zoom_lock.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/time_panel_blueprint.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/time_panel_blueprint.rs index bbcd6c302743..5e2e7a17a671 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/time_panel_blueprint.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/time_panel_blueprint.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: Time panel specific state. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct TimePanelBlueprint { /// Current state of the panel. pub state: Option, @@ -58,11 +59,13 @@ impl TimePanelBlueprint { /// The corresponding component is [`crate::blueprint::components::PanelState`]. #[inline] pub fn descriptor_state() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), - component: "TimePanelBlueprint:state".into(), - component_type: Some("rerun.blueprint.components.PanelState".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:state".into(), + component_type: Some("rerun.blueprint.components.PanelState".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::timeline`]. @@ -70,11 +73,13 @@ impl TimePanelBlueprint { /// The corresponding component is [`crate::blueprint::components::TimelineName`]. #[inline] pub fn descriptor_timeline() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), - component: "TimePanelBlueprint:timeline".into(), - component_type: Some("rerun.blueprint.components.TimelineName".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:timeline".into(), + component_type: Some("rerun.blueprint.components.TimelineName".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::playback_speed`]. @@ -82,11 +87,13 @@ impl TimePanelBlueprint { /// The corresponding component is [`crate::blueprint::components::PlaybackSpeed`]. #[inline] pub fn descriptor_playback_speed() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), - component: "TimePanelBlueprint:playback_speed".into(), - component_type: Some("rerun.blueprint.components.PlaybackSpeed".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:playback_speed".into(), + component_type: Some("rerun.blueprint.components.PlaybackSpeed".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fps`]. @@ -94,11 +101,13 @@ impl TimePanelBlueprint { /// The corresponding component is [`crate::blueprint::components::Fps`]. #[inline] pub fn descriptor_fps() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), - component: "TimePanelBlueprint:fps".into(), - component_type: Some("rerun.blueprint.components.Fps".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:fps".into(), + component_type: Some("rerun.blueprint.components.Fps".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::play_state`]. @@ -106,11 +115,13 @@ impl TimePanelBlueprint { /// The corresponding component is [`crate::blueprint::components::PlayState`]. #[inline] pub fn descriptor_play_state() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), - component: "TimePanelBlueprint:play_state".into(), - component_type: Some("rerun.blueprint.components.PlayState".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:play_state".into(), + component_type: Some("rerun.blueprint.components.PlayState".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::loop_mode`]. @@ -118,11 +129,13 @@ impl TimePanelBlueprint { /// The corresponding component is [`crate::blueprint::components::LoopMode`]. #[inline] pub fn descriptor_loop_mode() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), - component: "TimePanelBlueprint:loop_mode".into(), - component_type: Some("rerun.blueprint.components.LoopMode".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:loop_mode".into(), + component_type: Some("rerun.blueprint.components.LoopMode".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::time_selection`]. @@ -130,11 +143,13 @@ impl TimePanelBlueprint { /// The corresponding component is [`crate::blueprint::components::AbsoluteTimeRange`]. #[inline] pub fn descriptor_time_selection() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), - component: "TimePanelBlueprint:time_selection".into(), - component_type: Some("rerun.blueprint.components.AbsoluteTimeRange".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:time_selection".into(), + component_type: Some("rerun.blueprint.components.AbsoluteTimeRange".into()), + }); + (*DESCRIPTOR).clone() } } @@ -178,7 +193,10 @@ impl TimePanelBlueprint { impl ::re_types_core::Archetype for TimePanelBlueprint { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.TimePanelBlueprint".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.TimePanelBlueprint" + ) } #[inline] @@ -405,16 +423,3 @@ impl TimePanelBlueprint { self } } - -impl ::re_byte_size::SizeBytes for TimePanelBlueprint { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.state.heap_size_bytes() - + self.timeline.heap_size_bytes() - + self.playback_speed.heap_size_bytes() - + self.fps.heap_size_bytes() - + self.play_state.heap_size_bytes() - + self.loop_mode.heap_size_bytes() - + self.time_selection.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/view_blueprint.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/view_blueprint.rs index 2c945def97e0..6cf742e5afd5 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/view_blueprint.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/view_blueprint.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: The description of a single view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ViewBlueprint { /// The class of the view. pub class_identifier: Option, @@ -55,11 +56,13 @@ impl ViewBlueprint { /// The corresponding component is [`crate::blueprint::components::ViewClass`]. #[inline] pub fn descriptor_class_identifier() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewBlueprint".into()), - component: "ViewBlueprint:class_identifier".into(), - component_type: Some("rerun.blueprint.components.ViewClass".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewBlueprint".into()), + component: "ViewBlueprint:class_identifier".into(), + component_type: Some("rerun.blueprint.components.ViewClass".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::display_name`]. @@ -67,11 +70,13 @@ impl ViewBlueprint { /// The corresponding component is [`crate::components::Name`]. #[inline] pub fn descriptor_display_name() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewBlueprint".into()), - component: "ViewBlueprint:display_name".into(), - component_type: Some("rerun.components.Name".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewBlueprint".into()), + component: "ViewBlueprint:display_name".into(), + component_type: Some("rerun.components.Name".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::space_origin`]. @@ -79,11 +84,13 @@ impl ViewBlueprint { /// The corresponding component is [`crate::blueprint::components::ViewOrigin`]. #[inline] pub fn descriptor_space_origin() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewBlueprint".into()), - component: "ViewBlueprint:space_origin".into(), - component_type: Some("rerun.blueprint.components.ViewOrigin".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewBlueprint".into()), + component: "ViewBlueprint:space_origin".into(), + component_type: Some("rerun.blueprint.components.ViewOrigin".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::visible`]. @@ -91,11 +98,13 @@ impl ViewBlueprint { /// The corresponding component is [`crate::components::Visible`]. #[inline] pub fn descriptor_visible() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewBlueprint".into()), - component: "ViewBlueprint:visible".into(), - component_type: Some("rerun.components.Visible".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewBlueprint".into()), + component: "ViewBlueprint:visible".into(), + component_type: Some("rerun.components.Visible".into()), + }); + (*DESCRIPTOR).clone() } } @@ -132,7 +141,10 @@ impl ViewBlueprint { impl ::re_types_core::Archetype for ViewBlueprint { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ViewBlueprint".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ViewBlueprint" + ) } #[inline] @@ -302,13 +314,3 @@ impl ViewBlueprint { self } } - -impl ::re_byte_size::SizeBytes for ViewBlueprint { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.class_identifier.heap_size_bytes() - + self.display_name.heap_size_bytes() - + self.space_origin.heap_size_bytes() - + self.visible.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/view_contents.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/view_contents.rs index 073ac9f051b8..00abe073a6c4 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/view_contents.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/view_contents.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -61,7 +62,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// The last rule matching `/world/house` is `+ /world/**`, so it is included. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ViewContents { /// The `QueryExpression` that populates the contents for the view. /// @@ -75,11 +76,13 @@ impl ViewContents { /// The corresponding component is [`crate::blueprint::components::QueryExpression`]. #[inline] pub fn descriptor_query() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewContents".into()), - component: "ViewContents:query".into(), - component_type: Some("rerun.blueprint.components.QueryExpression".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewContents".into()), + component: "ViewContents:query".into(), + component_type: Some("rerun.blueprint.components.QueryExpression".into()), + }); + (*DESCRIPTOR).clone() } } @@ -103,7 +106,10 @@ impl ViewContents { impl ::re_types_core::Archetype for ViewContents { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ViewContents".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ViewContents" + ) } #[inline] @@ -196,10 +202,3 @@ impl ViewContents { self } } - -impl ::re_byte_size::SizeBytes for ViewContents { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.query.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/viewport_blueprint.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/viewport_blueprint.rs index 1cf5738345f6..8472c5327480 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/viewport_blueprint.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/viewport_blueprint.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: The top-level description of the viewport. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ViewportBlueprint { /// The layout of the views pub root_container: Option, @@ -60,11 +61,13 @@ impl ViewportBlueprint { /// The corresponding component is [`crate::blueprint::components::RootContainer`]. #[inline] pub fn descriptor_root_container() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), - component: "ViewportBlueprint:root_container".into(), - component_type: Some("rerun.blueprint.components.RootContainer".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), + component: "ViewportBlueprint:root_container".into(), + component_type: Some("rerun.blueprint.components.RootContainer".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::maximized`]. @@ -72,11 +75,13 @@ impl ViewportBlueprint { /// The corresponding component is [`crate::blueprint::components::ViewMaximized`]. #[inline] pub fn descriptor_maximized() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), - component: "ViewportBlueprint:maximized".into(), - component_type: Some("rerun.blueprint.components.ViewMaximized".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), + component: "ViewportBlueprint:maximized".into(), + component_type: Some("rerun.blueprint.components.ViewMaximized".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::auto_layout`]. @@ -84,11 +89,13 @@ impl ViewportBlueprint { /// The corresponding component is [`crate::blueprint::components::AutoLayout`]. #[inline] pub fn descriptor_auto_layout() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), - component: "ViewportBlueprint:auto_layout".into(), - component_type: Some("rerun.blueprint.components.AutoLayout".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), + component: "ViewportBlueprint:auto_layout".into(), + component_type: Some("rerun.blueprint.components.AutoLayout".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::auto_views`]. @@ -96,11 +103,13 @@ impl ViewportBlueprint { /// The corresponding component is [`crate::blueprint::components::AutoViews`]. #[inline] pub fn descriptor_auto_views() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), - component: "ViewportBlueprint:auto_views".into(), - component_type: Some("rerun.blueprint.components.AutoViews".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), + component: "ViewportBlueprint:auto_views".into(), + component_type: Some("rerun.blueprint.components.AutoViews".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::past_viewer_recommendations`]. @@ -108,11 +117,13 @@ impl ViewportBlueprint { /// The corresponding component is [`crate::blueprint::components::ViewerRecommendationHash`]. #[inline] pub fn descriptor_past_viewer_recommendations() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), - component: "ViewportBlueprint:past_viewer_recommendations".into(), - component_type: Some("rerun.blueprint.components.ViewerRecommendationHash".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.ViewportBlueprint".into()), + component: "ViewportBlueprint:past_viewer_recommendations".into(), + component_type: Some("rerun.blueprint.components.ViewerRecommendationHash".into()), + }); + (*DESCRIPTOR).clone() } } @@ -152,7 +163,10 @@ impl ViewportBlueprint { impl ::re_types_core::Archetype for ViewportBlueprint { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.ViewportBlueprint".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.ViewportBlueprint" + ) } #[inline] @@ -359,14 +373,3 @@ impl ViewportBlueprint { self } } - -impl ::re_byte_size::SizeBytes for ViewportBlueprint { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.root_container.heap_size_bytes() - + self.maximized.heap_size_bytes() - + self.auto_layout.heap_size_bytes() - + self.auto_views.heap_size_bytes() - + self.past_viewer_recommendations.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/visible_time_ranges.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/visible_time_ranges.rs index 7c9586cda718..0a427828ec25 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/visible_time_ranges.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/visible_time_ranges.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -32,7 +33,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// - For any other view, the default is to apply latest-at semantics. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct VisibleTimeRanges { /// The time ranges to show for each timeline unless specified otherwise on a per-entity basis. /// @@ -46,11 +47,13 @@ impl VisibleTimeRanges { /// The corresponding component is [`crate::blueprint::components::VisibleTimeRange`]. #[inline] pub fn descriptor_ranges() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.VisibleTimeRanges".into()), - component: "VisibleTimeRanges:ranges".into(), - component_type: Some("rerun.blueprint.components.VisibleTimeRange".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.VisibleTimeRanges".into()), + component: "VisibleTimeRanges:ranges".into(), + component_type: Some("rerun.blueprint.components.VisibleTimeRange".into()), + }); + (*DESCRIPTOR).clone() } } @@ -74,7 +77,10 @@ impl VisibleTimeRanges { impl ::re_types_core::Archetype for VisibleTimeRanges { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.VisibleTimeRanges".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.VisibleTimeRanges" + ) } #[inline] @@ -167,10 +173,3 @@ impl VisibleTimeRanges { self } } - -impl ::re_byte_size::SizeBytes for VisibleTimeRanges { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.ranges.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/visual_bounds2d.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/visual_bounds2d.rs index b23e5b0a96b3..ea0fa80c13b2 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/visual_bounds2d.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/visual_bounds2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// based on the bounding-box of the data or other camera information present in the view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct VisualBounds2D { /// Controls the visible range of a 2D view. /// @@ -44,11 +45,13 @@ impl VisualBounds2D { /// The corresponding component is [`crate::blueprint::components::VisualBounds2D`]. #[inline] pub fn descriptor_range() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.VisualBounds2D".into()), - component: "VisualBounds2D:range".into(), - component_type: Some("rerun.blueprint.components.VisualBounds2D".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.VisualBounds2D".into()), + component: "VisualBounds2D:range".into(), + component_type: Some("rerun.blueprint.components.VisualBounds2D".into()), + }); + (*DESCRIPTOR).clone() } } @@ -72,7 +75,10 @@ impl VisualBounds2D { impl ::re_types_core::Archetype for VisualBounds2D { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.VisualBounds2D".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.VisualBounds2D" + ) } #[inline] @@ -163,10 +169,3 @@ impl VisualBounds2D { self } } - -impl ::re_byte_size::SizeBytes for VisualBounds2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.range.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/archetypes/visualizer_instruction.rs b/crates/store/re_sdk_types/src/blueprint/archetypes/visualizer_instruction.rs index 01ef9647a157..1ab361b5ac61 100644 --- a/crates/store/re_sdk_types/src/blueprint/archetypes/visualizer_instruction.rs +++ b/crates/store/re_sdk_types/src/blueprint/archetypes/visualizer_instruction.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Archetype**: A visualizer instruction for an entity. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct VisualizerInstruction { /// The type of the visualizer. pub visualizer_type: Option, @@ -39,11 +40,13 @@ impl VisualizerInstruction { /// The corresponding component is [`crate::blueprint::components::VisualizerType`]. #[inline] pub fn descriptor_visualizer_type() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.VisualizerInstruction".into()), - component: "VisualizerInstruction:visualizer_type".into(), - component_type: Some("rerun.blueprint.components.VisualizerType".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.VisualizerInstruction".into()), + component: "VisualizerInstruction:visualizer_type".into(), + component_type: Some("rerun.blueprint.components.VisualizerType".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::component_map`]. @@ -51,11 +54,15 @@ impl VisualizerInstruction { /// The corresponding component is [`crate::blueprint::components::VisualizerComponentMapping`]. #[inline] pub fn descriptor_component_map() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.blueprint.archetypes.VisualizerInstruction".into()), - component: "VisualizerInstruction:component_map".into(), - component_type: Some("rerun.blueprint.components.VisualizerComponentMapping".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.VisualizerInstruction".into()), + component: "VisualizerInstruction:component_map".into(), + component_type: Some( + "rerun.blueprint.components.VisualizerComponentMapping".into(), + ), + }); + (*DESCRIPTOR).clone() } } @@ -84,7 +91,10 @@ impl VisualizerInstruction { impl ::re_types_core::Archetype for VisualizerInstruction { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.blueprint.archetypes.VisualizerInstruction".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.blueprint.archetypes.VisualizerInstruction" + ) } #[inline] @@ -207,10 +217,3 @@ impl VisualizerInstruction { self } } - -impl ::re_byte_size::SizeBytes for VisualizerInstruction { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.visualizer_type.heap_size_bytes() + self.component_map.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/.gitattributes b/crates/store/re_sdk_types/src/blueprint/components/.gitattributes index ade60b56d169..a638e3c2a6d8 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/.gitattributes +++ b/crates/store/re_sdk_types/src/blueprint/components/.gitattributes @@ -9,6 +9,7 @@ auto_layout.rs linguist-generated=true auto_scroll.rs linguist-generated=true auto_views.rs linguist-generated=true background_kind.rs linguist-generated=true +column_name.rs linguist-generated=true column_order.rs linguist-generated=true column_share.rs linguist-generated=true component_column_selector.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/blueprint/components/absolute_time_range.rs b/crates/store/re_sdk_types/src/blueprint/components/absolute_time_range.rs index d178899c2b78..9cad27c26e9f 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/absolute_time_range.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/absolute_time_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A reference to a range of time. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct AbsoluteTimeRange(pub crate::datatypes::AbsoluteTimeRange); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for AbsoluteTimeRange { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AbsoluteTimeRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/active_tab.rs b/crates/store/re_sdk_types/src/blueprint/components/active_tab.rs index 2e00b4349a9a..a0ec78f5436a 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/active_tab.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/active_tab.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The active tab in a tabbed container. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ActiveTab( /// Which tab is currently active. /// @@ -76,15 +77,3 @@ impl std::ops::DerefMut for ActiveTab { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ActiveTab { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/angular_speed.rs b/crates/store/re_sdk_types/src/blueprint/components/angular_speed.rs index 4a31b49dfadc..524269cac222 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/angular_speed.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/angular_speed.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Angular speed, used for rotation speed for example. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct AngularSpeed( /// Speed value in radians per second. pub crate::datatypes::Float64, @@ -74,15 +75,3 @@ impl std::ops::DerefMut for AngularSpeed { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AngularSpeed { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/apply_latest_at.rs b/crates/store/re_sdk_types/src/blueprint/components/apply_latest_at.rs index 94a047b7fe70..717cf3c7fc7a 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/apply_latest_at.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/apply_latest_at.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Whether empty cells in a dataframe should be filled with a latest-at query. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct ApplyLatestAt(pub crate::datatypes::Bool); @@ -72,15 +75,3 @@ impl std::ops::DerefMut for ApplyLatestAt { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ApplyLatestAt { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/auto_layout.rs b/crates/store/re_sdk_types/src/blueprint/components/auto_layout.rs index 6c9704015ce3..a21cac3496bd 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/auto_layout.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/auto_layout.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Whether the viewport layout is determined automatically. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy)] +#[derive(Clone, Debug, Copy, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct AutoLayout(pub crate::datatypes::Bool); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for AutoLayout { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AutoLayout { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/auto_scroll.rs b/crates/store/re_sdk_types/src/blueprint/components/auto_scroll.rs index 0b5708af44ce..75395d65eae1 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/auto_scroll.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/auto_scroll.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Whether the view should auto-scroll to follow the time cursor. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct AutoScroll(pub crate::datatypes::Bool); @@ -72,15 +75,3 @@ impl std::ops::DerefMut for AutoScroll { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AutoScroll { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/auto_views.rs b/crates/store/re_sdk_types/src/blueprint/components/auto_views.rs index ba158d2f3b17..93a37d5c4f02 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/auto_views.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/auto_views.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Whether or not views should be created automatically. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct AutoViews(pub crate::datatypes::Bool); @@ -72,15 +75,3 @@ impl std::ops::DerefMut for AutoViews { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AutoViews { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/background_kind.rs b/crates/store/re_sdk_types/src/blueprint/components/background_kind.rs index 263468719fb3..537c8c7b53ce 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/background_kind.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/background_kind.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The type of the background in a view. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum BackgroundKind { /// A dark gradient. @@ -164,15 +165,3 @@ impl ::re_types_core::reflection::Enum for BackgroundKind { .copied() } } - -impl ::re_byte_size::SizeBytes for BackgroundKind { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/column_name.rs b/crates/store/re_sdk_types/src/blueprint/components/column_name.rs new file mode 100644 index 000000000000..36932b6028de --- /dev/null +++ b/crates/store/re_sdk_types/src/blueprint/components/column_name.rs @@ -0,0 +1,75 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/column_name.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: The name of a column in a table. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ::re_byte_size::SizeBytes)] +#[repr(transparent)] +pub struct ColumnName(pub crate::datatypes::Utf8); + +impl ::re_types_core::WrapperComponent for ColumnName { + type Datatype = crate::datatypes::Utf8; + + #[inline] + fn name() -> ComponentType { + "rerun.blueprint.components.ColumnName".into() + } + + #[inline] + fn into_inner(self) -> Self::Datatype { + self.0 + } +} + +::re_types_core::macros::impl_into_cow!(ColumnName); + +impl> From for ColumnName { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for ColumnName { + #[inline] + fn borrow(&self) -> &crate::datatypes::Utf8 { + &self.0 + } +} + +impl std::ops::Deref for ColumnName { + type Target = crate::datatypes::Utf8; + + #[inline] + fn deref(&self) -> &crate::datatypes::Utf8 { + &self.0 + } +} + +impl std::ops::DerefMut for ColumnName { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Utf8 { + &mut self.0 + } +} diff --git a/crates/store/re_sdk_types/src/blueprint/components/column_order.rs b/crates/store/re_sdk_types/src/blueprint/components/column_order.rs index dcb128c23893..e1fc3afe6f6b 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/column_order.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/column_order.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Entities in this list that are not present in the view are ignored. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct ColumnOrder(pub Vec); impl ::re_types_core::Component for ColumnOrder { @@ -220,15 +221,3 @@ impl, T: IntoIterator> From f Self(v.into_iter().map(|v| v.into()).collect()) } } - -impl ::re_byte_size::SizeBytes for ColumnOrder { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/column_share.rs b/crates/store/re_sdk_types/src/blueprint/components/column_share.rs index 00923600e8c5..38318cc8cf2a 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/column_share.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/column_share.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The layout share of a column in the container. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ColumnShare( /// The layout shares of a column in the container. pub crate::datatypes::Float32, @@ -74,15 +75,3 @@ impl std::ops::DerefMut for ColumnShare { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ColumnShare { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/component_column_selector.rs b/crates/store/re_sdk_types/src/blueprint/components/component_column_selector.rs index e7c387f7c916..c98fcdde9093 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/component_column_selector.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/component_column_selector.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Describe a component column to be selected in the dataframe view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ComponentColumnSelector(pub crate::blueprint::datatypes::ComponentColumnSelector); @@ -76,15 +77,3 @@ impl std::ops::DerefMut for ComponentColumnSelector { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ComponentColumnSelector { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/container_kind.rs b/crates/store/re_sdk_types/src/blueprint/components/container_kind.rs index 52b88cc25b59..cdb846aa7951 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/container_kind.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/container_kind.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The kind of a blueprint container (tabs, grid, …). -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum ContainerKind { /// Put children in separate tabs @@ -161,15 +162,3 @@ impl ::re_types_core::reflection::Enum for ContainerKind { .copied() } } - -impl ::re_byte_size::SizeBytes for ContainerKind { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/corner2d.rs b/crates/store/re_sdk_types/src/blueprint/components/corner2d.rs index 27f9b39426f4..591365d4819b 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/corner2d.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/corner2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: One of four 2D corners, typically used to align objects. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum Corner2D { /// Left top corner. @@ -166,15 +167,3 @@ impl ::re_types_core::reflection::Enum for Corner2D { .copied() } } - -impl ::re_byte_size::SizeBytes for Corner2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/enabled.rs b/crates/store/re_sdk_types/src/blueprint/components/enabled.rs index 43419f7a45d4..3865136fbf9a 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/enabled.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/enabled.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Whether a procedure is enabled. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Enabled(pub crate::datatypes::Bool); @@ -72,15 +75,3 @@ impl std::ops::DerefMut for Enabled { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Enabled { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/eye3d_kind.rs b/crates/store/re_sdk_types/src/blueprint/components/eye3d_kind.rs index 86a53bc2ba1d..fdafc81bbee6 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/eye3d_kind.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/eye3d_kind.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The kind of the 3D eye to view a scene in a [`views::Spatial3DView`][crate::blueprint::views::Spatial3DView]. /// /// This is used to specify how the controls of the view react to user input (such as mouse gestures). -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum Eye3DKind { /// First person point of view. @@ -165,15 +166,3 @@ impl ::re_types_core::reflection::Enum for Eye3DKind { .copied() } } - -impl ::re_byte_size::SizeBytes for Eye3DKind { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/filter_by_range.rs b/crates/store/re_sdk_types/src/blueprint/components/filter_by_range.rs index da90ec12267e..ad4f6945a60c 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/filter_by_range.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/filter_by_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Configuration for a filter-by-range feature of the dataframe view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct FilterByRange(pub crate::blueprint::datatypes::FilterByRange); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for FilterByRange { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for FilterByRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/filter_is_not_null.rs b/crates/store/re_sdk_types/src/blueprint/components/filter_is_not_null.rs index 124b6b16392b..c9a37500d286 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/filter_is_not_null.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/filter_is_not_null.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Configuration for the filter is not null feature of the dataframe view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct FilterIsNotNull(pub crate::blueprint::datatypes::FilterIsNotNull); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for FilterIsNotNull { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for FilterIsNotNull { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/force_distance.rs b/crates/store/re_sdk_types/src/blueprint/components/force_distance.rs index 0dcfde5e2c7c..a70cdee3d15f 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/force_distance.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/force_distance.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// This is helpful to scale the layout, for example if long labels are involved. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, Copy, PartialEq)] +#[derive(Clone, Debug, Default, Copy, PartialEq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ForceDistance(pub crate::datatypes::Float64); @@ -74,15 +75,3 @@ impl std::ops::DerefMut for ForceDistance { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ForceDistance { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/force_iterations.rs b/crates/store/re_sdk_types/src/blueprint/components/force_iterations.rs index 62bc6bb643bc..2a55d512268c 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/force_iterations.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/force_iterations.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Increasing this parameter can lead to better results at the cost of longer computation time. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ForceIterations(pub crate::datatypes::UInt64); @@ -74,15 +75,3 @@ impl std::ops::DerefMut for ForceIterations { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ForceIterations { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/force_strength.rs b/crates/store/re_sdk_types/src/blueprint/components/force_strength.rs index 8697fbe1de06..2f3e47fb5269 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/force_strength.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/force_strength.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Allows to assign different weights to the individual forces, prioritizing one over the other. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, Copy, PartialEq)] +#[derive(Clone, Debug, Default, Copy, PartialEq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ForceStrength(pub crate::datatypes::Float64); @@ -74,15 +75,3 @@ impl std::ops::DerefMut for ForceStrength { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ForceStrength { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/fps.rs b/crates/store/re_sdk_types/src/blueprint/components/fps.rs index e793cac59b72..fc58a764c9dc 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/fps.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/fps.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Frames per second for a sequence timeline. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Fps(pub crate::datatypes::Float64); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for Fps { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Fps { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/grid_columns.rs b/crates/store/re_sdk_types/src/blueprint/components/grid_columns.rs index dffc66eb39ed..df6616bdd6be 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/grid_columns.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/grid_columns.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: How many columns a grid container should have. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] pub struct GridColumns( /// The number of columns. pub crate::datatypes::UInt32, @@ -74,15 +75,3 @@ impl std::ops::DerefMut for GridColumns { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for GridColumns { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/grid_spacing.rs b/crates/store/re_sdk_types/src/blueprint/components/grid_spacing.rs index 406294c60af6..f38ddff69b55 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/grid_spacing.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/grid_spacing.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Space between grid lines of one line to the next in scene units. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct GridSpacing( /// Space between grid lines of one line to the next in scene units. pub crate::datatypes::Float32, @@ -74,15 +75,3 @@ impl std::ops::DerefMut for GridSpacing { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for GridSpacing { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/included_content.rs b/crates/store/re_sdk_types/src/blueprint/components/included_content.rs index 7d22ea437333..4e7b7a19973b 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/included_content.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/included_content.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: All the contents in the container. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct IncludedContent( /// List of the contents by [`datatypes::EntityPath`][crate::datatypes::EntityPath]. /// @@ -77,15 +78,3 @@ impl std::ops::DerefMut for IncludedContent { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for IncludedContent { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/link_axis.rs b/crates/store/re_sdk_types/src/blueprint/components/link_axis.rs index eb0b80435aa1..2caa10ce6270 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/link_axis.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/link_axis.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: How should the horizontal/X/time axis be linked across multiple plots -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum LinkAxis { /// The axis is independent from all other plots. @@ -151,15 +152,3 @@ impl ::re_types_core::reflection::Enum for LinkAxis { .copied() } } - -impl ::re_byte_size::SizeBytes for LinkAxis { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/lock_range_during_zoom.rs b/crates/store/re_sdk_types/src/blueprint/components/lock_range_during_zoom.rs index 3c7f495bcb6b..f34cc1f7eb1c 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/lock_range_during_zoom.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/lock_range_during_zoom.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Default is `false`, i.e. zoom will change the visualized range. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct LockRangeDuringZoom(pub crate::datatypes::Bool); @@ -74,15 +77,3 @@ impl std::ops::DerefMut for LockRangeDuringZoom { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for LockRangeDuringZoom { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/loop_mode.rs b/crates/store/re_sdk_types/src/blueprint/components/loop_mode.rs index 9a482c8b8dd0..aa8cf513c704 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/loop_mode.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/loop_mode.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: If playing, whether and how the playback time should loop. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum LoopMode { /// Looping is off. @@ -158,15 +159,3 @@ impl ::re_types_core::reflection::Enum for LoopMode { .copied() } } - -impl ::re_byte_size::SizeBytes for LoopMode { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/map_provider.rs b/crates/store/re_sdk_types/src/blueprint/components/map_provider.rs index a7595f36e8a8..6ad63d9d57c6 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/map_provider.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/map_provider.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Name of the map provider to be used in Map views. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum MapProvider { /// `OpenStreetMap` is the default map provider. @@ -172,15 +173,3 @@ impl ::re_types_core::reflection::Enum for MapProvider { .copied() } } - -impl ::re_byte_size::SizeBytes for MapProvider { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/mod.rs b/crates/store/re_sdk_types/src/blueprint/components/mod.rs index fea4631c39da..f125be6251b0 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/mod.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/mod.rs @@ -9,6 +9,7 @@ mod auto_layout_ext; mod auto_scroll; mod auto_views; mod background_kind; +mod column_name; mod column_order; mod column_share; mod component_column_selector; @@ -78,6 +79,7 @@ pub use self::auto_layout::AutoLayout; pub use self::auto_scroll::AutoScroll; pub use self::auto_views::AutoViews; pub use self::background_kind::BackgroundKind; +pub use self::column_name::ColumnName; pub use self::column_order::ColumnOrder; pub use self::column_share::ColumnShare; pub use self::component_column_selector::ComponentColumnSelector; diff --git a/crates/store/re_sdk_types/src/blueprint/components/near_clip_plane.rs b/crates/store/re_sdk_types/src/blueprint/components/near_clip_plane.rs index 17f3ad4075a9..766a7c3c2e82 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/near_clip_plane.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/near_clip_plane.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Distance to the near clip plane used for `Spatial2DView`. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct NearClipPlane( /// Distance to the near clip plane in 3D scene units. @@ -75,15 +78,3 @@ impl std::ops::DerefMut for NearClipPlane { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for NearClipPlane { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/panel_state.rs b/crates/store/re_sdk_types/src/blueprint/components/panel_state.rs index dd46e666e868..22e13d6373bf 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/panel_state.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/panel_state.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Tri-state for panel controls. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum PanelState { /// Completely hidden. @@ -156,15 +157,3 @@ impl ::re_types_core::reflection::Enum for PanelState { .copied() } } - -impl ::re_byte_size::SizeBytes for PanelState { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/play_state.rs b/crates/store/re_sdk_types/src/blueprint/components/play_state.rs index c72506813d7f..066f407df185 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/play_state.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/play_state.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The current play state. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum PlayState { /// Time doesn't move. @@ -156,15 +157,3 @@ impl ::re_types_core::reflection::Enum for PlayState { .copied() } } - -impl ::re_byte_size::SizeBytes for PlayState { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/playback_speed.rs b/crates/store/re_sdk_types/src/blueprint/components/playback_speed.rs index c745e39bef26..ac991a9c7b7e 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/playback_speed.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/playback_speed.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A playback speed which determines how fast time progresses. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct PlaybackSpeed( /// 1.0 is default speed. @@ -75,15 +76,3 @@ impl std::ops::DerefMut for PlaybackSpeed { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for PlaybackSpeed { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/query_expression.rs b/crates/store/re_sdk_types/src/blueprint/components/query_expression.rs index 692732d71d73..f64c0cf865ab 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/query_expression.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/query_expression.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -33,7 +34,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Other uses of `*` are not (yet) supported. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct QueryExpression(pub crate::datatypes::Utf8); @@ -81,15 +82,3 @@ impl std::ops::DerefMut for QueryExpression { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for QueryExpression { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/root_container.rs b/crates/store/re_sdk_types/src/blueprint/components/root_container.rs index 82e0fdbd4bd5..1813df89716e 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/root_container.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/root_container.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The container that sits at the root of a viewport. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct RootContainer( /// `ContainerId` for the root. pub crate::datatypes::Uuid, @@ -74,15 +75,3 @@ impl std::ops::DerefMut for RootContainer { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for RootContainer { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/row_share.rs b/crates/store/re_sdk_types/src/blueprint/components/row_share.rs index a8a3499bd944..c6d8c2c98f74 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/row_share.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/row_share.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The layout share of a row in the container. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct RowShare( /// The layout share of a row in the container. pub crate::datatypes::Float32, @@ -74,15 +75,3 @@ impl std::ops::DerefMut for RowShare { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for RowShare { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/selected_columns.rs b/crates/store/re_sdk_types/src/blueprint/components/selected_columns.rs index 1078b71636a2..3404f3bd6286 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/selected_columns.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/selected_columns.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Describe a component column to be selected in the dataframe view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct SelectedColumns(pub crate::blueprint::datatypes::SelectedColumns); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for SelectedColumns { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for SelectedColumns { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/tensor_dimension_index_slider.rs b/crates/store/re_sdk_types/src/blueprint/components/tensor_dimension_index_slider.rs index d4eb02599d4e..990e47d0dc3c 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/tensor_dimension_index_slider.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/tensor_dimension_index_slider.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Show a slider for the index of some dimension of a slider. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TensorDimensionIndexSlider(pub crate::blueprint::datatypes::TensorDimensionIndexSlider); @@ -76,15 +77,3 @@ impl std::ops::DerefMut for TensorDimensionIndexSlider { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TensorDimensionIndexSlider { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/text_log_column.rs b/crates/store/re_sdk_types/src/blueprint/components/text_log_column.rs index 5a76668ed637..6513c4a3097e 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/text_log_column.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/text_log_column.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A text log column /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, Default, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Hash, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TextLogColumn( /// The text log column. @@ -75,15 +76,3 @@ impl std::ops::DerefMut for TextLogColumn { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TextLogColumn { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/time_int.rs b/crates/store/re_sdk_types/src/blueprint/components/time_int.rs index 0271468d3949..334775dde693 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/time_int.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/time_int.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A reference to a time. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TimeInt(pub crate::datatypes::TimeInt); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for TimeInt { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TimeInt { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/time_range.rs b/crates/store/re_sdk_types/src/blueprint/components/time_range.rs index 907a4581f3dc..5a41e34f0a13 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/time_range.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/time_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A time range on an unspecified timeline using either relative or absolute boundaries. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TimeRange(pub crate::datatypes::TimeRange); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for TimeRange { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TimeRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/timeline_column.rs b/crates/store/re_sdk_types/src/blueprint/components/timeline_column.rs index 910bc245bc0b..e2ab60b7d5df 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/timeline_column.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/timeline_column.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A timeline column in a text log table. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, Default, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Default, Hash, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TimelineColumn( /// The timeline column. @@ -75,15 +76,3 @@ impl std::ops::DerefMut for TimelineColumn { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TimelineColumn { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/timeline_name.rs b/crates/store/re_sdk_types/src/blueprint/components/timeline_name.rs index 508647a052c3..2a8ba5b0b9a1 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/timeline_name.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/timeline_name.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A timeline identified by its name. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TimelineName(pub crate::datatypes::Utf8); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for TimelineName { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TimelineName { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/timeline_name_ext.rs b/crates/store/re_sdk_types/src/blueprint/components/timeline_name_ext.rs index f1a1df4f04af..9829de2a81bd 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/timeline_name_ext.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/timeline_name_ext.rs @@ -3,15 +3,21 @@ use super::TimelineName; // Not needed as there is a blanket implementation for impl Into // impl From for TimelineName {} -impl From for re_log_types::TimelineName { - fn from(value: TimelineName) -> Self { - Self::from(value.as_str()) +// NOTE: fallible, because the blueprint component is a free-form string that can hold +// an empty value, which `re_log_types::TimelineName` forbids. +impl TryFrom for re_log_types::TimelineName { + type Error = re_types_core::InvalidTimelineNameError; + + fn try_from(value: TimelineName) -> Result { + Self::try_new(value.as_str()) } } -impl From<&TimelineName> for re_log_types::TimelineName { - fn from(value: &TimelineName) -> Self { - Self::from(value.as_str()) +impl TryFrom<&TimelineName> for re_log_types::TimelineName { + type Error = re_types_core::InvalidTimelineNameError; + + fn try_from(value: &TimelineName) -> Result { + Self::try_new(value.as_str()) } } @@ -20,10 +26,15 @@ impl TimelineName { pub fn from_timeline(timeline: &re_log_types::Timeline) -> Self { Self::from(timeline.name().as_str()) } + + /// The log time timeline (`"log_time"`). + pub fn log_time() -> Self { + Self::from(re_log_types::TimelineName::log_time().as_str()) + } } impl Default for TimelineName { fn default() -> Self { - Self::from("log_time") + Self::log_time() } } diff --git a/crates/store/re_sdk_types/src/blueprint/components/view_class.rs b/crates/store/re_sdk_types/src/blueprint/components/view_class.rs index deb040a8aea5..fce0a759122f 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/view_class.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/view_class.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The class identifier of view, e.g. `"2D"`, `"TextLog"`, …. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ViewClass(pub crate::datatypes::Utf8); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for ViewClass { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ViewClass { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/view_fit.rs b/crates/store/re_sdk_types/src/blueprint/components/view_fit.rs index c8407d0d427d..4197b1338626 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/view_fit.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/view_fit.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Determines whether an image or texture should be scaled to fit the viewport. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum ViewFit { /// No scaling, pixel size will match the image's width/height dimensions in pixels. @@ -160,15 +161,3 @@ impl ::re_types_core::reflection::Enum for ViewFit { .copied() } } - -impl ::re_byte_size::SizeBytes for ViewFit { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/view_maximized.rs b/crates/store/re_sdk_types/src/blueprint/components/view_maximized.rs index ae7bb29e1112..faf99efffcf3 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/view_maximized.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/view_maximized.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Whether a view is maximized. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ViewMaximized(pub crate::datatypes::Uuid); impl ::re_types_core::WrapperComponent for ViewMaximized { @@ -71,15 +72,3 @@ impl std::ops::DerefMut for ViewMaximized { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ViewMaximized { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/view_origin.rs b/crates/store/re_sdk_types/src/blueprint/components/view_origin.rs index 9beaa53d1dce..c5271c78d00e 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/view_origin.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/view_origin.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The origin of a view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ViewOrigin(pub crate::datatypes::EntityPath); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for ViewOrigin { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ViewOrigin { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/viewer_recommendation_hash.rs b/crates/store/re_sdk_types/src/blueprint/components/viewer_recommendation_hash.rs index eac0e799febd..72d947852d77 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/viewer_recommendation_hash.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/viewer_recommendation_hash.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// The formation of this hash is considered an internal implementation detail of the viewer. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ViewerRecommendationHash(pub crate::datatypes::UInt64); @@ -74,15 +75,3 @@ impl std::ops::DerefMut for ViewerRecommendationHash { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ViewerRecommendationHash { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/visible_time_range.rs b/crates/store/re_sdk_types/src/blueprint/components/visible_time_range.rs index 467ffa28ca7a..1a3740c031f2 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/visible_time_range.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/visible_time_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Refer to `VisibleTimeRanges` archetype for more information. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct VisibleTimeRange(pub crate::datatypes::VisibleTimeRange); @@ -74,15 +75,3 @@ impl std::ops::DerefMut for VisibleTimeRange { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for VisibleTimeRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/visual_bounds2d.rs b/crates/store/re_sdk_types/src/blueprint/components/visual_bounds2d.rs index 0de755d287e0..b92ea0854553 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/visual_bounds2d.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/visual_bounds2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Visual bounds in 2D space used for `Spatial2DView`. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct VisualBounds2D( /// X and y ranges that should be visible. @@ -75,15 +78,3 @@ impl std::ops::DerefMut for VisualBounds2D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for VisualBounds2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/visualizer_component_mapping.rs b/crates/store/re_sdk_types/src/blueprint/components/visualizer_component_mapping.rs index 042192319655..f37548752874 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/visualizer_component_mapping.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/visualizer_component_mapping.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Associates components of an entity to components of a visualizer. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct VisualizerComponentMapping( /// The component mapping pairs. pub crate::blueprint::datatypes::VisualizerComponentMapping, @@ -78,15 +79,3 @@ impl std::ops::DerefMut for VisualizerComponentMapping { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for VisualizerComponentMapping { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/visualizer_instruction_id.rs b/crates/store/re_sdk_types/src/blueprint/components/visualizer_instruction_id.rs index 330ce3d6d4e3..5d62a240b4e9 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/visualizer_instruction_id.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/visualizer_instruction_id.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// For details see [`archetypes::ActiveVisualizers`][crate::blueprint::archetypes::ActiveVisualizers]. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy)] +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct VisualizerInstructionId( /// IDs of a single visualizer instruction. @@ -78,15 +81,3 @@ impl std::ops::DerefMut for VisualizerInstructionId { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for VisualizerInstructionId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/visualizer_type.rs b/crates/store/re_sdk_types/src/blueprint/components/visualizer_type.rs index 80656653e210..5b8a909f09e5 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/visualizer_type.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/visualizer_type.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The type of the visualizer. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, ::re_byte_size::SizeBytes)] pub struct VisualizerType( /// The type of the visualizer. pub crate::datatypes::Utf8, @@ -74,15 +75,3 @@ impl std::ops::DerefMut for VisualizerType { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for VisualizerType { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/components/zoom_level.rs b/crates/store/re_sdk_types/src/blueprint/components/zoom_level.rs index a5c47dce6cc2..f86962e6957b 100644 --- a/crates/store/re_sdk_types/src/blueprint/components/zoom_level.rs +++ b/crates/store/re_sdk_types/src/blueprint/components/zoom_level.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A zoom level determines how much of the world is visible on a map. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, ::re_byte_size::SizeBytes)] pub struct ZoomLevel( /// Zoom level: 0 being the lowest zoom level (fully zoomed out) and 22 being the highest (fully zoomed in). pub crate::datatypes::Float64, @@ -74,15 +75,3 @@ impl std::ops::DerefMut for ZoomLevel { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ZoomLevel { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/component_column_selector.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/component_column_selector.rs index 7903ba6515c9..6bb66f8c62fb 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/component_column_selector.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/component_column_selector.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Describe a component column to be selected in the dataframe view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, ::re_byte_size::SizeBytes)] pub struct ComponentColumnSelector { /// The entity path for this component. pub entity_path: crate::datatypes::EntityPath, @@ -191,11 +192,11 @@ impl ::re_types_core::Loggable for ComponentColumnSelector { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let entity_path = { if !arrays_by_name.contains_key("entity_path") { return Err(DeserializationError::missing_struct_field( @@ -335,15 +336,3 @@ impl ::re_types_core::Loggable for ComponentColumnSelector { }) } } - -impl ::re_byte_size::SizeBytes for ComponentColumnSelector { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.entity_path.heap_size_bytes() + self.component.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/component_source_kind.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/component_source_kind.rs index 3aeb4057fba4..b82b115c7d9c 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/component_source_kind.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/component_source_kind.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: What kind of source to use for a visualizer component mapping. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum ComponentSourceKind { /// Use an explicit selection defined by `source_component`. @@ -168,15 +169,3 @@ impl ::re_types_core::reflection::Enum for ComponentSourceKind { .copied() } } - -impl ::re_byte_size::SizeBytes for ComponentSourceKind { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/filter_by_range.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/filter_by_range.rs index 3e914f211fe8..24bf00a49a75 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/filter_by_range.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/filter_by_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Configuration for the filter-by-range feature of the dataframe view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct FilterByRange { /// Beginning of the time range. pub start: crate::datatypes::TimeInt, @@ -153,11 +154,11 @@ impl ::re_types_core::Loggable for FilterByRange { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let start = { if !arrays_by_name.contains_key("start") { return Err(DeserializationError::missing_struct_field( @@ -222,15 +223,3 @@ impl ::re_types_core::Loggable for FilterByRange { }) } } - -impl ::re_byte_size::SizeBytes for FilterByRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.start.heap_size_bytes() + self.end.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/filter_is_not_null.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/filter_is_not_null.rs index ad4d6e89b15f..2f3f8e2d9947 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/filter_is_not_null.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/filter_is_not_null.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Configuration for the filter is not null feature of the dataframe view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct FilterIsNotNull { /// Whether the filter by event feature is active. pub active: crate::datatypes::Bool, @@ -151,11 +152,11 @@ impl ::re_types_core::Loggable for FilterIsNotNull { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let active = { if !arrays_by_name.contains_key("active") { return Err(DeserializationError::missing_struct_field( @@ -213,16 +214,3 @@ impl ::re_types_core::Loggable for FilterIsNotNull { }) } } - -impl ::re_byte_size::SizeBytes for FilterIsNotNull { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.active.heap_size_bytes() + self.column.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/selected_columns.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/selected_columns.rs index 83c871680ed3..630678b3f672 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/selected_columns.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/selected_columns.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: List of selected columns in a dataframe. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct SelectedColumns { /// The time columns to include pub time_columns: Vec, @@ -222,11 +223,11 @@ impl ::re_types_core::Loggable for SelectedColumns { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let time_columns = { if !arrays_by_name.contains_key("time_columns") { return Err(DeserializationError::missing_struct_field( @@ -456,16 +457,3 @@ impl ::re_types_core::Loggable for SelectedColumns { }) } } - -impl ::re_byte_size::SizeBytes for SelectedColumns { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.time_columns.heap_size_bytes() + self.component_columns.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - && >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/tensor_dimension_index_slider.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/tensor_dimension_index_slider.rs index e4b5c44f453b..6b50f37d19cf 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/tensor_dimension_index_slider.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/tensor_dimension_index_slider.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Defines a slider for the index of some dimension. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, Copy, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Copy, Hash, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct TensorDimensionIndexSlider { /// The dimension number. pub dimension: u32, @@ -117,11 +118,11 @@ impl ::re_types_core::Loggable for TensorDimensionIndexSlider { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let dimension = { if !arrays_by_name.contains_key("dimension") { return Err(DeserializationError::missing_struct_field( @@ -181,15 +182,3 @@ impl From for u32 { value.dimension } } - -impl ::re_byte_size::SizeBytes for TensorDimensionIndexSlider { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.dimension.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/text_log_column.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/text_log_column.rs index 62397b1506f8..f84f8b412a28 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/text_log_column.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/text_log_column.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A text log column. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, ::re_byte_size::SizeBytes)] pub struct TextLogColumn { /// Is this column visible? /// @@ -151,11 +152,11 @@ impl ::re_types_core::Loggable for TextLogColumn { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let visible = { if !arrays_by_name.contains_key("visible") { return Err(DeserializationError::missing_struct_field( @@ -213,16 +214,3 @@ impl ::re_types_core::Loggable for TextLogColumn { }) } } - -impl ::re_byte_size::SizeBytes for TextLogColumn { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.visible.heap_size_bytes() + self.kind.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/text_log_column_kind.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/text_log_column_kind.rs index 166b943f86c1..f8ba0a4ae0c0 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/text_log_column_kind.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/text_log_column_kind.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A text log column kind. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum TextLogColumnKind { /// Which entity path this was logged to. @@ -149,15 +150,3 @@ impl ::re_types_core::reflection::Enum for TextLogColumnKind { .copied() } } - -impl ::re_byte_size::SizeBytes for TextLogColumnKind { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/timeline_column.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/timeline_column.rs index 193a582c7da7..7f00e6a66477 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/timeline_column.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/timeline_column.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A timeline column in a table. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, ::re_byte_size::SizeBytes)] pub struct TimelineColumn { /// Is this column visible? /// @@ -167,11 +168,11 @@ impl ::re_types_core::Loggable for TimelineColumn { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let visible = { if !arrays_by_name.contains_key("visible") { return Err(DeserializationError::missing_struct_field( @@ -270,15 +271,3 @@ impl ::re_types_core::Loggable for TimelineColumn { }) } } - -impl ::re_byte_size::SizeBytes for TimelineColumn { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.visible.heap_size_bytes() + self.timeline.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/timeline_column_ext.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/timeline_column_ext.rs index 35f8a1eaa6f2..a2ce8bd512a9 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/timeline_column_ext.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/timeline_column_ext.rs @@ -4,7 +4,7 @@ impl Default for TimelineColumn { #[inline] fn default() -> Self { Self { - timeline: crate::blueprint::components::TimelineName::default().0, + timeline: crate::blueprint::components::TimelineName::log_time().0, visible: true.into(), } } diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/visualizer_component_mapping.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/visualizer_component_mapping.rs index 3ec2e60261f4..ad9f6a77a914 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/visualizer_component_mapping.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/visualizer_component_mapping.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Associate components of an entity to components of a visualizer. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct VisualizerComponentMapping { /// Target component name which is being mapped to. /// @@ -253,11 +254,11 @@ impl ::re_types_core::Loggable for VisualizerComponentMapping { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let target = { if !arrays_by_name.contains_key("target") { return Err(DeserializationError::missing_struct_field( @@ -472,21 +473,3 @@ impl ::re_types_core::Loggable for VisualizerComponentMapping { }) } } - -impl ::re_byte_size::SizeBytes for VisualizerComponentMapping { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.target.heap_size_bytes() - + self.source_kind.heap_size_bytes() - + self.source_component.heap_size_bytes() - + self.selector.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - && >::is_pod() - && >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/datatypes/visualizer_component_mapping_ext.rs b/crates/store/re_sdk_types/src/blueprint/datatypes/visualizer_component_mapping_ext.rs index 592e7e59020c..ae73f2ba7b1d 100644 --- a/crates/store/re_sdk_types/src/blueprint/datatypes/visualizer_component_mapping_ext.rs +++ b/crates/store/re_sdk_types/src/blueprint/datatypes/visualizer_component_mapping_ext.rs @@ -1,9 +1,16 @@ -use re_types_core::ArrowString; use re_types_core::datatypes::Utf8; +use re_types_core::{ArrowString, ComponentIdentifier, InvalidComponentIdentifierError}; use crate::blueprint::datatypes::{ComponentSourceKind, VisualizerComponentMapping}; impl VisualizerComponentMapping { + /// The [`ComponentIdentifier`] this mapping targets. + /// + /// Fails if `target` is invalid (e.g. empty). + pub fn target_component(&self) -> Result { + ComponentIdentifier::try_new(self.target.as_str()) + } + /// Create a new visualizer component mapping. /// /// This is the most general constructor. For common cases, prefer the more specific constructors like diff --git a/crates/store/re_sdk_types/src/blueprint/views/.gitattributes b/crates/store/re_sdk_types/src/blueprint/views/.gitattributes index dcc2d8675efb..2be407c4bab7 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/.gitattributes +++ b/crates/store/re_sdk_types/src/blueprint/views/.gitattributes @@ -8,7 +8,7 @@ map_view.rs linguist-generated=true mod.rs linguist-generated=true spatial2d_view.rs linguist-generated=true spatial3d_view.rs linguist-generated=true -status_view.rs linguist-generated=true +state_timeline_view.rs linguist-generated=true tensor_view.rs linguist-generated=true text_document_view.rs linguist-generated=true text_log_view.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/blueprint/views/bar_chart_view.rs b/crates/store/re_sdk_types/src/blueprint/views/bar_chart_view.rs index c7a953c564b5..a2c1c9b91f04 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/bar_chart_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/bar_chart_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: A bar chart view. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct BarChartView { /// Configures the legend of the plot. pub plot_legend: crate::blueprint::archetypes::PlotLegend, @@ -36,19 +37,9 @@ pub struct BarChartView { impl ::re_types_core::View for BarChartView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "BarChart".into() - } -} - -impl ::re_byte_size::SizeBytes for BarChartView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.plot_legend.heap_size_bytes() + self.background.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "BarChart" + ) } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/dataframe_view.rs b/crates/store/re_sdk_types/src/blueprint/views/dataframe_view.rs index 825f886448cb..d67b5446f637 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/dataframe_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/dataframe_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -28,7 +29,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// See [Dataframe queries](https://rerun.io/docs/concepts/query-and-transform/dataframe-queries) to learn more about the query model. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct DataframeView { /// Query of the dataframe. pub query: crate::blueprint::archetypes::DataframeQuery, @@ -37,7 +38,10 @@ pub struct DataframeView { impl ::re_types_core::View for DataframeView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "Dataframe".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "Dataframe" + ) } } @@ -69,15 +73,3 @@ impl std::ops::DerefMut for DataframeView { &mut self.query } } - -impl ::re_byte_size::SizeBytes for DataframeView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.query.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/blueprint/views/graph_view.rs b/crates/store/re_sdk_types/src/blueprint/views/graph_view.rs index 2738e99a31c5..7e7d4e6851c4 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/graph_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/graph_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,14 +25,14 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: A graph view to display time-variying, directed or undirected graph visualization. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct GraphView { /// Configures the background of the graph. pub background: crate::blueprint::archetypes::GraphBackground, /// Everything within these bounds is guaranteed to be visible. /// - /// Somethings outside of these bounds may also be visible due to letterboxing. + /// Some things outside of these bounds may also be visible due to letterboxing. pub visual_bounds: crate::blueprint::archetypes::VisualBounds2D, /// Allows to control the interaction between two nodes connected by an edge. @@ -53,30 +54,9 @@ pub struct GraphView { impl ::re_types_core::View for GraphView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "Graph".into() - } -} - -impl ::re_byte_size::SizeBytes for GraphView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.background.heap_size_bytes() - + self.visual_bounds.heap_size_bytes() - + self.force_link.heap_size_bytes() - + self.force_many_body.heap_size_bytes() - + self.force_position.heap_size_bytes() - + self.force_collision_radius.heap_size_bytes() - + self.force_center.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - && ::is_pod() - && ::is_pod() - && ::is_pod() - && ::is_pod() - && ::is_pod() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "Graph" + ) } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/map_view.rs b/crates/store/re_sdk_types/src/blueprint/views/map_view.rs index 077331ed800c..12d596a984a6 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/map_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/map_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: A 2D map view to display geospatial primitives. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct MapView { /// Configures the zoom level of the map view. pub zoom: crate::blueprint::archetypes::MapZoom, @@ -36,19 +37,9 @@ pub struct MapView { impl ::re_types_core::View for MapView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "Map".into() - } -} - -impl ::re_byte_size::SizeBytes for MapView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.zoom.heap_size_bytes() + self.background.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "Map" + ) } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/mod.rs b/crates/store/re_sdk_types/src/blueprint/views/mod.rs index 01fa75bf503f..61959e84dbe0 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/mod.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/mod.rs @@ -6,7 +6,7 @@ mod graph_view; mod map_view; mod spatial2d_view; mod spatial3d_view; -mod status_view; +mod state_timeline_view; mod tensor_view; mod text_document_view; mod text_log_view; @@ -18,7 +18,7 @@ pub use self::graph_view::GraphView; pub use self::map_view::MapView; pub use self::spatial2d_view::Spatial2DView; pub use self::spatial3d_view::Spatial3DView; -pub use self::status_view::StatusView; +pub use self::state_timeline_view::StateTimelineView; pub use self::tensor_view::TensorView; pub use self::text_document_view::TextDocumentView; pub use self::text_log_view::TextLogView; diff --git a/crates/store/re_sdk_types/src/blueprint/views/spatial2d_view.rs b/crates/store/re_sdk_types/src/blueprint/views/spatial2d_view.rs index 913fd8e45bf6..045ff56d99d6 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/spatial2d_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/spatial2d_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: For viewing spatial 2D data. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct Spatial2DView { /// Configuration for the background of the view. pub background: crate::blueprint::archetypes::Background, @@ -35,6 +36,9 @@ pub struct Spatial2DView { /// Somethings outside of these bounds may also be visible due to letterboxing. pub visual_bounds: crate::blueprint::archetypes::VisualBounds2D, + /// Configuration of spatial information shown in the view. + pub spatial_information: crate::blueprint::archetypes::SpatialInformation, + /// Configures which range on each timeline is shown by this view (unless specified differently per entity). /// /// If not specified, the default is to show the latest state of each component. @@ -45,22 +49,9 @@ pub struct Spatial2DView { impl ::re_types_core::View for Spatial2DView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "2D".into() - } -} - -impl ::re_byte_size::SizeBytes for Spatial2DView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.background.heap_size_bytes() - + self.visual_bounds.heap_size_bytes() - + self.time_ranges.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - && ::is_pod() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "2D" + ) } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/spatial3d_view.rs b/crates/store/re_sdk_types/src/blueprint/views/spatial3d_view.rs index a9bbfa3484b6..fbe52b54aa32 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/spatial3d_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/spatial3d_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: For viewing spatial 3D data. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct Spatial3DView { /// Configuration for the background of the view. pub background: crate::blueprint::archetypes::Background, @@ -48,26 +49,9 @@ pub struct Spatial3DView { impl ::re_types_core::View for Spatial3DView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "3D".into() - } -} - -impl ::re_byte_size::SizeBytes for Spatial3DView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.background.heap_size_bytes() - + self.line_grid.heap_size_bytes() - + self.spatial_information.heap_size_bytes() - + self.eye_controls.heap_size_bytes() - + self.time_ranges.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - && ::is_pod() - && ::is_pod() - && ::is_pod() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "3D" + ) } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/status_view.rs b/crates/store/re_sdk_types/src/blueprint/views/state_timeline_view.rs similarity index 69% rename from crates/store/re_sdk_types/src/blueprint/views/status_view.rs rename to crates/store/re_sdk_types/src/blueprint/views/state_timeline_view.rs index 1d233174a0c2..2a7d0fd9cbcd 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/status_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/state_timeline_view.rs @@ -1,5 +1,5 @@ // DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs -// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/views/status.fbs". +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/views/state_timeline.fbs". #![allow(unused_braces)] #![allow(unused_imports)] @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,27 +22,18 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -/// **View**: A view for displaying status transitions over time, for use with [`archetypes::Status`][crate::archetypes::Status]. +/// **View**: A view for displaying state transitions over time, for use with [`archetypes::StateChange`][crate::archetypes::StateChange]. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] -pub struct StatusView {} +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] +pub struct StateTimelineView {} -impl ::re_types_core::View for StatusView { +impl ::re_types_core::View for StateTimelineView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "Status".into() - } -} - -impl ::re_byte_size::SizeBytes for StatusView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "StateTimeline" + ) } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/tensor_view.rs b/crates/store/re_sdk_types/src/blueprint/views/tensor_view.rs index 315fedb2c179..46c70221871e 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/tensor_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/tensor_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: A view on a tensor of any dimensionality. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct TensorView { /// How to select the slice of the tensor to show. pub slice_selection: crate::blueprint::archetypes::TensorSliceSelection, @@ -39,22 +40,9 @@ pub struct TensorView { impl ::re_types_core::View for TensorView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "Tensor".into() - } -} - -impl ::re_byte_size::SizeBytes for TensorView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.slice_selection.heap_size_bytes() - + self.scalar_mapping.heap_size_bytes() - + self.view_fit.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - && ::is_pod() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "Tensor" + ) } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/text_document_view.rs b/crates/store/re_sdk_types/src/blueprint/views/text_document_view.rs index 1bafc22341aa..501b615af2a9 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/text_document_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/text_document_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,24 +25,49 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: A view of a single text document, for use with [`archetypes::TextDocument`][crate::archetypes::TextDocument]. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] -pub struct TextDocumentView {} +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] +pub struct TextDocumentView { + /// Formatting options for the text document view. + pub format_options: crate::blueprint::archetypes::TextDocumentFormat, +} impl ::re_types_core::View for TextDocumentView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "TextDocument".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "TextDocument" + ) + } +} + +impl> From for TextDocumentView { + fn from(v: T) -> Self { + Self { + format_options: v.into(), + } + } +} + +impl std::borrow::Borrow for TextDocumentView { + #[inline] + fn borrow(&self) -> &crate::blueprint::archetypes::TextDocumentFormat { + &self.format_options } } -impl ::re_byte_size::SizeBytes for TextDocumentView { +impl std::ops::Deref for TextDocumentView { + type Target = crate::blueprint::archetypes::TextDocumentFormat; + #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 + fn deref(&self) -> &crate::blueprint::archetypes::TextDocumentFormat { + &self.format_options } +} +impl std::ops::DerefMut for TextDocumentView { #[inline] - fn is_pod() -> bool { - true + fn deref_mut(&mut self) -> &mut crate::blueprint::archetypes::TextDocumentFormat { + &mut self.format_options } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/text_log_view.rs b/crates/store/re_sdk_types/src/blueprint/views/text_log_view.rs index 6e18d0aeb428..1d5c249e0e32 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/text_log_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/text_log_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: A view of a text log, for use with [`archetypes::TextLog`][crate::archetypes::TextLog]. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct TextLogView { /// The columns to display in the view. pub columns: crate::blueprint::archetypes::TextLogColumns, @@ -39,22 +40,9 @@ pub struct TextLogView { impl ::re_types_core::View for TextLogView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "TextLog".into() - } -} - -impl ::re_byte_size::SizeBytes for TextLogView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.columns.heap_size_bytes() - + self.rows.heap_size_bytes() - + self.format_options.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - && ::is_pod() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "TextLog" + ) } } diff --git a/crates/store/re_sdk_types/src/blueprint/views/time_series_view.rs b/crates/store/re_sdk_types/src/blueprint/views/time_series_view.rs index bbfdc232f3d9..38fe10e2ad5b 100644 --- a/crates/store/re_sdk_types/src/blueprint/views/time_series_view.rs +++ b/crates/store/re_sdk_types/src/blueprint/views/time_series_view.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **View**: A time series view for scalars over time, for use with [`archetypes::Scalars`][crate::archetypes::Scalars]. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct TimeSeriesView { /// Configures the horizontal axis of the plot. pub axis_x: crate::blueprint::archetypes::TimeAxis, @@ -48,26 +49,9 @@ pub struct TimeSeriesView { impl ::re_types_core::View for TimeSeriesView { #[inline] fn identifier() -> ::re_types_core::ViewClassIdentifier { - "TimeSeries".into() - } -} - -impl ::re_byte_size::SizeBytes for TimeSeriesView { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.axis_x.heap_size_bytes() - + self.axis_y.heap_size_bytes() - + self.plot_legend.heap_size_bytes() - + self.background.heap_size_bytes() - + self.time_ranges.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - && ::is_pod() - && ::is_pod() - && ::is_pod() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ViewClassIdentifier, + "TimeSeries" + ) } } diff --git a/crates/store/re_sdk_types/src/components/.gitattributes b/crates/store/re_sdk_types/src/components/.gitattributes index 159dbd596574..682a705f0bd0 100644 --- a/crates/store/re_sdk_types/src/components/.gitattributes +++ b/crates/store/re_sdk_types/src/components/.gitattributes @@ -30,6 +30,7 @@ image_format.rs linguist-generated=true image_plane_distance.rs linguist-generated=true interactive.rs linguist-generated=true interpolation_mode.rs linguist-generated=true +is_keyframe.rs linguist-generated=true key_value_pairs.rs linguist-generated=true keypoint_id.rs linguist-generated=true lat_lon.rs linguist-generated=true @@ -47,6 +48,7 @@ name.rs linguist-generated=true opacity.rs linguist-generated=true pinhole_projection.rs linguist-generated=true plane3d.rs linguist-generated=true +point_shading.rs linguist-generated=true position2d.rs linguist-generated=true position3d.rs linguist-generated=true radius.rs linguist-generated=true @@ -80,3 +82,6 @@ video_sample.rs linguist-generated=true video_timestamp.rs linguist-generated=true view_coordinates.rs linguist-generated=true visible.rs linguist-generated=true +voxel_index.rs linguist-generated=true +voxel_size.rs linguist-generated=true +voxel_value.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/components/aggregation_policy.rs b/crates/store/re_sdk_types/src/components/aggregation_policy.rs index 819ce1a2749f..f5d461b76af6 100644 --- a/crates/store/re_sdk_types/src/components/aggregation_policy.rs +++ b/crates/store/re_sdk_types/src/components/aggregation_policy.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// This is used for lines in plots when the X axis distance of individual points goes below a single pixel, /// i.e. a single pixel covers more than one tick worth of data. It can greatly improve performance /// (and readability) in such situations as it prevents overdraw. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum AggregationPolicy { /// No aggregation. @@ -188,15 +189,3 @@ impl ::re_types_core::reflection::Enum for AggregationPolicy { .copied() } } - -impl ::re_byte_size::SizeBytes for AggregationPolicy { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/albedo_factor.rs b/crates/store/re_sdk_types/src/components/albedo_factor.rs index fa3327d63fd5..aa7a1123996c 100644 --- a/crates/store/re_sdk_types/src/components/albedo_factor.rs +++ b/crates/store/re_sdk_types/src/components/albedo_factor.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,17 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A color multiplier, usually applied to a whole entity, e.g. a mesh. #[derive( - Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, bytemuck::Pod, bytemuck::Zeroable, + Clone, + Debug, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, )] #[repr(transparent)] pub struct AlbedoFactor(pub crate::datatypes::Rgba32); @@ -72,15 +83,3 @@ impl std::ops::DerefMut for AlbedoFactor { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AlbedoFactor { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/annotation_context.rs b/crates/store/re_sdk_types/src/components/annotation_context.rs index cc46612426ed..9ecc3ad0a2dd 100644 --- a/crates/store/re_sdk_types/src/components/annotation_context.rs +++ b/crates/store/re_sdk_types/src/components/annotation_context.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// path. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] pub struct AnnotationContext( /// List of class descriptions, mapping class indices to class names, colors etc. pub Vec, @@ -176,15 +177,3 @@ impl, T: IntoIterator u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/axis_length.rs b/crates/store/re_sdk_types/src/components/axis_length.rs index 56cf68db63b6..935d648f9298 100644 --- a/crates/store/re_sdk_types/src/components/axis_length.rs +++ b/crates/store/re_sdk_types/src/components/axis_length.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The length of an axis in local units of the space. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct AxisLength(pub crate::datatypes::Float32); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for AxisLength { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AxisLength { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/blob.rs b/crates/store/re_sdk_types/src/components/blob.rs index cec26a1cb03c..a29cf9ee588e 100644 --- a/crates/store/re_sdk_types/src/components/blob.rs +++ b/crates/store/re_sdk_types/src/components/blob.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A binary blob of data. /// /// Ref-counted internally and therefore cheap to clone. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Blob(pub crate::datatypes::Blob); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for Blob { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Blob { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/cell_size.rs b/crates/store/re_sdk_types/src/components/cell_size.rs index 8d03013446c1..8850b25b9726 100644 --- a/crates/store/re_sdk_types/src/components/cell_size.rs +++ b/crates/store/re_sdk_types/src/components/cell_size.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The metric size of one grid cell in local scene units. /// /// E.g. for 2D grid maps, this is the physical size represented by a single pixel or cell. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct CellSize(pub crate::datatypes::Float32); @@ -72,15 +82,3 @@ impl std::ops::DerefMut for CellSize { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for CellSize { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/channel_id.rs b/crates/store/re_sdk_types/src/components/channel_id.rs index 29473b323ad3..78a6ad9687a4 100644 --- a/crates/store/re_sdk_types/src/components/channel_id.rs +++ b/crates/store/re_sdk_types/src/components/channel_id.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A 16-bit ID representing an MCAP channel. /// /// Used to identify specific channels within an MCAP file. -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ChannelId(pub crate::datatypes::UInt16); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for ChannelId { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ChannelId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/channel_message_counts.rs b/crates/store/re_sdk_types/src/components/channel_message_counts.rs index e7d15a6f2b86..a16626ddf93b 100644 --- a/crates/store/re_sdk_types/src/components/channel_message_counts.rs +++ b/crates/store/re_sdk_types/src/components/channel_message_counts.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Used in MCAP statistics to track how many messages were recorded per channel. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct ChannelMessageCounts( /// The channel ID to message count pairs. pub Vec, @@ -172,15 +173,3 @@ impl, T: IntoIterator> Fro Self(v.into_iter().map(|v| v.into()).collect()) } } - -impl ::re_byte_size::SizeBytes for ChannelMessageCounts { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/class_id.rs b/crates/store/re_sdk_types/src/components/class_id.rs index 692472cf4e20..b93a3f6240fc 100644 --- a/crates/store/re_sdk_types/src/components/class_id.rs +++ b/crates/store/re_sdk_types/src/components/class_id.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,10 +26,20 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// Used to look up a [`crate::datatypes::ClassDescription`] within the [`crate::components::AnnotationContext`]. #[derive( - Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, bytemuck::Pod, bytemuck::Zeroable, + Clone, + Debug, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, )] #[repr(transparent)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[derive(::serde::Serialize, ::serde::Deserialize)] pub struct ClassId(pub crate::datatypes::ClassId); impl ::re_types_core::WrapperComponent for ClassId { @@ -75,15 +86,3 @@ impl std::ops::DerefMut for ClassId { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ClassId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/color.rs b/crates/store/re_sdk_types/src/components/color.rs index 20cf261e3ebf..7dcffcba4c43 100644 --- a/crates/store/re_sdk_types/src/components/color.rs +++ b/crates/store/re_sdk_types/src/components/color.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,18 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// The color is stored as a 32-bit integer, where the most significant /// byte is `R` and the least significant byte is `A`. -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Color(pub crate::datatypes::Rgba32); @@ -73,15 +85,3 @@ impl std::ops::DerefMut for Color { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Color { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/colormap.rs b/crates/store/re_sdk_types/src/components/colormap.rs index a548496fabf1..b7ab53190be9 100644 --- a/crates/store/re_sdk_types/src/components/colormap.rs +++ b/crates/store/re_sdk_types/src/components/colormap.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// This provides a number of popular pre-defined colormaps. /// In the future, the Rerun Viewer will allow users to define their own colormaps, /// but currently the Viewer is limited to the types defined here. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum Colormap { /// A simple black to white gradient. @@ -272,15 +273,3 @@ impl ::re_types_core::reflection::Enum for Colormap { .copied() } } - -impl ::re_byte_size::SizeBytes for Colormap { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/count.rs b/crates/store/re_sdk_types/src/components/count.rs index f605dfd8457c..f305eecca76f 100644 --- a/crates/store/re_sdk_types/src/components/count.rs +++ b/crates/store/re_sdk_types/src/components/count.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Used for counting various entities like messages, schemas, channels, etc. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Count(pub crate::datatypes::UInt64); @@ -74,15 +75,3 @@ impl std::ops::DerefMut for Count { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Count { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/depth_meter.rs b/crates/store/re_sdk_types/src/components/depth_meter.rs index 7d2808ce3241..d7347bb6205f 100644 --- a/crates/store/re_sdk_types/src/components/depth_meter.rs +++ b/crates/store/re_sdk_types/src/components/depth_meter.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -31,7 +32,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// In 3D views on the other hand, this affects where the points of the point cloud are placed. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct DepthMeter(pub crate::datatypes::Float32); @@ -79,15 +89,3 @@ impl std::ops::DerefMut for DepthMeter { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for DepthMeter { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/draw_order.rs b/crates/store/re_sdk_types/src/components/draw_order.rs index e72df4936458..82af4bd8f2bc 100644 --- a/crates/store/re_sdk_types/src/components/draw_order.rs +++ b/crates/store/re_sdk_types/src/components/draw_order.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Within an entity draw order is governed by the order of the components. /// /// Draw order for entities with the same draw order is generally undefined. -#[derive(Clone, Debug, Copy)] +#[derive(Clone, Debug, Copy, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct DrawOrder(pub crate::datatypes::Float32); @@ -75,15 +76,3 @@ impl std::ops::DerefMut for DrawOrder { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for DrawOrder { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/draw_order_ext.rs b/crates/store/re_sdk_types/src/components/draw_order_ext.rs index 1d2e21b61e70..6649d91ce1f1 100644 --- a/crates/store/re_sdk_types/src/components/draw_order_ext.rs +++ b/crates/store/re_sdk_types/src/components/draw_order_ext.rs @@ -17,8 +17,8 @@ impl DrawOrder { /// Draw order used for segmentation images if no draw order was specified. pub const DEFAULT_SEGMENTATION_IMAGE: Self = Self(Float32(0.0)); - /// Draw order used for 2D boxes if no draw order was specified. - pub const DEFAULT_BOX2D: Self = Self(Float32(10.0)); + /// Draw order used for 2D shapes (boxes, ellipses) if no draw order was specified. + pub const DEFAULT_SHAPE_2D: Self = Self(Float32(10.0)); /// Draw order used for 2D lines if no draw order was specified. pub const DEFAULT_LINES2D: Self = Self(Float32(20.0)); diff --git a/crates/store/re_sdk_types/src/components/entity_path.rs b/crates/store/re_sdk_types/src/components/entity_path.rs index 874c8bf2d453..0c2ed12f96a8 100644 --- a/crates/store/re_sdk_types/src/components/entity_path.rs +++ b/crates/store/re_sdk_types/src/components/entity_path.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A path to an entity, usually to reference some data that is part of the target entity. -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct EntityPath(pub crate::datatypes::EntityPath); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for EntityPath { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for EntityPath { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/fill_mode.rs b/crates/store/re_sdk_types/src/components/fill_mode.rs index 6154de28a725..5ce1e2c2de83 100644 --- a/crates/store/re_sdk_types/src/components/fill_mode.rs +++ b/crates/store/re_sdk_types/src/components/fill_mode.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: How a geometric shape is drawn and colored. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum FillMode { /// Lines are drawn around the parts of the shape which directly correspond to the logged data. @@ -190,15 +191,3 @@ impl ::re_types_core::reflection::Enum for FillMode { .copied() } } - -impl ::re_byte_size::SizeBytes for FillMode { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/fill_ratio.rs b/crates/store/re_sdk_types/src/components/fill_ratio.rs index 9f98e00bdff5..047d6613f6da 100644 --- a/crates/store/re_sdk_types/src/components/fill_ratio.rs +++ b/crates/store/re_sdk_types/src/components/fill_ratio.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Valid range is from 0 to max float although typically values above 1.0 are not useful. /// /// Defaults to 1.0. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct FillRatio(pub crate::datatypes::Float32); @@ -75,15 +85,3 @@ impl std::ops::DerefMut for FillRatio { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for FillRatio { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/gamma_correction.rs b/crates/store/re_sdk_types/src/components/gamma_correction.rs index 8eb21ddb7b91..1e3ddba7fac4 100644 --- a/crates/store/re_sdk_types/src/components/gamma_correction.rs +++ b/crates/store/re_sdk_types/src/components/gamma_correction.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -28,7 +29,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// Must be a positive number. /// Defaults to 1.0 unless otherwise specified. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct GammaCorrection(pub crate::datatypes::Float32); @@ -76,15 +86,3 @@ impl std::ops::DerefMut for GammaCorrection { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for GammaCorrection { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/geo_line_string.rs b/crates/store/re_sdk_types/src/components/geo_line_string.rs index b8e39d5ae813..d230204040e0 100644 --- a/crates/store/re_sdk_types/src/components/geo_line_string.rs +++ b/crates/store/re_sdk_types/src/components/geo_line_string.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A geospatial line string expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees). -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct GeoLineString(pub Vec); @@ -152,9 +153,10 @@ impl ::re_types_core::Loggable for GeoLineString { if arrow_data_inner.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(2usize) - .zip((2usize..).step_by(2usize).take(arrow_data_inner.len())); + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data_inner.len()), + ); let arrow_data_inner_inner = { let arrow_data_inner_inner = &**arrow_data_inner.values(); arrow_data_inner_inner @@ -244,15 +246,3 @@ impl, T: IntoIterator> From for G Self(v.into_iter().map(|v| v.into()).collect()) } } - -impl ::re_byte_size::SizeBytes for GeoLineString { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/graph_edge.rs b/crates/store/re_sdk_types/src/components/graph_edge.rs index 8b3d96ccc7ae..0f718a46aef3 100644 --- a/crates/store/re_sdk_types/src/components/graph_edge.rs +++ b/crates/store/re_sdk_types/src/components/graph_edge.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: An edge in a graph connecting two nodes. -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct GraphEdge(pub crate::datatypes::Utf8Pair); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for GraphEdge { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for GraphEdge { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/graph_node.rs b/crates/store/re_sdk_types/src/components/graph_node.rs index 4b6b4eeaae4c..8c11694c0c8c 100644 --- a/crates/store/re_sdk_types/src/components/graph_node.rs +++ b/crates/store/re_sdk_types/src/components/graph_node.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A string-based ID representing a node in a graph. -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct GraphNode(pub crate::datatypes::Utf8); @@ -70,15 +73,3 @@ impl std::ops::DerefMut for GraphNode { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for GraphNode { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/graph_type.rs b/crates/store/re_sdk_types/src/components/graph_type.rs index e25f152792df..b529258a4886 100644 --- a/crates/store/re_sdk_types/src/components/graph_type.rs +++ b/crates/store/re_sdk_types/src/components/graph_type.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Specifies if a graph has directed or undirected edges. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum GraphType { /// The graph has undirected edges. @@ -151,15 +152,3 @@ impl ::re_types_core::reflection::Enum for GraphType { .copied() } } - -impl ::re_byte_size::SizeBytes for GraphType { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/half_size2d.rs b/crates/store/re_sdk_types/src/components/half_size2d.rs index 8ebedfd9e1d6..e8bf1513caef 100644 --- a/crates/store/re_sdk_types/src/components/half_size2d.rs +++ b/crates/store/re_sdk_types/src/components/half_size2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// The box extends both in negative and positive direction along each axis. /// Negative sizes indicate that the box is flipped along the respective axis, but this has no effect on how it is displayed. -#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct HalfSize2D(pub crate::datatypes::Vec2D); @@ -75,15 +78,3 @@ impl std::ops::DerefMut for HalfSize2D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for HalfSize2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/half_size3d.rs b/crates/store/re_sdk_types/src/components/half_size3d.rs index 3a072a6f00df..35e5f6bea4e0 100644 --- a/crates/store/re_sdk_types/src/components/half_size3d.rs +++ b/crates/store/re_sdk_types/src/components/half_size3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// The box extends both in negative and positive direction along each axis. /// Negative sizes indicate that the box is flipped along the respective axis, but this has no effect on how it is displayed. -#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct HalfSize3D(pub crate::datatypes::Vec3D); @@ -75,15 +78,3 @@ impl std::ops::DerefMut for HalfSize3D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for HalfSize3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/image_buffer.rs b/crates/store/re_sdk_types/src/components/image_buffer.rs index 028225f00e6d..5a1fcf6bdec6 100644 --- a/crates/store/re_sdk_types/src/components/image_buffer.rs +++ b/crates/store/re_sdk_types/src/components/image_buffer.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A buffer that is known to store image data. /// /// To interpret the contents of this buffer, see, [`components::ImageFormat`][crate::components::ImageFormat]. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ImageBuffer(pub crate::datatypes::Blob); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for ImageBuffer { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ImageBuffer { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/image_format.rs b/crates/store/re_sdk_types/src/components/image_format.rs index 91dd97874fce..44e5aa0bd72f 100644 --- a/crates/store/re_sdk_types/src/components/image_format.rs +++ b/crates/store/re_sdk_types/src/components/image_format.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The metadata describing the contents of a [`components::ImageBuffer`][crate::components::ImageBuffer]. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct ImageFormat(pub crate::datatypes::ImageFormat); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for ImageFormat { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ImageFormat { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/image_plane_distance.rs b/crates/store/re_sdk_types/src/components/image_plane_distance.rs index 17bacc9d6467..963e4f81e675 100644 --- a/crates/store/re_sdk_types/src/components/image_plane_distance.rs +++ b/crates/store/re_sdk_types/src/components/image_plane_distance.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The distance from the camera origin to the image plane when the projection is shown in a 3D viewer. /// /// This is only used for visualization purposes, and does not affect the projection itself. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, ::re_byte_size::SizeBytes)] pub struct ImagePlaneDistance(pub crate::datatypes::Float32); impl ::re_types_core::WrapperComponent for ImagePlaneDistance { @@ -71,15 +72,3 @@ impl std::ops::DerefMut for ImagePlaneDistance { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ImagePlaneDistance { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/interactive.rs b/crates/store/re_sdk_types/src/components/interactive.rs index 48dca3ed317f..431ab5e09b94 100644 --- a/crates/store/re_sdk_types/src/components/interactive.rs +++ b/crates/store/re_sdk_types/src/components/interactive.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Whether the entity can be interacted with. /// /// Non interactive components are still visible, but mouse interactions in the view are disabled. -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Interactive(pub crate::datatypes::Bool); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for Interactive { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Interactive { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/interpolation_mode.rs b/crates/store/re_sdk_types/src/components/interpolation_mode.rs index c6066f23247e..0e630fdfec22 100644 --- a/crates/store/re_sdk_types/src/components/interpolation_mode.rs +++ b/crates/store/re_sdk_types/src/components/interpolation_mode.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Specifies how values between data points are interpolated in time series. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum InterpolationMode { /// Connect data points with straight line segments. @@ -178,15 +179,3 @@ impl ::re_types_core::reflection::Enum for InterpolationMode { .copied() } } - -impl ::re_byte_size::SizeBytes for InterpolationMode { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/is_keyframe.rs b/crates/store/re_sdk_types/src/components/is_keyframe.rs new file mode 100644 index 000000000000..390911dd8f24 --- /dev/null +++ b/crates/store/re_sdk_types/src/components/is_keyframe.rs @@ -0,0 +1,79 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/is_keyframe.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Whether a [`components::VideoSample`][crate::components::VideoSample] contains a keyframe (also known as a sync sample or IDR). +/// +/// A keyframe in this sense must be _decoder re-entrant_: a decoder must be able to start +/// decoding the stream from this sample alone, with no prior decoder state. +/// Not every intra-coded frame qualifies. Some codecs have intra-only frames that may +/// still reference existing decoder state and are therefore not valid sync points. +/// See [`components::VideoCodec`][crate::components::VideoCodec] for the codec-specific definition of a keyframe. +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ::re_byte_size::SizeBytes)] +#[repr(transparent)] +pub struct IsKeyframe(pub crate::datatypes::Bool); + +impl ::re_types_core::WrapperComponent for IsKeyframe { + type Datatype = crate::datatypes::Bool; + + #[inline] + fn name() -> ComponentType { + "rerun.components.IsKeyframe".into() + } + + #[inline] + fn into_inner(self) -> Self::Datatype { + self.0 + } +} + +::re_types_core::macros::impl_into_cow!(IsKeyframe); + +impl> From for IsKeyframe { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for IsKeyframe { + #[inline] + fn borrow(&self) -> &crate::datatypes::Bool { + &self.0 + } +} + +impl std::ops::Deref for IsKeyframe { + type Target = crate::datatypes::Bool; + + #[inline] + fn deref(&self) -> &crate::datatypes::Bool { + &self.0 + } +} + +impl std::ops::DerefMut for IsKeyframe { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Bool { + &mut self.0 + } +} diff --git a/crates/store/re_sdk_types/src/components/key_value_pairs.rs b/crates/store/re_sdk_types/src/components/key_value_pairs.rs index fc1d147177df..699cd9cd527c 100644 --- a/crates/store/re_sdk_types/src/components/key_value_pairs.rs +++ b/crates/store/re_sdk_types/src/components/key_value_pairs.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Each key-value pair is stored as a UTF-8 string mapping. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct KeyValuePairs( /// The key-value pairs that make up this string map. pub Vec, @@ -171,15 +172,3 @@ impl, T: IntoIterator> From for Self(v.into_iter().map(|v| v.into()).collect()) } } - -impl ::re_byte_size::SizeBytes for KeyValuePairs { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/keypoint_id.rs b/crates/store/re_sdk_types/src/components/keypoint_id.rs index c7a14d6550bc..06671847680c 100644 --- a/crates/store/re_sdk_types/src/components/keypoint_id.rs +++ b/crates/store/re_sdk_types/src/components/keypoint_id.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -38,9 +39,10 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; Hash, bytemuck::Pod, bytemuck::Zeroable, + ::re_byte_size::SizeBytes, )] #[repr(transparent)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[derive(::serde::Serialize, ::serde::Deserialize)] pub struct KeypointId(pub crate::datatypes::KeypointId); impl ::re_types_core::WrapperComponent for KeypointId { @@ -87,15 +89,3 @@ impl std::ops::DerefMut for KeypointId { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for KeypointId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/lat_lon.rs b/crates/store/re_sdk_types/src/components/lat_lon.rs index 219774437266..4e3451be7093 100644 --- a/crates/store/re_sdk_types/src/components/lat_lon.rs +++ b/crates/store/re_sdk_types/src/components/lat_lon.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A geospatial position expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees). -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct LatLon(pub crate::datatypes::DVec2D); @@ -70,15 +80,3 @@ impl std::ops::DerefMut for LatLon { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for LatLon { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/length.rs b/crates/store/re_sdk_types/src/components/length.rs index b82db287450b..eac41f6502a5 100644 --- a/crates/store/re_sdk_types/src/components/length.rs +++ b/crates/store/re_sdk_types/src/components/length.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// Measured in its local coordinate system; consult the archetype in use to determine which /// axis or part of the entity this is the length of. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Length(pub crate::datatypes::Float32); @@ -73,15 +83,3 @@ impl std::ops::DerefMut for Length { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Length { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/line_strip2d.rs b/crates/store/re_sdk_types/src/components/line_strip2d.rs index 265dd8760768..3cfcd2971892 100644 --- a/crates/store/re_sdk_types/src/components/line_strip2d.rs +++ b/crates/store/re_sdk_types/src/components/line_strip2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -33,7 +34,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// 0----1 \ / /// 4 /// ``` -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct LineStrip2D(pub Vec); impl ::re_types_core::Component for LineStrip2D { @@ -162,9 +163,10 @@ impl ::re_types_core::Loggable for LineStrip2D { if arrow_data_inner.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(2usize) - .zip((2usize..).step_by(2usize).take(arrow_data_inner.len())); + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data_inner.len()), + ); let arrow_data_inner_inner = { let arrow_data_inner_inner = &**arrow_data_inner.values(); arrow_data_inner_inner @@ -254,15 +256,3 @@ impl, T: IntoIterator> From for Li Self(v.into_iter().map(|v| v.into()).collect()) } } - -impl ::re_byte_size::SizeBytes for LineStrip2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/line_strip3d.rs b/crates/store/re_sdk_types/src/components/line_strip3d.rs index 320a2b708b19..df9c3d5125f4 100644 --- a/crates/store/re_sdk_types/src/components/line_strip3d.rs +++ b/crates/store/re_sdk_types/src/components/line_strip3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -33,7 +34,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// 0----1 \ / /// 4 /// ``` -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct LineStrip3D(pub Vec); impl ::re_types_core::Component for LineStrip3D { @@ -162,9 +163,10 @@ impl ::re_types_core::Loggable for LineStrip3D { if arrow_data_inner.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(3usize) - .zip((3usize..).step_by(3usize).take(arrow_data_inner.len())); + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data_inner.len()), + ); let arrow_data_inner_inner = { let arrow_data_inner_inner = &**arrow_data_inner.values(); arrow_data_inner_inner @@ -254,15 +256,3 @@ impl, T: IntoIterator> From for Li Self(v.into_iter().map(|v| v.into()).collect()) } } - -impl ::re_byte_size::SizeBytes for LineStrip3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/linear_speed.rs b/crates/store/re_sdk_types/src/components/linear_speed.rs index 04e529431fb5..726e8c69c99c 100644 --- a/crates/store/re_sdk_types/src/components/linear_speed.rs +++ b/crates/store/re_sdk_types/src/components/linear_speed.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Linear speed, used for translation speed for example. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, ::re_byte_size::SizeBytes)] pub struct LinearSpeed( /// Speed value in units of length per unit of time. pub crate::datatypes::Float64, @@ -72,15 +73,3 @@ impl std::ops::DerefMut for LinearSpeed { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for LinearSpeed { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/magnification_filter.rs b/crates/store/re_sdk_types/src/components/magnification_filter.rs index b92765bc0271..fd0195e4ddcc 100644 --- a/crates/store/re_sdk_types/src/components/magnification_filter.rs +++ b/crates/store/re_sdk_types/src/components/magnification_filter.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// This happens when zooming into an image, when displaying a low-resolution image in a large area, /// or when viewing an image up close in 3D space. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum MagnificationFilter { /// Show the nearest pixel value. @@ -173,15 +174,3 @@ impl ::re_types_core::reflection::Enum for MagnificationFilter { .copied() } } - -impl ::re_byte_size::SizeBytes for MagnificationFilter { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/marker_shape.rs b/crates/store/re_sdk_types/src/components/marker_shape.rs index 51d2a7fdf8aa..2ecc09b2e032 100644 --- a/crates/store/re_sdk_types/src/components/marker_shape.rs +++ b/crates/store/re_sdk_types/src/components/marker_shape.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The visual appearance of a point in e.g. a 2D plot. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum MarkerShape { /// `⏺` @@ -202,15 +203,3 @@ impl ::re_types_core::reflection::Enum for MarkerShape { .copied() } } - -impl ::re_byte_size::SizeBytes for MarkerShape { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/marker_size.rs b/crates/store/re_sdk_types/src/components/marker_size.rs index 14f66005651a..695ae9bb4b91 100644 --- a/crates/store/re_sdk_types/src/components/marker_size.rs +++ b/crates/store/re_sdk_types/src/components/marker_size.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Radius of a marker of a point in e.g. a 2D plot, measured in UI points. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct MarkerSize(pub crate::datatypes::Float32); @@ -70,15 +80,3 @@ impl std::ops::DerefMut for MarkerSize { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for MarkerSize { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/media_type.rs b/crates/store/re_sdk_types/src/components/media_type.rs index a51a0241d427..bf62d4d99d19 100644 --- a/crates/store/re_sdk_types/src/components/media_type.rs +++ b/crates/store/re_sdk_types/src/components/media_type.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// The complete reference of officially registered media types is maintained by the IANA and can be /// consulted at . -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct MediaType(pub crate::datatypes::Utf8); @@ -73,15 +74,3 @@ impl std::ops::DerefMut for MediaType { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for MediaType { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/mesh_face_rendering.rs b/crates/store/re_sdk_types/src/components/mesh_face_rendering.rs index bd83bddc42a8..4159f0343c55 100644 --- a/crates/store/re_sdk_types/src/components/mesh_face_rendering.rs +++ b/crates/store/re_sdk_types/src/components/mesh_face_rendering.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// For this purpose, we assume that the winding order of vertices in a mesh is /// consistent and that front faces are defined as those with vertices in counter clockwise order. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum MeshFaceRendering { /// Show both back and front faces. @@ -167,15 +168,3 @@ impl ::re_types_core::reflection::Enum for MeshFaceRendering { .copied() } } - -impl ::re_byte_size::SizeBytes for MeshFaceRendering { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/mod.rs b/crates/store/re_sdk_types/src/components/mod.rs index 19d3504b43e7..f161a797a5ab 100644 --- a/crates/store/re_sdk_types/src/components/mod.rs +++ b/crates/store/re_sdk_types/src/components/mod.rs @@ -47,6 +47,7 @@ mod image_plane_distance_ext; mod interactive; mod interactive_ext; mod interpolation_mode; +mod is_keyframe; mod key_value_pairs; mod keypoint_id; mod keypoint_id_ext; @@ -76,6 +77,7 @@ mod pinhole_projection; mod pinhole_projection_ext; mod plane3d; mod plane3d_ext; +mod point_shading; mod position2d; mod position2d_ext; mod position3d; @@ -136,6 +138,9 @@ mod view_coordinates; mod view_coordinates_ext; mod visible; mod visible_ext; +mod voxel_index; +mod voxel_size; +mod voxel_value; pub use self::aggregation_policy::AggregationPolicy; pub use self::albedo_factor::AlbedoFactor; @@ -166,6 +171,7 @@ pub use self::image_format::ImageFormat; pub use self::image_plane_distance::ImagePlaneDistance; pub use self::interactive::Interactive; pub use self::interpolation_mode::InterpolationMode; +pub use self::is_keyframe::IsKeyframe; pub use self::key_value_pairs::KeyValuePairs; pub use self::keypoint_id::KeypointId; pub use self::lat_lon::LatLon; @@ -182,6 +188,7 @@ pub use self::name::Name; pub use self::opacity::Opacity; pub use self::pinhole_projection::PinholeProjection; pub use self::plane3d::Plane3D; +pub use self::point_shading::PointShading; pub use self::position2d::Position2D; pub use self::position3d::Position3D; pub use self::radius::Radius; @@ -215,3 +222,6 @@ pub use self::video_sample::VideoSample; pub use self::video_timestamp::VideoTimestamp; pub use self::view_coordinates::ViewCoordinates; pub use self::visible::Visible; +pub use self::voxel_index::VoxelIndex; +pub use self::voxel_size::VoxelSize; +pub use self::voxel_value::VoxelValue; diff --git a/crates/store/re_sdk_types/src/components/name.rs b/crates/store/re_sdk_types/src/components/name.rs index b30feb8c6cb1..3534c4350e35 100644 --- a/crates/store/re_sdk_types/src/components/name.rs +++ b/crates/store/re_sdk_types/src/components/name.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A display name, typically for an entity or a item like a plot series. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Name(pub crate::datatypes::Utf8); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for Name { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Name { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/opacity.rs b/crates/store/re_sdk_types/src/components/opacity.rs index 6121a02bb18c..d29d4aaa7a96 100644 --- a/crates/store/re_sdk_types/src/components/opacity.rs +++ b/crates/store/re_sdk_types/src/components/opacity.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// The final opacity value may be a result of multiplication with alpha values as specified by other color sources. /// Unless otherwise specified, the default value is 1. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Opacity(pub crate::datatypes::Float32); @@ -73,15 +83,3 @@ impl std::ops::DerefMut for Opacity { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Opacity { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/pinhole_projection.rs b/crates/store/re_sdk_types/src/components/pinhole_projection.rs index 4be4f25a649f..5ca40b62c36a 100644 --- a/crates/store/re_sdk_types/src/components/pinhole_projection.rs +++ b/crates/store/re_sdk_types/src/components/pinhole_projection.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -32,7 +33,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// 0.0 1496.1 744.5 /// 0.0 0.0 1.0 /// ``` -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, ::re_byte_size::SizeBytes)] pub struct PinholeProjection(pub crate::datatypes::Mat3x3); impl ::re_types_core::WrapperComponent for PinholeProjection { @@ -79,15 +80,3 @@ impl std::ops::DerefMut for PinholeProjection { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for PinholeProjection { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/plane3d.rs b/crates/store/re_sdk_types/src/components/plane3d.rs index 8911d6350918..d8aa6e2a0cac 100644 --- a/crates/store/re_sdk_types/src/components/plane3d.rs +++ b/crates/store/re_sdk_types/src/components/plane3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Note: although the normal will be passed through to the /// datastore as provided, when used in the Viewer, planes will always be normalized. /// I.e. the plane with xyz = (2, 0, 0), d = 1 is equivalent to xyz = (1, 0, 0), d = 0.5 -#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Plane3D(pub crate::datatypes::Plane3D); @@ -78,15 +81,3 @@ impl std::ops::DerefMut for Plane3D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Plane3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/point_shading.rs b/crates/store/re_sdk_types/src/components/point_shading.rs new file mode 100644 index 000000000000..5116f4e91cf5 --- /dev/null +++ b/crates/store/re_sdk_types/src/components/point_shading.rs @@ -0,0 +1,154 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/point_shading.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] +#![allow(non_camel_case_types)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Defines how points are shaded. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] +#[repr(u8)] +pub enum PointShading { + /// Radial gradient for a spherical shadow effect. + #[default] + Gradient = 1, + + /// Flat shading. + Flat = 2, +} + +impl ::re_types_core::Component for PointShading { + #[inline] + fn name() -> ComponentType { + "rerun.components.PointShading".into() + } +} + +::re_types_core::macros::impl_into_cow!(PointShading); + +impl ::re_types_core::Loggable for PointShading { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + use arrow::datatypes::*; + DataType::UInt8 + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult + where + Self: Clone + 'a, + { + #![allow(clippy::manual_is_variant_and)] + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_helpers::as_array_ref}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| *datum as u8); + (datum.is_some(), datum) + }) + .unzip(); + let data0_validity: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + as_array_ref(PrimitiveArray::::new( + ScalarBuffer::from( + data0 + .into_iter() + .map(|v| v.unwrap_or_default()) + .collect::>(), + ), + data0_validity, + )) + }) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_zip_validity::ZipValidity}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok(arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.components.PointShading#enum")? + .into_iter() + .map(|typ| match typ { + Some(val) => ::try_from_integer(val) + .map(Some) + .ok_or_else(|| { + DeserializationError::missing_union_arm( + Self::arrow_datatype(), + "", + val as _, + ) + }), + None => Ok(None), + }) + .collect::>>>() + .with_context("rerun.components.PointShading")?) + } +} + +impl std::fmt::Display for PointShading { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Gradient => write!(f, "Gradient"), + Self::Flat => write!(f, "Flat"), + } + } +} + +impl ::re_types_core::reflection::Enum for PointShading { + type Repr = u8; + + #[inline] + fn variants() -> &'static [Self] { + &[Self::Gradient, Self::Flat] + } + + #[inline] + fn docstring_md(self) -> &'static str { + match self { + Self::Gradient => "Radial gradient for a spherical shadow effect.", + Self::Flat => "Flat shading.", + } + } + + #[inline] + fn try_from_integer(value: u8) -> Option { + Self::variants() + .get((value as usize).wrapping_sub(1)) + .copied() + } +} diff --git a/crates/store/re_sdk_types/src/components/position2d.rs b/crates/store/re_sdk_types/src/components/position2d.rs index e70b5f85f5d0..1abbdcf61b92 100644 --- a/crates/store/re_sdk_types/src/components/position2d.rs +++ b/crates/store/re_sdk_types/src/components/position2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A position in 2D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Position2D(pub crate::datatypes::Vec2D); @@ -70,15 +80,3 @@ impl std::ops::DerefMut for Position2D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Position2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/position3d.rs b/crates/store/re_sdk_types/src/components/position3d.rs index af7b11cb2c48..00ae134fbd1e 100644 --- a/crates/store/re_sdk_types/src/components/position3d.rs +++ b/crates/store/re_sdk_types/src/components/position3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A position in 3D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Position3D(pub crate::datatypes::Vec3D); @@ -70,15 +80,3 @@ impl std::ops::DerefMut for Position3D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Position3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/radius.rs b/crates/store/re_sdk_types/src/components/radius.rs index 0703c2920d1c..691515f63a5b 100644 --- a/crates/store/re_sdk_types/src/components/radius.rs +++ b/crates/store/re_sdk_types/src/components/radius.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -29,7 +30,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// UI points are independent of zooming in Views, but are sensitive to the application UI scaling. /// at 100% UI scaling, UI points are equal to pixels /// The Viewer's UI scaling defaults to the OS scaling which typically is 100% for full HD screens and 200% for 4k screens. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Radius(pub crate::datatypes::Float32); @@ -77,15 +87,3 @@ impl std::ops::DerefMut for Radius { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Radius { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/range1d.rs b/crates/store/re_sdk_types/src/components/range1d.rs index 28133d1b9e39..8c96d2c7b000 100644 --- a/crates/store/re_sdk_types/src/components/range1d.rs +++ b/crates/store/re_sdk_types/src/components/range1d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A 1D range, specifying a lower and upper bound. -#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Range1D(pub crate::datatypes::Range1D); @@ -70,15 +73,3 @@ impl std::ops::DerefMut for Range1D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Range1D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/range1d_ext.rs b/crates/store/re_sdk_types/src/components/range1d_ext.rs index b910313a516b..7da195527964 100644 --- a/crates/store/re_sdk_types/src/components/range1d_ext.rs +++ b/crates/store/re_sdk_types/src/components/range1d_ext.rs @@ -41,7 +41,7 @@ impl Range1D { impl Display for Range1D { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[{}, {}]", self.start(), self.end(),) + write!(f, "[{}, {}]", self.start(), self.end()) } } diff --git a/crates/store/re_sdk_types/src/components/resolution.rs b/crates/store/re_sdk_types/src/components/resolution.rs index 6b718b791309..c2572196021f 100644 --- a/crates/store/re_sdk_types/src/components/resolution.rs +++ b/crates/store/re_sdk_types/src/components/resolution.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Pixel resolution width & height, e.g. of a camera sensor. /// /// Typically in integer units, but for some use cases floating point may be used. -#[derive(Clone, Debug, Copy, PartialEq)] +#[derive(Clone, Debug, Copy, PartialEq, ::re_byte_size::SizeBytes)] pub struct Resolution(pub crate::datatypes::Vec2D); impl ::re_types_core::WrapperComponent for Resolution { @@ -71,15 +72,3 @@ impl std::ops::DerefMut for Resolution { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Resolution { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/rotation_axis_angle.rs b/crates/store/re_sdk_types/src/components/rotation_axis_angle.rs index f1859eb42d23..2486b63757c1 100644 --- a/crates/store/re_sdk_types/src/components/rotation_axis_angle.rs +++ b/crates/store/re_sdk_types/src/components/rotation_axis_angle.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// If normalization of the rotation axis fails the rotation is treated as an invalid transform, unless the /// angle is zero in which case it is treated as an identity. -#[derive(Clone, Debug, Default, Copy, PartialEq)] +#[derive(Clone, Debug, Default, Copy, PartialEq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct RotationAxisAngle(pub crate::datatypes::RotationAxisAngle); @@ -73,15 +74,3 @@ impl std::ops::DerefMut for RotationAxisAngle { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for RotationAxisAngle { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/rotation_quat.rs b/crates/store/re_sdk_types/src/components/rotation_quat.rs index 7f0ff91a569e..ca0bd07fff55 100644 --- a/crates/store/re_sdk_types/src/components/rotation_quat.rs +++ b/crates/store/re_sdk_types/src/components/rotation_quat.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Note: although the x,y,z,w components of the quaternion will be passed through to the /// datastore as provided, when used in the Viewer, quaternions will always be normalized. /// If normalization fails the rotation is treated as an invalid transform. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct RotationQuat(pub crate::datatypes::Quaternion); @@ -74,15 +84,3 @@ impl std::ops::DerefMut for RotationQuat { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for RotationQuat { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/scalar.rs b/crates/store/re_sdk_types/src/components/scalar.rs index fb50623c8b62..e1fa681ffe7a 100644 --- a/crates/store/re_sdk_types/src/components/scalar.rs +++ b/crates/store/re_sdk_types/src/components/scalar.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A scalar value, encoded as a 64-bit floating point. /// /// Used for time series plots. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Scalar(pub crate::datatypes::Float64); @@ -72,15 +82,3 @@ impl std::ops::DerefMut for Scalar { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Scalar { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/scale3d.rs b/crates/store/re_sdk_types/src/components/scale3d.rs index 486bc17074ae..682e45488f1e 100644 --- a/crates/store/re_sdk_types/src/components/scale3d.rs +++ b/crates/store/re_sdk_types/src/components/scale3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// A scale of 1.0 means no scaling. /// A scale of 2.0 means doubling the size. /// Each component scales along the corresponding axis. -#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Scale3D(pub crate::datatypes::Vec3D); @@ -74,15 +77,3 @@ impl std::ops::DerefMut for Scale3D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Scale3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/schema_id.rs b/crates/store/re_sdk_types/src/components/schema_id.rs index 2ef3b93b07b4..6d162b8e8a74 100644 --- a/crates/store/re_sdk_types/src/components/schema_id.rs +++ b/crates/store/re_sdk_types/src/components/schema_id.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A 16-bit unique identifier for a schema within the MCAP file. -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct SchemaId(pub crate::datatypes::UInt16); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for SchemaId { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for SchemaId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/show_labels.rs b/crates/store/re_sdk_types/src/components/show_labels.rs index f5c49c7853c2..fe3106898d64 100644 --- a/crates/store/re_sdk_types/src/components/show_labels.rs +++ b/crates/store/re_sdk_types/src/components/show_labels.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// The main purpose of this component existing separately from the labels themselves /// is to be overridden when desired, to allow hiding and showing from the viewer and /// blueprints. -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct ShowLabels( /// Whether the entity's [`components::Text`][crate::components::Text] label is shown. pub crate::datatypes::Bool, @@ -76,15 +77,3 @@ impl std::ops::DerefMut for ShowLabels { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ShowLabels { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/stroke_width.rs b/crates/store/re_sdk_types/src/components/stroke_width.rs index 48889ecfefcf..325a7e130c40 100644 --- a/crates/store/re_sdk_types/src/components/stroke_width.rs +++ b/crates/store/re_sdk_types/src/components/stroke_width.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The width of a stroke specified in UI points. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct StrokeWidth(pub crate::datatypes::Float32); @@ -70,15 +80,3 @@ impl std::ops::DerefMut for StrokeWidth { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for StrokeWidth { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/tensor_data.rs b/crates/store/re_sdk_types/src/components/tensor_data.rs index 0d1c542a42d8..7596dbb6b00c 100644 --- a/crates/store/re_sdk_types/src/components/tensor_data.rs +++ b/crates/store/re_sdk_types/src/components/tensor_data.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -29,7 +30,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// These dimensions are combined with an index to look up values from the `buffer` field, /// which stores a contiguous array of typed values. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TensorData(pub crate::datatypes::TensorData); @@ -77,15 +78,3 @@ impl std::ops::DerefMut for TensorData { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TensorData { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/tensor_dimension_index_selection.rs b/crates/store/re_sdk_types/src/components/tensor_dimension_index_selection.rs index 3291bd392317..ad482ff9e87c 100644 --- a/crates/store/re_sdk_types/src/components/tensor_dimension_index_selection.rs +++ b/crates/store/re_sdk_types/src/components/tensor_dimension_index_selection.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Specifies a concrete index on a tensor dimension. -#[derive(Clone, Debug, Hash, Copy, PartialEq, Eq, Default)] +#[derive(Clone, Debug, Hash, Copy, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TensorDimensionIndexSelection(pub crate::datatypes::TensorDimensionIndexSelection); @@ -74,15 +75,3 @@ impl std::ops::DerefMut for TensorDimensionIndexSelection { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TensorDimensionIndexSelection { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/tensor_height_dimension.rs b/crates/store/re_sdk_types/src/components/tensor_height_dimension.rs index 9703287a8fc1..2ecc59de9639 100644 --- a/crates/store/re_sdk_types/src/components/tensor_height_dimension.rs +++ b/crates/store/re_sdk_types/src/components/tensor_height_dimension.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Specifies which dimension to use for height. -#[derive(Clone, Debug, Hash, Copy, PartialEq, Eq, Default)] +#[derive(Clone, Debug, Hash, Copy, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TensorHeightDimension(pub crate::datatypes::TensorDimensionSelection); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for TensorHeightDimension { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TensorHeightDimension { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/tensor_width_dimension.rs b/crates/store/re_sdk_types/src/components/tensor_width_dimension.rs index 9742029688a6..7abc50a8a637 100644 --- a/crates/store/re_sdk_types/src/components/tensor_width_dimension.rs +++ b/crates/store/re_sdk_types/src/components/tensor_width_dimension.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Specifies which dimension to use for width. -#[derive(Clone, Debug, Hash, Copy, PartialEq, Eq, Default)] +#[derive(Clone, Debug, Hash, Copy, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TensorWidthDimension(pub crate::datatypes::TensorDimensionSelection); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for TensorWidthDimension { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TensorWidthDimension { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/texcoord2d.rs b/crates/store/re_sdk_types/src/components/texcoord2d.rs index 51dc8298d2e0..ba53a306c4dc 100644 --- a/crates/store/re_sdk_types/src/components/texcoord2d.rs +++ b/crates/store/re_sdk_types/src/components/texcoord2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -37,7 +38,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// This is the same convention as in Vulkan/Metal/DX12/WebGPU, but (!) unlike OpenGL, /// which places the origin at the bottom-left. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Texcoord2D(pub crate::datatypes::Vec2D); @@ -85,15 +95,3 @@ impl std::ops::DerefMut for Texcoord2D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Texcoord2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/text.rs b/crates/store/re_sdk_types/src/components/text.rs index 3d1e103457d5..695b0d4d045d 100644 --- a/crates/store/re_sdk_types/src/components/text.rs +++ b/crates/store/re_sdk_types/src/components/text.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A string of text, e.g. for labels and text documents. -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Text(pub crate::datatypes::Utf8); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for Text { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Text { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/text_log_level.rs b/crates/store/re_sdk_types/src/components/text_log_level.rs index dbe2ea688ecf..66ed750420b5 100644 --- a/crates/store/re_sdk_types/src/components/text_log_level.rs +++ b/crates/store/re_sdk_types/src/components/text_log_level.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// * `"INFO"` /// * `"DEBUG"` /// * `"TRACE"` -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TextLogLevel(pub crate::datatypes::Utf8); @@ -78,15 +79,3 @@ impl std::ops::DerefMut for TextLogLevel { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TextLogLevel { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/timestamp.rs b/crates/store/re_sdk_types/src/components/timestamp.rs index f299d9e7f438..7b5965144cc1 100644 --- a/crates/store/re_sdk_types/src/components/timestamp.rs +++ b/crates/store/re_sdk_types/src/components/timestamp.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: When the recording started. /// /// Should be an absolute time, i.e. relative to Unix Epoch. -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Timestamp(pub crate::datatypes::TimeInt); @@ -72,15 +73,3 @@ impl std::ops::DerefMut for Timestamp { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Timestamp { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/transform_frame_id.rs b/crates/store/re_sdk_types/src/components/transform_frame_id.rs index dfae1cdbc025..eaecca646e5c 100644 --- a/crates/store/re_sdk_types/src/components/transform_frame_id.rs +++ b/crates/store/re_sdk_types/src/components/transform_frame_id.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Note that any [`archetypes::Transform3D`][crate::archetypes::Transform3D]s logged with both `parent_frame` and `child_frame` set /// describes a relationship between these parent and child transform frames, **not** the transform frame /// that the entity path may be using (defined by an [`archetypes::CoordinateFrame`][crate::archetypes::CoordinateFrame]). -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct TransformFrameId(pub crate::datatypes::Utf8); @@ -78,15 +79,3 @@ impl std::ops::DerefMut for TransformFrameId { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TransformFrameId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/transform_mat3x3.rs b/crates/store/re_sdk_types/src/components/transform_mat3x3.rs index 7dbb78e3a337..439c48697074 100644 --- a/crates/store/re_sdk_types/src/components/transform_mat3x3.rs +++ b/crates/store/re_sdk_types/src/components/transform_mat3x3.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -34,7 +35,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// row 1 | flat_columns[1] flat_columns[4] flat_columns[7] /// row 2 | flat_columns[2] flat_columns[5] flat_columns[8] /// ``` -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct TransformMat3x3(pub crate::datatypes::Mat3x3); @@ -82,15 +92,3 @@ impl std::ops::DerefMut for TransformMat3x3 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TransformMat3x3 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/transform_relation.rs b/crates/store/re_sdk_types/src/components/transform_relation.rs index 9e6b8aa31254..16e419abf9b2 100644 --- a/crates/store/re_sdk_types/src/components/transform_relation.rs +++ b/crates/store/re_sdk_types/src/components/transform_relation.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Specifies relation a spatial transform describes. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum TransformRelation { /// The transform describes how to transform into the parent entity's space. @@ -163,15 +164,3 @@ impl ::re_types_core::reflection::Enum for TransformRelation { .copied() } } - -impl ::re_byte_size::SizeBytes for TransformRelation { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/translation3d.rs b/crates/store/re_sdk_types/src/components/translation3d.rs index 89623eb0abcc..96fd8a7bbf7a 100644 --- a/crates/store/re_sdk_types/src/components/translation3d.rs +++ b/crates/store/re_sdk_types/src/components/translation3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A translation vector in 3D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Translation3D(pub crate::datatypes::Vec3D); @@ -70,15 +80,3 @@ impl std::ops::DerefMut for Translation3D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Translation3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/triangle_indices.rs b/crates/store/re_sdk_types/src/components/triangle_indices.rs index a32ce08c7994..65eff1ee2e99 100644 --- a/crates/store/re_sdk_types/src/components/triangle_indices.rs +++ b/crates/store/re_sdk_types/src/components/triangle_indices.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: The three indices of a triangle in a triangle mesh. -#[derive(Clone, Debug, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct TriangleIndices(pub crate::datatypes::UVec3D); @@ -70,15 +73,3 @@ impl std::ops::DerefMut for TriangleIndices { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for TriangleIndices { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/value_range.rs b/crates/store/re_sdk_types/src/components/value_range.rs index 93604ea51b50..9d2fecde19e6 100644 --- a/crates/store/re_sdk_types/src/components/value_range.rs +++ b/crates/store/re_sdk_types/src/components/value_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Range of expected or valid values, specifying a lower and upper bound. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct ValueRange(pub crate::datatypes::Range1D); @@ -72,15 +75,3 @@ impl std::ops::DerefMut for ValueRange { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ValueRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/value_range_ext.rs b/crates/store/re_sdk_types/src/components/value_range_ext.rs index e8a3dbd12f63..1fbfd4f7ca19 100644 --- a/crates/store/re_sdk_types/src/components/value_range_ext.rs +++ b/crates/store/re_sdk_types/src/components/value_range_ext.rs @@ -37,7 +37,7 @@ impl ValueRange { impl Display for ValueRange { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[{}, {}]", self.start(), self.end(),) + write!(f, "[{}, {}]", self.start(), self.end()) } } diff --git a/crates/store/re_sdk_types/src/components/vector2d.rs b/crates/store/re_sdk_types/src/components/vector2d.rs index b2c73ee9ea51..3ef904db4d3d 100644 --- a/crates/store/re_sdk_types/src/components/vector2d.rs +++ b/crates/store/re_sdk_types/src/components/vector2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A vector in 2D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Vector2D(pub crate::datatypes::Vec2D); @@ -70,15 +80,3 @@ impl std::ops::DerefMut for Vector2D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Vector2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/vector3d.rs b/crates/store/re_sdk_types/src/components/vector3d.rs index 19a279b76ffd..9356e2f105d6 100644 --- a/crates/store/re_sdk_types/src/components/vector3d.rs +++ b/crates/store/re_sdk_types/src/components/vector3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: A vector in 3D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Vector3D(pub crate::datatypes::Vec3D); @@ -70,15 +80,3 @@ impl std::ops::DerefMut for Vector3D { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Vector3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/video_codec.rs b/crates/store/re_sdk_types/src/components/video_codec.rs index a76600c0954f..61243d89b86c 100644 --- a/crates/store/re_sdk_types/src/components/video_codec.rs +++ b/crates/store/re_sdk_types/src/components/video_codec.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -28,7 +29,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// For more details see check the [video reference](https://rerun.io/docs/reference/video). /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(u32)] pub enum VideoCodec { /// AOMedia Video 1 (AV1) @@ -69,6 +70,22 @@ pub enum VideoCodec { /// /// Enum value is the fourcc for 'hev1' (the WebCodec string assigned to this codec) in big endian. H265 = 0x68657631, + + /// VP8 + /// + /// See + /// + /// Enum value is the fourcc for 'vp08' (the WebCodec string assigned to this codec) in big endian. + #[allow(clippy::upper_case_acronyms)] + VP8 = 0x76703038, + + /// VP9 + /// + /// See + /// + /// Enum value is the fourcc for 'vp09' (the WebCodec string assigned to this codec) in big endian. + #[allow(clippy::upper_case_acronyms)] + VP9 = 0x76703039, } impl ::re_types_core::Component for VideoCodec { @@ -162,6 +179,8 @@ impl std::fmt::Display for VideoCodec { Self::AV1 => write!(f, "AV1"), Self::H264 => write!(f, "H264"), Self::H265 => write!(f, "H265"), + Self::VP8 => write!(f, "VP8"), + Self::VP9 => write!(f, "VP9"), } } } @@ -171,7 +190,7 @@ impl ::re_types_core::reflection::Enum for VideoCodec { #[inline] fn variants() -> &'static [Self] { - &[Self::AV1, Self::H264, Self::H265] + &[Self::AV1, Self::H264, Self::H265, Self::VP8, Self::VP9] } #[inline] @@ -186,6 +205,12 @@ impl ::re_types_core::reflection::Enum for VideoCodec { Self::H265 => { "High Efficiency Video Coding (HEVC/H.265)\n\nSee \n\n[`components.VideoSample`](https://rerun.io/docs/reference/types/components/video_sample)s using this codec should be formatted according to Annex B specification.\n(Note that this is different from AVCC format found in MP4 files.\nTo learn more about Annex B, check for instance )\nKey frames (IRAP) require inclusion of a SPS (Sequence Parameter Set)\n\nEnum value is the fourcc for 'hev1' (the WebCodec string assigned to this codec) in big endian." } + Self::VP8 => { + "VP8\n\nSee \n\nEnum value is the fourcc for 'vp08' (the WebCodec string assigned to this codec) in big endian." + } + Self::VP9 => { + "VP9\n\nSee \n\nEnum value is the fourcc for 'vp09' (the WebCodec string assigned to this codec) in big endian." + } } } @@ -195,19 +220,9 @@ impl ::re_types_core::reflection::Enum for VideoCodec { 0x61763031 => Some(Self::AV1), 0x61766331 => Some(Self::H264), 0x68657631 => Some(Self::H265), + 0x76703038 => Some(Self::VP8), + 0x76703039 => Some(Self::VP9), _ => None, } } } - -impl ::re_byte_size::SizeBytes for VideoCodec { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/components/video_codec_ext.rs b/crates/store/re_sdk_types/src/components/video_codec_ext.rs index 238c11642a02..0c1b838331c0 100644 --- a/crates/store/re_sdk_types/src/components/video_codec_ext.rs +++ b/crates/store/re_sdk_types/src/components/video_codec_ext.rs @@ -9,9 +9,8 @@ impl TryFrom for VideoCodec { re_video::VideoCodec::H264 => Ok(Self::H264), re_video::VideoCodec::H265 => Ok(Self::H265), re_video::VideoCodec::AV1 => Ok(Self::AV1), - re_video::VideoCodec::VP8 | re_video::VideoCodec::VP9 => Err(format!( - "Video codec {value:?} is not supported for VideoStream yet", - )), + re_video::VideoCodec::VP8 => Ok(Self::VP8), + re_video::VideoCodec::VP9 => Ok(Self::VP9), re_video::VideoCodec::ImageSequence(_) => Err("Not a real video".to_owned()), } } @@ -24,9 +23,8 @@ impl From for re_video::VideoCodec { crate::components::VideoCodec::H264 => Self::H264, crate::components::VideoCodec::H265 => Self::H265, crate::components::VideoCodec::AV1 => Self::AV1, - // TODO(#10186): Add support for VP9. - // VideoCodec::VP8 => Self::VP8, - // VideoCodec::VP9 => Self::VP9, + crate::components::VideoCodec::VP8 => Self::VP8, + crate::components::VideoCodec::VP9 => Self::VP9, } } } @@ -38,6 +36,8 @@ impl VideoCodec { 0x61763031 => Some(Self::AV1), 0x61766331 => Some(Self::H264), 0x68657631 => Some(Self::H265), + 0x76703038 => Some(Self::VP8), + 0x76703039 => Some(Self::VP9), _ => None, } } diff --git a/crates/store/re_sdk_types/src/components/video_sample.rs b/crates/store/re_sdk_types/src/components/video_sample.rs index 37c84a126a23..d2a29055cc8f 100644 --- a/crates/store/re_sdk_types/src/components/video_sample.rs +++ b/crates/store/re_sdk_types/src/components/video_sample.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -27,7 +28,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// (this restriction may be relaxed in the future for some codecs). /// /// Keyframes may require additional data, for details see [`components::VideoCodec`][crate::components::VideoCodec]. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct VideoSample(pub crate::datatypes::Blob); @@ -75,15 +76,3 @@ impl std::ops::DerefMut for VideoSample { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for VideoSample { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/video_timestamp.rs b/crates/store/re_sdk_types/src/components/video_timestamp.rs index 45cf080d0c64..b22449f24244 100644 --- a/crates/store/re_sdk_types/src/components/video_timestamp.rs +++ b/crates/store/re_sdk_types/src/components/video_timestamp.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Timestamp inside a [`archetypes::AssetVideo`][crate::archetypes::AssetVideo]. -#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct VideoTimestamp(pub crate::datatypes::VideoTimestamp); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for VideoTimestamp { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for VideoTimestamp { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/view_coordinates.rs b/crates/store/re_sdk_types/src/components/view_coordinates.rs index 86a443d7735a..8c3b9717aca8 100644 --- a/crates/store/re_sdk_types/src/components/view_coordinates.rs +++ b/crates/store/re_sdk_types/src/components/view_coordinates.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -41,7 +42,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// * Back = 6 /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct ViewCoordinates( /// The directions of the [x, y, z] axes. @@ -92,15 +95,3 @@ impl std::ops::DerefMut for ViewCoordinates { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ViewCoordinates { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/view_coordinates_ext.rs b/crates/store/re_sdk_types/src/components/view_coordinates_ext.rs index 37716da4a307..a928f38be39f 100644 --- a/crates/store/re_sdk_types/src/components/view_coordinates_ext.rs +++ b/crates/store/re_sdk_types/src/components/view_coordinates_ext.rs @@ -114,7 +114,7 @@ impl ViewCoordinates { let x_long = ViewDir::try_from(x).map(|x| x.long()).unwrap_or("?"); let y_long = ViewDir::try_from(y).map(|y| y.long()).unwrap_or("?"); let z_long = ViewDir::try_from(z).map(|z| z.long()).unwrap_or("?"); - format!("{x_short}{y_short}{z_short} (X={x_long}, Y={y_long}, Z={z_long})",) + format!("{x_short}{y_short}{z_short} (X={x_long}, Y={y_long}, Z={z_long})") } /// Returns a matrix that transforms from another coordinate system to this (self) one. diff --git a/crates/store/re_sdk_types/src/components/visible.rs b/crates/store/re_sdk_types/src/components/visible.rs index 757bf52bb18c..1c46ceb6318d 100644 --- a/crates/store/re_sdk_types/src/components/visible.rs +++ b/crates/store/re_sdk_types/src/components/visible.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Component**: Whether the container, view, entity or instance is currently visible. -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Visible(pub crate::datatypes::Bool); @@ -70,15 +71,3 @@ impl std::ops::DerefMut for Visible { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for Visible { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/components/voxel_index.rs b/crates/store/re_sdk_types/src/components/voxel_index.rs new file mode 100644 index 000000000000..89ede5de534d --- /dev/null +++ b/crates/store/re_sdk_types/src/components/voxel_index.rs @@ -0,0 +1,85 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_index.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Integer index of a voxel in a sparse 3D voxel grid. +/// +/// The voxel center in local grid coordinates is `(index + 0.5) * voxel_size`. +#[derive( + Clone, + Debug, + Copy, + PartialEq, + Eq, + Hash, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] +#[repr(transparent)] +pub struct VoxelIndex(pub crate::datatypes::IVec3D); + +impl ::re_types_core::WrapperComponent for VoxelIndex { + type Datatype = crate::datatypes::IVec3D; + + #[inline] + fn name() -> ComponentType { + "rerun.components.VoxelIndex".into() + } + + #[inline] + fn into_inner(self) -> Self::Datatype { + self.0 + } +} + +::re_types_core::macros::impl_into_cow!(VoxelIndex); + +impl> From for VoxelIndex { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for VoxelIndex { + #[inline] + fn borrow(&self) -> &crate::datatypes::IVec3D { + &self.0 + } +} + +impl std::ops::Deref for VoxelIndex { + type Target = crate::datatypes::IVec3D; + + #[inline] + fn deref(&self) -> &crate::datatypes::IVec3D { + &self.0 + } +} + +impl std::ops::DerefMut for VoxelIndex { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::IVec3D { + &mut self.0 + } +} diff --git a/crates/store/re_sdk_types/src/components/voxel_size.rs b/crates/store/re_sdk_types/src/components/voxel_size.rs new file mode 100644 index 000000000000..1a8168ce0ae2 --- /dev/null +++ b/crates/store/re_sdk_types/src/components/voxel_size.rs @@ -0,0 +1,78 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_size.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: The scene-unit dimensions of one voxel in a sparse 3D voxel grid. +/// +/// Each component is the size of a voxel along the corresponding local grid axis. +/// All components must be finite and positive. +#[derive( + Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] +#[repr(transparent)] +pub struct VoxelSize(pub crate::datatypes::Vec3D); + +impl ::re_types_core::WrapperComponent for VoxelSize { + type Datatype = crate::datatypes::Vec3D; + + #[inline] + fn name() -> ComponentType { + "rerun.components.VoxelSize".into() + } + + #[inline] + fn into_inner(self) -> Self::Datatype { + self.0 + } +} + +::re_types_core::macros::impl_into_cow!(VoxelSize); + +impl> From for VoxelSize { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for VoxelSize { + #[inline] + fn borrow(&self) -> &crate::datatypes::Vec3D { + &self.0 + } +} + +impl std::ops::Deref for VoxelSize { + type Target = crate::datatypes::Vec3D; + + #[inline] + fn deref(&self) -> &crate::datatypes::Vec3D { + &self.0 + } +} + +impl std::ops::DerefMut for VoxelSize { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Vec3D { + &mut self.0 + } +} diff --git a/crates/store/re_sdk_types/src/components/voxel_value.rs b/crates/store/re_sdk_types/src/components/voxel_value.rs new file mode 100644 index 000000000000..ed42e58ce92a --- /dev/null +++ b/crates/store/re_sdk_types/src/components/voxel_value.rs @@ -0,0 +1,82 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_value.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Optional scalar occupancy or value associated with a voxel. +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] +#[repr(transparent)] +pub struct VoxelValue(pub crate::datatypes::Float32); + +impl ::re_types_core::WrapperComponent for VoxelValue { + type Datatype = crate::datatypes::Float32; + + #[inline] + fn name() -> ComponentType { + "rerun.components.VoxelValue".into() + } + + #[inline] + fn into_inner(self) -> Self::Datatype { + self.0 + } +} + +::re_types_core::macros::impl_into_cow!(VoxelValue); + +impl> From for VoxelValue { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for VoxelValue { + #[inline] + fn borrow(&self) -> &crate::datatypes::Float32 { + &self.0 + } +} + +impl std::ops::Deref for VoxelValue { + type Target = crate::datatypes::Float32; + + #[inline] + fn deref(&self) -> &crate::datatypes::Float32 { + &self.0 + } +} + +impl std::ops::DerefMut for VoxelValue { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Float32 { + &mut self.0 + } +} diff --git a/crates/store/re_sdk_types/src/datatypes/.gitattributes b/crates/store/re_sdk_types/src/datatypes/.gitattributes index 227863f665ac..e2b37bb4f829 100644 --- a/crates/store/re_sdk_types/src/datatypes/.gitattributes +++ b/crates/store/re_sdk_types/src/datatypes/.gitattributes @@ -12,6 +12,7 @@ class_id.rs linguist-generated=true color_model.rs linguist-generated=true dvec2d.rs linguist-generated=true image_format.rs linguist-generated=true +ivec3d.rs linguist-generated=true keypoint_id.rs linguist-generated=true keypoint_pair.rs linguist-generated=true mat3x3.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/datatypes/angle.rs b/crates/store/re_sdk_types/src/datatypes/angle.rs index b0cb91653b9b..2969e4b2b8c1 100644 --- a/crates/store/re_sdk_types/src/datatypes/angle.rs +++ b/crates/store/re_sdk_types/src/datatypes/angle.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,17 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Angle in radians. -#[derive(Clone, Debug, Copy, Default, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + Default, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Angle { /// Angle in radians. One turn is equal to 2π (or τ) radians. @@ -145,15 +156,3 @@ impl From for f32 { value.radians } } - -impl ::re_byte_size::SizeBytes for Angle { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.radians.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/annotation_info.rs b/crates/store/re_sdk_types/src/datatypes/annotation_info.rs index d6a2a17c5ce1..ed2c7dd1d030 100644 --- a/crates/store/re_sdk_types/src/datatypes/annotation_info.rs +++ b/crates/store/re_sdk_types/src/datatypes/annotation_info.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// Color and label will be used to annotate entities/keypoints which reference the id. /// The id refers either to a class or key-point id -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] pub struct AnnotationInfo { /// [`datatypes::ClassId`][crate::datatypes::ClassId] or [`datatypes::KeypointId`][crate::datatypes::KeypointId] to which this annotation info belongs. pub id: u16, @@ -186,11 +187,11 @@ impl ::re_types_core::Loggable for AnnotationInfo { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let id = { if !arrays_by_name.contains_key("id") { return Err(DeserializationError::missing_struct_field( @@ -306,17 +307,3 @@ impl ::re_types_core::Loggable for AnnotationInfo { }) } } - -impl ::re_byte_size::SizeBytes for AnnotationInfo { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.id.heap_size_bytes() + self.label.heap_size_bytes() + self.color.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && >::is_pod() - && >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/blob.rs b/crates/store/re_sdk_types/src/datatypes/blob.rs index da1b76b5a622..66563734a983 100644 --- a/crates/store/re_sdk_types/src/datatypes/blob.rs +++ b/crates/store/re_sdk_types/src/datatypes/blob.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A binary blob of data. /// /// Ref-counted internally and therefore cheap to clone. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Blob(pub ::arrow::buffer::ScalarBuffer); @@ -75,14 +76,14 @@ impl ::re_types_core::Loggable for Blob { let second = iter.next(); match (first, second) { (Some(single), None) => single.clone(), - (Some(first_buf), Some(second_buf)) => { - std::iter::once(first_buf.as_ref() as &[_]) - .chain(std::iter::once(second_buf.as_ref() as &[_])) - .chain(iter.map(|b| b.as_ref() as &[_])) - .collect::>() - .concat() - .into() - } + (Some(first_buf), Some(second_buf)) => ::itertools::chain!( + ::std::iter::once(first_buf.as_ref() as &[_]), + ::std::iter::once(second_buf.as_ref() as &[_]), + iter.map(|b| b.as_ref() as &[_]), + ) + .collect::>() + .concat() + .into(), _ => Vec::new().into(), } }; @@ -176,15 +177,3 @@ impl From for ::arrow::buffer::ScalarBuffer { value.0 } } - -impl ::re_byte_size::SizeBytes for Blob { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <::arrow::buffer::ScalarBuffer>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/channel_count_pair.rs b/crates/store/re_sdk_types/src/datatypes/channel_count_pair.rs index e0f8444503bf..54b9842f5f6e 100644 --- a/crates/store/re_sdk_types/src/datatypes/channel_count_pair.rs +++ b/crates/store/re_sdk_types/src/datatypes/channel_count_pair.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A pair representing a channel ID and its associated message count. -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] pub struct ChannelCountPair { /// The channel ID. pub channel_id: crate::datatypes::UInt16, @@ -160,11 +161,11 @@ impl ::re_types_core::Loggable for ChannelCountPair { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let channel_id = { if !arrays_by_name.contains_key("channel_id") { return Err(DeserializationError::missing_struct_field( @@ -232,15 +233,3 @@ impl ::re_types_core::Loggable for ChannelCountPair { }) } } - -impl ::re_byte_size::SizeBytes for ChannelCountPair { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.channel_id.heap_size_bytes() + self.message_count.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/channel_datatype.rs b/crates/store/re_sdk_types/src/datatypes/channel_datatype.rs index 8596c3e199db..6379d6f1a8ec 100644 --- a/crates/store/re_sdk_types/src/datatypes/channel_datatype.rs +++ b/crates/store/re_sdk_types/src/datatypes/channel_datatype.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: The innermost datatype of an image. /// /// How individual color channel components are encoded. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum ChannelDatatype { /// 8-bit unsigned integer. @@ -214,15 +215,3 @@ impl ::re_types_core::reflection::Enum for ChannelDatatype { } } } - -impl ::re_byte_size::SizeBytes for ChannelDatatype { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/class_description.rs b/crates/store/re_sdk_types/src/datatypes/class_description.rs index 8cb59a7d60b0..2d5481ea4d9a 100644 --- a/crates/store/re_sdk_types/src/datatypes/class_description.rs +++ b/crates/store/re_sdk_types/src/datatypes/class_description.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -35,7 +36,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// defined, and both keypoints exist within the instance of the class, then the /// keypoints should be connected with an edge. The edge should be labeled and /// colored as described by the class's [`datatypes::AnnotationInfo`][crate::datatypes::AnnotationInfo]. -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] pub struct ClassDescription { /// The [`datatypes::AnnotationInfo`][crate::datatypes::AnnotationInfo] for the class. pub info: crate::datatypes::AnnotationInfo, @@ -265,11 +266,11 @@ impl ::re_types_core::Loggable for ClassDescription { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let info = { if !arrays_by_name.contains_key("info") { return Err(DeserializationError::missing_struct_field( @@ -447,19 +448,3 @@ impl ::re_types_core::Loggable for ClassDescription { }) } } - -impl ::re_byte_size::SizeBytes for ClassDescription { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.info.heap_size_bytes() - + self.keypoint_annotations.heap_size_bytes() - + self.keypoint_connections.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && >::is_pod() - && >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/class_description_map_elem.rs b/crates/store/re_sdk_types/src/datatypes/class_description_map_elem.rs index 47ef106bd214..888993b171c6 100644 --- a/crates/store/re_sdk_types/src/datatypes/class_description_map_elem.rs +++ b/crates/store/re_sdk_types/src/datatypes/class_description_map_elem.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// This is internal to [`components::AnnotationContext`][crate::components::AnnotationContext]. /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] pub struct ClassDescriptionMapElem { /// The key: the [`components::ClassId`][crate::components::ClassId]. pub class_id: crate::datatypes::ClassId, @@ -160,11 +161,11 @@ impl ::re_types_core::Loggable for ClassDescriptionMapElem { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let class_id = { if !arrays_by_name.contains_key("class_id") { return Err(DeserializationError::missing_struct_field( @@ -224,15 +225,3 @@ impl ::re_types_core::Loggable for ClassDescriptionMapElem { }) } } - -impl ::re_byte_size::SizeBytes for ClassDescriptionMapElem { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.class_id.heap_size_bytes() + self.class_description.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/class_id.rs b/crates/store/re_sdk_types/src/datatypes/class_id.rs index a052e7fd2529..adb8ac4cf458 100644 --- a/crates/store/re_sdk_types/src/datatypes/class_id.rs +++ b/crates/store/re_sdk_types/src/datatypes/class_id.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -36,9 +37,10 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; Hash, bytemuck::Pod, bytemuck::Zeroable, + ::re_byte_size::SizeBytes, )] #[repr(transparent)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[derive(::serde::Serialize, ::serde::Deserialize)] pub struct ClassId(pub u16); ::re_types_core::macros::impl_into_cow!(ClassId); @@ -151,15 +153,3 @@ impl From for u16 { value.0 } } - -impl ::re_byte_size::SizeBytes for ClassId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/color_model.rs b/crates/store/re_sdk_types/src/datatypes/color_model.rs index 73b08f017c81..2da9fe326b1c 100644 --- a/crates/store/re_sdk_types/src/datatypes/color_model.rs +++ b/crates/store/re_sdk_types/src/datatypes/color_model.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Specified what color components are present in an [`archetypes::Image`][crate::archetypes::Image]. /// /// This combined with [`datatypes::ChannelDatatype`][crate::datatypes::ChannelDatatype] determines the pixel format of an image. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum ColorModel { /// Grayscale luminance intencity/brightness/value, sometimes called `Y` @@ -165,15 +166,3 @@ impl ::re_types_core::reflection::Enum for ColorModel { .copied() } } - -impl ::re_byte_size::SizeBytes for ColorModel { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/dvec2d.rs b/crates/store/re_sdk_types/src/datatypes/dvec2d.rs index a8a5bb463e95..d60cec54d61f 100644 --- a/crates/store/re_sdk_types/src/datatypes/dvec2d.rs +++ b/crates/store/re_sdk_types/src/datatypes/dvec2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A double-precision vector in 2D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct DVec2D(pub [f64; 2usize]); @@ -114,9 +124,10 @@ impl ::re_types_core::Loggable for DVec2D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(2usize) - .zip((2usize..).step_by(2usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -190,7 +201,7 @@ impl ::re_types_core::Loggable for DVec2D { }) .with_context("rerun.datatypes.DVec2D#xy")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 2usize]>( + bytemuck::cast_slice::<_, [f64; 2usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -222,15 +233,3 @@ impl From for [f64; 2usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for DVec2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f64; 2usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/dvec2d_ext.rs b/crates/store/re_sdk_types/src/datatypes/dvec2d_ext.rs index 9e2a801b6758..d90a9750154b 100644 --- a/crates/store/re_sdk_types/src/datatypes/dvec2d_ext.rs +++ b/crates/store/re_sdk_types/src/datatypes/dvec2d_ext.rs @@ -98,6 +98,6 @@ impl From> for DVec2D { impl std::fmt::Display for DVec2D { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let prec = f.precision().unwrap_or(crate::DEFAULT_DISPLAY_DECIMALS); - write!(f, "[{:.prec$}, {:.prec$}]", self.x(), self.y(),) + write!(f, "[{:.prec$}, {:.prec$}]", self.x(), self.y()) } } diff --git a/crates/store/re_sdk_types/src/datatypes/image_format.rs b/crates/store/re_sdk_types/src/datatypes/image_format.rs index b715c9e1f8fc..4240cdcecbef 100644 --- a/crates/store/re_sdk_types/src/datatypes/image_format.rs +++ b/crates/store/re_sdk_types/src/datatypes/image_format.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: The metadata describing the contents of a [`components::ImageBuffer`][crate::components::ImageBuffer]. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash, ::re_byte_size::SizeBytes)] pub struct ImageFormat { /// The width of the image in pixels. pub width: u32, @@ -249,11 +250,11 @@ impl ::re_types_core::Loggable for ImageFormat { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let width = { if !arrays_by_name.contains_key("width") { return Err(DeserializationError::missing_struct_field( @@ -361,23 +362,3 @@ impl ::re_types_core::Loggable for ImageFormat { }) } } - -impl ::re_byte_size::SizeBytes for ImageFormat { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.width.heap_size_bytes() - + self.height.heap_size_bytes() - + self.pixel_format.heap_size_bytes() - + self.color_model.heap_size_bytes() - + self.channel_datatype.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - && >::is_pod() - && >::is_pod() - && >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/ivec3d.rs b/crates/store/re_sdk_types/src/datatypes/ivec3d.rs new file mode 100644 index 000000000000..d08c88c1c470 --- /dev/null +++ b/crates/store/re_sdk_types/src/datatypes/ivec3d.rs @@ -0,0 +1,237 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/datatypes/ivec3d.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Datatype**: An int32 vector in 3D space. +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + Eq, + Hash, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] +#[repr(C)] +pub struct IVec3D(pub [i32; 3usize]); + +::re_types_core::macros::impl_into_cow!(IVec3D); + +impl ::re_types_core::Loggable for IVec3D { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + use arrow::datatypes::*; + DataType::FixedSizeList( + std::sync::Arc::new(Field::new("item", DataType::Int32, false)), + 3, + ) + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult + where + Self: Clone + 'a, + { + #![allow(clippy::manual_is_variant_and)] + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_helpers::as_array_ref}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| datum.into_owned().0); + (datum.is_some(), datum) + }) + .unzip(); + let data0_validity: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + { + let data0_inner_data: Vec<_> = data0 + .into_iter() + .flat_map(|v| match v { + Some(v) => itertools::Either::Left(v.into_iter()), + None => itertools::Either::Right(std::iter::repeat_n( + Default::default(), + 3usize, + )), + }) + .collect(); + let data0_inner_validity: Option = + data0_validity.as_ref().map(|validity| { + validity + .iter() + .map(|b| std::iter::repeat_n(b, 3usize)) + .flatten() + .collect::>() + .into() + }); + as_array_ref(FixedSizeListArray::new( + std::sync::Arc::new(Field::new("item", DataType::Int32, false)), + 3, + as_array_ref(PrimitiveArray::::new( + ScalarBuffer::from(data0_inner_data.into_iter().collect::>()), + data0_inner_validity, + )), + data0_validity, + )) + } + }) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_zip_validity::ZipValidity}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok({ + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.datatypes.IVec3D#xyz")?; + if arrow_data.is_empty() { + Vec::new() + } else { + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data.len()), + ); + let arrow_data_inner = { + let arrow_data_inner = &**arrow_data.values(); + arrow_data_inner + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::Int32; + let actual = arrow_data_inner.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.datatypes.IVec3D#xyz")? + .into_iter() + .collect::>() + }; + ZipValidity::new_with_validity(offsets, arrow_data.nulls()) + .map(|elem| { + elem.map(|(start, end): (usize, usize)| { + re_log::debug_assert!(end - start == 3usize); + if arrow_data_inner.len() < end { + return Err(DeserializationError::offset_slice_oob( + (start, end), + arrow_data_inner.len(), + )); + } + + #[expect(unsafe_code, clippy::undocumented_unsafe_blocks)] + let data = unsafe { arrow_data_inner.get_unchecked(start..end) }; + let data = data.iter().cloned().map(Option::unwrap_or_default); + + // NOTE: Unwrapping cannot fail: the length must be correct. + #[expect(clippy::unwrap_used)] + Ok(array_init::from_iter(data).unwrap()) + }) + .transpose() + }) + .collect::>>>()? + } + .into_iter() + } + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .map(|res| res.map(|v| Some(Self(v)))) + .collect::>>>() + .with_context("rerun.datatypes.IVec3D#xyz") + .with_context("rerun.datatypes.IVec3D")?) + } + + #[inline] + fn from_arrow(arrow_data: &dyn arrow::array::Array) -> DeserializationResult> + where + Self: Sized, + { + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_zip_validity::ZipValidity}; + use arrow::{array::*, buffer::*, datatypes::*}; + if let Some(nulls) = arrow_data.nulls() + && nulls.null_count() != 0 + { + return Err(DeserializationError::missing_data()); + } + Ok({ + let slice = { + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::FixedSizeList( + std::sync::Arc::new(Field::new("item", DataType::Int32, false)), + 3, + ); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.datatypes.IVec3D#xyz")?; + let arrow_data_inner = &**arrow_data.values(); + bytemuck::cast_slice::<_, [i32; 3usize]>( + arrow_data_inner + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::Int32; + let actual = arrow_data_inner.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.datatypes.IVec3D#xyz")? + .values() + .as_ref(), + ) + }; + { slice.iter().copied().map(Self).collect::>() } + }) + } +} + +impl From<[i32; 3usize]> for IVec3D { + #[inline] + fn from(xyz: [i32; 3usize]) -> Self { + Self(xyz) + } +} + +impl From for [i32; 3usize] { + #[inline] + fn from(value: IVec3D) -> Self { + value.0 + } +} diff --git a/crates/store/re_sdk_types/src/datatypes/ivec3d_ext.rs b/crates/store/re_sdk_types/src/datatypes/ivec3d_ext.rs new file mode 100644 index 000000000000..9c2a50dbeeb8 --- /dev/null +++ b/crates/store/re_sdk_types/src/datatypes/ivec3d_ext.rs @@ -0,0 +1,98 @@ +use super::IVec3D; + +impl IVec3D { + /// The zero vector, i.e. the additive identity. + pub const ZERO: Self = Self([0; 3]); + + /// The unit vector `[1, 1, 1]`, i.e. the multiplicative identity. + pub const ONE: Self = Self([1; 3]); + + /// Create a new vector. + #[inline] + pub const fn new(x: i32, y: i32, z: i32) -> Self { + Self([x, y, z]) + } + + /// The x-coordinate, i.e. index 0. + #[inline] + pub fn x(&self) -> i32 { + self.0[0] + } + + /// The y-coordinate, i.e. index 1. + #[inline] + pub fn y(&self) -> i32 { + self.0[1] + } + + /// The z-coordinate, i.e. index 2. + #[inline] + pub fn z(&self) -> i32 { + self.0[2] + } +} + +impl From<(i32, i32, i32)> for IVec3D { + #[inline] + fn from((x, y, z): (i32, i32, i32)) -> Self { + Self::new(x, y, z) + } +} + +// NOTE: All these by-ref impls make the lives of end-users much easier when juggling around with +// slices, because Rust cannot keep track of the inherent `Copy` capability of it all across all the +// layers of `Into`/`IntoIterator`. + +impl<'a> From<&'a Self> for IVec3D { + fn from(v: &'a Self) -> Self { + Self(v.0) + } +} + +impl<'a> From<&'a (i32, i32, i32)> for IVec3D { + #[inline] + fn from((x, y, z): &'a (i32, i32, i32)) -> Self { + Self::new(*x, *y, *z) + } +} + +impl<'a> From<&'a [i32; 3]> for IVec3D { + #[inline] + fn from(v: &'a [i32; 3]) -> Self { + Self(*v) + } +} + +impl std::ops::Index for IVec3D +where + Idx: std::slice::SliceIndex<[i32]>, +{ + type Output = Idx::Output; + + #[inline] + fn index(&self, index: Idx) -> &Self::Output { + &self.0[index] + } +} + +#[cfg(feature = "glam")] +impl From for glam::IVec3 { + #[inline] + fn from(v: IVec3D) -> Self { + Self::from_slice(&v.0) + } +} + +#[cfg(feature = "glam")] +impl From for IVec3D { + #[inline] + fn from(v: glam::IVec3) -> Self { + Self(v.to_array()) + } +} + +impl std::fmt::Display for IVec3D { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}, {}, {}]", self.x(), self.y(), self.z()) + } +} diff --git a/crates/store/re_sdk_types/src/datatypes/keypoint_id.rs b/crates/store/re_sdk_types/src/datatypes/keypoint_id.rs index 3c0e5f6141cd..e42c63f40afc 100644 --- a/crates/store/re_sdk_types/src/datatypes/keypoint_id.rs +++ b/crates/store/re_sdk_types/src/datatypes/keypoint_id.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -38,9 +39,10 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; Hash, bytemuck::Pod, bytemuck::Zeroable, + ::re_byte_size::SizeBytes, )] #[repr(transparent)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[derive(::serde::Serialize, ::serde::Deserialize)] pub struct KeypointId(pub u16); ::re_types_core::macros::impl_into_cow!(KeypointId); @@ -153,15 +155,3 @@ impl From for u16 { value.0 } } - -impl ::re_byte_size::SizeBytes for KeypointId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/keypoint_pair.rs b/crates/store/re_sdk_types/src/datatypes/keypoint_pair.rs index 314f395a4225..b5a28ad1ba93 100644 --- a/crates/store/re_sdk_types/src/datatypes/keypoint_pair.rs +++ b/crates/store/re_sdk_types/src/datatypes/keypoint_pair.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A connection between two [`datatypes::KeypointId`][crate::datatypes::KeypointId]s. -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] pub struct KeypointPair { /// The first point of the pair. pub keypoint0: crate::datatypes::KeypointId, @@ -160,11 +161,11 @@ impl ::re_types_core::Loggable for KeypointPair { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let keypoint0 = { if !arrays_by_name.contains_key("keypoint0") { return Err(DeserializationError::missing_struct_field( @@ -230,15 +231,3 @@ impl ::re_types_core::Loggable for KeypointPair { }) } } - -impl ::re_byte_size::SizeBytes for KeypointPair { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.keypoint0.heap_size_bytes() + self.keypoint1.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/mat3x3.rs b/crates/store/re_sdk_types/src/datatypes/mat3x3.rs index 2c5c39f1f0a3..131998cb423b 100644 --- a/crates/store/re_sdk_types/src/datatypes/mat3x3.rs +++ b/crates/store/re_sdk_types/src/datatypes/mat3x3.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -31,7 +32,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// row 1 | flat_columns[1] flat_columns[4] flat_columns[7] /// row 2 | flat_columns[2] flat_columns[5] flat_columns[8] /// ``` -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Mat3x3( /// Flat list of matrix coefficients in column-major order. @@ -126,9 +136,10 @@ impl ::re_types_core::Loggable for Mat3x3 { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(9usize) - .zip((9usize..).step_by(9usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(9usize), + (9usize..).step_by(9usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -202,7 +213,7 @@ impl ::re_types_core::Loggable for Mat3x3 { }) .with_context("rerun.datatypes.Mat3x3#flat_columns")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 9usize]>( + bytemuck::cast_slice::<_, [f32; 9usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -234,15 +245,3 @@ impl From for [f32; 9usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for Mat3x3 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f32; 9usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/mat4x4.rs b/crates/store/re_sdk_types/src/datatypes/mat4x4.rs index 64bc5141a2c2..c07a2bbe7e0f 100644 --- a/crates/store/re_sdk_types/src/datatypes/mat4x4.rs +++ b/crates/store/re_sdk_types/src/datatypes/mat4x4.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -32,7 +33,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// row 2 | flat_columns[2] flat_columns[6] flat_columns[10] flat_columns[14] /// row 3 | flat_columns[3] flat_columns[7] flat_columns[11] flat_columns[15] /// ``` -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, ::re_byte_size::SizeBytes)] pub struct Mat4x4( /// Flat list of matrix coefficients in column-major order. pub [f32; 16usize], @@ -126,9 +127,10 @@ impl ::re_types_core::Loggable for Mat4x4 { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(16usize) - .zip((16usize..).step_by(16usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(16usize), + (16usize..).step_by(16usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -202,7 +204,7 @@ impl ::re_types_core::Loggable for Mat4x4 { }) .with_context("rerun.datatypes.Mat4x4#flat_columns")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 16usize]>( + bytemuck::cast_slice::<_, [f32; 16usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -234,15 +236,3 @@ impl From for [f32; 16usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for Mat4x4 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f32; 16usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/mod.rs b/crates/store/re_sdk_types/src/datatypes/mod.rs index 6f1acc8db037..28aa1056d901 100644 --- a/crates/store/re_sdk_types/src/datatypes/mod.rs +++ b/crates/store/re_sdk_types/src/datatypes/mod.rs @@ -21,6 +21,8 @@ mod dvec2d; mod dvec2d_ext; mod image_format; mod image_format_ext; +mod ivec3d; +mod ivec3d_ext; mod keypoint_id; mod keypoint_id_ext; mod keypoint_pair; @@ -82,6 +84,7 @@ pub use self::class_id::ClassId; pub use self::color_model::ColorModel; pub use self::dvec2d::DVec2D; pub use self::image_format::ImageFormat; +pub use self::ivec3d::IVec3D; pub use self::keypoint_id::KeypointId; pub use self::keypoint_pair::KeypointPair; pub use self::mat3x3::Mat3x3; diff --git a/crates/store/re_sdk_types/src/datatypes/pixel_format.rs b/crates/store/re_sdk_types/src/datatypes/pixel_format.rs index a3e9784f7da3..753415b0d057 100644 --- a/crates/store/re_sdk_types/src/datatypes/pixel_format.rs +++ b/crates/store/re_sdk_types/src/datatypes/pixel_format.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -33,7 +34,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// All these formats support random access. /// /// For more compressed image formats, see [`archetypes::EncodedImage`][crate::archetypes::EncodedImage]. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum PixelFormat { /// `Y_U_V12` is a YUV 4:2:0 fully planar YUV format without chroma downsampling, also known as `I420`. @@ -295,15 +296,3 @@ impl ::re_types_core::reflection::Enum for PixelFormat { } } } - -impl ::re_byte_size::SizeBytes for PixelFormat { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/plane3d.rs b/crates/store/re_sdk_types/src/datatypes/plane3d.rs index ace941d65568..76db38e4c872 100644 --- a/crates/store/re_sdk_types/src/datatypes/plane3d.rs +++ b/crates/store/re_sdk_types/src/datatypes/plane3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -30,7 +31,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// Note: although the normal will be passed through to the /// datastore as provided, when used in the Viewer, planes will always be normalized. /// I.e. the plane with xyz = (2, 0, 0), d = 1 is equivalent to xyz = (1, 0, 0), d = 0.5 -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct Plane3D(pub [f32; 4usize]); @@ -122,9 +132,10 @@ impl ::re_types_core::Loggable for Plane3D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(4usize) - .zip((4usize..).step_by(4usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(4usize), + (4usize..).step_by(4usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -198,7 +209,7 @@ impl ::re_types_core::Loggable for Plane3D { }) .with_context("rerun.datatypes.Plane3D#xyzd")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 4usize]>( + bytemuck::cast_slice::<_, [f32; 4usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -230,15 +241,3 @@ impl From for [f32; 4usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for Plane3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f32; 4usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/quaternion.rs b/crates/store/re_sdk_types/src/datatypes/quaternion.rs index 2a7d5972cc7e..ac5acbecf6e5 100644 --- a/crates/store/re_sdk_types/src/datatypes/quaternion.rs +++ b/crates/store/re_sdk_types/src/datatypes/quaternion.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,16 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// Note: although the x,y,z,w components of the quaternion will be passed through to the /// datastore as provided, when used in the Viewer Quaternions will always be normalized. -#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct Quaternion(pub [f32; 4usize]); @@ -117,9 +127,10 @@ impl ::re_types_core::Loggable for Quaternion { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(4usize) - .zip((4usize..).step_by(4usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(4usize), + (4usize..).step_by(4usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -193,7 +204,7 @@ impl ::re_types_core::Loggable for Quaternion { }) .with_context("rerun.datatypes.Quaternion#xyzw")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 4usize]>( + bytemuck::cast_slice::<_, [f32; 4usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -225,15 +236,3 @@ impl From for [f32; 4usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for Quaternion { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f32; 4usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/range1d.rs b/crates/store/re_sdk_types/src/datatypes/range1d.rs index 642450b5818e..ffb82f9d998e 100644 --- a/crates/store/re_sdk_types/src/datatypes/range1d.rs +++ b/crates/store/re_sdk_types/src/datatypes/range1d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A 1D range, specifying a lower and upper bound. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct Range1D(pub [f64; 2usize]); @@ -114,9 +124,10 @@ impl ::re_types_core::Loggable for Range1D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(2usize) - .zip((2usize..).step_by(2usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -190,7 +201,7 @@ impl ::re_types_core::Loggable for Range1D { }) .with_context("rerun.datatypes.Range1D#range")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 2usize]>( + bytemuck::cast_slice::<_, [f64; 2usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -222,15 +233,3 @@ impl From for [f64; 2usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for Range1D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f64; 2usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/range2d.rs b/crates/store/re_sdk_types/src/datatypes/range2d.rs index 7d5bdc8998c1..5a1bcbb7c531 100644 --- a/crates/store/re_sdk_types/src/datatypes/range2d.rs +++ b/crates/store/re_sdk_types/src/datatypes/range2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: An Axis-Aligned Bounding Box in 2D space, implemented as the minimum and maximum corners. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct Range2D { /// The range of the X-axis (usually left and right bounds). @@ -197,11 +207,11 @@ impl ::re_types_core::Loggable for Range2D { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let x_range = { if !arrays_by_name.contains_key("x_range") { return Err(DeserializationError::missing_struct_field( @@ -231,9 +241,10 @@ impl ::re_types_core::Loggable for Range2D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(2usize) - .zip((2usize..).step_by(2usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -309,9 +320,10 @@ impl ::re_types_core::Loggable for Range2D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(2usize) - .zip((2usize..).step_by(2usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -381,15 +393,3 @@ impl ::re_types_core::Loggable for Range2D { }) } } - -impl ::re_byte_size::SizeBytes for Range2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.x_range.heap_size_bytes() + self.y_range.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/rgba32.rs b/crates/store/re_sdk_types/src/datatypes/rgba32.rs index fe326347cd53..624b6acaefd8 100644 --- a/crates/store/re_sdk_types/src/datatypes/rgba32.rs +++ b/crates/store/re_sdk_types/src/datatypes/rgba32.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -26,7 +27,17 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// The color is stored as a 32-bit integer, where the most significant /// byte is `R` and the least significant byte is `A`. #[derive( - Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, bytemuck::Pod, bytemuck::Zeroable, + Clone, + Debug, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, )] #[repr(transparent)] pub struct Rgba32(pub u32); @@ -141,15 +152,3 @@ impl From for u32 { value.0 } } - -impl ::re_byte_size::SizeBytes for Rgba32 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/rotation_axis_angle.rs b/crates/store/re_sdk_types/src/datatypes/rotation_axis_angle.rs index d0ff81785dc0..cfb61490f544 100644 --- a/crates/store/re_sdk_types/src/datatypes/rotation_axis_angle.rs +++ b/crates/store/re_sdk_types/src/datatypes/rotation_axis_angle.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: 3D rotation represented by a rotation around a given axis. -#[derive(Clone, Debug, Copy, PartialEq)] +#[derive(Clone, Debug, Copy, PartialEq, ::re_byte_size::SizeBytes)] pub struct RotationAxisAngle { /// Axis to rotate around. /// @@ -169,11 +170,11 @@ impl ::re_types_core::Loggable for RotationAxisAngle { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let axis = { if !arrays_by_name.contains_key("axis") { return Err(DeserializationError::missing_struct_field( @@ -203,9 +204,10 @@ impl ::re_types_core::Loggable for RotationAxisAngle { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(3usize) - .zip((3usize..).step_by(3usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -295,15 +297,3 @@ impl ::re_types_core::Loggable for RotationAxisAngle { }) } } - -impl ::re_byte_size::SizeBytes for RotationAxisAngle { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.axis.heap_size_bytes() + self.angle.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/tensor_buffer.rs b/crates/store/re_sdk_types/src/datatypes/tensor_buffer.rs index 1e0440638e40..ffa6f12cde36 100644 --- a/crates/store/re_sdk_types/src/datatypes/tensor_buffer.rs +++ b/crates/store/re_sdk_types/src/datatypes/tensor_buffer.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: The underlying storage for [`archetypes::Tensor`][crate::archetypes::Tensor]. /// /// Tensor elements are stored in a contiguous buffer of a single type. -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, ::re_byte_size::SizeBytes)] pub enum TensorBuffer { /// 8bit unsigned integer. U8(::arrow::buffer::ScalarBuffer), @@ -414,14 +415,14 @@ impl ::re_types_core::Loggable for TensorBuffer { let second = iter.next(); match (first, second) { (Some(single), None) => single.clone(), - (Some(first_buf), Some(second_buf)) => { - std::iter::once(first_buf.as_ref() as &[_]) - .chain(std::iter::once(second_buf.as_ref() as &[_])) - .chain(iter.map(|b| b.as_ref() as &[_])) - .collect::>() - .concat() - .into() - } + (Some(first_buf), Some(second_buf)) => ::itertools::chain!( + ::std::iter::once(first_buf.as_ref() as &[_]), + ::std::iter::once(second_buf.as_ref() as &[_]), + iter.map(|b| b.as_ref() as &[_]), + ) + .collect::>() + .concat() + .into(), _ => Vec::new().into(), } }; @@ -1602,38 +1603,3 @@ impl ::re_types_core::Loggable for TensorBuffer { }) } } - -impl ::re_byte_size::SizeBytes for TensorBuffer { - #[inline] - fn heap_size_bytes(&self) -> u64 { - #![allow(clippy::match_same_arms)] - match self { - Self::U8(v) => v.heap_size_bytes(), - Self::U16(v) => v.heap_size_bytes(), - Self::U32(v) => v.heap_size_bytes(), - Self::U64(v) => v.heap_size_bytes(), - Self::I8(v) => v.heap_size_bytes(), - Self::I16(v) => v.heap_size_bytes(), - Self::I32(v) => v.heap_size_bytes(), - Self::I64(v) => v.heap_size_bytes(), - Self::F16(v) => v.heap_size_bytes(), - Self::F32(v) => v.heap_size_bytes(), - Self::F64(v) => v.heap_size_bytes(), - } - } - - #[inline] - fn is_pod() -> bool { - <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - && <::arrow::buffer::ScalarBuffer>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/tensor_data.rs b/crates/store/re_sdk_types/src/datatypes/tensor_data.rs index a135cf0af3d5..5f3809b26101 100644 --- a/crates/store/re_sdk_types/src/datatypes/tensor_data.rs +++ b/crates/store/re_sdk_types/src/datatypes/tensor_data.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -29,7 +30,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// These dimensions are combined with an index to look up values from the `buffer` field, /// which stores a contiguous array of typed values. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, ::re_byte_size::SizeBytes)] pub struct TensorData { /// The shape of the tensor, i.e. the length of each dimension. pub shape: ::arrow::buffer::ScalarBuffer, @@ -263,11 +264,11 @@ impl ::re_types_core::Loggable for TensorData { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let shape = { if !arrays_by_name.contains_key("shape") { return Err(DeserializationError::missing_struct_field( @@ -474,17 +475,3 @@ impl ::re_types_core::Loggable for TensorData { }) } } - -impl ::re_byte_size::SizeBytes for TensorData { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.shape.heap_size_bytes() + self.names.heap_size_bytes() + self.buffer.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <::arrow::buffer::ScalarBuffer>::is_pod() - && >>::is_pod() - && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/tensor_data_ext.rs b/crates/store/re_sdk_types/src/datatypes/tensor_data_ext.rs index fe174ee1cc65..6a631234cda5 100644 --- a/crates/store/re_sdk_types/src/datatypes/tensor_data_ext.rs +++ b/crates/store/re_sdk_types/src/datatypes/tensor_data_ext.rs @@ -99,7 +99,7 @@ impl TensorData { pub fn get(&self, index: &[u64]) -> Option { let mut stride: usize = 1; let mut offset: usize = 0; - for (&size, &index) in self.shape.iter().zip(index).rev() { + for (&size, &index) in std::iter::zip(&self.shape, index).rev() { if size <= index { return None; } diff --git a/crates/store/re_sdk_types/src/datatypes/tensor_dimension_index_selection.rs b/crates/store/re_sdk_types/src/datatypes/tensor_dimension_index_selection.rs index e764b96a30e6..1021fd887749 100644 --- a/crates/store/re_sdk_types/src/datatypes/tensor_dimension_index_selection.rs +++ b/crates/store/re_sdk_types/src/datatypes/tensor_dimension_index_selection.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -24,7 +25,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Indexing a specific tensor dimension. /// /// Selecting `dimension=2` and `index=42` is similar to doing `tensor[:, :, 42, :, :, …]` in numpy. -#[derive(Clone, Debug, Default, Copy, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Copy, Hash, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct TensorDimensionIndexSelection { /// The dimension number to select. pub dimension: u32, @@ -146,11 +147,11 @@ impl ::re_types_core::Loggable for TensorDimensionIndexSelection { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let dimension = { if !arrays_by_name.contains_key("dimension") { return Err(DeserializationError::missing_struct_field( @@ -218,15 +219,3 @@ impl ::re_types_core::Loggable for TensorDimensionIndexSelection { }) } } - -impl ::re_byte_size::SizeBytes for TensorDimensionIndexSelection { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.dimension.heap_size_bytes() + self.index.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/tensor_dimension_selection.rs b/crates/store/re_sdk_types/src/datatypes/tensor_dimension_selection.rs index 099d23b6e727..fcb526c574f4 100644 --- a/crates/store/re_sdk_types/src/datatypes/tensor_dimension_selection.rs +++ b/crates/store/re_sdk_types/src/datatypes/tensor_dimension_selection.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Selection of a single tensor dimension. -#[derive(Clone, Debug, Default, Copy, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Copy, Hash, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct TensorDimensionSelection { /// The dimension number to select. pub dimension: u32, @@ -144,11 +145,11 @@ impl ::re_types_core::Loggable for TensorDimensionSelection { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let dimension = { if !arrays_by_name.contains_key("dimension") { return Err(DeserializationError::missing_struct_field( @@ -214,15 +215,3 @@ impl ::re_types_core::Loggable for TensorDimensionSelection { }) } } - -impl ::re_byte_size::SizeBytes for TensorDimensionSelection { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.dimension.heap_size_bytes() + self.invert.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/utf8pair.rs b/crates/store/re_sdk_types/src/datatypes/utf8pair.rs index 1f9d12137778..9b8a02fe28e7 100644 --- a/crates/store/re_sdk_types/src/datatypes/utf8pair.rs +++ b/crates/store/re_sdk_types/src/datatypes/utf8pair.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: Stores a tuple of UTF-8 strings. -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] pub struct Utf8Pair { /// The first string. pub first: crate::datatypes::Utf8, @@ -166,11 +167,11 @@ impl ::re_types_core::Loggable for Utf8Pair { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let first = { if !arrays_by_name.contains_key("first") { return Err(DeserializationError::missing_struct_field( @@ -298,15 +299,3 @@ impl ::re_types_core::Loggable for Utf8Pair { }) } } - -impl ::re_byte_size::SizeBytes for Utf8Pair { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.first.heap_size_bytes() + self.second.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/uuid.rs b/crates/store/re_sdk_types/src/datatypes/uuid.rs index d995d813d84d..2ce4fd6e2872 100644 --- a/crates/store/re_sdk_types/src/datatypes/uuid.rs +++ b/crates/store/re_sdk_types/src/datatypes/uuid.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A 16-byte UUID. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Uuid { /// The raw bytes representing the UUID. @@ -117,9 +120,10 @@ impl ::re_types_core::Loggable for Uuid { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(16usize) - .zip((16usize..).step_by(16usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(16usize), + (16usize..).step_by(16usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -193,7 +197,7 @@ impl ::re_types_core::Loggable for Uuid { }) .with_context("rerun.datatypes.Uuid#bytes")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 16usize]>( + bytemuck::cast_slice::<_, [u8; 16usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -231,15 +235,3 @@ impl From for [u8; 16usize] { value.bytes } } - -impl ::re_byte_size::SizeBytes for Uuid { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.bytes.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[u8; 16usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/uvec2d.rs b/crates/store/re_sdk_types/src/datatypes/uvec2d.rs index 42c6de4dbab1..9b9266d3ea8a 100644 --- a/crates/store/re_sdk_types/src/datatypes/uvec2d.rs +++ b/crates/store/re_sdk_types/src/datatypes/uvec2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,18 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A uint32 vector in 2D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + Eq, + Hash, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct UVec2D(pub [u32; 2usize]); @@ -114,9 +126,10 @@ impl ::re_types_core::Loggable for UVec2D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(2usize) - .zip((2usize..).step_by(2usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -190,7 +203,7 @@ impl ::re_types_core::Loggable for UVec2D { }) .with_context("rerun.datatypes.UVec2D#xy")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 2usize]>( + bytemuck::cast_slice::<_, [u32; 2usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -222,15 +235,3 @@ impl From for [u32; 2usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for UVec2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[u32; 2usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/uvec3d.rs b/crates/store/re_sdk_types/src/datatypes/uvec3d.rs index 9399c745a671..178fc5bee14c 100644 --- a/crates/store/re_sdk_types/src/datatypes/uvec3d.rs +++ b/crates/store/re_sdk_types/src/datatypes/uvec3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,18 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A uint32 vector in 3D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + Eq, + Hash, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct UVec3D(pub [u32; 3usize]); @@ -114,9 +126,10 @@ impl ::re_types_core::Loggable for UVec3D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(3usize) - .zip((3usize..).step_by(3usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -190,7 +203,7 @@ impl ::re_types_core::Loggable for UVec3D { }) .with_context("rerun.datatypes.UVec3D#xyz")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 3usize]>( + bytemuck::cast_slice::<_, [u32; 3usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -222,15 +235,3 @@ impl From for [u32; 3usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for UVec3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[u32; 3usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/uvec3d_ext.rs b/crates/store/re_sdk_types/src/datatypes/uvec3d_ext.rs index 04ce45a35974..e57843c6c657 100644 --- a/crates/store/re_sdk_types/src/datatypes/uvec3d_ext.rs +++ b/crates/store/re_sdk_types/src/datatypes/uvec3d_ext.rs @@ -93,6 +93,6 @@ impl From for UVec3D { impl std::fmt::Display for UVec3D { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[{}, {}, {}]", self.x(), self.y(), self.z(),) + write!(f, "[{}, {}, {}]", self.x(), self.y(), self.z()) } } diff --git a/crates/store/re_sdk_types/src/datatypes/uvec4d.rs b/crates/store/re_sdk_types/src/datatypes/uvec4d.rs index 9287c439edf9..d9695b60c7ed 100644 --- a/crates/store/re_sdk_types/src/datatypes/uvec4d.rs +++ b/crates/store/re_sdk_types/src/datatypes/uvec4d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,18 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A uint vector in 4D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + Eq, + Hash, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct UVec4D(pub [u32; 4usize]); @@ -114,9 +126,10 @@ impl ::re_types_core::Loggable for UVec4D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(4usize) - .zip((4usize..).step_by(4usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(4usize), + (4usize..).step_by(4usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -190,7 +203,7 @@ impl ::re_types_core::Loggable for UVec4D { }) .with_context("rerun.datatypes.UVec4D#xyzw")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 4usize]>( + bytemuck::cast_slice::<_, [u32; 4usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -222,15 +235,3 @@ impl From for [u32; 4usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for UVec4D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[u32; 4usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/vec2d.rs b/crates/store/re_sdk_types/src/datatypes/vec2d.rs index a8812b9131aa..ddb6180d6460 100644 --- a/crates/store/re_sdk_types/src/datatypes/vec2d.rs +++ b/crates/store/re_sdk_types/src/datatypes/vec2d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A vector in 2D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct Vec2D(pub [f32; 2usize]); @@ -114,9 +124,10 @@ impl ::re_types_core::Loggable for Vec2D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(2usize) - .zip((2usize..).step_by(2usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -190,7 +201,7 @@ impl ::re_types_core::Loggable for Vec2D { }) .with_context("rerun.datatypes.Vec2D#xy")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 2usize]>( + bytemuck::cast_slice::<_, [f32; 2usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -222,15 +233,3 @@ impl From for [f32; 2usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for Vec2D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f32; 2usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/vec2d_ext.rs b/crates/store/re_sdk_types/src/datatypes/vec2d_ext.rs index d0929b53437b..7bb6bbea2b11 100644 --- a/crates/store/re_sdk_types/src/datatypes/vec2d_ext.rs +++ b/crates/store/re_sdk_types/src/datatypes/vec2d_ext.rs @@ -112,6 +112,6 @@ impl From> for Vec2D { impl std::fmt::Display for Vec2D { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let prec = f.precision().unwrap_or(crate::DEFAULT_DISPLAY_DECIMALS); - write!(f, "[{:.prec$}, {:.prec$}]", self.x(), self.y(),) + write!(f, "[{:.prec$}, {:.prec$}]", self.x(), self.y()) } } diff --git a/crates/store/re_sdk_types/src/datatypes/vec3d.rs b/crates/store/re_sdk_types/src/datatypes/vec3d.rs index cc1db4ae583b..9479261102f9 100644 --- a/crates/store/re_sdk_types/src/datatypes/vec3d.rs +++ b/crates/store/re_sdk_types/src/datatypes/vec3d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A vector in 3D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct Vec3D(pub [f32; 3usize]); @@ -114,9 +124,10 @@ impl ::re_types_core::Loggable for Vec3D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(3usize) - .zip((3usize..).step_by(3usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -190,7 +201,7 @@ impl ::re_types_core::Loggable for Vec3D { }) .with_context("rerun.datatypes.Vec3D#xyz")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 3usize]>( + bytemuck::cast_slice::<_, [f32; 3usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -222,15 +233,3 @@ impl From for [f32; 3usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for Vec3D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f32; 3usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/vec4d.rs b/crates/store/re_sdk_types/src/datatypes/vec4d.rs index 7a652271c873..0c7c25bf3648 100644 --- a/crates/store/re_sdk_types/src/datatypes/vec4d.rs +++ b/crates/store/re_sdk_types/src/datatypes/vec4d.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,16 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A vector in 4D space. -#[derive(Clone, Debug, Default, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(C)] pub struct Vec4D(pub [f32; 4usize]); @@ -114,9 +124,10 @@ impl ::re_types_core::Loggable for Vec4D { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(4usize) - .zip((4usize..).step_by(4usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(4usize), + (4usize..).step_by(4usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -190,7 +201,7 @@ impl ::re_types_core::Loggable for Vec4D { }) .with_context("rerun.datatypes.Vec4D#xyzw")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 4usize]>( + bytemuck::cast_slice::<_, [f32; 4usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -222,15 +233,3 @@ impl From for [f32; 4usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for Vec4D { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[f32; 4usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/video_timestamp.rs b/crates/store/re_sdk_types/src/datatypes/video_timestamp.rs index 6c316593522c..b673735fb174 100644 --- a/crates/store/re_sdk_types/src/datatypes/video_timestamp.rs +++ b/crates/store/re_sdk_types/src/datatypes/video_timestamp.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -25,7 +26,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// /// Specified in nanoseconds. /// Presentation timestamps are typically measured as time since video start. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] pub struct VideoTimestamp( /// Presentation timestamp value in nanoseconds. pub i64, @@ -141,15 +144,3 @@ impl From for i64 { value.0 } } - -impl ::re_byte_size::SizeBytes for VideoTimestamp { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/datatypes/view_coordinates.rs b/crates/store/re_sdk_types/src/datatypes/view_coordinates.rs index c6ca119cc605..5c722b057791 100644 --- a/crates/store/re_sdk_types/src/datatypes/view_coordinates.rs +++ b/crates/store/re_sdk_types/src/datatypes/view_coordinates.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -41,7 +42,9 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// * Back = 6 /// /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -#[derive(Clone, Debug, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, Debug, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct ViewCoordinates( /// The directions of the [x, y, z] axes. @@ -136,9 +139,10 @@ impl ::re_types_core::Loggable for ViewCoordinates { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(3usize) - .zip((3usize..).step_by(3usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -212,7 +216,7 @@ impl ::re_types_core::Loggable for ViewCoordinates { }) .with_context("rerun.datatypes.ViewCoordinates#coordinates")?; let arrow_data_inner = &**arrow_data.values(); - bytemuck::cast_slice::<_, [_; 3usize]>( + bytemuck::cast_slice::<_, [u8; 3usize]>( arrow_data_inner .as_any() .downcast_ref::() @@ -244,15 +248,3 @@ impl From for [u8; 3usize] { value.0 } } - -impl ::re_byte_size::SizeBytes for ViewCoordinates { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[u8; 3usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/image.rs b/crates/store/re_sdk_types/src/image.rs index ea4d060ae679..c9f5114a9811 100644 --- a/crates/store/re_sdk_types/src/image.rs +++ b/crates/store/re_sdk_types/src/image.rs @@ -12,7 +12,7 @@ use crate::datatypes::{Blob, ChannelDatatype, TensorBuffer, TensorData}; // ---------------------------------------------------------------------------- /// The kind of image data, either color, segmentation, or depth image. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, re_byte_size::SizeBytes)] pub enum ImageKind { /// A normal grayscale or color image ([`archetypes::Image`]). Color, @@ -42,16 +42,6 @@ impl ImageKind { } } -impl re_byte_size::SizeBytes for ImageKind { - fn heap_size_bytes(&self) -> u64 { - 0 - } - - fn is_pod() -> bool { - true - } -} - // ---------------------------------------------------------------------------- /// Errors when converting images from the [`image`] crate to an [`archetypes::Image`]. diff --git a/crates/store/re_sdk_types/src/reflection/mod.rs b/crates/store/re_sdk_types/src/reflection/mod.rs index a0e7003ab013..fa24cd9a7365 100644 --- a/crates/store/re_sdk_types/src/reflection/mod.rs +++ b/crates/store/re_sdk_types/src/reflection/mod.rs @@ -128,6 +128,17 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "The name of a column in a table.\n\n⚠\u{fe0f} **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.**", + deprecation_summary: None, + custom_placeholder: None, + datatype: ColumnName::arrow_datatype(), + is_enum: false, + verify_arrow_array: ColumnName::verify_arrow_array, + }, + ), ( ::name(), ComponentReflection { @@ -953,6 +964,17 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "Whether a [`components.VideoSample`](https://rerun.io/docs/reference/types/components/video_sample) contains a keyframe (also known as a sync sample or IDR).\n\nA keyframe in this sense must be _decoder re-entrant_: a decoder must be able to start\ndecoding the stream from this sample alone, with no prior decoder state.\nNot every intra-coded frame qualifies. Some codecs have intra-only frames that may\nstill reference existing decoder state and are therefore not valid sync points.\nSee [`components.VideoCodec`](https://rerun.io/docs/reference/types/components/video_codec) for the codec-specific definition of a keyframe.", + deprecation_summary: None, + custom_placeholder: None, + datatype: IsKeyframe::arrow_datatype(), + is_enum: false, + verify_arrow_array: IsKeyframe::verify_arrow_array, + }, + ), ( ::name(), ComponentReflection { @@ -1129,6 +1151,17 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "Defines how points are shaded.", + deprecation_summary: None, + custom_placeholder: Some(PointShading::default().to_arrow()?), + datatype: PointShading::arrow_datatype(), + is_enum: true, + verify_arrow_array: PointShading::verify_arrow_array, + }, + ), ( ::name(), ComponentReflection { @@ -1492,6 +1525,39 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "Integer index of a voxel in a sparse 3D voxel grid.\n\nThe voxel center in local grid coordinates is `(index + 0.5) * voxel_size`.", + deprecation_summary: None, + custom_placeholder: None, + datatype: VoxelIndex::arrow_datatype(), + is_enum: false, + verify_arrow_array: VoxelIndex::verify_arrow_array, + }, + ), + ( + ::name(), + ComponentReflection { + docstring_md: "The scene-unit dimensions of one voxel in a sparse 3D voxel grid.\n\nEach component is the size of a voxel along the corresponding local grid axis.\nAll components must be finite and positive.", + deprecation_summary: None, + custom_placeholder: None, + datatype: VoxelSize::arrow_datatype(), + is_enum: false, + verify_arrow_array: VoxelSize::verify_arrow_array, + }, + ), + ( + ::name(), + ComponentReflection { + docstring_md: "Optional scalar occupancy or value associated with a voxel.", + deprecation_summary: None, + custom_placeholder: None, + datatype: VoxelValue::arrow_datatype(), + is_enum: false, + verify_arrow_array: VoxelValue::verify_arrow_array, + }, + ), ]; Ok(ComponentReflectionMap::from_iter(array)) } @@ -1504,7 +1570,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { re_tracing::profile_function!(); let array = [ ( - ArchetypeName::new("rerun.archetypes.AnnotationContext"), + ArchetypeName::from("rerun.archetypes.AnnotationContext"), ArchetypeReflection { display_name: "Annotation context", deprecation_summary: None, @@ -1520,7 +1586,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Arrows2D"), + ArchetypeName::from("rerun.archetypes.Arrows2D"), ArchetypeReflection { display_name: "Arrows 2D", deprecation_summary: None, @@ -1587,7 +1653,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Arrows3D"), + ArchetypeName::from("rerun.archetypes.Arrows3D"), ArchetypeReflection { display_name: "Arrows 3D", deprecation_summary: None, @@ -1647,7 +1713,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Asset3D"), + ArchetypeName::from("rerun.archetypes.Asset3D"), ArchetypeReflection { display_name: "Asset 3D", deprecation_summary: None, @@ -1679,7 +1745,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.AssetVideo"), + ArchetypeName::from("rerun.archetypes.AssetVideo"), ArchetypeReflection { display_name: "Asset video", deprecation_summary: None, @@ -1704,7 +1770,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.BarChart"), + ArchetypeName::from("rerun.archetypes.BarChart"), ArchetypeReflection { display_name: "Bar chart", deprecation_summary: None, @@ -1743,7 +1809,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Boxes2D"), + ArchetypeName::from("rerun.archetypes.Boxes2D"), ArchetypeReflection { display_name: "Boxes 2D", deprecation_summary: None, @@ -1810,7 +1876,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Boxes3D"), + ArchetypeName::from("rerun.archetypes.Boxes3D"), ArchetypeReflection { display_name: "Boxes 3D", deprecation_summary: None, @@ -1891,7 +1957,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Capsules3D"), + ArchetypeName::from("rerun.archetypes.Capsules3D"), ArchetypeReflection { display_name: "Capsules 3D", deprecation_summary: None, @@ -1979,7 +2045,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Clear"), + ArchetypeName::from("rerun.archetypes.Clear"), ArchetypeReflection { display_name: "Clear", deprecation_summary: None, @@ -1995,7 +2061,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.CoordinateFrame"), + ArchetypeName::from("rerun.archetypes.CoordinateFrame"), ArchetypeReflection { display_name: "Coordinate frame", deprecation_summary: None, @@ -2011,7 +2077,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Cylinders3D"), + ArchetypeName::from("rerun.archetypes.Cylinders3D"), ArchetypeReflection { display_name: "Cylinders 3D", deprecation_summary: None, @@ -2099,7 +2165,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.DepthImage"), + ArchetypeName::from("rerun.archetypes.DepthImage"), ArchetypeReflection { display_name: "Depth image", deprecation_summary: None, @@ -2166,7 +2232,74 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Ellipsoids3D"), + ArchetypeName::from("rerun.archetypes.Ellipses2D"), + ArchetypeReflection { + display_name: "Ellipses 2D", + deprecation_summary: None, + scope: None, + view_types: &["Spatial2DView", "Spatial3DView"], + fields: vec![ + ArchetypeFieldReflection { + name: "half_sizes", + display_name: "Half sizes", + component_type: "rerun.components.HalfSize2D".into(), + docstring_md: "All half-extents (semi-axes) that make up the batch of ellipses.", + flags: ArchetypeFieldFlags::REQUIRED | ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "centers", + display_name: "Centers", + component_type: "rerun.components.Position2D".into(), + docstring_md: "Optional center positions of the ellipses.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "colors", + display_name: "Colors", + component_type: "rerun.components.Color".into(), + docstring_md: "Optional colors for the ellipses.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "line_radii", + display_name: "Line radii", + component_type: "rerun.components.Radius".into(), + docstring_md: "Optional radii for the lines that make up the ellipses.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "labels", + display_name: "Labels", + component_type: "rerun.components.Text".into(), + docstring_md: "Optional text labels for the ellipses.\n\nIf there's a single label present, it will be placed at the center of the entity.\nOtherwise, each instance will have its own label.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "show_labels", + display_name: "Show labels", + component_type: "rerun.components.ShowLabels".into(), + docstring_md: "Whether the text labels should be shown.\n\nIf not set, labels will automatically appear when there is exactly one label for this entity\nor the number of instances on this entity is under a certain threshold.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "draw_order", + display_name: "Draw order", + component_type: "rerun.components.DrawOrder".into(), + docstring_md: "An optional floating point value that specifies the 2D drawing order.\n\nObjects with higher values are drawn on top of those with lower values.\nDefaults to `10.0`.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "class_ids", + display_name: "Class ids", + component_type: "rerun.components.ClassId".into(), + docstring_md: "Optional [`components.ClassId`](https://rerun.io/docs/reference/types/components/class_id)s for the ellipses.\n\nThe [`components.ClassId`](https://rerun.io/docs/reference/types/components/class_id) provides colors and labels if not specified explicitly.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ], + }, + ), + ( + ArchetypeName::from("rerun.archetypes.Ellipsoids3D"), ArchetypeReflection { display_name: "Ellipsoids 3D", deprecation_summary: None, @@ -2247,7 +2380,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.EncodedDepthImage"), + ArchetypeName::from("rerun.archetypes.EncodedDepthImage"), ArchetypeReflection { display_name: "Encoded depth image", deprecation_summary: None, @@ -2314,7 +2447,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.EncodedImage"), + ArchetypeName::from("rerun.archetypes.EncodedImage"), ArchetypeReflection { display_name: "Encoded image", deprecation_summary: None, @@ -2360,7 +2493,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.GeoLineStrings"), + ArchetypeName::from("rerun.archetypes.GeoLineStrings"), ArchetypeReflection { display_name: "Geo line strings", deprecation_summary: None, @@ -2392,7 +2525,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.GeoPoints"), + ArchetypeName::from("rerun.archetypes.GeoPoints"), ArchetypeReflection { display_name: "Geo points", deprecation_summary: None, @@ -2431,7 +2564,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.GraphEdges"), + ArchetypeName::from("rerun.archetypes.GraphEdges"), ArchetypeReflection { display_name: "Graph edges", deprecation_summary: None, @@ -2456,7 +2589,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.GraphNodes"), + ArchetypeName::from("rerun.archetypes.GraphNodes"), ArchetypeReflection { display_name: "Graph nodes", deprecation_summary: None, @@ -2509,7 +2642,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.GridMap"), + ArchetypeName::from("rerun.archetypes.GridMap"), ArchetypeReflection { display_name: "Grid map", deprecation_summary: None, @@ -2583,7 +2716,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Image"), + ArchetypeName::from("rerun.archetypes.Image"), ArchetypeReflection { display_name: "Image", deprecation_summary: None, @@ -2629,7 +2762,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.InstancePoses3D"), + ArchetypeName::from("rerun.archetypes.InstancePoses3D"), ArchetypeReflection { display_name: "Instance poses 3D", deprecation_summary: None, @@ -2675,7 +2808,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.LineStrips2D"), + ArchetypeName::from("rerun.archetypes.LineStrips2D"), ArchetypeReflection { display_name: "Line strips 2D", deprecation_summary: None, @@ -2735,7 +2868,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.LineStrips3D"), + ArchetypeName::from("rerun.archetypes.LineStrips3D"), ArchetypeReflection { display_name: "Line strips 3D", deprecation_summary: None, @@ -2760,7 +2893,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { name: "colors", display_name: "Colors", component_type: "rerun.components.Color".into(), - docstring_md: "Optional colors for the line strips.", + docstring_md: "Optional colors for the line strips.\n\nThe alpha channel is ignored.", flags: ArchetypeFieldFlags::UI_EDITABLE, }, ArchetypeFieldReflection { @@ -2788,7 +2921,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.McapChannel"), + ArchetypeName::from("rerun.archetypes.McapChannel"), ArchetypeReflection { display_name: "Mcap channel", deprecation_summary: None, @@ -2827,7 +2960,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.McapMessage"), + ArchetypeName::from("rerun.archetypes.McapMessage"), ArchetypeReflection { display_name: "Mcap message", deprecation_summary: None, @@ -2843,7 +2976,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.McapSchema"), + ArchetypeName::from("rerun.archetypes.McapSchema"), ArchetypeReflection { display_name: "Mcap schema", deprecation_summary: None, @@ -2882,7 +3015,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.McapStatistics"), + ArchetypeName::from("rerun.archetypes.McapStatistics"), ArchetypeReflection { display_name: "Mcap statistics", deprecation_summary: None, @@ -2956,7 +3089,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Mesh3D"), + ArchetypeName::from("rerun.archetypes.Mesh3D"), ArchetypeReflection { display_name: "Mesh 3D", deprecation_summary: None, @@ -3037,7 +3170,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Pinhole"), + ArchetypeName::from("rerun.archetypes.Pinhole"), ArchetypeReflection { display_name: "Pinhole", deprecation_summary: None, @@ -3104,7 +3237,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Points2D"), + ArchetypeName::from("rerun.archetypes.Points2D"), ArchetypeReflection { display_name: "Points 2D", deprecation_summary: None, @@ -3171,7 +3304,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Points3D"), + ArchetypeName::from("rerun.archetypes.Points3D"), ArchetypeReflection { display_name: "Points 3D", deprecation_summary: None, @@ -3196,7 +3329,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { name: "colors", display_name: "Colors", component_type: "rerun.components.Color".into(), - docstring_md: "Optional colors for the points.", + docstring_md: "Optional colors for the points.\n\nBy default, the alpha channel affects brightness rather than transparency.\nTODO(#1611): To use the alpha channel for transparency, enable the experimental \"Transparent point clouds\" feature flag.", flags: ArchetypeFieldFlags::UI_EDITABLE, }, ArchetypeFieldReflection { @@ -3213,6 +3346,13 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { docstring_md: "Whether the text labels should be shown.\n\nIf not set, labels will automatically appear when there is exactly one label for this entity\nor the number of instances on this entity is under a certain threshold.", flags: ArchetypeFieldFlags::UI_EDITABLE, }, + ArchetypeFieldReflection { + name: "point_shading", + display_name: "Point shading", + component_type: "rerun.components.PointShading".into(), + docstring_md: "How points should be shaded.\n\nIf not set, points are rendered with [`components.PointShading#Gradient`](https://rerun.io/docs/reference/types/components/point_shading) by default.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, ArchetypeFieldReflection { name: "class_ids", display_name: "Class ids", @@ -3231,7 +3371,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.RecordingInfo"), + ArchetypeName::from("rerun.archetypes.RecordingInfo"), ArchetypeReflection { display_name: "Recording info", deprecation_summary: None, @@ -3256,7 +3396,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Scalars"), + ArchetypeName::from("rerun.archetypes.Scalars"), ArchetypeReflection { display_name: "Scalars", deprecation_summary: None, @@ -3272,7 +3412,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.SegmentationImage"), + ArchetypeName::from("rerun.archetypes.SegmentationImage"), ArchetypeReflection { display_name: "Segmentation image", deprecation_summary: None, @@ -3311,7 +3451,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.SeriesLines"), + ArchetypeName::from("rerun.archetypes.SeriesLines"), ArchetypeReflection { display_name: "Series lines", deprecation_summary: None, @@ -3364,7 +3504,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.SeriesPoints"), + ArchetypeName::from("rerun.archetypes.SeriesPoints"), ArchetypeReflection { display_name: "Series points", deprecation_summary: None, @@ -3410,23 +3550,62 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Status"), + ArchetypeName::from("rerun.archetypes.StateChange"), ArchetypeReflection { - display_name: "Status", + display_name: "State change", deprecation_summary: None, scope: None, - view_types: &["StatusView"], + view_types: &["StateTimelineView"], fields: vec![ArchetypeFieldReflection { - name: "status", - display_name: "Status", + name: "state", + display_name: "State", component_type: "rerun.components.Text".into(), - docstring_md: "The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array.", + docstring_md: "The new state values; each instance gets its own lane in the state timeline view.\n\nA reset ends the previous state and shows a gap in the state timeline view until the\nnext state. An empty string, a null array entry, and an empty state array (e.g. from\nclearing the field) all act as resets.\n\nThe length of the state array should not change over time.", flags: ArchetypeFieldFlags::REQUIRED | ArchetypeFieldFlags::UI_EDITABLE, }], }, ), ( - ArchetypeName::new("rerun.archetypes.Tensor"), + ArchetypeName::from("rerun.archetypes.StateConfiguration"), + ArchetypeReflection { + display_name: "State configuration", + deprecation_summary: None, + scope: None, + view_types: &["StateTimelineView"], + fields: vec![ + ArchetypeFieldReflection { + name: "values", + display_name: "Values", + component_type: "rerun.components.Text".into(), + docstring_md: "The raw state values that this configuration applies to.\n\nEach entry defines a known state value. The order determines the mapping to\n`labels`, `colors`, and `visible` (by index).", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "labels", + display_name: "Labels", + component_type: "rerun.components.Text".into(), + docstring_md: "Display labels for each state value.\n\nIf provided, the label at index `i` is shown instead of the raw value at index `i`.\nIf not provided or shorter than `values`, the raw value is used as the label.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "colors", + display_name: "Colors", + component_type: "rerun.components.Color".into(), + docstring_md: "Colors for each state value.\n\nIf provided, the color at index `i` is used for the state at index `i`.\nIf not provided, colors are assigned automatically from a built-in palette.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "visible", + display_name: "Visible", + component_type: "rerun.components.Visible".into(), + docstring_md: "Visibility for each state value.\n\nIf provided, the visibility at index `i` controls whether the state at index `i` is shown.\nIf not provided, all state values are visible.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ], + }, + ), + ( + ArchetypeName::from("rerun.archetypes.Tensor"), ArchetypeReflection { display_name: "Tensor", deprecation_summary: None, @@ -3451,7 +3630,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.TextDocument"), + ArchetypeName::from("rerun.archetypes.TextDocument"), ArchetypeReflection { display_name: "Text document", deprecation_summary: None, @@ -3476,7 +3655,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.TextLog"), + ArchetypeName::from("rerun.archetypes.TextLog"), ArchetypeReflection { display_name: "Text log", deprecation_summary: None, @@ -3508,7 +3687,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.Transform3D"), + ArchetypeName::from("rerun.archetypes.Transform3D"), ArchetypeReflection { display_name: "Transform 3D", deprecation_summary: None, @@ -3575,7 +3754,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.TransformAxes3D"), + ArchetypeName::from("rerun.archetypes.TransformAxes3D"), ArchetypeReflection { display_name: "Transform axes 3D", deprecation_summary: None, @@ -3600,7 +3779,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.VideoFrameReference"), + ArchetypeName::from("rerun.archetypes.VideoFrameReference"), ArchetypeReflection { display_name: "Video frame reference", deprecation_summary: None, @@ -3639,7 +3818,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.VideoStream"), + ArchetypeName::from("rerun.archetypes.VideoStream"), ArchetypeReflection { display_name: "Video stream", deprecation_summary: None, @@ -3660,6 +3839,13 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { docstring_md: "Video sample data (also known as \"video chunk\").\n\nThe current timestamp is used as presentation timestamp (PTS) for all data in this sample.\nThere is currently no way to log differing decoding timestamps, meaning\nthat there is no support for B-frames.\nSee for more details.\n\nRerun chunks containing frames (i.e. bundles of sample data) may arrive out of order,\nbut may cause the video playback in the Viewer to reset.\nIt is recommended to have all chunks for a video stream to be ordered temporally order.\n\nLogging separate videos on the same entity is allowed iff they share the exact same\ncodec parameters & resolution.\n\nThe samples are expected to be encoded using the `codec` field.\nEach video sample must contain enough data for exactly one video frame\n(this restriction may be relaxed in the future for some codecs).\n\nUnless your stream consists entirely of key-frames (in which case you should consider [`archetypes.EncodedImage`](https://rerun.io/docs/reference/types/archetypes/encoded_image))\nnever log this component as static data as this means that you loose all information of\nprevious samples which may be required to decode an image.\n\nSee [`components.VideoCodec`](https://rerun.io/docs/reference/types/components/video_codec) for codec specific requirements.", flags: ArchetypeFieldFlags::UI_EDITABLE, }, + ArchetypeFieldReflection { + name: "is_keyframe", + display_name: "Is keyframe", + component_type: "rerun.components.IsKeyframe".into(), + docstring_md: "Whether the corresponding [`components.VideoSample`](https://rerun.io/docs/reference/types/components/video_sample) contains a keyframe.\n\nA keyframe (also known as a sync sample or IDR) is a frame from which a decoder can\nstart decoding the stream with no prior decoder state. See [`components.IsKeyframe`](https://rerun.io/docs/reference/types/components/is_keyframe)\nand [`components.VideoCodec`](https://rerun.io/docs/reference/types/components/video_codec) for the codec-specific definition.\n\nThis field is optional. It does not change how the stream itself is decoded: it is\nmetadata that travels with the sample and can be inspected when querying the data\nback, for example to locate sync points or build a frame index.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, ArchetypeFieldReflection { name: "opacity", display_name: "Opacity", @@ -3678,7 +3864,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.archetypes.ViewCoordinates"), + ArchetypeName::from("rerun.archetypes.ViewCoordinates"), ArchetypeReflection { display_name: "View coordinates", deprecation_summary: None, @@ -3694,7 +3880,88 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ActiveVisualizers"), + ArchetypeName::from("rerun.archetypes.VoxelGridMap"), + ArchetypeReflection { + display_name: "Voxel grid map", + deprecation_summary: None, + scope: None, + view_types: &["Spatial3DView"], + fields: vec![ + ArchetypeFieldReflection { + name: "voxel_indices", + display_name: "Voxel indices", + component_type: "rerun.components.VoxelIndex".into(), + docstring_md: "Indices of the voxels within the grid volume.", + flags: ArchetypeFieldFlags::REQUIRED, + }, + ArchetypeFieldReflection { + name: "voxel_size", + display_name: "Voxel size", + component_type: "rerun.components.VoxelSize".into(), + docstring_md: "The scene-unit dimensions of a single voxel cell.\n\nThis defines the voxel size along the local grid X/Y/Z axes.\nEach dimension must be finite and positive.", + flags: ArchetypeFieldFlags::REQUIRED, + }, + ArchetypeFieldReflection { + name: "values", + display_name: "Values", + component_type: "rerun.components.VoxelValue".into(), + docstring_md: "Optional scalar occupancy or value data for each voxel.\n\nIf explicit colors are not provided, values are mapped through `colormap` and `value_range`.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "colors", + display_name: "Colors", + component_type: "rerun.components.Color".into(), + docstring_md: "Optional colors for each voxel.\n\nIf set, these colors take precedence over color-mapped scalar values.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "translation", + display_name: "Translation", + component_type: "rerun.components.Translation3D".into(), + docstring_md: "Translation of the minimum corner of voxel `[0, 0, 0]`.\n\nTogether with [`components.RotationAxisAngle`](https://rerun.io/docs/reference/types/components/rotation_axis_angle) or [`components.RotationQuat`](https://rerun.io/docs/reference/types/components/rotation_quat), this defines the pose of the\ngrid relative to the map's parent coordinate frame.\n\nIf not set, the minimum corner is placed at the origin of the map's parent coordinate frame.", + flags: ArchetypeFieldFlags::empty(), + }, + ArchetypeFieldReflection { + name: "rotation_axis_angle", + display_name: "Rotation axis angle", + component_type: "rerun.components.RotationAxisAngle".into(), + docstring_md: "Rotation of the grid via axis + angle.\n\nTogether with [`components.Translation3D`](https://rerun.io/docs/reference/types/components/translation3d), this defines the pose of the grid relative to the\nmap's parent coordinate frame.\n\nNote: either this or [`components.RotationQuat`](https://rerun.io/docs/reference/types/components/rotation_quat) can be set to specify the grid's rotation, but not both.\nIf both this and [`components.RotationQuat`](https://rerun.io/docs/reference/types/components/rotation_quat) are set, this is ignored in favor of the quaternion.", + flags: ArchetypeFieldFlags::empty(), + }, + ArchetypeFieldReflection { + name: "quaternion", + display_name: "Quaternion", + component_type: "rerun.components.RotationQuat".into(), + docstring_md: "Rotation of the grid via quaternion.\n\nTogether with [`components.Translation3D`](https://rerun.io/docs/reference/types/components/translation3d), this defines the pose of the grid relative to the\nmap's parent coordinate frame.", + flags: ArchetypeFieldFlags::empty(), + }, + ArchetypeFieldReflection { + name: "opacity", + display_name: "Opacity", + component_type: "rerun.components.Opacity".into(), + docstring_md: "Opacity of the voxels after color or colormap application.\n\nDefaults to 1.0 (fully opaque).", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "value_range", + display_name: "Value range", + component_type: "rerun.components.ValueRange".into(), + docstring_md: "Scalar value range for color-mapping.\n\nDefaults to `[0.0, 1.0]`.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "colormap", + display_name: "Colormap", + component_type: "rerun.components.Colormap".into(), + docstring_md: "Colormap to use when `values` are present and explicit `colors` are not provided.\n\nDefaults to Turbo.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ], + }, + ), + ( + ArchetypeName::from("rerun.blueprint.archetypes.ActiveVisualizers"), ArchetypeReflection { display_name: "Active visualizers", deprecation_summary: None, @@ -3710,7 +3977,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.Background"), + ArchetypeName::from("rerun.blueprint.archetypes.Background"), ArchetypeReflection { display_name: "Background", deprecation_summary: None, @@ -3735,7 +4002,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ContainerBlueprint"), + ArchetypeName::from("rerun.blueprint.archetypes.ContainerBlueprint"), ArchetypeReflection { display_name: "Container blueprint", deprecation_summary: None, @@ -3802,7 +4069,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.DataframeQuery"), + ArchetypeName::from("rerun.blueprint.archetypes.DataframeQuery"), ArchetypeReflection { display_name: "Dataframe query", deprecation_summary: None, @@ -3862,7 +4129,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.EntityBehavior"), + ArchetypeName::from("rerun.blueprint.archetypes.EntityBehavior"), ArchetypeReflection { display_name: "Entity behavior", deprecation_summary: None, @@ -3887,7 +4154,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.EyeControls3D"), + ArchetypeName::from("rerun.blueprint.archetypes.EyeControls3D"), ArchetypeReflection { display_name: "Eye controls 3D", deprecation_summary: None, @@ -3947,7 +4214,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ForceCenter"), + ArchetypeName::from("rerun.blueprint.archetypes.ForceCenter"), ArchetypeReflection { display_name: "Force center", deprecation_summary: None, @@ -3972,7 +4239,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ForceCollisionRadius"), + ArchetypeName::from("rerun.blueprint.archetypes.ForceCollisionRadius"), ArchetypeReflection { display_name: "Force collision radius", deprecation_summary: None, @@ -4004,7 +4271,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ForceLink"), + ArchetypeName::from("rerun.blueprint.archetypes.ForceLink"), ArchetypeReflection { display_name: "Force link", deprecation_summary: None, @@ -4036,7 +4303,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ForceManyBody"), + ArchetypeName::from("rerun.blueprint.archetypes.ForceManyBody"), ArchetypeReflection { display_name: "Force many body", deprecation_summary: None, @@ -4061,7 +4328,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ForcePosition"), + ArchetypeName::from("rerun.blueprint.archetypes.ForcePosition"), ArchetypeReflection { display_name: "Force position", deprecation_summary: None, @@ -4093,7 +4360,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.GraphBackground"), + ArchetypeName::from("rerun.blueprint.archetypes.GraphBackground"), ArchetypeReflection { display_name: "Graph background", deprecation_summary: None, @@ -4109,7 +4376,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.LineGrid3D"), + ArchetypeName::from("rerun.blueprint.archetypes.LineGrid3D"), ArchetypeReflection { display_name: "Line grid 3D", deprecation_summary: None, @@ -4155,7 +4422,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.MapBackground"), + ArchetypeName::from("rerun.blueprint.archetypes.MapBackground"), ArchetypeReflection { display_name: "Map background", deprecation_summary: None, @@ -4171,7 +4438,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.MapZoom"), + ArchetypeName::from("rerun.blueprint.archetypes.MapZoom"), ArchetypeReflection { display_name: "Map zoom", deprecation_summary: None, @@ -4187,7 +4454,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.NearClipPlane"), + ArchetypeName::from("rerun.blueprint.archetypes.NearClipPlane"), ArchetypeReflection { display_name: "Near clip plane", deprecation_summary: None, @@ -4203,7 +4470,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.PanelBlueprint"), + ArchetypeName::from("rerun.blueprint.archetypes.PanelBlueprint"), ArchetypeReflection { display_name: "Panel blueprint", deprecation_summary: None, @@ -4219,7 +4486,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.PlotBackground"), + ArchetypeName::from("rerun.blueprint.archetypes.PlotBackground"), ArchetypeReflection { display_name: "Plot background", deprecation_summary: None, @@ -4244,7 +4511,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.PlotLegend"), + ArchetypeName::from("rerun.blueprint.archetypes.PlotLegend"), ArchetypeReflection { display_name: "Plot legend", deprecation_summary: None, @@ -4269,7 +4536,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ScalarAxis"), + ArchetypeName::from("rerun.blueprint.archetypes.ScalarAxis"), ArchetypeReflection { display_name: "Scalar axis", deprecation_summary: None, @@ -4294,7 +4561,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.SpatialInformation"), + ArchetypeName::from("rerun.blueprint.archetypes.SpatialInformation"), ArchetypeReflection { display_name: "Spatial information", deprecation_summary: None, @@ -4326,7 +4593,46 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.TensorScalarMapping"), + ArchetypeName::from("rerun.blueprint.archetypes.TableBlueprint"), + ArchetypeReflection { + display_name: "Table blueprint", + deprecation_summary: None, + scope: Some("blueprint"), + view_types: &[], + fields: vec![ + ArchetypeFieldReflection { + name: "segment_preview_column", + display_name: "Segment preview column", + component_type: "rerun.blueprint.components.ColumnName".into(), + docstring_md: "The name of the column that contains recording URIs for segment previews.\n\nEvery row can at most preview a single segment.\n\nFor the preview, the rest of the blueprint data is read it as it would be with regular recording blueprints,\nmeaning that the regular structure of archetypes.ViewportBlueprint, and archetypes.ViewBlueprint structure applies.\nHowever, this mostly ignores layout container types as well as automatic spawning.\n\nIf unset, defaults to the first URL column in the table that points to the same Rerun server", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "flag_column", + display_name: "Flag column", + component_type: "rerun.blueprint.components.ColumnName".into(), + docstring_md: "The name of the boolean column used for flag/annotation toggles.\n\nMust be set for flagging to be available. The named column must exist in the\ntable and be of boolean type.\nAdditionally, the table must be remote and have another column with\n`rerun:is_table_index` metadata since flag changes are persisted to the server\nvia upsert.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "grid_view_card_title", + display_name: "Grid view card title", + component_type: "rerun.blueprint.components.ColumnName".into(), + docstring_md: "The name of the column to use as the card title in grid view.\n\nIf unset, the first visible string column is used as the title.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "url_column", + display_name: "Url column", + component_type: "rerun.blueprint.components.ColumnName".into(), + docstring_md: "The name of the column containing URLs to open when a card is clicked in grid view.\n\nIf unset, defaults to the segment preview column.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ], + }, + ), + ( + ArchetypeName::from("rerun.blueprint.archetypes.TensorScalarMapping"), ArchetypeReflection { display_name: "Tensor scalar mapping", deprecation_summary: None, @@ -4358,7 +4664,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.TensorSliceSelection"), + ArchetypeName::from("rerun.blueprint.archetypes.TensorSliceSelection"), ArchetypeReflection { display_name: "Tensor slice selection", deprecation_summary: None, @@ -4398,7 +4704,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.TensorViewFit"), + ArchetypeName::from("rerun.blueprint.archetypes.TensorViewFit"), ArchetypeReflection { display_name: "Tensor view fit", deprecation_summary: None, @@ -4414,7 +4720,32 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.TextLogColumns"), + ArchetypeName::from("rerun.blueprint.archetypes.TextDocumentFormat"), + ArchetypeReflection { + display_name: "Text document format", + deprecation_summary: None, + scope: Some("blueprint"), + view_types: &[], + fields: vec![ + ArchetypeFieldReflection { + name: "monospace", + display_name: "Monospace", + component_type: "rerun.blueprint.components.Enabled".into(), + docstring_md: "Whether to use a monospace font for the document body.\n\nDefaults to disabled.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ArchetypeFieldReflection { + name: "word_wrap", + display_name: "Word wrap", + component_type: "rerun.blueprint.components.Enabled".into(), + docstring_md: "Whether to wrap long lines in the document body.\n\nDefaults to enabled.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + }, + ], + }, + ), + ( + ArchetypeName::from("rerun.blueprint.archetypes.TextLogColumns"), ArchetypeReflection { display_name: "Text log columns", deprecation_summary: None, @@ -4439,7 +4770,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.TextLogFormat"), + ArchetypeName::from("rerun.blueprint.archetypes.TextLogFormat"), ArchetypeReflection { display_name: "Text log format", deprecation_summary: None, @@ -4455,7 +4786,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.TextLogRows"), + ArchetypeName::from("rerun.blueprint.archetypes.TextLogRows"), ArchetypeReflection { display_name: "Text log rows", deprecation_summary: None, @@ -4471,7 +4802,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.TimeAxis"), + ArchetypeName::from("rerun.blueprint.archetypes.TimeAxis"), ArchetypeReflection { display_name: "Time axis", deprecation_summary: None, @@ -4503,7 +4834,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.TimePanelBlueprint"), + ArchetypeName::from("rerun.blueprint.archetypes.TimePanelBlueprint"), ArchetypeReflection { display_name: "Time panel blueprint", deprecation_summary: None, @@ -4563,7 +4894,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ViewBlueprint"), + ArchetypeName::from("rerun.blueprint.archetypes.ViewBlueprint"), ArchetypeReflection { display_name: "View blueprint", deprecation_summary: None, @@ -4602,7 +4933,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ViewContents"), + ArchetypeName::from("rerun.blueprint.archetypes.ViewContents"), ArchetypeReflection { display_name: "View contents", deprecation_summary: None, @@ -4618,7 +4949,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.ViewportBlueprint"), + ArchetypeName::from("rerun.blueprint.archetypes.ViewportBlueprint"), ArchetypeReflection { display_name: "Viewport blueprint", deprecation_summary: None, @@ -4665,7 +4996,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.VisibleTimeRanges"), + ArchetypeName::from("rerun.blueprint.archetypes.VisibleTimeRanges"), ArchetypeReflection { display_name: "Visible time ranges", deprecation_summary: None, @@ -4681,7 +5012,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.VisualBounds2D"), + ArchetypeName::from("rerun.blueprint.archetypes.VisualBounds2D"), ArchetypeReflection { display_name: "Visual bounds 2D", deprecation_summary: None, @@ -4697,7 +5028,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }, ), ( - ArchetypeName::new("rerun.blueprint.archetypes.VisualizerInstruction"), + ArchetypeName::from("rerun.blueprint.archetypes.VisualizerInstruction"), ArchetypeReflection { display_name: "Visualizer instruction", deprecation_summary: None, diff --git a/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer1.rs b/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer1.rs index eada49b59eeb..ad258f2c496f 100644 --- a/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer1.rs +++ b/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer1.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer1 { pub fuzz1001: Option, pub fuzz1002: Option, @@ -53,11 +54,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer1`]. #[inline] pub fn descriptor_fuzz1001() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1001".into(), - component_type: Some("rerun.testing.components.AffixFuzzer1".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1001".into(), + component_type: Some("rerun.testing.components.AffixFuzzer1".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1002`]. @@ -65,11 +68,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer2`]. #[inline] pub fn descriptor_fuzz1002() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1002".into(), - component_type: Some("rerun.testing.components.AffixFuzzer2".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1002".into(), + component_type: Some("rerun.testing.components.AffixFuzzer2".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1003`]. @@ -77,11 +82,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer3`]. #[inline] pub fn descriptor_fuzz1003() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1003".into(), - component_type: Some("rerun.testing.components.AffixFuzzer3".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1003".into(), + component_type: Some("rerun.testing.components.AffixFuzzer3".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1004`]. @@ -89,11 +96,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer4`]. #[inline] pub fn descriptor_fuzz1004() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1004".into(), - component_type: Some("rerun.testing.components.AffixFuzzer4".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1004".into(), + component_type: Some("rerun.testing.components.AffixFuzzer4".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1005`]. @@ -101,11 +110,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer5`]. #[inline] pub fn descriptor_fuzz1005() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1005".into(), - component_type: Some("rerun.testing.components.AffixFuzzer5".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1005".into(), + component_type: Some("rerun.testing.components.AffixFuzzer5".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1006`]. @@ -113,11 +124,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer6`]. #[inline] pub fn descriptor_fuzz1006() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1006".into(), - component_type: Some("rerun.testing.components.AffixFuzzer6".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1006".into(), + component_type: Some("rerun.testing.components.AffixFuzzer6".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1007`]. @@ -125,11 +138,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer7`]. #[inline] pub fn descriptor_fuzz1007() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1007".into(), - component_type: Some("rerun.testing.components.AffixFuzzer7".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1007".into(), + component_type: Some("rerun.testing.components.AffixFuzzer7".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1008`]. @@ -137,11 +152,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer8`]. #[inline] pub fn descriptor_fuzz1008() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1008".into(), - component_type: Some("rerun.testing.components.AffixFuzzer8".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1008".into(), + component_type: Some("rerun.testing.components.AffixFuzzer8".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1009`]. @@ -149,11 +166,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer9`]. #[inline] pub fn descriptor_fuzz1009() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1009".into(), - component_type: Some("rerun.testing.components.AffixFuzzer9".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1009".into(), + component_type: Some("rerun.testing.components.AffixFuzzer9".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1010`]. @@ -161,11 +180,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer10`]. #[inline] pub fn descriptor_fuzz1010() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1010".into(), - component_type: Some("rerun.testing.components.AffixFuzzer10".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1010".into(), + component_type: Some("rerun.testing.components.AffixFuzzer10".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1011`]. @@ -173,11 +194,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer11`]. #[inline] pub fn descriptor_fuzz1011() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1011".into(), - component_type: Some("rerun.testing.components.AffixFuzzer11".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1011".into(), + component_type: Some("rerun.testing.components.AffixFuzzer11".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1012`]. @@ -185,11 +208,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer12`]. #[inline] pub fn descriptor_fuzz1012() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1012".into(), - component_type: Some("rerun.testing.components.AffixFuzzer12".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1012".into(), + component_type: Some("rerun.testing.components.AffixFuzzer12".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1013`]. @@ -197,11 +222,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer13`]. #[inline] pub fn descriptor_fuzz1013() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1013".into(), - component_type: Some("rerun.testing.components.AffixFuzzer13".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1013".into(), + component_type: Some("rerun.testing.components.AffixFuzzer13".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1014`]. @@ -209,11 +236,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer14`]. #[inline] pub fn descriptor_fuzz1014() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1014".into(), - component_type: Some("rerun.testing.components.AffixFuzzer14".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1014".into(), + component_type: Some("rerun.testing.components.AffixFuzzer14".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1015`]. @@ -221,11 +250,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer15`]. #[inline] pub fn descriptor_fuzz1015() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1015".into(), - component_type: Some("rerun.testing.components.AffixFuzzer15".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1015".into(), + component_type: Some("rerun.testing.components.AffixFuzzer15".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1016`]. @@ -233,11 +264,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer16`]. #[inline] pub fn descriptor_fuzz1016() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1016".into(), - component_type: Some("rerun.testing.components.AffixFuzzer16".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1016".into(), + component_type: Some("rerun.testing.components.AffixFuzzer16".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1017`]. @@ -245,11 +278,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer17`]. #[inline] pub fn descriptor_fuzz1017() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1017".into(), - component_type: Some("rerun.testing.components.AffixFuzzer17".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1017".into(), + component_type: Some("rerun.testing.components.AffixFuzzer17".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1018`]. @@ -257,11 +292,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer18`]. #[inline] pub fn descriptor_fuzz1018() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1018".into(), - component_type: Some("rerun.testing.components.AffixFuzzer18".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1018".into(), + component_type: Some("rerun.testing.components.AffixFuzzer18".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1019`]. @@ -269,11 +306,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer19`]. #[inline] pub fn descriptor_fuzz1019() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1019".into(), - component_type: Some("rerun.testing.components.AffixFuzzer19".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1019".into(), + component_type: Some("rerun.testing.components.AffixFuzzer19".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1020`]. @@ -281,11 +320,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer20`]. #[inline] pub fn descriptor_fuzz1020() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1020".into(), - component_type: Some("rerun.testing.components.AffixFuzzer20".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1020".into(), + component_type: Some("rerun.testing.components.AffixFuzzer20".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1021`]. @@ -293,11 +334,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer21`]. #[inline] pub fn descriptor_fuzz1021() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1021".into(), - component_type: Some("rerun.testing.components.AffixFuzzer21".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1021".into(), + component_type: Some("rerun.testing.components.AffixFuzzer21".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1022`]. @@ -305,11 +348,13 @@ impl AffixFuzzer1 { /// The corresponding component is [`crate::testing::components::AffixFuzzer22`]. #[inline] pub fn descriptor_fuzz1022() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), - component: "AffixFuzzer1:fuzz1022".into(), - component_type: Some("rerun.testing.components.AffixFuzzer22".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer1".into()), + component: "AffixFuzzer1:fuzz1022".into(), + component_type: Some("rerun.testing.components.AffixFuzzer22".into()), + }); + (*DESCRIPTOR).clone() } } @@ -383,7 +428,10 @@ impl AffixFuzzer1 { impl ::re_types_core::Archetype for AffixFuzzer1 { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.testing.archetypes.AffixFuzzer1".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.testing.archetypes.AffixFuzzer1" + ) } #[inline] @@ -1332,31 +1380,3 @@ impl AffixFuzzer1 { self } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer1 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.fuzz1001.heap_size_bytes() - + self.fuzz1002.heap_size_bytes() - + self.fuzz1003.heap_size_bytes() - + self.fuzz1004.heap_size_bytes() - + self.fuzz1005.heap_size_bytes() - + self.fuzz1006.heap_size_bytes() - + self.fuzz1007.heap_size_bytes() - + self.fuzz1008.heap_size_bytes() - + self.fuzz1009.heap_size_bytes() - + self.fuzz1010.heap_size_bytes() - + self.fuzz1011.heap_size_bytes() - + self.fuzz1012.heap_size_bytes() - + self.fuzz1013.heap_size_bytes() - + self.fuzz1014.heap_size_bytes() - + self.fuzz1015.heap_size_bytes() - + self.fuzz1016.heap_size_bytes() - + self.fuzz1017.heap_size_bytes() - + self.fuzz1018.heap_size_bytes() - + self.fuzz1019.heap_size_bytes() - + self.fuzz1020.heap_size_bytes() - + self.fuzz1021.heap_size_bytes() - + self.fuzz1022.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer2.rs b/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer2.rs index c4b2382787f4..6abe57754efe 100644 --- a/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer2.rs +++ b/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer2.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer2 { pub fuzz1101: Option, pub fuzz1102: Option, @@ -50,11 +51,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer1`]. #[inline] pub fn descriptor_fuzz1101() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1101".into(), - component_type: Some("rerun.testing.components.AffixFuzzer1".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1101".into(), + component_type: Some("rerun.testing.components.AffixFuzzer1".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1102`]. @@ -62,11 +65,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer2`]. #[inline] pub fn descriptor_fuzz1102() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1102".into(), - component_type: Some("rerun.testing.components.AffixFuzzer2".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1102".into(), + component_type: Some("rerun.testing.components.AffixFuzzer2".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1103`]. @@ -74,11 +79,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer3`]. #[inline] pub fn descriptor_fuzz1103() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1103".into(), - component_type: Some("rerun.testing.components.AffixFuzzer3".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1103".into(), + component_type: Some("rerun.testing.components.AffixFuzzer3".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1104`]. @@ -86,11 +93,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer4`]. #[inline] pub fn descriptor_fuzz1104() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1104".into(), - component_type: Some("rerun.testing.components.AffixFuzzer4".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1104".into(), + component_type: Some("rerun.testing.components.AffixFuzzer4".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1105`]. @@ -98,11 +107,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer5`]. #[inline] pub fn descriptor_fuzz1105() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1105".into(), - component_type: Some("rerun.testing.components.AffixFuzzer5".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1105".into(), + component_type: Some("rerun.testing.components.AffixFuzzer5".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1106`]. @@ -110,11 +121,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer6`]. #[inline] pub fn descriptor_fuzz1106() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1106".into(), - component_type: Some("rerun.testing.components.AffixFuzzer6".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1106".into(), + component_type: Some("rerun.testing.components.AffixFuzzer6".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1107`]. @@ -122,11 +135,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer7`]. #[inline] pub fn descriptor_fuzz1107() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1107".into(), - component_type: Some("rerun.testing.components.AffixFuzzer7".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1107".into(), + component_type: Some("rerun.testing.components.AffixFuzzer7".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1108`]. @@ -134,11 +149,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer8`]. #[inline] pub fn descriptor_fuzz1108() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1108".into(), - component_type: Some("rerun.testing.components.AffixFuzzer8".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1108".into(), + component_type: Some("rerun.testing.components.AffixFuzzer8".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1109`]. @@ -146,11 +163,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer9`]. #[inline] pub fn descriptor_fuzz1109() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1109".into(), - component_type: Some("rerun.testing.components.AffixFuzzer9".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1109".into(), + component_type: Some("rerun.testing.components.AffixFuzzer9".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1110`]. @@ -158,11 +177,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer10`]. #[inline] pub fn descriptor_fuzz1110() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1110".into(), - component_type: Some("rerun.testing.components.AffixFuzzer10".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1110".into(), + component_type: Some("rerun.testing.components.AffixFuzzer10".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1111`]. @@ -170,11 +191,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer11`]. #[inline] pub fn descriptor_fuzz1111() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1111".into(), - component_type: Some("rerun.testing.components.AffixFuzzer11".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1111".into(), + component_type: Some("rerun.testing.components.AffixFuzzer11".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1112`]. @@ -182,11 +205,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer12`]. #[inline] pub fn descriptor_fuzz1112() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1112".into(), - component_type: Some("rerun.testing.components.AffixFuzzer12".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1112".into(), + component_type: Some("rerun.testing.components.AffixFuzzer12".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1113`]. @@ -194,11 +219,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer13`]. #[inline] pub fn descriptor_fuzz1113() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1113".into(), - component_type: Some("rerun.testing.components.AffixFuzzer13".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1113".into(), + component_type: Some("rerun.testing.components.AffixFuzzer13".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1114`]. @@ -206,11 +233,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer14`]. #[inline] pub fn descriptor_fuzz1114() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1114".into(), - component_type: Some("rerun.testing.components.AffixFuzzer14".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1114".into(), + component_type: Some("rerun.testing.components.AffixFuzzer14".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1115`]. @@ -218,11 +247,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer15`]. #[inline] pub fn descriptor_fuzz1115() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1115".into(), - component_type: Some("rerun.testing.components.AffixFuzzer15".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1115".into(), + component_type: Some("rerun.testing.components.AffixFuzzer15".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1116`]. @@ -230,11 +261,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer16`]. #[inline] pub fn descriptor_fuzz1116() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1116".into(), - component_type: Some("rerun.testing.components.AffixFuzzer16".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1116".into(), + component_type: Some("rerun.testing.components.AffixFuzzer16".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1117`]. @@ -242,11 +275,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer17`]. #[inline] pub fn descriptor_fuzz1117() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1117".into(), - component_type: Some("rerun.testing.components.AffixFuzzer17".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1117".into(), + component_type: Some("rerun.testing.components.AffixFuzzer17".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1118`]. @@ -254,11 +289,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer18`]. #[inline] pub fn descriptor_fuzz1118() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1118".into(), - component_type: Some("rerun.testing.components.AffixFuzzer18".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1118".into(), + component_type: Some("rerun.testing.components.AffixFuzzer18".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz1122`]. @@ -266,11 +303,13 @@ impl AffixFuzzer2 { /// The corresponding component is [`crate::testing::components::AffixFuzzer22`]. #[inline] pub fn descriptor_fuzz1122() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), - component: "AffixFuzzer2:fuzz1122".into(), - component_type: Some("rerun.testing.components.AffixFuzzer22".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer2".into()), + component: "AffixFuzzer2:fuzz1122".into(), + component_type: Some("rerun.testing.components.AffixFuzzer22".into()), + }); + (*DESCRIPTOR).clone() } } @@ -338,7 +377,10 @@ impl AffixFuzzer2 { impl ::re_types_core::Archetype for AffixFuzzer2 { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.testing.archetypes.AffixFuzzer2".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.testing.archetypes.AffixFuzzer2" + ) } #[inline] @@ -926,28 +968,3 @@ impl AffixFuzzer2 { self } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer2 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.fuzz1101.heap_size_bytes() - + self.fuzz1102.heap_size_bytes() - + self.fuzz1103.heap_size_bytes() - + self.fuzz1104.heap_size_bytes() - + self.fuzz1105.heap_size_bytes() - + self.fuzz1106.heap_size_bytes() - + self.fuzz1107.heap_size_bytes() - + self.fuzz1108.heap_size_bytes() - + self.fuzz1109.heap_size_bytes() - + self.fuzz1110.heap_size_bytes() - + self.fuzz1111.heap_size_bytes() - + self.fuzz1112.heap_size_bytes() - + self.fuzz1113.heap_size_bytes() - + self.fuzz1114.heap_size_bytes() - + self.fuzz1115.heap_size_bytes() - + self.fuzz1116.heap_size_bytes() - + self.fuzz1117.heap_size_bytes() - + self.fuzz1118.heap_size_bytes() - + self.fuzz1122.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer3.rs b/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer3.rs index 9b334ca9975a..a0b0cbc3c5ba 100644 --- a/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer3.rs +++ b/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer3.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer3 { pub fuzz2001: Option, pub fuzz2002: Option, @@ -49,11 +50,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer1`]. #[inline] pub fn descriptor_fuzz2001() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2001".into(), - component_type: Some("rerun.testing.components.AffixFuzzer1".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2001".into(), + component_type: Some("rerun.testing.components.AffixFuzzer1".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2002`]. @@ -61,11 +64,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer2`]. #[inline] pub fn descriptor_fuzz2002() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2002".into(), - component_type: Some("rerun.testing.components.AffixFuzzer2".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2002".into(), + component_type: Some("rerun.testing.components.AffixFuzzer2".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2003`]. @@ -73,11 +78,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer3`]. #[inline] pub fn descriptor_fuzz2003() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2003".into(), - component_type: Some("rerun.testing.components.AffixFuzzer3".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2003".into(), + component_type: Some("rerun.testing.components.AffixFuzzer3".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2004`]. @@ -85,11 +92,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer4`]. #[inline] pub fn descriptor_fuzz2004() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2004".into(), - component_type: Some("rerun.testing.components.AffixFuzzer4".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2004".into(), + component_type: Some("rerun.testing.components.AffixFuzzer4".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2005`]. @@ -97,11 +106,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer5`]. #[inline] pub fn descriptor_fuzz2005() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2005".into(), - component_type: Some("rerun.testing.components.AffixFuzzer5".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2005".into(), + component_type: Some("rerun.testing.components.AffixFuzzer5".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2006`]. @@ -109,11 +120,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer6`]. #[inline] pub fn descriptor_fuzz2006() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2006".into(), - component_type: Some("rerun.testing.components.AffixFuzzer6".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2006".into(), + component_type: Some("rerun.testing.components.AffixFuzzer6".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2007`]. @@ -121,11 +134,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer7`]. #[inline] pub fn descriptor_fuzz2007() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2007".into(), - component_type: Some("rerun.testing.components.AffixFuzzer7".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2007".into(), + component_type: Some("rerun.testing.components.AffixFuzzer7".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2008`]. @@ -133,11 +148,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer8`]. #[inline] pub fn descriptor_fuzz2008() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2008".into(), - component_type: Some("rerun.testing.components.AffixFuzzer8".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2008".into(), + component_type: Some("rerun.testing.components.AffixFuzzer8".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2009`]. @@ -145,11 +162,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer9`]. #[inline] pub fn descriptor_fuzz2009() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2009".into(), - component_type: Some("rerun.testing.components.AffixFuzzer9".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2009".into(), + component_type: Some("rerun.testing.components.AffixFuzzer9".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2010`]. @@ -157,11 +176,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer10`]. #[inline] pub fn descriptor_fuzz2010() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2010".into(), - component_type: Some("rerun.testing.components.AffixFuzzer10".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2010".into(), + component_type: Some("rerun.testing.components.AffixFuzzer10".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2011`]. @@ -169,11 +190,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer11`]. #[inline] pub fn descriptor_fuzz2011() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2011".into(), - component_type: Some("rerun.testing.components.AffixFuzzer11".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2011".into(), + component_type: Some("rerun.testing.components.AffixFuzzer11".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2012`]. @@ -181,11 +204,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer12`]. #[inline] pub fn descriptor_fuzz2012() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2012".into(), - component_type: Some("rerun.testing.components.AffixFuzzer12".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2012".into(), + component_type: Some("rerun.testing.components.AffixFuzzer12".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2013`]. @@ -193,11 +218,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer13`]. #[inline] pub fn descriptor_fuzz2013() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2013".into(), - component_type: Some("rerun.testing.components.AffixFuzzer13".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2013".into(), + component_type: Some("rerun.testing.components.AffixFuzzer13".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2014`]. @@ -205,11 +232,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer14`]. #[inline] pub fn descriptor_fuzz2014() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2014".into(), - component_type: Some("rerun.testing.components.AffixFuzzer14".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2014".into(), + component_type: Some("rerun.testing.components.AffixFuzzer14".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2015`]. @@ -217,11 +246,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer15`]. #[inline] pub fn descriptor_fuzz2015() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2015".into(), - component_type: Some("rerun.testing.components.AffixFuzzer15".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2015".into(), + component_type: Some("rerun.testing.components.AffixFuzzer15".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2016`]. @@ -229,11 +260,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer16`]. #[inline] pub fn descriptor_fuzz2016() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2016".into(), - component_type: Some("rerun.testing.components.AffixFuzzer16".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2016".into(), + component_type: Some("rerun.testing.components.AffixFuzzer16".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2017`]. @@ -241,11 +274,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer17`]. #[inline] pub fn descriptor_fuzz2017() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2017".into(), - component_type: Some("rerun.testing.components.AffixFuzzer17".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2017".into(), + component_type: Some("rerun.testing.components.AffixFuzzer17".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2018`]. @@ -253,11 +288,13 @@ impl AffixFuzzer3 { /// The corresponding component is [`crate::testing::components::AffixFuzzer18`]. #[inline] pub fn descriptor_fuzz2018() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), - component: "AffixFuzzer3:fuzz2018".into(), - component_type: Some("rerun.testing.components.AffixFuzzer18".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer3".into()), + component: "AffixFuzzer3:fuzz2018".into(), + component_type: Some("rerun.testing.components.AffixFuzzer18".into()), + }); + (*DESCRIPTOR).clone() } } @@ -323,7 +360,10 @@ impl AffixFuzzer3 { impl ::re_types_core::Archetype for AffixFuzzer3 { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.testing.archetypes.AffixFuzzer3".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.testing.archetypes.AffixFuzzer3" + ) } #[inline] @@ -1101,27 +1141,3 @@ impl AffixFuzzer3 { self } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer3 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.fuzz2001.heap_size_bytes() - + self.fuzz2002.heap_size_bytes() - + self.fuzz2003.heap_size_bytes() - + self.fuzz2004.heap_size_bytes() - + self.fuzz2005.heap_size_bytes() - + self.fuzz2006.heap_size_bytes() - + self.fuzz2007.heap_size_bytes() - + self.fuzz2008.heap_size_bytes() - + self.fuzz2009.heap_size_bytes() - + self.fuzz2010.heap_size_bytes() - + self.fuzz2011.heap_size_bytes() - + self.fuzz2012.heap_size_bytes() - + self.fuzz2013.heap_size_bytes() - + self.fuzz2014.heap_size_bytes() - + self.fuzz2015.heap_size_bytes() - + self.fuzz2016.heap_size_bytes() - + self.fuzz2017.heap_size_bytes() - + self.fuzz2018.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer4.rs b/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer4.rs index 3bbea6cf0ac6..57f495b1b354 100644 --- a/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer4.rs +++ b/crates/store/re_sdk_types/src/testing/archetypes/affix_fuzzer4.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer4 { pub fuzz2101: Option, pub fuzz2102: Option, @@ -49,11 +50,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer1`]. #[inline] pub fn descriptor_fuzz2101() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2101".into(), - component_type: Some("rerun.testing.components.AffixFuzzer1".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2101".into(), + component_type: Some("rerun.testing.components.AffixFuzzer1".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2102`]. @@ -61,11 +64,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer2`]. #[inline] pub fn descriptor_fuzz2102() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2102".into(), - component_type: Some("rerun.testing.components.AffixFuzzer2".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2102".into(), + component_type: Some("rerun.testing.components.AffixFuzzer2".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2103`]. @@ -73,11 +78,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer3`]. #[inline] pub fn descriptor_fuzz2103() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2103".into(), - component_type: Some("rerun.testing.components.AffixFuzzer3".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2103".into(), + component_type: Some("rerun.testing.components.AffixFuzzer3".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2104`]. @@ -85,11 +92,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer4`]. #[inline] pub fn descriptor_fuzz2104() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2104".into(), - component_type: Some("rerun.testing.components.AffixFuzzer4".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2104".into(), + component_type: Some("rerun.testing.components.AffixFuzzer4".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2105`]. @@ -97,11 +106,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer5`]. #[inline] pub fn descriptor_fuzz2105() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2105".into(), - component_type: Some("rerun.testing.components.AffixFuzzer5".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2105".into(), + component_type: Some("rerun.testing.components.AffixFuzzer5".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2106`]. @@ -109,11 +120,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer6`]. #[inline] pub fn descriptor_fuzz2106() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2106".into(), - component_type: Some("rerun.testing.components.AffixFuzzer6".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2106".into(), + component_type: Some("rerun.testing.components.AffixFuzzer6".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2107`]. @@ -121,11 +134,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer7`]. #[inline] pub fn descriptor_fuzz2107() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2107".into(), - component_type: Some("rerun.testing.components.AffixFuzzer7".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2107".into(), + component_type: Some("rerun.testing.components.AffixFuzzer7".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2108`]. @@ -133,11 +148,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer8`]. #[inline] pub fn descriptor_fuzz2108() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2108".into(), - component_type: Some("rerun.testing.components.AffixFuzzer8".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2108".into(), + component_type: Some("rerun.testing.components.AffixFuzzer8".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2109`]. @@ -145,11 +162,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer9`]. #[inline] pub fn descriptor_fuzz2109() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2109".into(), - component_type: Some("rerun.testing.components.AffixFuzzer9".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2109".into(), + component_type: Some("rerun.testing.components.AffixFuzzer9".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2110`]. @@ -157,11 +176,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer10`]. #[inline] pub fn descriptor_fuzz2110() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2110".into(), - component_type: Some("rerun.testing.components.AffixFuzzer10".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2110".into(), + component_type: Some("rerun.testing.components.AffixFuzzer10".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2111`]. @@ -169,11 +190,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer11`]. #[inline] pub fn descriptor_fuzz2111() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2111".into(), - component_type: Some("rerun.testing.components.AffixFuzzer11".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2111".into(), + component_type: Some("rerun.testing.components.AffixFuzzer11".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2112`]. @@ -181,11 +204,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer12`]. #[inline] pub fn descriptor_fuzz2112() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2112".into(), - component_type: Some("rerun.testing.components.AffixFuzzer12".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2112".into(), + component_type: Some("rerun.testing.components.AffixFuzzer12".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2113`]. @@ -193,11 +218,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer13`]. #[inline] pub fn descriptor_fuzz2113() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2113".into(), - component_type: Some("rerun.testing.components.AffixFuzzer13".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2113".into(), + component_type: Some("rerun.testing.components.AffixFuzzer13".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2114`]. @@ -205,11 +232,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer14`]. #[inline] pub fn descriptor_fuzz2114() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2114".into(), - component_type: Some("rerun.testing.components.AffixFuzzer14".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2114".into(), + component_type: Some("rerun.testing.components.AffixFuzzer14".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2115`]. @@ -217,11 +246,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer15`]. #[inline] pub fn descriptor_fuzz2115() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2115".into(), - component_type: Some("rerun.testing.components.AffixFuzzer15".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2115".into(), + component_type: Some("rerun.testing.components.AffixFuzzer15".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2116`]. @@ -229,11 +260,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer16`]. #[inline] pub fn descriptor_fuzz2116() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2116".into(), - component_type: Some("rerun.testing.components.AffixFuzzer16".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2116".into(), + component_type: Some("rerun.testing.components.AffixFuzzer16".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2117`]. @@ -241,11 +274,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer17`]. #[inline] pub fn descriptor_fuzz2117() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2117".into(), - component_type: Some("rerun.testing.components.AffixFuzzer17".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2117".into(), + component_type: Some("rerun.testing.components.AffixFuzzer17".into()), + }); + (*DESCRIPTOR).clone() } /// Returns the [`ComponentDescriptor`] for [`Self::fuzz2118`]. @@ -253,11 +288,13 @@ impl AffixFuzzer4 { /// The corresponding component is [`crate::testing::components::AffixFuzzer18`]. #[inline] pub fn descriptor_fuzz2118() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), - component: "AffixFuzzer4:fuzz2118".into(), - component_type: Some("rerun.testing.components.AffixFuzzer18".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.testing.archetypes.AffixFuzzer4".into()), + component: "AffixFuzzer4:fuzz2118".into(), + component_type: Some("rerun.testing.components.AffixFuzzer18".into()), + }); + (*DESCRIPTOR).clone() } } @@ -323,7 +360,10 @@ impl AffixFuzzer4 { impl ::re_types_core::Archetype for AffixFuzzer4 { #[inline] fn name() -> ::re_types_core::ArchetypeName { - "rerun.testing.archetypes.AffixFuzzer4".into() + ::re_types_core::external::re_string_interner::intern_static_nonempty!( + ::re_types_core::ArchetypeName, + "rerun.testing.archetypes.AffixFuzzer4" + ) } #[inline] @@ -867,27 +907,3 @@ impl AffixFuzzer4 { self } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer4 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.fuzz2101.heap_size_bytes() - + self.fuzz2102.heap_size_bytes() - + self.fuzz2103.heap_size_bytes() - + self.fuzz2104.heap_size_bytes() - + self.fuzz2105.heap_size_bytes() - + self.fuzz2106.heap_size_bytes() - + self.fuzz2107.heap_size_bytes() - + self.fuzz2108.heap_size_bytes() - + self.fuzz2109.heap_size_bytes() - + self.fuzz2110.heap_size_bytes() - + self.fuzz2111.heap_size_bytes() - + self.fuzz2112.heap_size_bytes() - + self.fuzz2113.heap_size_bytes() - + self.fuzz2114.heap_size_bytes() - + self.fuzz2115.heap_size_bytes() - + self.fuzz2116.heap_size_bytes() - + self.fuzz2117.heap_size_bytes() - + self.fuzz2118.heap_size_bytes() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/.gitattributes b/crates/store/re_sdk_types/src/testing/components/.gitattributes index 96b24405e755..969d317770fa 100644 --- a/crates/store/re_sdk_types/src/testing/components/.gitattributes +++ b/crates/store/re_sdk_types/src/testing/components/.gitattributes @@ -24,4 +24,5 @@ affix_fuzzer6.rs linguist-generated=true affix_fuzzer7.rs linguist-generated=true affix_fuzzer8.rs linguist-generated=true affix_fuzzer9.rs linguist-generated=true +many_vec3.rs linguist-generated=true mod.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer1.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer1.rs index c2670e4c12d4..9f7ee53b59fb 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer1.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer1.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer1(pub crate::testing::datatypes::AffixFuzzer1); impl ::re_types_core::WrapperComponent for AffixFuzzer1 { @@ -68,15 +69,3 @@ impl std::ops::DerefMut for AffixFuzzer1 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer1 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer10.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer10.rs index 47c9950a0fb2..8a3841bcff80 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer10.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer10.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer10(pub Option<::re_types_core::ArrowString>); impl ::re_types_core::Component for AffixFuzzer10 { @@ -167,15 +168,3 @@ impl std::ops::DerefMut for AffixFuzzer10 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer10 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer11.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer11.rs index f38be43e3aa5..f672999e7bba 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer11.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer11.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer11(pub Option<::arrow::buffer::ScalarBuffer>); impl ::re_types_core::Component for AffixFuzzer11 { @@ -187,15 +188,3 @@ impl std::ops::DerefMut for AffixFuzzer11 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer11 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer12.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer12.rs index f9fc2ba9c876..531305eea1a6 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer12.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer12.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer12(pub Vec<::re_types_core::ArrowString>); impl ::re_types_core::Component for AffixFuzzer12 { @@ -234,15 +235,3 @@ impl std::ops::DerefMut for AffixFuzzer12 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer12 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer13.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer13.rs index eb60bbd2c7b7..046fcea224f1 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer13.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer13.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer13(pub Option>); impl ::re_types_core::Component for AffixFuzzer13 { @@ -234,15 +235,3 @@ impl std::ops::DerefMut for AffixFuzzer13 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer13 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer14.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer14.rs index 736467179634..28e3fd564f73 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer14.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer14.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer14(pub crate::testing::datatypes::AffixFuzzer3); impl ::re_types_core::WrapperComponent for AffixFuzzer14 { @@ -68,15 +69,3 @@ impl std::ops::DerefMut for AffixFuzzer14 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer14 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer15.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer15.rs index 2b869e8c56b9..1a59d1329030 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer15.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer15.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer15(pub Option); impl ::re_types_core::Component for AffixFuzzer15 { @@ -146,15 +147,3 @@ impl std::ops::DerefMut for AffixFuzzer15 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer15 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer16.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer16.rs index cbeb8f20ce3c..dfa09d4ba895 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer16.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer16.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer16(pub Vec); impl ::re_types_core::Component for AffixFuzzer16 { @@ -166,15 +167,3 @@ impl, T: IntoIterator Self(v.into_iter().map(|v| v.into()).collect()) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer16 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer17.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer17.rs index 152456d60c7a..efc5cf76c8f6 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer17.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer17.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer17(pub Option>); impl ::re_types_core::Component for AffixFuzzer17 { @@ -166,15 +167,3 @@ impl, T: IntoIterator Self(v.map(|v| v.into_iter().map(|v| v.into()).collect())) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer17 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer18.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer18.rs index 0d00b0a7682e..d0570ab7150b 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer18.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer18.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer18(pub Option>); impl ::re_types_core::Component for AffixFuzzer18 { @@ -166,15 +167,3 @@ impl, T: IntoIterator Self(v.map(|v| v.into_iter().map(|v| v.into()).collect())) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer18 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer19.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer19.rs index b963ff8859ed..14e866d7c6d8 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer19.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer19.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer19(pub crate::testing::datatypes::AffixFuzzer5); impl ::re_types_core::WrapperComponent for AffixFuzzer19 { @@ -68,15 +69,3 @@ impl std::ops::DerefMut for AffixFuzzer19 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer19 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer2.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer2.rs index 20581cd140ba..5e2d9ff2d876 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer2.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer2.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer2(pub crate::testing::datatypes::AffixFuzzer1); impl ::re_types_core::WrapperComponent for AffixFuzzer2 { @@ -68,15 +69,3 @@ impl std::ops::DerefMut for AffixFuzzer2 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer2 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer20.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer20.rs index 57c5d8213ea0..bb6ec27a007b 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer20.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer20.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer20(pub crate::testing::datatypes::AffixFuzzer20); impl ::re_types_core::WrapperComponent for AffixFuzzer20 { @@ -68,15 +69,3 @@ impl std::ops::DerefMut for AffixFuzzer20 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer20 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer21.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer21.rs index 6d73d5047e53..253fcc7c36ac 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer21.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer21.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer21(pub crate::testing::datatypes::AffixFuzzer21); impl ::re_types_core::WrapperComponent for AffixFuzzer21 { @@ -68,15 +69,3 @@ impl std::ops::DerefMut for AffixFuzzer21 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer21 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer22.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer22.rs index 2954b40c7e5c..dbe2479f6d46 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer22.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer22.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer22(pub Option); impl ::re_types_core::Component for AffixFuzzer22 { @@ -125,15 +126,3 @@ impl std::ops::DerefMut for AffixFuzzer22 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer22 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer23.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer23.rs index 1d2f1d5d68e1..474c881d4ed3 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer23.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer23.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer23(pub Option); impl ::re_types_core::Component for AffixFuzzer23 { @@ -129,15 +130,3 @@ impl std::ops::DerefMut for AffixFuzzer23 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer23 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer3.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer3.rs index 4a8f44c8da47..b3e57da25f04 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer3.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer3.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer3(pub crate::testing::datatypes::AffixFuzzer1); impl ::re_types_core::WrapperComponent for AffixFuzzer3 { @@ -68,15 +69,3 @@ impl std::ops::DerefMut for AffixFuzzer3 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer3 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer4.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer4.rs index 646ca93a9aab..77134b0ddfe8 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer4.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer4.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer4(pub Option); impl ::re_types_core::Component for AffixFuzzer4 { @@ -156,15 +157,3 @@ impl std::ops::DerefMut for AffixFuzzer4 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer4 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer5.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer5.rs index 861387ba0bd5..92d4fc43867a 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer5.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer5.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer5(pub Option); impl ::re_types_core::Component for AffixFuzzer5 { @@ -156,15 +157,3 @@ impl std::ops::DerefMut for AffixFuzzer5 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer5 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer6.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer6.rs index 5be34b3639ac..38676f4b5f5f 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer6.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer6.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer6(pub Option); impl ::re_types_core::Component for AffixFuzzer6 { @@ -156,15 +157,3 @@ impl std::ops::DerefMut for AffixFuzzer6 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer6 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer7.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer7.rs index 73eec4313bb4..0727774e987d 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer7.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer7.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer7(pub Option>); impl ::re_types_core::Component for AffixFuzzer7 { @@ -164,15 +165,3 @@ impl, T: IntoIterator Self(v.map(|v| v.into_iter().map(|v| v.into()).collect())) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer7 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer8.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer8.rs index 687de38061b1..ccd3923bad8b 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer8.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer8.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer8(pub Option); impl ::re_types_core::Component for AffixFuzzer8 { @@ -129,15 +130,3 @@ impl std::ops::DerefMut for AffixFuzzer8 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer8 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer9.rs b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer9.rs index e5e118bb79be..9fefb73bd163 100644 --- a/crates/store/re_sdk_types/src/testing/components/affix_fuzzer9.rs +++ b/crates/store/re_sdk_types/src/testing/components/affix_fuzzer9.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer9(pub ::re_types_core::ArrowString); impl ::re_types_core::Component for AffixFuzzer9 { @@ -167,15 +168,3 @@ impl std::ops::DerefMut for AffixFuzzer9 { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer9 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <::re_types_core::ArrowString>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/components/many_vec3.rs b/crates/store/re_sdk_types/src/testing/components/many_vec3.rs new file mode 100644 index 000000000000..414ba17438a0 --- /dev/null +++ b/crates/store/re_sdk_types/src/testing/components/many_vec3.rs @@ -0,0 +1,81 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/fuzzy.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] +#[repr(transparent)] +pub struct ManyVec3(pub crate::testing::datatypes::ManyVec3); + +impl ::re_types_core::WrapperComponent for ManyVec3 { + type Datatype = crate::testing::datatypes::ManyVec3; + + #[inline] + fn name() -> ComponentType { + "rerun.testing.components.ManyVec3".into() + } + + #[inline] + fn into_inner(self) -> Self::Datatype { + self.0 + } +} + +::re_types_core::macros::impl_into_cow!(ManyVec3); + +impl> From for ManyVec3 { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for ManyVec3 { + #[inline] + fn borrow(&self) -> &crate::testing::datatypes::ManyVec3 { + &self.0 + } +} + +impl std::ops::Deref for ManyVec3 { + type Target = crate::testing::datatypes::ManyVec3; + + #[inline] + fn deref(&self) -> &crate::testing::datatypes::ManyVec3 { + &self.0 + } +} + +impl std::ops::DerefMut for ManyVec3 { + #[inline] + fn deref_mut(&mut self) -> &mut crate::testing::datatypes::ManyVec3 { + &mut self.0 + } +} diff --git a/crates/store/re_sdk_types/src/testing/components/mod.rs b/crates/store/re_sdk_types/src/testing/components/mod.rs index fb9ca01c778d..6658400bc096 100644 --- a/crates/store/re_sdk_types/src/testing/components/mod.rs +++ b/crates/store/re_sdk_types/src/testing/components/mod.rs @@ -23,6 +23,7 @@ mod affix_fuzzer6; mod affix_fuzzer7; mod affix_fuzzer8; mod affix_fuzzer9; +mod many_vec3; pub use self::affix_fuzzer1::AffixFuzzer1; pub use self::affix_fuzzer2::AffixFuzzer2; @@ -47,3 +48,4 @@ pub use self::affix_fuzzer20::AffixFuzzer20; pub use self::affix_fuzzer21::AffixFuzzer21; pub use self::affix_fuzzer22::AffixFuzzer22; pub use self::affix_fuzzer23::AffixFuzzer23; +pub use self::many_vec3::ManyVec3; diff --git a/crates/store/re_sdk_types/src/testing/datatypes/.gitattributes b/crates/store/re_sdk_types/src/testing/datatypes/.gitattributes index 33657574e83d..d84fd134f995 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/.gitattributes +++ b/crates/store/re_sdk_types/src/testing/datatypes/.gitattributes @@ -10,9 +10,13 @@ affix_fuzzer3.rs linguist-generated=true affix_fuzzer4.rs linguist-generated=true affix_fuzzer5.rs linguist-generated=true enum_test.rs linguist-generated=true +fixed_size_enum_array.rs linguist-generated=true +fixed_size_wide_enum_array.rs linguist-generated=true flattened_scalar.rs linguist-generated=true +many_vec3.rs linguist-generated=true mod.rs linguist-generated=true multi_enum.rs linguist-generated=true primitive_component.rs linguist-generated=true string_component.rs linguist-generated=true valued_enum.rs linguist-generated=true +wide_enum.rs linguist-generated=true diff --git a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer1.rs b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer1.rs index b81424f66436..736bc8b26846 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer1.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer1.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer1 { pub single_float_optional: Option, pub single_string_required: ::re_types_core::ArrowString, @@ -510,11 +511,11 @@ impl ::re_types_core::Loggable for AffixFuzzer1 { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let single_float_optional = { if !arrays_by_name.contains_key("single_float_optional") { return Err(DeserializationError::missing_struct_field( @@ -1056,31 +1057,3 @@ impl ::re_types_core::Loggable for AffixFuzzer1 { }) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer1 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.single_float_optional.heap_size_bytes() - + self.single_string_required.heap_size_bytes() - + self.single_string_optional.heap_size_bytes() - + self.many_floats_optional.heap_size_bytes() - + self.many_strings_required.heap_size_bytes() - + self.many_strings_optional.heap_size_bytes() - + self.flattened_scalar.heap_size_bytes() - + self.almost_flattened_scalar.heap_size_bytes() - + self.from_parent.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - && <::re_types_core::ArrowString>::is_pod() - && >::is_pod() - && >>::is_pod() - && >::is_pod() - && >>::is_pod() - && ::is_pod() - && ::is_pod() - && >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer2.rs b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer2.rs index 36db75287eeb..143637b8788a 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer2.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer2.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer2(pub Option); ::re_types_core::macros::impl_into_cow!(AffixFuzzer2); @@ -106,15 +107,3 @@ impl From for Option { value.0 } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer2 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer20.rs b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer20.rs index f9ee00ff4b4d..90ccf0357947 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer20.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer20.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer20 { pub p: crate::testing::datatypes::PrimitiveComponent, pub s: crate::testing::datatypes::StringComponent, @@ -165,11 +166,11 @@ impl ::re_types_core::Loggable for AffixFuzzer20 { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let p = { if !arrays_by_name.contains_key("p") { return Err(DeserializationError::missing_struct_field( @@ -263,16 +264,3 @@ impl ::re_types_core::Loggable for AffixFuzzer20 { }) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer20 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.p.heap_size_bytes() + self.s.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer21.rs b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer21.rs index 1318ac725ae5..0b5a6d28dded 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer21.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer21.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer21 { pub single_half: half::f16, pub many_halves: ::arrow::buffer::ScalarBuffer, @@ -172,11 +173,11 @@ impl ::re_types_core::Loggable for AffixFuzzer21 { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let single_half = { if !arrays_by_name.contains_key("single_half") { return Err(DeserializationError::missing_struct_field( @@ -288,15 +289,3 @@ impl ::re_types_core::Loggable for AffixFuzzer21 { }) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer21 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.single_half.heap_size_bytes() + self.many_halves.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && <::arrow::buffer::ScalarBuffer>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer22.rs b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer22.rs index 4905c92dc3d8..3c1457281143 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer22.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer22.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer22 { pub fixed_sized_native: [u8; 4usize], } @@ -149,11 +150,11 @@ impl ::re_types_core::Loggable for AffixFuzzer22 { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let fixed_sized_native = { if !arrays_by_name.contains_key("fixed_sized_native") { return Err(DeserializationError::missing_struct_field( @@ -181,9 +182,10 @@ impl ::re_types_core::Loggable for AffixFuzzer22 { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(4usize) - .zip((4usize..).step_by(4usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(4usize), + (4usize..).step_by(4usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -264,15 +266,3 @@ impl From for [u8; 4usize] { value.fixed_sized_native } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer22 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.fixed_sized_native.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <[u8; 4usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer3.rs b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer3.rs index dc2d2a1317af..79744486377f 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer3.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer3.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, ::re_byte_size::SizeBytes)] pub enum AffixFuzzer3 { Degrees(f32), Craziness(Vec), @@ -381,9 +382,10 @@ impl ::re_types_core::Loggable for AffixFuzzer3 { if arrow_data.is_empty() { Vec::new() } else { - let offsets = (0..) - .step_by(3usize) - .zip((3usize..).step_by(3usize).take(arrow_data.len())); + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data.len()), + ); let arrow_data_inner = { let arrow_data_inner = &**arrow_data.values(); arrow_data_inner @@ -535,23 +537,3 @@ impl ::re_types_core::Loggable for AffixFuzzer3 { }) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer3 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - #![allow(clippy::match_same_arms)] - match self { - Self::Degrees(v) => v.heap_size_bytes(), - Self::Craziness(v) => v.heap_size_bytes(), - Self::FixedSizeShenanigans(v) => v.heap_size_bytes(), - Self::EmptyVariant => 0, - } - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && >::is_pod() - && <[f32; 3usize]>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer4.rs b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer4.rs index 81768d69de02..ff26d0af2a56 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer4.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer4.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, ::re_byte_size::SizeBytes)] pub enum AffixFuzzer4 { SingleRequired(crate::testing::datatypes::AffixFuzzer3), ManyRequired(Vec), @@ -356,20 +357,3 @@ impl ::re_types_core::Loggable for AffixFuzzer4 { }) } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer4 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - #![allow(clippy::match_same_arms)] - match self { - Self::SingleRequired(v) => v.heap_size_bytes(), - Self::ManyRequired(v) => v.heap_size_bytes(), - } - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer5.rs b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer5.rs index 680ed6a9892d..39b5830043eb 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer5.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/affix_fuzzer5.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct AffixFuzzer5 { pub single_optional_union: Option, } @@ -117,11 +118,11 @@ impl ::re_types_core::Loggable for AffixFuzzer5 { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let single_optional_union = { if !arrays_by_name.contains_key("single_optional_union") { return Err(DeserializationError::missing_struct_field( @@ -184,15 +185,3 @@ impl std::ops::DerefMut for AffixFuzzer5 { &mut self.single_optional_union } } - -impl ::re_byte_size::SizeBytes for AffixFuzzer5 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.single_optional_union.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/enum_test.rs b/crates/store/re_sdk_types/src/testing/datatypes/enum_test.rs index 97ea5be886b5..65f077e3e3f3 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/enum_test.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/enum_test.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A test of the enum type. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum EnumTest { /// Great film. @@ -171,15 +172,3 @@ impl ::re_types_core::reflection::Enum for EnumTest { .copied() } } - -impl ::re_byte_size::SizeBytes for EnumTest { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/fixed_size_enum_array.rs b/crates/store/re_sdk_types/src/testing/datatypes/fixed_size_enum_array.rs new file mode 100644 index 000000000000..d5293b9c5b0c --- /dev/null +++ b/crates/store/re_sdk_types/src/testing/datatypes/fixed_size_enum_array.rs @@ -0,0 +1,228 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Datatype**: Test datatype for fixed-size enum arrays. +#[derive(Clone, Debug, Copy, PartialEq, Eq, ::re_byte_size::SizeBytes)] +#[repr(transparent)] +pub struct FixedSizeEnumArray( + /// Fixed-size enum array. + pub [crate::testing::datatypes::EnumTest; 3usize], +); + +::re_types_core::macros::impl_into_cow!(FixedSizeEnumArray); + +impl ::re_types_core::Loggable for FixedSizeEnumArray { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + use arrow::datatypes::*; + DataType::FixedSizeList( + std::sync::Arc::new(Field::new( + "item", + ::arrow_datatype(), + false, + )), + 3, + ) + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult + where + Self: Clone + 'a, + { + #![allow(clippy::manual_is_variant_and)] + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_helpers::as_array_ref}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| datum.into_owned().0); + (datum.is_some(), datum) + }) + .unzip(); + let data0_validity: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + { + let data0_inner_data: Vec<_> = data0 + .into_iter() + .flat_map(|v| match v { + Some(v) => itertools::Either::Left(v.into_iter()), + None => { + itertools::Either::Right( + std::iter::repeat_n( + ::variants()[0], + 3usize, + ), + ) + } + }) + .collect(); + let data0_inner_validity: Option = + data0_validity.as_ref().map(|validity| { + validity + .iter() + .map(|b| std::iter::repeat_n(b, 3usize)) + .flatten() + .collect::>() + .into() + }); + as_array_ref(FixedSizeListArray::new( + std::sync::Arc::new(Field::new( + "item", + ::arrow_datatype(), + false, + )), + 3, + { + _ = data0_inner_validity; + crate::testing::datatypes::EnumTest::to_arrow_opt( + data0_inner_data.into_iter().map(Some), + )? + }, + data0_validity, + )) + } + }) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_zip_validity::ZipValidity}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok( + { + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.testing.datatypes.FixedSizeEnumArray#values")?; + if arrow_data.is_empty() { + Vec::new() + } else { + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data.len()), + ); + let arrow_data_inner = { + let arrow_data_inner = &**arrow_data.values(); + crate::testing::datatypes::EnumTest::from_arrow_opt( + arrow_data_inner, + ) + .with_context( + "rerun.testing.datatypes.FixedSizeEnumArray#values", + )? + .into_iter() + .collect::>() + }; + ZipValidity::new_with_validity(offsets, arrow_data.nulls()) + .map(|elem| { + elem + .map(|(start, end): (usize, usize)| { + re_log::debug_assert!(end - start == 3usize); + if arrow_data_inner.len() < end { + return Err( + DeserializationError::offset_slice_oob( + (start, end), + arrow_data_inner.len(), + ), + ); + } + + #[expect(unsafe_code, clippy::undocumented_unsafe_blocks)] + let data = unsafe { + arrow_data_inner.get_unchecked(start..end) + }; + if data.iter().any(Option::is_none) { + return Err(DeserializationError::missing_data()); + } + let data = data + .iter() + .cloned() + .map(|opt| { + opt + .unwrap_or_else(|| { + ::variants()[0] + }) + }); + + // NOTE: Unwrapping cannot fail: the length must be correct. + #[expect(clippy::unwrap_used)] + Ok(array_init::from_iter(data).unwrap()) + }) + .transpose() + }) + .collect::>>>()? + } + .into_iter() + } + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .map(|res| res.map(|v| Some(Self(v)))) + .collect::>>>() + .with_context("rerun.testing.datatypes.FixedSizeEnumArray#values") + .with_context("rerun.testing.datatypes.FixedSizeEnumArray")?, + ) + } +} + +impl> From for FixedSizeEnumArray { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow<[crate::testing::datatypes::EnumTest; 3usize]> for FixedSizeEnumArray { + #[inline] + fn borrow(&self) -> &[crate::testing::datatypes::EnumTest; 3usize] { + &self.0 + } +} + +impl std::ops::Deref for FixedSizeEnumArray { + type Target = [crate::testing::datatypes::EnumTest; 3usize]; + + #[inline] + fn deref(&self) -> &[crate::testing::datatypes::EnumTest; 3usize] { + &self.0 + } +} + +impl std::ops::DerefMut for FixedSizeEnumArray { + #[inline] + fn deref_mut(&mut self) -> &mut [crate::testing::datatypes::EnumTest; 3usize] { + &mut self.0 + } +} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/fixed_size_wide_enum_array.rs b/crates/store/re_sdk_types/src/testing/datatypes/fixed_size_wide_enum_array.rs new file mode 100644 index 000000000000..e6c8cc39f50a --- /dev/null +++ b/crates/store/re_sdk_types/src/testing/datatypes/fixed_size_wide_enum_array.rs @@ -0,0 +1,230 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Datatype**: Test datatype for fixed-size arrays of wide enums. +#[derive(Clone, Debug, Copy, PartialEq, Eq, ::re_byte_size::SizeBytes)] +#[repr(transparent)] +pub struct FixedSizeWideEnumArray( + /// Fixed-size wide enum array. + pub [crate::testing::datatypes::WideEnum; 2usize], +); + +::re_types_core::macros::impl_into_cow!(FixedSizeWideEnumArray); + +impl ::re_types_core::Loggable for FixedSizeWideEnumArray { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + use arrow::datatypes::*; + DataType::FixedSizeList( + std::sync::Arc::new(Field::new( + "item", + ::arrow_datatype(), + false, + )), + 2, + ) + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult + where + Self: Clone + 'a, + { + #![allow(clippy::manual_is_variant_and)] + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_helpers::as_array_ref}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| datum.into_owned().0); + (datum.is_some(), datum) + }) + .unzip(); + let data0_validity: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + { + let data0_inner_data: Vec<_> = data0 + .into_iter() + .flat_map(|v| match v { + Some(v) => itertools::Either::Left(v.into_iter()), + None => { + itertools::Either::Right( + std::iter::repeat_n( + ::variants()[0], + 2usize, + ), + ) + } + }) + .collect(); + let data0_inner_validity: Option = + data0_validity.as_ref().map(|validity| { + validity + .iter() + .map(|b| std::iter::repeat_n(b, 2usize)) + .flatten() + .collect::>() + .into() + }); + as_array_ref(FixedSizeListArray::new( + std::sync::Arc::new(Field::new( + "item", + ::arrow_datatype(), + false, + )), + 2, + { + _ = data0_inner_validity; + crate::testing::datatypes::WideEnum::to_arrow_opt( + data0_inner_data.into_iter().map(Some), + )? + }, + data0_validity, + )) + } + }) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_zip_validity::ZipValidity}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok( + { + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context( + "rerun.testing.datatypes.FixedSizeWideEnumArray#values", + )?; + if arrow_data.is_empty() { + Vec::new() + } else { + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data.len()), + ); + let arrow_data_inner = { + let arrow_data_inner = &**arrow_data.values(); + crate::testing::datatypes::WideEnum::from_arrow_opt( + arrow_data_inner, + ) + .with_context( + "rerun.testing.datatypes.FixedSizeWideEnumArray#values", + )? + .into_iter() + .collect::>() + }; + ZipValidity::new_with_validity(offsets, arrow_data.nulls()) + .map(|elem| { + elem + .map(|(start, end): (usize, usize)| { + re_log::debug_assert!(end - start == 2usize); + if arrow_data_inner.len() < end { + return Err( + DeserializationError::offset_slice_oob( + (start, end), + arrow_data_inner.len(), + ), + ); + } + + #[expect(unsafe_code, clippy::undocumented_unsafe_blocks)] + let data = unsafe { + arrow_data_inner.get_unchecked(start..end) + }; + if data.iter().any(Option::is_none) { + return Err(DeserializationError::missing_data()); + } + let data = data + .iter() + .cloned() + .map(|opt| { + opt + .unwrap_or_else(|| { + ::variants()[0] + }) + }); + + // NOTE: Unwrapping cannot fail: the length must be correct. + #[expect(clippy::unwrap_used)] + Ok(array_init::from_iter(data).unwrap()) + }) + .transpose() + }) + .collect::>>>()? + } + .into_iter() + } + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .map(|res| res.map(|v| Some(Self(v)))) + .collect::>>>() + .with_context("rerun.testing.datatypes.FixedSizeWideEnumArray#values") + .with_context("rerun.testing.datatypes.FixedSizeWideEnumArray")?, + ) + } +} + +impl> From for FixedSizeWideEnumArray { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow<[crate::testing::datatypes::WideEnum; 2usize]> for FixedSizeWideEnumArray { + #[inline] + fn borrow(&self) -> &[crate::testing::datatypes::WideEnum; 2usize] { + &self.0 + } +} + +impl std::ops::Deref for FixedSizeWideEnumArray { + type Target = [crate::testing::datatypes::WideEnum; 2usize]; + + #[inline] + fn deref(&self) -> &[crate::testing::datatypes::WideEnum; 2usize] { + &self.0 + } +} + +impl std::ops::DerefMut for FixedSizeWideEnumArray { + #[inline] + fn deref_mut(&mut self) -> &mut [crate::testing::datatypes::WideEnum; 2usize] { + &mut self.0 + } +} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/flattened_scalar.rs b/crates/store/re_sdk_types/src/testing/datatypes/flattened_scalar.rs index ddb357ca18d4..0a9265600989 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/flattened_scalar.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/flattened_scalar.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, ::re_byte_size::SizeBytes)] pub struct FlattenedScalar { pub value: f32, } @@ -113,11 +114,11 @@ impl ::re_types_core::Loggable for FlattenedScalar { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let value = { if !arrays_by_name.contains_key("value") { return Err(DeserializationError::missing_struct_field( @@ -171,15 +172,3 @@ impl From for f32 { value.value } } - -impl ::re_byte_size::SizeBytes for FlattenedScalar { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.value.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/many_vec3.rs b/crates/store/re_sdk_types/src/testing/datatypes/many_vec3.rs new file mode 100644 index 000000000000..e7127d75e394 --- /dev/null +++ b/crates/store/re_sdk_types/src/testing/datatypes/many_vec3.rs @@ -0,0 +1,344 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/datatypes/fuzzy.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Datatype**: A fixed-size array of arrays — exercises nested fixed-size lists in Arrow. +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] +#[repr(C)] +pub struct ManyVec3(pub [[f32; 3usize]; 2usize]); + +::re_types_core::macros::impl_into_cow!(ManyVec3); + +impl ::re_types_core::Loggable for ManyVec3 { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + use arrow::datatypes::*; + DataType::FixedSizeList( + std::sync::Arc::new(Field::new( + "item", + DataType::FixedSizeList( + std::sync::Arc::new(Field::new("item", DataType::Float32, false)), + 3, + ), + false, + )), + 2, + ) + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult + where + Self: Clone + 'a, + { + #![allow(clippy::manual_is_variant_and)] + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_helpers::as_array_ref}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| datum.into_owned().0); + (datum.is_some(), datum) + }) + .unzip(); + let data0_validity: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + { + let data0_inner_data: Vec<_> = data0 + .into_iter() + .flat_map(|v| match v { + Some(v) => itertools::Either::Left(v.into_iter()), + None => itertools::Either::Right(std::iter::repeat_n( + Default::default(), + 2usize, + )), + }) + .collect(); + let data0_inner_validity: Option = + data0_validity.as_ref().map(|validity| { + validity + .iter() + .map(|b| std::iter::repeat_n(b, 2usize)) + .flatten() + .collect::>() + .into() + }); + as_array_ref(FixedSizeListArray::new( + std::sync::Arc::new(Field::new( + "item", + DataType::FixedSizeList( + std::sync::Arc::new(Field::new("item", DataType::Float32, false)), + 3, + ), + false, + )), + 2, + { + let data0_inner_data_inner_data: Vec<_> = + data0_inner_data.into_iter().flatten().collect(); + let data0_inner_data_inner_validity: Option = + None; + as_array_ref(FixedSizeListArray::new( + std::sync::Arc::new(Field::new("item", DataType::Float32, false)), + 3, + as_array_ref(PrimitiveArray::::new( + ScalarBuffer::from( + data0_inner_data_inner_data.into_iter().collect::>(), + ), + data0_inner_data_inner_validity, + )), + data0_inner_validity, + )) + }, + data0_validity, + )) + } + }) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_zip_validity::ZipValidity}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok({ + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.testing.datatypes.ManyVec3#triples")?; + if arrow_data.is_empty() { + Vec::new() + } else { + let offsets = ::std::iter::zip( + (0..).step_by(2usize), + (2usize..).step_by(2usize).take(arrow_data.len()), + ); + let arrow_data_inner = { + let arrow_data_inner = &**arrow_data.values(); + { + let arrow_data_inner = arrow_data_inner + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::FixedSizeList( + std::sync::Arc::new(Field::new( + "item", + DataType::Float32, + false, + )), + 3, + ); + let actual = arrow_data_inner.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.testing.datatypes.ManyVec3#triples")?; + if arrow_data_inner.is_empty() { + Vec::new() + } else { + let offsets = ::std::iter::zip( + (0..).step_by(3usize), + (3usize..).step_by(3usize).take(arrow_data_inner.len()), + ); + let arrow_data_inner_inner = { + let arrow_data_inner_inner = &**arrow_data_inner.values(); + arrow_data_inner_inner + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::Float32; + let actual = arrow_data_inner_inner.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.testing.datatypes.ManyVec3#triples")? + .into_iter() + .collect::>() + }; + ZipValidity::new_with_validity(offsets, arrow_data_inner.nulls()) + .map(|elem| { + elem.map(|(start, end): (usize, usize)| { + re_log::debug_assert!(end - start == 3usize); + if arrow_data_inner_inner.len() < end { + return Err(DeserializationError::offset_slice_oob( + (start, end), + arrow_data_inner_inner.len(), + )); + } + + #[expect(unsafe_code, clippy::undocumented_unsafe_blocks)] + let data = unsafe { + arrow_data_inner_inner.get_unchecked(start..end) + }; + let data = + data.iter().cloned().map(Option::unwrap_or_default); + + // NOTE: Unwrapping cannot fail: the length must be correct. + #[expect(clippy::unwrap_used)] + Ok(array_init::from_iter(data).unwrap()) + }) + .transpose() + }) + .collect::>>>()? + } + .into_iter() + } + .collect::>() + }; + ZipValidity::new_with_validity(offsets, arrow_data.nulls()) + .map(|elem| { + elem.map(|(start, end): (usize, usize)| { + re_log::debug_assert!(end - start == 2usize); + if arrow_data_inner.len() < end { + return Err(DeserializationError::offset_slice_oob( + (start, end), + arrow_data_inner.len(), + )); + } + + #[expect(unsafe_code, clippy::undocumented_unsafe_blocks)] + let data = unsafe { arrow_data_inner.get_unchecked(start..end) }; + let data = data.iter().cloned().map(Option::unwrap_or_default); + + // NOTE: Unwrapping cannot fail: the length must be correct. + #[expect(clippy::unwrap_used)] + Ok(array_init::from_iter(data).unwrap()) + }) + .transpose() + }) + .collect::>>>()? + } + .into_iter() + } + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .map(|res| res.map(|v| Some(Self(v)))) + .collect::>>>() + .with_context("rerun.testing.datatypes.ManyVec3#triples") + .with_context("rerun.testing.datatypes.ManyVec3")?) + } + + #[inline] + fn from_arrow(arrow_data: &dyn arrow::array::Array) -> DeserializationResult> + where + Self: Sized, + { + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_zip_validity::ZipValidity}; + use arrow::{array::*, buffer::*, datatypes::*}; + if let Some(nulls) = arrow_data.nulls() + && nulls.null_count() != 0 + { + return Err(DeserializationError::missing_data()); + } + Ok({ + let slice = { + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::FixedSizeList( + std::sync::Arc::new(Field::new( + "item", + DataType::FixedSizeList( + std::sync::Arc::new(Field::new( + "item", + DataType::Float32, + false, + )), + 3, + ), + false, + )), + 2, + ); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.testing.datatypes.ManyVec3#triples")?; + let arrow_data_inner = &**arrow_data.values(); + bytemuck::cast_slice::<_, [[f32; 3usize]; 2usize]>({ + let arrow_data_inner = arrow_data_inner + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::FixedSizeList( + std::sync::Arc::new(Field::new("item", DataType::Float32, false)), + 3, + ); + let actual = arrow_data_inner.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.testing.datatypes.ManyVec3#triples")?; + let arrow_data_inner_inner = &**arrow_data_inner.values(); + bytemuck::cast_slice::<_, [f32; 3usize]>( + arrow_data_inner_inner + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::Float32; + let actual = arrow_data_inner_inner.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.testing.datatypes.ManyVec3#triples")? + .values() + .as_ref(), + ) + }) + }; + { slice.iter().copied().map(Self).collect::>() } + }) + } +} + +impl From<[[f32; 3usize]; 2usize]> for ManyVec3 { + #[inline] + fn from(triples: [[f32; 3usize]; 2usize]) -> Self { + Self(triples) + } +} + +impl From for [[f32; 3usize]; 2usize] { + #[inline] + fn from(value: ManyVec3) -> Self { + value.0 + } +} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/mod.rs b/crates/store/re_sdk_types/src/testing/datatypes/mod.rs index c00498e0e1ec..9209d244bb1b 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/mod.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/mod.rs @@ -11,11 +11,15 @@ mod affix_fuzzer4; mod affix_fuzzer4_ext; mod affix_fuzzer5; mod enum_test; +mod fixed_size_enum_array; +mod fixed_size_wide_enum_array; mod flattened_scalar; +mod many_vec3; mod multi_enum; mod primitive_component; mod string_component; mod valued_enum; +mod wide_enum; pub use self::affix_fuzzer1::AffixFuzzer1; pub use self::affix_fuzzer2::AffixFuzzer2; @@ -26,8 +30,12 @@ pub use self::affix_fuzzer20::AffixFuzzer20; pub use self::affix_fuzzer21::AffixFuzzer21; pub use self::affix_fuzzer22::AffixFuzzer22; pub use self::enum_test::EnumTest; +pub use self::fixed_size_enum_array::FixedSizeEnumArray; +pub use self::fixed_size_wide_enum_array::FixedSizeWideEnumArray; pub use self::flattened_scalar::FlattenedScalar; +pub use self::many_vec3::ManyVec3; pub use self::multi_enum::MultiEnum; pub use self::primitive_component::PrimitiveComponent; pub use self::string_component::StringComponent; pub use self::valued_enum::ValuedEnum; +pub use self::wide_enum::WideEnum; diff --git a/crates/store/re_sdk_types/src/testing/datatypes/multi_enum.rs b/crates/store/re_sdk_types/src/testing/datatypes/multi_enum.rs index 1bac12b2bf29..a27eebbe1d0b 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/multi_enum.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/multi_enum.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct MultiEnum { /// The first value. pub value1: crate::testing::datatypes::EnumTest, @@ -150,11 +151,11 @@ impl ::re_types_core::Loggable for MultiEnum { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let value1 = { if !arrays_by_name.contains_key("value1") { return Err(DeserializationError::missing_struct_field( @@ -202,16 +203,3 @@ impl ::re_types_core::Loggable for MultiEnum { }) } } - -impl ::re_byte_size::SizeBytes for MultiEnum { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.value1.heap_size_bytes() + self.value2.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && >::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/primitive_component.rs b/crates/store/re_sdk_types/src/testing/datatypes/primitive_component.rs index 956471c53568..e60237460781 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/primitive_component.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/primitive_component.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct PrimitiveComponent(pub u32); @@ -135,15 +136,3 @@ impl From for u32 { value.0 } } - -impl ::re_byte_size::SizeBytes for PrimitiveComponent { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/string_component.rs b/crates/store/re_sdk_types/src/testing/datatypes/string_component.rs index 0db475b67b46..1238fdb88169 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/string_component.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/string_component.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -21,7 +22,7 @@ use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct StringComponent(pub ::re_types_core::ArrowString); @@ -145,15 +146,3 @@ impl From for ::re_types_core::ArrowString { value.0 } } - -impl ::re_byte_size::SizeBytes for StringComponent { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - <::re_types_core::ArrowString>::is_pod() - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/valued_enum.rs b/crates/store/re_sdk_types/src/testing/datatypes/valued_enum.rs index 154be20eca15..3f3e4ef8adc9 100644 --- a/crates/store/re_sdk_types/src/testing/datatypes/valued_enum.rs +++ b/crates/store/re_sdk_types/src/testing/datatypes/valued_enum.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -23,7 +24,7 @@ use ::re_types_core::{ComponentDescriptor, ComponentType}; use ::re_types_core::{DeserializationError, DeserializationResult}; /// **Datatype**: A test of an enumerate with specified values. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, ::re_byte_size::SizeBytes)] #[repr(u8)] pub enum ValuedEnum { /// One. @@ -157,15 +158,3 @@ impl ::re_types_core::reflection::Enum for ValuedEnum { } } } - -impl ::re_byte_size::SizeBytes for ValuedEnum { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} diff --git a/crates/store/re_sdk_types/src/testing/datatypes/wide_enum.rs b/crates/store/re_sdk_types/src/testing/datatypes/wide_enum.rs new file mode 100644 index 000000000000..124ab37bff94 --- /dev/null +++ b/crates/store/re_sdk_types/src/testing/datatypes/wide_enum.rs @@ -0,0 +1,148 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::allow_attributes)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::wildcard_imports)] +#![allow(non_camel_case_types)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Datatype**: A test enum with values that require more than one byte. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, ::re_byte_size::SizeBytes)] +#[repr(u32)] +pub enum WideEnum { + /// Low value. + Low = 0x1, + + /// High value. + High = 0x10000, +} + +::re_types_core::macros::impl_into_cow!(WideEnum); + +impl ::re_types_core::Loggable for WideEnum { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + use arrow::datatypes::*; + DataType::UInt32 + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult + where + Self: Clone + 'a, + { + #![allow(clippy::manual_is_variant_and)] + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_helpers::as_array_ref}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| *datum as u32); + (datum.is_some(), datum) + }) + .unzip(); + let data0_validity: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + as_array_ref(PrimitiveArray::::new( + ScalarBuffer::from( + data0 + .into_iter() + .map(|v| v.unwrap_or_default()) + .collect::>(), + ), + data0_validity, + )) + }) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + use ::re_types_core::{Loggable as _, ResultExt as _, arrow_zip_validity::ZipValidity}; + use arrow::{array::*, buffer::*, datatypes::*}; + Ok(arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.testing.datatypes.WideEnum#enum")? + .into_iter() + .map(|typ| match typ { + Some(val) => ::try_from_integer(val) + .map(Some) + .ok_or_else(|| { + DeserializationError::missing_union_arm( + Self::arrow_datatype(), + "", + val as _, + ) + }), + None => Ok(None), + }) + .collect::>>>() + .with_context("rerun.testing.datatypes.WideEnum")?) + } +} + +impl std::fmt::Display for WideEnum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Low => write!(f, "Low"), + Self::High => write!(f, "High"), + } + } +} + +impl ::re_types_core::reflection::Enum for WideEnum { + type Repr = u32; + + #[inline] + fn variants() -> &'static [Self] { + &[Self::Low, Self::High] + } + + #[inline] + fn docstring_md(self) -> &'static str { + match self { + Self::Low => "Low value.", + Self::High => "High value.", + } + } + + #[inline] + fn try_from_integer(value: u32) -> Option { + match value { + 0x1 => Some(Self::Low), + 0x10000 => Some(Self::High), + _ => None, + } + } +} diff --git a/crates/store/re_sdk_types/src/transform_frame_id_hash.rs b/crates/store/re_sdk_types/src/transform_frame_id_hash.rs index 4d1cfc0eaa1d..4a3e874d72a7 100644 --- a/crates/store/re_sdk_types/src/transform_frame_id_hash.rs +++ b/crates/store/re_sdk_types/src/transform_frame_id_hash.rs @@ -17,16 +17,9 @@ use crate::components::TransformFrameId; /// /// There's no `Into` conversions for entity paths in order to keep these conversions explicit, /// marking clearly where we retrieve the implicit frame id's of an entity path. -#[derive(Copy, Clone, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Eq, PartialOrd, Ord, re_byte_size::SizeBytes)] pub struct TransformFrameIdHash(Hash64); -impl re_byte_size::SizeBytes for TransformFrameIdHash { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } -} - impl std::hash::Hash for TransformFrameIdHash { #[inline] fn hash(&self, state: &mut H) { diff --git a/crates/store/re_sdk_types/src/view_coordinates.rs b/crates/store/re_sdk_types/src/view_coordinates.rs index 844736225915..6ef7c0510e55 100644 --- a/crates/store/re_sdk_types/src/view_coordinates.rs +++ b/crates/store/re_sdk_types/src/view_coordinates.rs @@ -85,8 +85,7 @@ impl ViewDir { } /// One of `X`, `Y`, `Z`. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub enum Axis3 { X, Y, diff --git a/crates/store/re_sdk_types/tests/types/clear.rs b/crates/store/re_sdk_types/tests/types/clear.rs index 5fce7677672a..cf63143e687f 100644 --- a/crates/store/re_sdk_types/tests/types/clear.rs +++ b/crates/store/re_sdk_types/tests/types/clear.rs @@ -19,7 +19,7 @@ fn roundtrip() { Clear::flat(), // ]; - for (expected, arch) in all_expected.into_iter().zip(all_arch) { + for (expected, arch) in std::iter::zip(all_expected, all_arch) { similar_asserts::assert_eq!(expected, arch); eprintln!("arch = {arch:#?}"); diff --git a/crates/store/re_sdk_types/tests/types/depth_image.rs b/crates/store/re_sdk_types/tests/types/depth_image.rs index 28e89814d74b..ab02ce7d8279 100644 --- a/crates/store/re_sdk_types/tests/types/depth_image.rs +++ b/crates/store/re_sdk_types/tests/types/depth_image.rs @@ -33,7 +33,7 @@ fn depth_image_roundtrip() { .unwrap(), ]; - for (expected, serialized) in all_expected.into_iter().zip(all_arch_serialized) { + for (expected, serialized) in std::iter::zip(all_expected, all_arch_serialized) { for (field, array) in &serialized { // NOTE: Keep those around please, very useful when debugging. // eprintln!("field = {field:#?}"); diff --git a/crates/store/re_sdk_types/tests/types/fixed_size_enum_array.rs b/crates/store/re_sdk_types/tests/types/fixed_size_enum_array.rs new file mode 100644 index 000000000000..ad824e7fcd1a --- /dev/null +++ b/crates/store/re_sdk_types/tests/types/fixed_size_enum_array.rs @@ -0,0 +1,12 @@ +use re_sdk_types::Loggable as _; +use re_sdk_types::testing::datatypes::{EnumTest, FixedSizeEnumArray}; + +#[test] +fn roundtrip() { + let values = FixedSizeEnumArray([EnumTest::Right, EnumTest::Down, EnumTest::Forward]); + + let arrow = FixedSizeEnumArray::to_arrow_opt([Some(values)]).unwrap(); + let roundtrip = FixedSizeEnumArray::from_arrow_opt(&*arrow).unwrap(); + + similar_asserts::assert_eq!(vec![Some(values)], roundtrip); +} diff --git a/crates/store/re_sdk_types/tests/types/fuzzy.rs b/crates/store/re_sdk_types/tests/types/fuzzy.rs index 1daba0797199..d16f25cdab31 100644 --- a/crates/store/re_sdk_types/tests/types/fuzzy.rs +++ b/crates/store/re_sdk_types/tests/types/fuzzy.rs @@ -330,3 +330,40 @@ fn roundtrip() { similar_asserts::assert_eq!(arch, deserialized); } } + +/// Fixed-size array of structs, i.e. a nested Arrow `FixedSizeList, 2>`. +#[test] +fn roundtrip_fixed_size_array_of_structs() { + use re_types_core::Loggable as _; + + let expected_datatype = arrow::datatypes::DataType::FixedSizeList( + std::sync::Arc::new(arrow::datatypes::Field::new( + "item", + arrow::datatypes::DataType::FixedSizeList( + std::sync::Arc::new(arrow::datatypes::Field::new( + "item", + arrow::datatypes::DataType::Float32, + false, + )), + 3, + ), + false, + )), + 2, + ); + // Slightly more readable than the above: + assert_eq!( + expected_datatype.to_string(), + "FixedSizeList(2 x non-null FixedSizeList(3 x non-null Float32))", + ); + assert_eq!(datatypes::ManyVec3::arrow_datatype(), expected_datatype); + + let data = vec![ + components::ManyVec3(datatypes::ManyVec3([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])), + components::ManyVec3(datatypes::ManyVec3([[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]])), + ]; + + let serialized = components::ManyVec3::to_arrow(data.clone()).unwrap(); + let deserialized = components::ManyVec3::from_arrow(serialized.as_ref()).unwrap(); + similar_asserts::assert_eq!(data, deserialized); +} diff --git a/crates/store/re_sdk_types/tests/types/image.rs b/crates/store/re_sdk_types/tests/types/image.rs index 99372555a452..ef31c23a1f17 100644 --- a/crates/store/re_sdk_types/tests/types/image.rs +++ b/crates/store/re_sdk_types/tests/types/image.rs @@ -12,7 +12,7 @@ fn image_roundtrip() { .to_arrow() .unwrap()]; - for (expected, serialized) in all_expected.into_iter().zip(all_arch_serialized) { + for (expected, serialized) in std::iter::zip(all_expected, all_arch_serialized) { for (field, array) in &serialized { // NOTE: Keep those around please, very useful when debugging. // eprintln!("field = {field:#?}"); @@ -48,7 +48,7 @@ fn dynamic_image_roundtrip() { let all_arch_serialized = [Image::from_image(img).unwrap().to_arrow().unwrap()]; - for (expected, serialized) in all_expected.into_iter().zip(all_arch_serialized) { + for (expected, serialized) in std::iter::zip(all_expected, all_arch_serialized) { for (field, array) in &serialized { // NOTE: Keep those around please, very useful when debugging. // eprintln!("field = {field:#?}"); diff --git a/crates/store/re_sdk_types/tests/types/main.rs b/crates/store/re_sdk_types/tests/types/main.rs index 57931926ee57..da7fbb7d634f 100644 --- a/crates/store/re_sdk_types/tests/types/main.rs +++ b/crates/store/re_sdk_types/tests/types/main.rs @@ -10,6 +10,8 @@ mod box3d; mod clear; mod depth_image; mod dynamic_archetype; +#[cfg(feature = "testing")] +mod fixed_size_enum_array; mod image; mod line_strips2d; mod line_strips3d; @@ -21,6 +23,7 @@ mod tensor; mod text_document; mod transform3d; mod view_coordinates; +mod voxel_grid_map; // Tests of other things diff --git a/crates/store/re_sdk_types/tests/types/points3d.rs b/crates/store/re_sdk_types/tests/types/points3d.rs index 83378aa2e385..b6143db32115 100644 --- a/crates/store/re_sdk_types/tests/types/points3d.rs +++ b/crates/store/re_sdk_types/tests/types/points3d.rs @@ -33,6 +33,8 @@ fn roundtrip() { .serialized(Points3D::descriptor_keypoint_ids()), show_labels: components::ShowLabels(true.into()) .serialized(Points3D::descriptor_show_labels()), + point_shading: components::PointShading::Gradient + .serialized(Points3D::descriptor_point_shading()), }; let arch = Points3D::new([(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)]) @@ -41,7 +43,8 @@ fn roundtrip() { .with_labels(["hello", "friend"]) .with_class_ids([126, 127]) .with_keypoint_ids([2, 3]) - .with_show_labels(true); + .with_show_labels(true) + .with_point_shading(components::PointShading::Gradient); similar_asserts::assert_eq!(expected, arch); eprintln!("arch = {arch:#?}"); diff --git a/crates/store/re_sdk_types/tests/types/segmentation_image.rs b/crates/store/re_sdk_types/tests/types/segmentation_image.rs index 8d23590e9cc2..a98ac0a3bcda 100644 --- a/crates/store/re_sdk_types/tests/types/segmentation_image.rs +++ b/crates/store/re_sdk_types/tests/types/segmentation_image.rs @@ -29,7 +29,7 @@ fn segmentation_image_roundtrip() { .to_arrow() .unwrap()]; - for (expected, serialized) in all_expected.into_iter().zip(all_arch_serialized) { + for (expected, serialized) in std::iter::zip(all_expected, all_arch_serialized) { for (field, array) in &serialized { // NOTE: Keep those around please, very useful when debugging. // eprintln!("field = {field:#?}"); diff --git a/crates/store/re_sdk_types/tests/types/tensor.rs b/crates/store/re_sdk_types/tests/types/tensor.rs index dec248b02acc..5ec3778a70ea 100644 --- a/crates/store/re_sdk_types/tests/types/tensor.rs +++ b/crates/store/re_sdk_types/tests/types/tensor.rs @@ -33,7 +33,7 @@ fn tensor_roundtrip() { .to_arrow() .unwrap()]; - for (expected, serialized) in all_expected.into_iter().zip(all_arch_serialized) { + for (expected, serialized) in std::iter::zip(all_expected, all_arch_serialized) { for (field, array) in &serialized { // NOTE: Keep those around please, very useful when debugging. // eprintln!("field = {field:#?}"); diff --git a/crates/store/re_sdk_types/tests/types/transform3d.rs b/crates/store/re_sdk_types/tests/types/transform3d.rs index b120f2c6a0d8..517de9c0e830 100644 --- a/crates/store/re_sdk_types/tests/types/transform3d.rs +++ b/crates/store/re_sdk_types/tests/types/transform3d.rs @@ -75,7 +75,7 @@ fn roundtrip() { .with_relation(TransformRelation::ParentFromChild), ]; - for (expected, arch) in all_expected.into_iter().zip(all_arch) { + for (expected, arch) in std::iter::zip(all_expected, all_arch) { similar_asserts::assert_eq!(expected, arch); eprintln!("arch = {arch:#?}"); diff --git a/crates/store/re_sdk_types/tests/types/voxel_grid_map.rs b/crates/store/re_sdk_types/tests/types/voxel_grid_map.rs new file mode 100644 index 000000000000..f543b5e1ffbb --- /dev/null +++ b/crates/store/re_sdk_types/tests/types/voxel_grid_map.rs @@ -0,0 +1,62 @@ +use re_sdk_types::archetypes::VoxelGridMap; +use re_sdk_types::{Archetype as _, AsComponents as _, ComponentBatch as _, components, datatypes}; + +#[test] +fn roundtrip() { + let expected = VoxelGridMap { + voxel_indices: vec![ + components::VoxelIndex::from([-1, 0, 2]), + components::VoxelIndex::from([3, 4, 5]), + ] + .serialized(VoxelGridMap::descriptor_voxel_indices()), + voxel_size: components::VoxelSize::from([0.25, 0.5, 0.75]) + .serialized(VoxelGridMap::descriptor_voxel_size()), + values: vec![ + components::VoxelValue::from(0.1), + components::VoxelValue::from(0.9), + ] + .serialized(VoxelGridMap::descriptor_values()), + colors: vec![ + components::Color::from_unmultiplied_rgba(0xAA, 0x00, 0x00, 0xCC), + components::Color::from_unmultiplied_rgba(0x00, 0xBB, 0x00, 0xDD), + ] + .serialized(VoxelGridMap::descriptor_colors()), + translation: components::Translation3D::new(1.0, 2.0, 3.0) + .serialized(VoxelGridMap::descriptor_translation()), + rotation_axis_angle: vec![components::RotationAxisAngle::new( + [1.0, 0.0, 0.0], + datatypes::Angle::from_radians(0.5), + )] + .serialized(VoxelGridMap::descriptor_rotation_axis_angle()), + quaternion: vec![components::RotationQuat::from( + datatypes::Quaternion::from_xyzw([0.0, 0.0, 0.0, 1.0]), + )] + .serialized(VoxelGridMap::descriptor_quaternion()), + opacity: components::Opacity::from(0.5).serialized(VoxelGridMap::descriptor_opacity()), + value_range: components::ValueRange::from([0.0, 1.0]) + .serialized(VoxelGridMap::descriptor_value_range()), + colormap: components::Colormap::Turbo.serialized(VoxelGridMap::descriptor_colormap()), + }; + + let arch = VoxelGridMap::new([(-1, 0, 2), (3, 4, 5)], [0.25, 0.5, 0.75]) + .with_values([0.1, 0.9]) + .with_colors([0xAA0000CC, 0x00BB00DD]) + .with_translation([1.0, 2.0, 3.0]) + .with_rotation_axis_angle(datatypes::RotationAxisAngle::new( + [1.0, 0.0, 0.0], + datatypes::Angle::from_radians(0.5), + )) + .with_quaternion(datatypes::Quaternion::from_xyzw([0.0, 0.0, 0.0, 1.0])) + .with_opacity(0.5) + .with_value_range([0.0, 1.0]) + .with_colormap(components::Colormap::Turbo); + similar_asserts::assert_eq!(expected, arch); + + let serialized = arch.to_arrow().unwrap(); + for (field, array) in &serialized { + eprintln!("{} = {array:#?}", field.name()); + } + + let deserialized = VoxelGridMap::from_arrow(serialized).unwrap(); + similar_asserts::assert_eq!(expected, deserialized); +} diff --git a/crates/store/re_server/Cargo.toml b/crates/store/re_server/Cargo.toml index 6de283920f4a..2a0449f6e9c0 100644 --- a/crates/store/re_server/Cargo.toml +++ b/crates/store/re_server/Cargo.toml @@ -20,12 +20,19 @@ workspace = true all-features = true +[package.metadata.cargo-shear] +ignored = [ + "chrono", # enables wasm support for transitive chrono users + "getrandom", # enables the wasm backend selected by CI RUSTFLAGS +] + + [features] default = [] # TODO(lancedb/lance#3073): lance depends on system protoc, so lance must be an opt-in dependency -## Enable reading in LanceDB files. -lance = ["dep:lance", "dep:lance-index", "dep:lance-linalg"] +## Enable reading in LanceDB files. Required for tables. +lance = ["dep:lance"] [dependencies] @@ -35,62 +42,98 @@ re_build_info.workspace = true re_byte_size.workspace = true re_chunk_store.workspace = true re_entity_db.workspace = true -re_format.workspace = true -re_grpc_server.workspace = true re_log = { workspace = true, features = ["setup"] } -re_log_encoding.workspace = true +re_log_encoding = { workspace = true, features = ["decoder"] } re_log_types.workspace = true re_protos.workspace = true re_sorbet.workspace = true re_span.workspace = true -re_tuid = { workspace = true, features = ["serde"] } +re_tuid.workspace = true re_types_core.workspace = true # External ahash.workspace = true anyhow.workspace = true arrow.workspace = true -axum.workspace = true bincode.workspace = true -bytes.workspace = true -cfg-if.workspace = true -clap = { workspace = true, features = ["derive", "env"] } datafusion.workspace = true futures.workspace = true -http.workspace = true -http-body.workspace = true itertools.workspace = true jiff.workspace = true nohash-hasher.workspace = true -opentelemetry.workspace = true parking_lot.workspace = true serde.workspace = true -tempfile.workspace = true thiserror.workspace = true -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } -tokio-stream.workspace = true -tokio-util.workspace = true -tonic-web.workspace = true +tokio = { workspace = true, features = ["sync"] } +tokio-util = { workspace = true, features = ["compat"] } tonic.workspace = true -tower.workspace = true -tower-service.workspace = true -tracing.workspace = true url.workspace = true +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +re_format.workspace = true +re_grpc_server.workspace = true -# Optional dependencies: -# TODO(lancedb/lance#3073): lance depends on system protoc, so lance must be an opt-in dependency +axum.workspace = true +bytes.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +http.workspace = true +http-body.workspace = true lance = { workspace = true, optional = true } -lance-index = { workspace = true, optional = true } -lance-linalg = { workspace = true, optional = true } +opentelemetry.workspace = true +tempfile.workspace = true +tokio = { workspace = true, features = [ + "fs", + "macros", + "net", + "rt-multi-thread", + "signal", + "sync", + "time", +] } +tokio-stream = { workspace = true, features = ["net"] } +tonic-web.workspace = true +tower.workspace = true +tower-service.workspace = true +tracing.workspace = true +[target.'cfg(target_arch = "wasm32")'.dependencies] +chrono = { workspace = true, features = ["wasmbind"] } +getrandom = { workspace = true, features = ["wasm_js"] } +js-sys.workspace = true +percent-encoding.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +web-sys = { workspace = true, features = [ + "Blob", + "DomException", + "File", + "FileSystemDirectoryHandle", + "FileSystemFileHandle", + "FileSystemGetDirectoryOptions", + "FileSystemGetFileOptions", + "FileSystemRemoveOptions", + "FileSystemWritableFileStream", + "Navigator", + "StorageManager", + "Window", + "WritableStream", +] } [dev-dependencies] +re_chunk.workspace = true re_chunk_store.workspace = true -re_redap_tests.workspace = true +# `encoder` (decoder comes transitively via re_chunk_store) lets tests author RRDs +# with and without footers to cross-check store enumeration vs lazy loading. +re_log_encoding = { workspace = true, features = ["encoder"] } re_tuid.workspace = true ehttp = { workspace = true, features = ["native-async"] } +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +re_redap_tests.workspace = true +tempfile.workspace = true [build-dependencies] re_build_tools.workspace = true diff --git a/crates/store/re_server/README.md b/crates/store/re_server/README.md index c075c6b5ed3b..6ba6d34dc774 100644 --- a/crates/store/re_server/README.md +++ b/crates/store/re_server/README.md @@ -9,7 +9,7 @@ Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. In-memory opensource implementation of the Rerun server. -The goal for this crate is to support most of the same gRPC endpoints that our commercial Rerun Cloud service supports, but do so in-memory for maximum simplicity. +The goal for this crate is to support most of the same gRPC endpoints that our commercial Rerun Hub service supports, but do so in-memory for maximum simplicity. We use this internally for testing, but in the future it might be useful for users too. diff --git a/crates/store/re_server/src/chunk_index/index.rs b/crates/store/re_server/src/chunk_index/index.rs deleted file mode 100644 index 82560c7fd333..000000000000 --- a/crates/store/re_server/src/chunk_index/index.rs +++ /dev/null @@ -1,524 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use arrow::array::{ - Array, ArrayRef, DictionaryArray, FixedSizeBinaryArray, Int64Array, Int64BufferBuilder, - RecordBatch, RecordBatchIterator, StringArray, UInt32Array, UInt32BufferBuilder, -}; -use arrow::buffer::ScalarBuffer; -use arrow::datatypes::{DataType, Field, Schema}; -use arrow::error::ArrowError; -use lance::deps::arrow_array::UInt8Array; -use lance_index::DatasetIndexExt as _; -use re_chunk_store::Chunk; -use re_log_types::{ComponentPath, EntityPath, TimelineName}; -use re_protos::cloud::v1alpha1::ext::{IndexConfig, IndexProperties}; -use re_protos::common::v1alpha1::ext::SegmentId; -use re_types_core::ComponentIdentifier; - -use crate::chunk_index::{ - ArcCell, FIELD_CHUNK_ID, FIELD_INSTANCE, FIELD_INSTANCE_ID, FIELD_RERUN_SEGMENT_ID, - FIELD_RERUN_SEGMENT_LAYER, FIELD_TIMEPOINT, -}; -use crate::store::{Dataset, Error as StoreError}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum IndexType { - Inverted, - VectorIvfPq, - BTree, -} - -/// Arrow types for indexed data coming from a chunk. -pub struct IndexDataTypes { - pub instances: DataType, - pub timepoints: DataType, -} - -impl From<&IndexProperties> for IndexType { - fn from(properties: &IndexProperties) -> Self { - match properties { - IndexProperties::Inverted { .. } => Self::Inverted, - IndexProperties::VectorIvfPq { .. } => Self::VectorIvfPq, - IndexProperties::Btree => Self::BTree, - } - } -} - -impl super::Index { - /// Store chunks in the index. - pub async fn store_chunks( - &self, - chunks: Vec<(SegmentId, String, Arc)>, - checkout_latest: bool, - ) -> Result<(), StoreError> { - let index_type: IndexType = (&self.config.properties).into(); - let timeline = self.config.time_index; - let component = self.config.column.descriptor.component; - - let batches = chunks - .into_iter() - .filter_map(move |(segment_id, layer, chunk)| { - Self::prepare_record_batch( - index_type, - &segment_id, - layer, - timeline, - component, - &chunk, - ) - .transpose() - }); - - let mut lance: lance::Dataset = self.lance_dataset.cloned(); - let mut iter = batches.peekable(); - - // Expect the first batch to be successfully prepared to get its schema. - if let Some(Ok(first)) = iter.peek() { - let schema = first.schema(); - lance - .append( - RecordBatchIterator::new(iter, schema), - Some(Default::default()), - ) - .await?; - - // TODO(swallez) we should call optimize_indices and compact_files sometimes. - // We can either do it every X insertions or use a debouncer to enforce a max frequency. - - if checkout_latest { - lance.checkout_latest().await?; - self.lance_dataset.replace(lance); - } - } else { - Err(StoreError::IndexingError( - "Cannot determine indexed data schema".to_owned(), - ))?; - } - - Ok(()) - } - - /// Remove layers from the index. - pub async fn remove_layers( - &self, - layers: &[(SegmentId, String)], - checkout_latest: bool, - ) -> Result<(), StoreError> { - let mut lance: lance::Dataset = self.lance_dataset.cloned(); - - let predicate = if cfg!(false) { - // TODO(cmc): The following fails in Lance for reasons that escape me. - - use datafusion::prelude::*; - - /// Creates a _balanced_ chain of binary expressions. - fn balanced_binary_exprs( - mut exprs: Vec, - op: datafusion::logical_expr::Operator, - ) -> Option { - while exprs.len() > 1 { - let mut exprs_next = Vec::with_capacity(exprs.len() / 2 + 1); - let mut exprs_prev = exprs.into_iter(); - - while let Some(left) = exprs_prev.next() { - if let Some(right) = exprs_prev.next() { - exprs_next.push(datafusion::prelude::binary_expr(left, op, right)); - } else { - exprs_next.push(left); - } - } - - exprs = exprs_next; - } - - exprs.into_iter().next() - } - - let predicates = layers - .iter() - .map(|(segment, layer)| { - (cast(col(FIELD_RERUN_SEGMENT_ID), DataType::Utf8).eq(lit(&segment.id))) - .and(cast(col(FIELD_RERUN_SEGMENT_LAYER), DataType::Utf8).eq(lit(layer))) - }) - .collect(); - - let Some(predicate) = - balanced_binary_exprs(predicates, datafusion::logical_expr::Operator::Or) - else { - if checkout_latest { - lance.checkout_latest().await?; - self.lance_dataset.replace(lance); - } - return Ok(()); - }; - - datafusion::sql::unparser::expr_to_sql(&predicate)?.to_string() - } else { - layers - .iter() - .map(|(segment, layer)| { - format!( - "(CAST({} AS string) = '{}' AND CAST({} AS string) = '{}')", - FIELD_RERUN_SEGMENT_ID, - segment.id.replace('\'', "''"), - FIELD_RERUN_SEGMENT_LAYER, - layer.replace('\'', "''"), - ) - }) - .collect::>() - .join(" OR ") - }; - - lance.delete(&predicate).await?; - lance - .optimize_indices(&lance_index::optimize::OptimizeOptions::append()) - .await?; - - // TODO(swallez) we should call optimize_indices and compact_files sometimes. - // We can either do it every X insertions or use a debouncer to enforce a max frequency. - - if checkout_latest { - lance.checkout_latest().await?; - self.lance_dataset.replace(lance); - } - - Ok(()) - } - - /// Prepare a record batch for a chunk, given a timeline and component. Other parameters are used - /// to add source information to the indexed instance values. - /// - /// Returns `None` if the chunk doesn't contain the `timeline` or the `component`. - pub fn prepare_record_batch( - index_type: IndexType, - segment_id: &SegmentId, - layer: String, - timeline: TimelineName, - component: ComponentIdentifier, - chunk: &Arc, - ) -> Result, ArrowError> { - let Some(timeline) = chunk.timelines().get(&timeline) else { - // No such timeline - return Ok(None); - }; - - let Some(component) = chunk.components().get(component) else { - // No such component - return Ok(None); - }; - - // Nominal cases: each row is a list of instance values. - // The other case is Vector indexing with rows being a single vector containing numbers - let row_is_array_of_instances = match index_type { - IndexType::Inverted | IndexType::BTree => true, - // see also `find_datatypes` - IndexType::VectorIvfPq if !component.list_array.value_type().is_numeric() => true, - IndexType::VectorIvfPq => false, - }; - - // To pre-size buffers and avoid reallocations. - let total_instances = if row_is_array_of_instances { - component - .list_array - .iter() - .map(|x| x.map(|x| x.len()).unwrap_or(0)) - .sum() - } else { - component.list_array.len() - component.list_array.null_count() - }; - - // Dictionary encoding of values repeated for each row. The keys are all zeroes, pointing - // to the first element of the dictionary values array. - let dict_keys = UInt8Array::from_iter_values(std::iter::repeat_n(0, total_instances)); - - let segment_id_array = { - let segment_id_values = StringArray::from_iter_values([segment_id.id.as_str()]); - DictionaryArray::new(dict_keys.clone(), Arc::new(segment_id_values)) - }; - - let layer_array = { - let layer_values = StringArray::from_iter_values([layer]); - DictionaryArray::new(dict_keys.clone(), Arc::new(layer_values)) - }; - - let chunk_id_array = FixedSizeBinaryArray::try_from_iter(std::iter::repeat_n( - chunk.id().as_bytes(), - total_instances, - ))?; - - let instance_id_array: UInt32Array; - let timepoint_array: ArrayRef; - let instance_array: ArrayRef; - - if row_is_array_of_instances { - let mut timepoints = Int64BufferBuilder::new(total_instances); - let mut instance_ids = UInt32BufferBuilder::new(total_instances); - - // Collect instance arrays, they will be concatenated later. - let mut instances = Vec::new(); - - for (row_num, instance) in component.list_array.iter().enumerate() { - let Some(instance) = instance else { - continue; - }; - - // Repeat time as many times as there are instances in the row. - timepoints.append_n(instance.len(), timeline.times_raw()[row_num]); - - for i in 0..instance.len() as u32 { - instance_ids.append(i); - } - instances.push(instance); - } - - // Note: no support for 64-bit seconds and millis, but time-based timelines use nanos. - timepoint_array = arrow::compute::cast( - &Int64Array::new(ScalarBuffer::from(timepoints), None), - &timeline.timeline().datatype(), - )?; - - let instance_arrays: Vec<&dyn Array> = instances.iter().map(|x| x.as_ref()).collect(); - instance_array = re_arrow_util::concat_arrays(instance_arrays.as_slice())?; - - instance_id_array = UInt32Array::new(ScalarBuffer::from(instance_ids), None); - } else { - // All rows are a single vector. Just filter out nulls, if any. - if component.list_array.null_count() == 0 { - instance_array = Arc::new(component.list_array.clone()); - timepoint_array = Arc::new(timeline.times_array().clone()); - } else { - let non_nulls = arrow::compute::is_not_null(&component.list_array)?; - - let list_array: ArrayRef = Arc::new(component.list_array.clone()); - instance_array = re_arrow_util::filter_array(&list_array, &non_nulls); - timepoint_array = re_arrow_util::filter_array(&timeline.times_array(), &non_nulls); - } - - let mut instance_ids = UInt32BufferBuilder::new(total_instances); - // One instance per row => instance ids are all zero. - instance_ids.append_n(total_instances, 0); - instance_id_array = UInt32Array::new(ScalarBuffer::from(instance_ids), None); - } - - // Keep in sync (including types) with `create_lance_dataset` - let batch = RecordBatch::try_from_iter([ - ( - FIELD_RERUN_SEGMENT_ID, - Arc::new(segment_id_array) as ArrayRef, - ), - (FIELD_RERUN_SEGMENT_LAYER, Arc::new(layer_array)), - (FIELD_CHUNK_ID, Arc::new(chunk_id_array)), - (FIELD_TIMEPOINT, Arc::new(timepoint_array)), - (FIELD_INSTANCE_ID, Arc::new(instance_id_array)), - (FIELD_INSTANCE, Arc::new(instance_array)), - ])?; - - Ok(Some(batch)) - } -} - -/// Create an index -pub async fn create_index( - dataset: &Dataset, - config: &IndexConfig, - path: PathBuf, -) -> Result { - let index_type: IndexType = (&config.properties).into(); - let types: IndexDataTypes = find_datatypes( - dataset, - index_type, - &config.column.entity_path, - &config.column.descriptor.component, - &config.time_index, - ) - .ok_or_else(|| { - StoreError::ComponentPathNotFound(ComponentPath::new( - config.column.entity_path.clone(), - config.column.descriptor.component, - )) - })?; - - let mut lance_table = create_lance_dataset(&path, types).await?; - - create_lance_index(&mut lance_table, &config.properties).await?; - - Ok(super::Index { - lance_dataset: ArcCell::new(lance_table), - config: config.clone(), - }) -} - -async fn create_lance_dataset( - path: &Path, - types: IndexDataTypes, -) -> Result { - let non_nullable = false; - - let schema = Arc::new( - // Keep in sync with `prepare_record_batch` - #[expect(clippy::disallowed_methods)] - Schema::new(vec![ - // Chunk identification values are the same for all rows: use a dictionary - Field::new_dictionary( - FIELD_RERUN_SEGMENT_ID, - DataType::UInt8, - DataType::Utf8, - non_nullable, - ) - .with_dict_is_ordered(true), - Field::new_dictionary( - FIELD_RERUN_SEGMENT_LAYER, - DataType::UInt8, - DataType::Utf8, - non_nullable, - ) - .with_dict_is_ordered(true), - // Will be repeated, but Lance doesn't support dictionaries for FixedSizeBinary because - // of stringly-typed checks in lance_core::data_types (look for "Unsupported dictionary type") - Field::new(FIELD_CHUNK_ID, DataType::FixedSizeBinary(16), non_nullable) - .with_dict_is_ordered(true), - Field::new(FIELD_TIMEPOINT, types.timepoints, non_nullable), - // Position of the instance value that matched the query. Arrow lists use 32-bit offsets. - Field::new(FIELD_INSTANCE_ID, DataType::UInt32, non_nullable), - Field::new(FIELD_INSTANCE, types.instances, true), - ]), - ); - - let batch = RecordBatch::new_empty(schema.clone()); - let batches = RecordBatchIterator::new(vec![batch].into_iter().map(Ok), schema.clone()); - - let dataset = lance::Dataset::write(batches, path.to_string_lossy().as_ref(), None).await?; - - Ok(dataset) -} - -async fn create_lance_index( - lance_table: &mut lance::Dataset, - properties: &IndexProperties, -) -> Result<(), StoreError> { - use lance::index::vector::VectorIndexParams; - use lance_index::scalar::{InvertedIndexParams, ScalarIndexParams}; - use lance_index::{DatasetIndexExt as _, IndexParams, IndexType}; - use lance_linalg::distance::MetricType; - use re_protos::cloud::v1alpha1::VectorDistanceMetric; - - // Convert index properties - let (index_type, index_params): (IndexType, &dyn IndexParams) = match properties { - IndexProperties::Inverted { - store_position, - base_tokenizer, - } => ( - IndexType::Inverted, - &InvertedIndexParams::default() - .with_position(*store_position) - .base_tokenizer(base_tokenizer.clone()), - ), - - IndexProperties::VectorIvfPq { - target_partition_num_rows, - num_sub_vectors, - metric, - } => { - let ivf_params = lance_index::vector::ivf::IvfBuildParams { - target_partition_size: target_partition_num_rows.map(|v| v as usize), - ..Default::default() - }; - - let pq_params = lance_index::vector::pq::PQBuildParams { - num_sub_vectors: *num_sub_vectors as usize, - ..Default::default() - }; - - let lance_metric = match metric { - VectorDistanceMetric::Unspecified => { - return Err(StoreError::IndexingError( - "Unspecified distance metric".to_owned(), - )); - } - VectorDistanceMetric::L2 => MetricType::L2, - VectorDistanceMetric::Cosine => MetricType::Cosine, - VectorDistanceMetric::Dot => MetricType::Dot, - VectorDistanceMetric::Hamming => MetricType::Hamming, - }; - - ( - IndexType::Vector, - &VectorIndexParams::with_ivf_pq_params(lance_metric, ivf_params, pq_params), - ) - } - - IndexProperties::Btree => (IndexType::BTree, &ScalarIndexParams::default()), - }; - - match lance_table - .create_index(&["instance"], index_type, None, index_params, false) - .await - { - Ok(_) => Ok(()), - - // Some failures are expected and ok - Err(lance::Error::Index { message, .. }) if message.contains("already exists") => Ok(()), - - Err(lance::Error::Index { ref message, .. }) - if message.contains("Not enough rows to train PQ") - || message.contains("KMeans: can not train") => - { - tracing::warn!("not enough rows to train index yet"); - Ok(()) - } - - Err(lance::Error::NotSupported { source, .. }) - if source - .to_string() - .contains("empty vector indices with train=False") => - { - tracing::warn!("not enough rows to train index yet"); - Ok(()) - } - Err(err) => Err(err), - }?; - - Ok(()) -} - -/// Find the datatype of a column by looking up the first chunk containing it. -fn find_datatypes( - dataset: &Dataset, - index_type: IndexType, - entity_path: &EntityPath, - component: &ComponentIdentifier, - timeline_name: &TimelineName, -) -> Option { - for segment in dataset.segments().values() { - for layer in segment.layers().values() { - let chunks: Vec> = match layer.resolved_store() { - crate::store::ResolvedStore::Eager(h) => { - h.read().iter_physical_chunks().cloned().collect() - } - crate::store::ResolvedStore::Lazy(lazy) => lazy.collect_physical_chunks().ok()?, - }; - for chunk in chunks { - if chunk.entity_path() == entity_path - && let Some(component) = chunk.components().0.get(component) - && let Some(timeline) = chunk.timelines().get(timeline_name) - { - let instance_type = if index_type == IndexType::VectorIvfPq - && component.list_array.value_type().is_numeric() - { - // Row is a single vector, not a list of instances. - // See also `prepare_record_batch`. - component.list_array.data_type().clone() - } else { - component.list_array.value_type() - }; - return Some(IndexDataTypes { - instances: instance_type, - timepoints: timeline.timeline().datatype(), - }); - } - } - } - } - None -} diff --git a/crates/store/re_server/src/chunk_index/mod.rs b/crates/store/re_server/src/chunk_index/mod.rs deleted file mode 100644 index 9ebec470f347..000000000000 --- a/crates/store/re_server/src/chunk_index/mod.rs +++ /dev/null @@ -1,528 +0,0 @@ -mod index; -mod search; - -use std::ops::Deref as _; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, OnceLock}; - -use ahash::{HashMap, HashMapExt as _}; -use futures::StreamExt as _; -use re_log_types::{ComponentPath, EntityPath, EntryId}; -use re_protos::cloud::v1alpha1::ext::{ - CreateIndexRequest, IndexColumn, IndexConfig, SearchDatasetRequest, -}; -use re_protos::cloud::v1alpha1::{ - CreateIndexResponse, DeleteIndexesResponse, ListIndexesRequest, ListIndexesResponse, - SearchDatasetResponse, -}; -use re_protos::common::v1alpha1::ext::SegmentId; -use re_tuid::Tuid; -use re_types_core::ComponentIdentifier; -use tracing::instrument; - -use crate::rerun_cloud::SearchDatasetResponseStream; -use crate::store::{Dataset, Error as StoreError}; -// Fields in an index table - -pub const FIELD_RERUN_SEGMENT_ID: &str = "rerun_segment_id"; -pub const FIELD_RERUN_SEGMENT_LAYER: &str = "rerun_segment_layer"; -pub const FIELD_CHUNK_ID: &str = "chunk_id"; -pub const FIELD_TIMEPOINT: &str = "timepoint"; - -// Indexed value -pub const FIELD_INSTANCE: &str = "instance"; -// Position of the instance in the column cell -pub const FIELD_INSTANCE_ID: &str = "instance_id"; - -/// A thread-safe cell that holds an `Arc` and can be updated atomically. -struct ArcCell { - inner: parking_lot::Mutex>, -} - -impl ArcCell { - pub fn new(value: T) -> Self { - Self { - inner: parking_lot::Mutex::new(Arc::new(value)), - } - } - - pub fn get(&self) -> Arc { - self.inner.lock().clone() - } - - pub fn replace(&self, new_value: T) -> Arc { - std::mem::replace(&mut *self.inner.lock(), Arc::new(new_value)) - } -} - -impl ArcCell { - /// Returns a cloned version of the inner value. - pub fn cloned(&self) -> T { - self.get().deref().clone() - } -} - -/// An index for a column of a dataset's chunks -struct Index { - config: IndexConfig, - // Mutex because we need to update the lance object after writing and checking out the latest version. - lance_dataset: ArcCell, -} - -/// All indexes for a dataset's chunks -/// -/// Index creation behavior (mimics Rerun Cloud): -/// - Cannot create an index that already exists. Changing and index's parameters requires -/// deleting it first. -/// - Cannot create a index for a column that doesn't already have data, as we don't know -/// its type yet. This should be revisited to provide a better DX. -/// -pub struct DatasetChunkIndexes { - dataset_id: EntryId, - // Created on demand with the first index, will be deleted when dropped - dir: OnceLock>, - // Nested hashmap to mimic the hierarchy in ChunkStoreHandle - // we use an async lock as creating a new index involves I/O and async operations - indexes: tokio::sync::RwLock>>>, -} - -impl DatasetChunkIndexes { - pub fn new(dataset_id: EntryId) -> Self { - Self { - dataset_id, - dir: OnceLock::new(), - indexes: tokio::sync::RwLock::new(HashMap::new()), - } - } - - // ---- GRPC API - - #[instrument(skip(self, dataset), fields(dataset_id = %self.dataset_id))] - pub async fn create_index( - &self, - dataset: &Dataset, - request: CreateIndexRequest, - ) -> tonic::Result> { - let config = request.config; - - // Lazily create the temp directory for this dataset's indexes if needed - let temp_dir = self - .dir - .get_or_init(|| { - tempfile::Builder::new() - .prefix(&format!("rerun-index-{}", self.dataset_id)) - .tempdir() - }) - .as_ref() - .map_err(|err| { - StoreError::IndexingError(format!("Cannot create index directory {err}")) - })?; - - self.add_index(dataset, &config, temp_dir.path()).await?; - - Ok(tonic::Response::new(CreateIndexResponse { - index: Some(config.into()), - statistics_json: Default::default(), - debug_info: None, - })) - } - - pub async fn list_indexes( - &self, - _request: ListIndexesRequest, - ) -> tonic::Result> { - let mut result = Vec::new(); - for path_indexes in self.indexes.read().await.values() { - for component_indexes in path_indexes.values() { - result.push(component_indexes.config.clone().into()); - } - } - - Ok(tonic::Response::new(ListIndexesResponse { - indexes: result, - statistics_json: Vec::new(), - })) - } - - pub async fn delete_indexes( - &self, - column: IndexColumn, - ) -> tonic::Result> { - // We just remove the index from the dataset's indexes but don't delete the underlying - // storage directory intact. This avoids any race condition if the Lance table is still in - // use after having been cloned. Cleanup will happen when the process exists, deleting - // the temp directory holding all indexes. - - let mut indexes = self.indexes.write().await; - - let result = if let Some(path_indexes) = indexes.get_mut(&column.entity_path) - && let Some(component_index) = path_indexes.remove(&column.descriptor.component) - { - vec![component_index.config.clone().into()] - } else { - Vec::new() - }; - - Ok(tonic::Response::new(DeleteIndexesResponse { - indexes: result, - })) - } - - pub async fn search_dataset( - dataset: &Dataset, - request: SearchDatasetRequest, - ) -> tonic::Result> { - let Some(index) = dataset - .indexes() - .get( - &request.column.entity_path, - &request.column.descriptor.component, - ) - .await - else { - return Err(StoreError::ComponentPathNotFound(ComponentPath::new( - request.column.entity_path, - request.column.descriptor.component, - )))?; - }; - - let stream = search::search_index(index, request).await?; - - let stream = stream.map(|batch| { - batch - .map(|batch| SearchDatasetResponse { - data: Some(batch.into()), - }) - .map_err(Into::into) - }); - - Ok(tonic::Response::new(Box::pin(stream))) - } - - // ----- Called by Dataset - - pub async fn on_layer_added( - &self, - segment_id: SegmentId, - resolved: &crate::store::ResolvedStore, - layer_name: &str, - _overwritten: bool, - ) -> Result<(), StoreError> { - // Fast path: no indexes exist, nothing to do (no chunk loading needed). - if self.indexes.read().await.is_empty() { - return Ok(()); - } - - // Collect physical chunks from the store, loading on demand for lazy stores. - let chunks: Vec> = match resolved { - crate::store::ResolvedStore::Eager(h) => { - h.read().iter_physical_chunks().cloned().collect() - } - crate::store::ResolvedStore::Lazy(lazy) => lazy - .collect_physical_chunks() - .map_err(|err| StoreError::IndexingError(format!("{err:#}")))?, - }; - - let mut worklist = vec![]; - { - let indexes = self.indexes.read().await; - for chunk in &chunks { - if let Some(entity_indexes) = indexes.get(chunk.entity_path()) { - for (name, index) in entity_indexes { - if chunk.components().0.contains_key(name) { - worklist.push(( - index.clone(), - segment_id.clone(), - layer_name.to_owned(), - chunk.clone(), - )); - } - } - } - } - } - - for (index, segment_id, layer_name, chunk) in worklist { - index - .store_chunks(vec![(segment_id.clone(), layer_name, chunk.clone())], true) - .await?; - } - - Ok(()) - } - - pub async fn on_layers_removed( - &self, - removed_layers: &[(SegmentId, String)], - ) -> Result<(), StoreError> { - let indexes = self.indexes.write().await; - - for index in indexes - .values() - .flat_map(|per_component| per_component.values()) - { - let checkout_latest = true; - index.remove_layers(removed_layers, checkout_latest).await?; - } - - Ok(()) - } - - // ---- implementation - - /// Get the index for a path and component, if any. - async fn get( - &self, - entity_path: &EntityPath, - component: &ComponentIdentifier, - ) -> Option> { - let indexes = self.indexes.read().await; - - indexes.get(entity_path)?.get(component).cloned() - } - - /// Add an index to a dataset - async fn add_index( - &self, - dataset: &Dataset, - config: &IndexConfig, - dir: impl Into<&Path>, - ) -> Result, StoreError> { - let entity_path = &config.column.entity_path.clone(); - let component = &config.column.descriptor.component.clone(); - - // Use a random string to name the index directory. Using entity path and component would - // be more user-friendly, but users should never have to look at this temporary directory, - // and this can create potential collisions if an index is deleted and recreated in rapid - // succession. - let path: PathBuf = dir.into().join(Tuid::new().to_string()); - - let mut indexes = self.indexes.write().await; - - // Do we have it already? - if let Some(path_indexes) = indexes.get(entity_path) - && path_indexes.contains_key(component) - { - return Err(StoreError::IndexAlreadyExists(format!( - "{entity_path}#{component}", - ))); - } - - let index = Arc::new(index::create_index(dataset, config, path).await?); - - // Register it and drop the lock - indexes - .entry(entity_path.clone()) - .or_default() - .insert(*component, index.clone()); - drop(indexes); - - // Backfill existing data in the index - let mut backfill = Vec::new(); - for (segment_id, segment) in dataset.segments() { - for (layer_name, layer) in segment.layers() { - let chunks: Vec> = match layer - .resolved_store() - { - crate::store::ResolvedStore::Eager(h) => { - h.read().iter_physical_chunks().cloned().collect() - } - crate::store::ResolvedStore::Lazy(lazy) => lazy - .collect_physical_chunks() - .map_err(|err| StoreError::IndexingError(format!("{err:#}")))?, - }; - for chunk in chunks { - if chunk.entity_path() == entity_path - && chunk.components().0.contains_key(component) - { - backfill.push((segment_id.clone(), layer_name.clone(), chunk)); - } - } - } - } - - index.store_chunks(backfill, true).await?; - - Ok(index) - } -} - -#[cfg(test)] -mod tests { - //! Simple test for vector search. More extensive tests are in the `redap_tests` package that - //! also tests consistency between this local server and Rerun Cloud. - - use arrow::array::{ - ArrayRef, FixedSizeBinaryArray, FixedSizeListArray, FixedSizeListBuilder, Float32Array, - Float32Builder, ListBuilder, RecordBatch, - }; - use arrow::buffer::ScalarBuffer; - use nohash_hasher::IntMap; - use re_arrow_util::ArrowArrayDowncastRef as _; - use re_chunk_store::external::re_chunk; - use re_chunk_store::external::re_chunk::{ChunkComponents, TimeColumn}; - use re_chunk_store::{ChunkStore, ChunkStoreConfig}; - use re_log_types::{EntryId, StoreId, StoreKind, TimeType, Timeline, TimelineName}; - use re_protos::cloud::v1alpha1::VectorDistanceMetric; - use re_protos::cloud::v1alpha1::ext::{IndexColumn, IndexProperties, IndexQueryProperties}; - use re_protos::common::v1alpha1::ext::{IfDuplicateBehavior, ScanParameters}; - use re_types_core::{ChunkId, ComponentDescriptor, Loggable as _, SerializedComponentColumn}; - - use super::*; - - #[tokio::test] - async fn test_vector_search() -> anyhow::Result<()> { - //---- Create a 3-rows dataset with a vector column - - let mut dataset = Dataset::new( - EntryId::new(), - re_protos::EntryName::new("test-data").unwrap(), - StoreKind::Recording, - Default::default(), - ); - - let segment_id = SegmentId::new("test-segment".to_owned()); - let layer_name = "test-layer".to_owned(); - - let row_ids: FixedSizeBinaryArray = { - let row_ids = Tuid::to_arrow(vec![Tuid::new(), Tuid::new(), Tuid::new()])?; - - row_ids - .downcast_array_ref::() - .unwrap() - .clone() - }; - - let timelines: IntMap = { - let times: ScalarBuffer = ScalarBuffer::from(vec![1, 2, 3]); - let time_column = - TimeColumn::new(Some(true), Timeline::new("tick", TimeType::Sequence), times); - IntMap::from_iter([(*time_column.timeline().name(), time_column)]) - }; - - let components: ChunkComponents = { - let descriptor = ComponentDescriptor::partial("embedding"); - let mut components = ChunkComponents::default(); - - let mut list_builder = - ListBuilder::new(FixedSizeListBuilder::new(Float32Builder::new(), 256)); - for value in [1.0, 2.0, 3.0] { - let list_values = list_builder.values(); - let coord_values = list_values.values(); - for _ in 0..256 { - coord_values.append_value(value); - } - list_values.append(true); - list_builder.append(true); - } - - let serialized_column = - SerializedComponentColumn::new(list_builder.finish(), descriptor); - - components.insert(serialized_column); - - components - }; - - let chunk = re_chunk::Chunk::new( - ChunkId::new(), - EntityPath::from("/some/vectors"), - Some(true), // is_sorted - row_ids, - timelines, - components, - )?; - - let mut store = ChunkStore::new( - StoreId::new(StoreKind::Recording, "app", "recording"), - ChunkStoreConfig::default(), - ); - store.insert_chunk(&Arc::new(chunk))?; - let handle = re_chunk_store::ChunkStoreHandle::new(store); - let store_slot_id = crate::store::StoreSlotId::new(); - - dataset - .add_layer( - segment_id, - layer_name, - store_slot_id, - crate::store::ResolvedStore::Eager(handle), - IfDuplicateBehavior::Error, - ) - .await?; - - //----- Create the index - let dir = tempfile::TempDir::new()?; - let column = IndexColumn { - entity_path: EntityPath::from("/some/vectors"), - descriptor: ComponentDescriptor { - component: ComponentIdentifier::new("embedding"), - archetype: None, - component_type: None, - }, - }; - - let config = IndexConfig { - time_index: TimelineName::new("tick"), - column: column.clone(), - properties: IndexProperties::VectorIvfPq { - target_partition_num_rows: None, - metric: VectorDistanceMetric::Cosine, - num_sub_vectors: 32, - }, - }; - let index = dataset - .indexes() - .add_index(&dataset, &config, dir.path()) - .await?; - - //----- Query the index - - // We search for [3.0 ... 3.0], that should come back with a distance of 0.0 - let query = { - let mut values = Float32Builder::new(); - for _ in 0..256 { - values.append_value(3.0); - } - let values: ArrayRef = Arc::new(values.finish()); - RecordBatch::try_from_iter([("item", values)])? - }; - - let mut result = search::search_index( - index, - SearchDatasetRequest { - column: column.clone(), - query, - properties: IndexQueryProperties::Vector { top_k: 2 }, - scan_parameters: ScanParameters { - columns: vec![FIELD_TIMEPOINT.to_owned(), FIELD_INSTANCE.to_owned()], - ..Default::default() - }, - }, - ) - .await?; - - while let Some(next) = result.next().await { - let next = next?; - let distances = next - .column_by_name("_distance") - .unwrap() - .downcast_array_ref::() - .unwrap(); - let instances = next - .column_by_name("instance") - .unwrap() - .downcast_array_ref::() - .unwrap() - .values() - .downcast_array_ref::() - .unwrap(); - - assert_eq!(distances.value(0), 0.0); - assert!(distances.value(1) > 0.0); - assert_eq!(instances.value(0), 3.0); - } - - Ok(()) - } -} diff --git a/crates/store/re_server/src/chunk_index/search.rs b/crates/store/re_server/src/chunk_index/search.rs deleted file mode 100644 index 13b2a0572c6a..000000000000 --- a/crates/store/re_server/src/chunk_index/search.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::sync::Arc; - -use arrow::array::{RecordBatch, StringArray}; -use datafusion::common::ScalarValue; -use futures::{Stream, StreamExt as _}; -use itertools::Itertools as _; -use lance_index::scalar::FullTextSearchQuery; -use re_arrow_util::ArrowArrayDowncastRef as _; -use re_protos::cloud::v1alpha1::ext::{IndexQueryProperties, SearchDatasetRequest}; -use re_protos::common::v1alpha1::ext::ScanParameters; -use tracing::info; - -use crate::chunk_index::{FIELD_INSTANCE, Index}; -use crate::store::Error as StoreError; - -pub async fn search_index( - index: Arc, - request: SearchDatasetRequest, -) -> Result> + use<>, StoreError> { - let lance_dataset = index.lance_dataset.get(); - - if request.query.columns().len() != 1 && request.query.num_rows() != 1 { - return Err(StoreError::IndexingError( - "Query must have exactly one row and one column".to_owned(), - )); - } - - let query_data = request.query.column(0); - - let length_zero = request.scan_parameters.limit_len == Some(0); - - let stream = match request.properties { - IndexQueryProperties::Inverted => { - let q = query_data.try_downcast_array_ref::()?.value(0); - - let fts = - FullTextSearchQuery::new(q.to_owned()).with_column(FIELD_INSTANCE.to_owned())?; - - let mut scanner = &mut lance_dataset.scan(); - scanner = scanner.full_text_search(fts)?; - apply_parameters(scanner, request.scan_parameters).await?; - scanner.try_into_stream().await? - } - - IndexQueryProperties::Vector { top_k } => { - let mut scanner = &mut lance_dataset.scan(); - scanner = scanner.nearest(FIELD_INSTANCE, query_data, top_k as usize)?; - apply_parameters(scanner, request.scan_parameters).await?; - - scanner.try_into_stream().await? - } - - IndexQueryProperties::Btree => { - let q = ScalarValue::try_from_array(query_data, 0)?; - - let scanner = &mut lance_dataset.scan(); - { - use datafusion::prelude::*; - scanner.filter_expr(col(FIELD_INSTANCE).eq(lit(q))); - } - - apply_parameters(scanner, request.scan_parameters).await?; - - scanner.try_into_stream().await? - } - }; - - use lance::io::RecordBatchStream as _; - - // To find the schema of the query results, we do a query with a limit of 0. - // However, such a query results in an empty stream. In that case we force - // creation of an empty record batch with the right schema, and return that - // instead. - // - // Note, it's important we do this here because the lance scanner actually - // mutates the schema based on the type of search being done. - let stream = if length_zero { - let rb = RecordBatch::new_empty(stream.schema()); - tokio_util::either::Either::Left(tokio_stream::iter(vec![Ok(rb)])) - } else { - tokio_util::either::Either::Right(stream) - }; - - let stream = stream.map(|s| s.map_err(Into::into)); - Ok(stream) -} - -// Borrowed from redap's ScannerExt -async fn apply_parameters( - scanner: &mut lance::dataset::scanner::Scanner, - parameters: ScanParameters, -) -> Result<(), StoreError> { - let ScanParameters { - columns, - on_missing_columns: _, - filter, - limit_offset, - limit_len, - order_by, - explain_plan, - explain_filter, - } = parameters; - - // `project_from_schema` added in https://github.com/rerun-io/lance/pull/10 - // Use regular projection instead for now. - // let projected_schema = lance_dataset.schema().project(&columns)?; - // scanner.project_from_schema(&projected_schema)?; - scanner.project(&columns)?; - - if let Some(filter) = filter.filter(|f| !f.is_empty()) { - let filter = - lance::io::exec::Planner::new(scanner.schema().await?).parse_filter(&filter)?; - match scanner.get_expr_filter()? { - Some(existing_filter) => { - scanner.filter_expr(existing_filter.and(filter)); - } - None => { - scanner.filter_expr(filter); - } - } - } - - scanner.limit(limit_len, limit_offset)?; - - if !order_by.is_empty() { - let order_by = order_by - .into_iter() - .map(|order_by| lance::dataset::scanner::ColumnOrdering { - ascending: !order_by.descending, - nulls_first: !order_by.nulls_last, - column_name: order_by.column_name, - }) - .collect_vec(); - scanner.order_by(Some(order_by))?; - } - - if explain_plan { - match scanner.explain_plan(false).await { - Ok(plan) => { - info!(plan); - } - Err(err) => { - info!("Failed to compute execution plan: {err:#}"); - } - } - } - - if explain_filter { - match scanner.get_expr_filter() { - Ok(Some(filter)) => { - info!(%filter); - } - Ok(_) => { - info!("No filter set"); - } - Err(err) => { - info!("Failed to fetch current filter: {err:#}"); - } - } - } - - Ok(()) -} diff --git a/crates/store/re_server/src/entrypoint.rs b/crates/store/re_server/src/entrypoint.rs index ca012cd23f16..26a8fa6233d3 100644 --- a/crates/store/re_server/src/entrypoint.rs +++ b/crates/store/re_server/src/entrypoint.rs @@ -1,16 +1,14 @@ use std::net::SocketAddr; -use std::path::PathBuf; -use std::str::FromStr; use anyhow::Context as _; -use re_protos::EntryName; +use re_protos::common::v1alpha1::ext; #[cfg(unix)] use tokio::signal::unix::{SignalKind, signal}; #[cfg(windows)] use tokio::signal::windows::{ctrl_break, ctrl_close}; use tracing::{info, warn}; -use crate::{ServerBuilder, ServerHandle}; +use crate::{NamedPath, NamedPathCollection, ServerBuilder, ServerHandle}; // --- @@ -84,37 +82,6 @@ impl Default for Args { } } -#[derive(Debug, Clone)] -pub struct NamedPath { - pub name: Option, - pub path: PathBuf, -} - -/// A named collection of paths. -#[derive(Debug, Clone)] -pub struct NamedPathCollection { - pub name: EntryName, - pub paths: Vec, -} - -impl FromStr for NamedPath { - type Err = String; - - fn from_str(s: &str) -> Result { - if let Some((name, path)) = s.split_once('=') { - Ok(Self { - name: Some(name.to_owned()), - path: PathBuf::from(path), - }) - } else { - Ok(Self { - name: None, - path: PathBuf::from(s), - }) - } - } -} - impl Args { /// Waits for the server to start, and return a handle to it. /// @@ -139,7 +106,7 @@ impl Args { .with_rrds_as_dataset( name, paths, - re_protos::common::v1alpha1::ext::IfDuplicateBehavior::Error, + ext::IfDuplicateBehavior::Error, crate::OnError::Continue, ) .await?; @@ -149,7 +116,7 @@ impl Args { builder = builder .with_directory_as_dataset( dataset_prefix, - re_protos::common::v1alpha1::ext::IfDuplicateBehavior::Error, + ext::IfDuplicateBehavior::Error, crate::OnError::Continue, ) .await?; @@ -157,15 +124,16 @@ impl Args { #[cfg_attr(not(feature = "lance"), expect(clippy::never_loop))] for table in &tables { - cfg_if::cfg_if! { - if #[cfg(feature = "lance")] { + cfg_select! { + feature = "lance" => { builder = builder .with_directory_as_table( table, - re_protos::common::v1alpha1::ext::IfDuplicateBehavior::Error, + ext::IfDuplicateBehavior::Error, ) .await?; - } else { + } + _ => { _ = table; anyhow::bail!("re_server was not compiled with the 'lance' feature"); } diff --git a/crates/store/re_server/src/bandwidth_layer.rs b/crates/store/re_server/src/layers/bandwidth.rs similarity index 100% rename from crates/store/re_server/src/bandwidth_layer.rs rename to crates/store/re_server/src/layers/bandwidth.rs diff --git a/crates/store/re_server/src/error_layer.rs b/crates/store/re_server/src/layers/error.rs similarity index 100% rename from crates/store/re_server/src/error_layer.rs rename to crates/store/re_server/src/layers/error.rs diff --git a/crates/store/re_server/src/latency_layer.rs b/crates/store/re_server/src/layers/latency.rs similarity index 100% rename from crates/store/re_server/src/latency_layer.rs rename to crates/store/re_server/src/layers/latency.rs diff --git a/crates/store/re_server/src/layers/mod.rs b/crates/store/re_server/src/layers/mod.rs new file mode 100644 index 000000000000..b3f6ee062f08 --- /dev/null +++ b/crates/store/re_server/src/layers/mod.rs @@ -0,0 +1,8 @@ +mod bandwidth; +mod error; +mod latency; + +pub(crate) use self::bandwidth::BandwidthLayer; +pub(crate) use self::error::ErrorInjectionLayer; +pub use self::error::InjectedErrors; +pub(crate) use self::latency::LatencyLayer; diff --git a/crates/store/re_server/src/lib.rs b/crates/store/re_server/src/lib.rs index 22b65252c761..df42c2661fb4 100644 --- a/crates/store/re_server/src/lib.rs +++ b/crates/store/re_server/src/lib.rs @@ -1,22 +1,27 @@ //! A Rerun server implementation backed by an in-memory store. -#[cfg(feature = "lance")] -mod chunk_index; - -mod bandwidth_layer; +#[cfg(not(target_arch = "wasm32"))] mod entrypoint; -mod error_layer; -mod latency_layer; +#[cfg(not(target_arch = "wasm32"))] +mod layers; +mod named_path; +#[cfg(target_arch = "wasm32")] +pub mod opfs; mod rerun_cloud; +#[cfg(not(target_arch = "wasm32"))] mod server; mod store; -pub use self::entrypoint::{Args, NamedPath, NamedPathCollection}; -pub use self::error_layer::InjectedErrors; +pub use self::named_path::{NamedPath, NamedPathCollection}; pub use self::rerun_cloud::{ RerunCloudHandler, RerunCloudHandlerBuilder, RerunCloudHandlerSettings, }; -pub use self::server::{Server, ServerBuilder, ServerError, ServerHandle}; +#[cfg(not(target_arch = "wasm32"))] +pub use self::{ + entrypoint::Args, + layers::InjectedErrors, + server::{Server, ServerBuilder, ServerError, ServerHandle}, +}; /// What should we do on error? #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] diff --git a/crates/store/re_server/src/named_path.rs b/crates/store/re_server/src/named_path.rs new file mode 100644 index 000000000000..ce46bf603052 --- /dev/null +++ b/crates/store/re_server/src/named_path.rs @@ -0,0 +1,35 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use re_protos::EntryName; + +#[derive(Debug, Clone)] +pub struct NamedPath { + pub name: Option, + pub path: PathBuf, +} + +/// A named collection of paths. +#[derive(Debug, Clone)] +pub struct NamedPathCollection { + pub name: EntryName, + pub paths: Vec, +} + +impl FromStr for NamedPath { + type Err = String; + + fn from_str(s: &str) -> Result { + if let Some((name, path)) = s.split_once('=') { + Ok(Self { + name: Some(name.to_owned()), + path: PathBuf::from(path), + }) + } else { + Ok(Self { + name: None, + path: PathBuf::from(s), + }) + } + } +} diff --git a/crates/store/re_server/src/opfs.rs b/crates/store/re_server/src/opfs.rs new file mode 100644 index 000000000000..41e182957437 --- /dev/null +++ b/crates/store/re_server/src/opfs.rs @@ -0,0 +1,367 @@ +//! Implementation of filesystem operations based on [OPFS](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system). +//! +//! The signatures loosely mirror [`tokio::fs`](https://docs.rs/tokio/latest/tokio/fs/index.html) +//! for familiarity. + +// TODO(grtlr): Maybe move this to a `re_opfs` crate. + +use std::io; +use std::path::{Component, Path}; +use std::sync::Arc; + +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + DomException, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemWritableFileStream, +}; + +pub struct Metadata { + is_file: bool, +} + +impl Metadata { + pub fn is_file(&self) -> bool { + self.is_file + } +} + +pub async fn metadata(path: &Path) -> io::Result { + let path = path.to_owned(); + run_local(async move { + match open_file(&path).await { + Ok(file_handle) => { + let _file: web_sys::File = await_js(file_handle.get_file()).await?; + Ok(Metadata { is_file: true }) + } + Err(err) if err.kind() == io::ErrorKind::InvalidInput => { + Ok(Metadata { is_file: false }) + } + Err(err) => Err(err), + } + }) + .await +} + +// TODO(RR-5154): Replace this with something akin to `read_exact_at`, to avoid +// copying all of the bytes via `to_vec`. +pub async fn read(path: &Path) -> io::Result> { + let path = path.to_owned(); + run_local(async move { + let file_handle = open_file(&path).await?; + let file: web_sys::File = await_js(file_handle.get_file()).await?; + let blob: &web_sys::Blob = file.as_ref(); + let buffer: js_sys::ArrayBuffer = await_js(blob.array_buffer()).await?; + + Ok(js_sys::Uint8Array::new(&buffer).to_vec()) + }) + .await +} + +/// Write `contents` to `path`, creating any missing parent directories. +/// +/// Takes `contents` by value so callers that already own the bytes avoid a copy; the whole +/// buffer would otherwise be duplicated on the Wasm heap for large uploads. +pub async fn write(path: impl AsRef, contents: Arc<[u8]>) -> io::Result<()> { + let path = path.as_ref().to_owned(); + run_local(async move { + let file_handle = create_file(&path).await?; + let writer: FileSystemWritableFileStream = await_js(file_handle.create_writable()).await?; + + if let Err(err) = write_all(&writer, &contents).await { + // Discard the partially-written file so a later read fails cleanly rather than + // returning truncated contents (e.g. when the quota is exceeded mid-write). + let writable_stream: &web_sys::WritableStream = writer.as_ref(); + JsFuture::from(writable_stream.abort()).await.ok(); + return Err(err); + } + + Ok(()) + }) + .await +} + +async fn write_all(writer: &FileSystemWritableFileStream, contents: &[u8]) -> io::Result<()> { + let _: JsValue = await_js( + writer + .write_with_u8_array(contents) + .map_err(|err| js_to_io_error(&err))?, + ) + .await?; + + let writable_stream: &web_sys::WritableStream = writer.as_ref(); + let _: JsValue = await_js(writable_stream.close()).await?; + Ok(()) +} + +/// Recursively remove the directory at `path` and everything under it. +/// +/// A missing `path` is treated as success, so this is an idempotent "clear". +pub async fn remove_dir_all(path: impl AsRef) -> io::Result<()> { + let path = path.as_ref().to_owned(); + run_local(async move { + let (directory, name) = match parent_directory_and_file_name(&path, false).await { + Ok(directory_and_name) => directory_and_name, + Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(err) => return Err(err), + }; + + let options = web_sys::FileSystemRemoveOptions::new(); + options.set_recursive(true); + + match await_js::(directory.remove_entry_with_options(&name, &options)).await { + Ok(_) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()), + Err(err) => Err(err), + } + }) + .await +} + +async fn open_file(path: &Path) -> io::Result { + let (directory, file_name) = parent_directory_and_file_name(path, false).await?; + await_js(directory.get_file_handle(&file_name)).await +} + +async fn create_file(path: &Path) -> io::Result { + let (directory, file_name) = parent_directory_and_file_name(path, true).await?; + let options = web_sys::FileSystemGetFileOptions::new(); + options.set_create(true); + await_js(directory.get_file_handle_with_options(&file_name, &options)).await +} + +/// The OPFS root directory handle. +async fn opfs_root() -> io::Result { + let navigator = web_sys::window() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::Unsupported, "OPFS requires a browser Window") + })? + .navigator(); + await_js(navigator.storage().get_directory()).await +} + +/// Resolve the parent directory of `path`, walking (and, when `create`, creating) each component. +async fn parent_directory_and_file_name( + path: &Path, + create: bool, +) -> io::Result<(FileSystemDirectoryHandle, String)> { + let components = opfs_components(path)?; + let Some((file_name, directory_names)) = components.split_last() else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "OPFS path must contain a file component", + )); + }; + + let mut directory = opfs_root().await?; + for directory_name in directory_names { + directory = if create { + let options = web_sys::FileSystemGetDirectoryOptions::new(); + options.set_create(true); + await_js(directory.get_directory_handle_with_options(directory_name, &options)).await? + } else { + await_js(directory.get_directory_handle(directory_name)).await? + }; + } + + Ok((directory, file_name.clone())) +} + +fn opfs_components(path: &Path) -> io::Result> { + let mut components = Vec::new(); + + for component in path.components() { + match component { + Component::RootDir | Component::CurDir => {} + Component::Normal(component) => components.push( + component + .to_str() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "OPFS path is not UTF-8") + })? + .to_owned(), + ), + Component::ParentDir | Component::Prefix(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "OPFS paths must not contain parent-directory or prefix components", + )); + } + } + } + + Ok(components) +} + +/// Converts a [`js_sys::Promise`] to a Rust `future`. +async fn await_js(promise: js_sys::Promise) -> io::Result +where + T: JsCast, +{ + JsFuture::from(promise) + .await + .map_err(|err| js_to_io_error(&err))? + .dyn_into() + .map_err(|err| js_to_io_error(&err)) +} + +/// `tonic` service futures are `Send`, while `JsFuture` is not. +/// Run browser API work on the local Wasm executor and await only the `Send` oneshot receiver. +fn run_local( + future: impl std::future::Future> + 'static, +) -> impl std::future::Future> + Send +where + T: Send + 'static, +{ + let (tx, rx) = futures::channel::oneshot::channel(); + + wasm_bindgen_futures::spawn_local(async move { + let result = future.await; + tx.send(result).ok(); + }); + + async move { + rx.await.map_err(|_err| { + io::Error::new( + io::ErrorKind::Interrupted, + "OPFS browser task was canceled before completion", + ) + })? + } +} + +fn js_to_io_error(value: &JsValue) -> io::Error { + if let Some(exception) = value.dyn_ref::() { + return err_from_dom_exception(exception); + } + + if let Some(error) = value.dyn_ref::() { + return err_from_js(error); + } + + io::Error::other(value.as_string().unwrap_or_else(|| format!("{value:?}"))) +} + +fn err_from_dom_exception(exception: &DomException) -> io::Error { + let kind = match exception.code() { + DomException::NOT_FOUND_ERR => io::ErrorKind::NotFound, + DomException::SECURITY_ERR => io::ErrorKind::PermissionDenied, + DomException::TYPE_MISMATCH_ERR => io::ErrorKind::InvalidInput, + _ => io::ErrorKind::Other, + }; + + io::Error::new(kind, exception.message()) +} + +fn err_from_js(error: &js_sys::Error) -> io::Error { + let name = String::from(error.name()); + let raw_message = String::from(error.message()); + let message = if raw_message.is_empty() { + name + } else { + format!("{name}: {raw_message}") + }; + + io::Error::other(message) +} + +#[cfg(test)] +mod test { + use super::*; + + use std::io; + + use wasm_bindgen_test::wasm_bindgen_test; + + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + fn unique_opfs_test_dir() -> String { + format!("opfs-test-{}", re_tuid::Tuid::new()) + } + + #[wasm_bindgen_test] + async fn write_read_metadata_and_overwrite_nested_file() { + let test_dir = unique_opfs_test_dir(); + let file_path = format!("/{test_dir}/./nested/file.bin"); + + write(&file_path, Vec::from(b"first write").into()) + .await + .expect("initial write should succeed"); + + let metadata = metadata(file_path.as_ref()) + .await + .expect("metadata should succeed for an OPFS file"); + assert!(metadata.is_file()); + assert_eq!( + read(file_path.as_ref()) + .await + .expect("read should return the bytes that were written"), + b"first write", + ); + + write(&file_path, Vec::from(b"second").into()) + .await + .expect("overwriting an OPFS file should succeed"); + assert_eq!( + read(file_path.as_ref()) + .await + .expect("read should return the overwritten bytes"), + b"second", + ); + + remove_dir_all(test_dir) + .await + .expect("test cleanup should remove the OPFS directory"); + } + + #[wasm_bindgen_test] + async fn remove_dir_all_is_recursive_and_idempotent() { + let test_dir = unique_opfs_test_dir(); + let first_file = format!("{test_dir}/a.bin"); + let second_file = format!("{test_dir}/nested/b.bin"); + + write(&first_file, Vec::from(b"a").into()) + .await + .expect("writing first OPFS file should succeed"); + write(&second_file, Vec::from(b"b").into()) + .await + .expect("writing nested OPFS file should succeed"); + + remove_dir_all(&test_dir) + .await + .expect("recursive remove should succeed"); + remove_dir_all(&test_dir) + .await + .expect("removing a missing OPFS directory should be a no-op"); + remove_dir_all(format!("{test_dir}/nested")) + .await + .expect("removing below a missing OPFS directory should be a no-op"); + + let err = read(first_file.as_ref()) + .await + .expect_err("removed OPFS file should not be readable"); + assert_eq!(err.kind(), io::ErrorKind::NotFound); + + let err = read(second_file.as_ref()) + .await + .expect_err("recursively removed OPFS file should not be readable"); + assert_eq!(err.kind(), io::ErrorKind::NotFound); + } + + #[wasm_bindgen_test] + async fn rejects_parent_directory_paths() { + let err = write("opfs-test/../escape.bin", Vec::from(b"x").into()) + .await + .expect_err("OPFS paths must not allow parent-directory traversal"); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + + let err = read("../escape.bin".as_ref()) + .await + .expect_err("OPFS paths must not allow parent-directory traversal"); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + + let err = remove_dir_all("../escape") + .await + .expect_err("OPFS paths must not allow parent-directory traversal"); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } +} diff --git a/crates/store/re_server/src/rerun_cloud.rs b/crates/store/re_server/src/rerun_cloud/mod.rs similarity index 57% rename from crates/store/re_server/src/rerun_cloud.rs rename to crates/store/re_server/src/rerun_cloud/mod.rs index 314e8a632bc1..bfe15ab60421 100644 --- a/crates/store/re_server/src/rerun_cloud.rs +++ b/crates/store/re_server/src/rerun_cloud/mod.rs @@ -1,14 +1,14 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +#[cfg(not(target_arch = "wasm32"))] use std::path::PathBuf; use std::sync::Arc; -use arrow::array::BinaryArray; +use arrow::array::{BinaryArray, BooleanArray, StringArray}; use arrow::record_batch::RecordBatch; -use cfg_if::cfg_if; -use datafusion::logical_expr::dml::InsertOp; use datafusion::prelude::SessionContext; +use futures::StreamExt as _; use nohash_hasher::{IntMap, IntSet}; -use tokio_stream::StreamExt as _; +use re_protos::common::v1alpha1::TaskId; use tonic::{Code, Request, Response, Status}; use re_arrow_util::RecordBatchExt as _; @@ -16,57 +16,106 @@ use re_chunk_store::{ Chunk, ChunkId, ChunkStore, ChunkStoreHandle, ChunkTrackingMode, LatestAtQuery, RangeQuery, }; use re_log_encoding::ToTransport as _; -use re_log_types::{AbsoluteTimeRange, EntityPath, EntryId, StoreId, StoreKind, Timeline}; +use re_log_types::{AbsoluteTimeRange, EntityPath, EntryId, StoreId, StoreKind, TimelineName}; +#[cfg(not(target_arch = "wasm32"))] +use re_protos::cloud::v1alpha1::ext::{CreateTableEntryResponse, ProviderDetails}; +use re_protos::cloud::v1alpha1::ext::{ + QueryDatasetDataframe, QueryTasksDataframe, RegisterWithDatasetDataframe, + ScanDatasetManifestDataframe, ScanSegmentTableDataframe, +}; use re_protos::cloud::v1alpha1::rerun_cloud_service_server::RerunCloudService; use re_protos::cloud::v1alpha1::{ - CancelTasksRequest, CancelTasksResponse, DeleteEntryResponse, EntryDetails, EntryKind, - FetchChunksRequest, GetDatasetManifestSchemaRequest, GetDatasetManifestSchemaResponse, - GetDatasetSchemaResponse, GetRrdManifestResponse, GetSegmentTableSchemaResponse, - QueryDatasetResponse, QueryTasksOnCompletionRequest, QueryTasksOnCompletionResponse, - QueryTasksRequest, QueryTasksResponse, RegisterTableRequest, RegisterTableResponse, - RegisterWithDatasetResponse, ScanDatasetManifestRequest, ScanDatasetManifestResponse, - ScanSegmentTableResponse, ScanTableResponse, + CancelTasksRequest, CancelTasksResponse, DeleteEntryResponse, DoBandwidthTestResponse, + EntryCreatedEvent, EntryDeletedEvent, EntryDetails, EntryKind, EventKind, FetchChunksRequest, + GetDatasetManifestSchemaRequest, GetDatasetManifestSchemaResponse, GetDatasetSchemaResponse, + GetRrdManifestResponse, GetSegmentTableSchemaResponse, QueryDatasetResponse, + QueryTasksOnCompletionRequest, QueryTasksOnCompletionResponse, QueryTasksRequest, + QueryTasksResponse, RegisterTableRequest, RegisterTableResponse, ScanDatasetManifestRequest, + ScanDatasetManifestResponse, ScanSegmentTableResponse, ScanTableResponse, SegmentIdFilter, + WatchEventsResponse, segment_id_filter, watch_events_response, }; -use re_protos::common::v1alpha1::TaskId; -use re_protos::common::v1alpha1::ext::{IfDuplicateBehavior, SegmentId}; +use re_protos::common::v1alpha1::ext::{DatasetKind, IfDuplicateBehavior, SegmentId}; use re_protos::headers::RerunHeadersExtractorExt as _; use re_protos::missing_field; use re_protos::{ EntryName, cloud::v1alpha1::ext::{ self, CreateDatasetEntryRequest, CreateDatasetEntryResponse, CreateTableEntryRequest, - CreateTableEntryResponse, DataSource, EntryDetailsUpdate, LanceTable, ProviderDetails, - QueryDatasetRequest, ReadDatasetEntryResponse, ReadTableEntryResponse, TableInsertMode, - UpdateDatasetEntryRequest, UpdateDatasetEntryResponse, UpdateEntryRequest, - UpdateEntryResponse, + DataSource, EntryDetailsUpdate, QueryDatasetRequest, ReadDatasetEntryResponse, + ReadTableEntryResponse, TableInsertMode, UpdateDatasetEntryRequest, + UpdateDatasetEntryResponse, UpdateEntryRequest, UpdateEntryResponse, + UpdateTableEntryRequest, UpdateTableEntryResponse, }, }; +#[cfg(not(target_arch = "wasm32"))] use re_tuid::Tuid; +use re_types_core::LayerName; + +mod register_with_dataset; +use self::register_with_dataset::{RegisterWithDatasetResult, do_register_with_dataset}; +#[cfg(not(target_arch = "wasm32"))] +use crate::NamedPath; +#[cfg(not(target_arch = "wasm32"))] use crate::OnError; -use crate::entrypoint::NamedPath; -use crate::store::ResolvedStore; use crate::store::{ - ChunkKey, Dataset, Error, InMemoryStore, StoreSlotId, TASK_ID_SUCCESS, Table, TaskResult, + ChunkKey, Dataset, InMemoryStore, ResolvedStore, StoreSlotId, Table, TaskResult, }; +use crate::store::{LayerInfo, TASK_ID_SUCCESS}; #[derive(Debug)] +#[cfg_attr(target_arch = "wasm32", derive(Clone, Copy, Default))] pub struct RerunCloudHandlerSettings { + #[cfg(not(target_arch = "wasm32"))] storage_dir: tempfile::TempDir, } +#[cfg(not(target_arch = "wasm32"))] impl Default for RerunCloudHandlerSettings { fn default() -> Self { Self { + #[cfg(not(target_arch = "wasm32"))] storage_dir: create_data_dir().expect("Failed to create data directory"), } } } +#[cfg(not(target_arch = "wasm32"))] fn create_data_dir() -> Result { Ok(tempfile::Builder::new().prefix("rerun-data-").tempdir()?) } +fn apply_segment_id_filter( + batch: RecordBatch, + filter: Option<&SegmentIdFilter>, +) -> tonic::Result { + let Some(filter) = filter else { + return Ok(batch); + }; + let Some(strategy) = filter.strategy.as_ref() else { + return Ok(batch); + }; + let (ids, scan_only) = match strategy { + segment_id_filter::Strategy::ScanOnly(ids) => (&ids.segment_ids, true), + segment_id_filter::Strategy::Skip(ids) => (&ids.segment_ids, false), + }; + let ids = ids.iter().map(String::as_str).collect::>(); + + let segment_ids = batch + .column_by_name(ScanSegmentTableDataframe::COLUMN_RERUN_SEGMENT_ID_NAME) + .ok_or_else(|| Status::internal("segment ID column is missing"))? + .as_any() + .downcast_ref::() + .ok_or_else(|| Status::internal("segment ID column is not UTF-8"))?; + let mask = segment_ids + .iter() + .map(|segment_id| segment_id.map(|segment_id| ids.contains(segment_id) == scan_only)) + .collect::(); + + arrow::compute::filter_record_batch(&batch, &mask) + .map_err(|err| Status::internal(format!("Unable to apply segment ID filter: {err:#}"))) +} + #[derive(Default)] pub struct RerunCloudHandlerBuilder { settings: RerunCloudHandlerSettings, @@ -78,6 +127,7 @@ impl RerunCloudHandlerBuilder { Self::default() } + #[cfg(not(target_arch = "wasm32"))] pub async fn with_directory_as_dataset( mut self, directory: &NamedPath, @@ -91,6 +141,7 @@ impl RerunCloudHandlerBuilder { Ok(self) } + #[cfg(not(target_arch = "wasm32"))] pub async fn with_rrds_as_dataset( mut self, dataset_name: EntryName, @@ -127,7 +178,7 @@ impl RerunCloudHandlerBuilder { Ok(self) } - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] pub async fn with_directory_as_table( mut self, path: &NamedPath, @@ -156,121 +207,157 @@ impl RerunCloudHandlerBuilder { // --- pub struct RerunCloudHandler { + #[cfg(not(target_arch = "wasm32"))] settings: RerunCloudHandlerSettings, eager_chunk_store_config: re_chunk_store::ChunkStoreConfig, store: tokio::sync::RwLock, + events_tx: tokio::sync::broadcast::Sender, } impl RerunCloudHandler { pub fn new(settings: RerunCloudHandlerSettings, store: InMemoryStore) -> Self { + #[cfg(target_arch = "wasm32")] + let _ = settings; let eager_chunk_store_config = store.eager_chunk_store_config(); + let (events_tx, _) = tokio::sync::broadcast::channel(1024); Self { + #[cfg(not(target_arch = "wasm32"))] settings, eager_chunk_store_config, store: tokio::sync::RwLock::new(store), + events_tx, } } + /// Broadcast a catalog event to all `WatchEvents` subscribers. + fn notify(&self, kind: watch_events_response::Kind) { + // A send error just means there are no subscribers, which is fine. + let _ = self + .events_tx + .send(WatchEventsResponse { kind: Some(kind) }) + .ok(); + } + /// Returns all the chunk stores of the specified dataset and segment ids. If `segment_ids` - /// is empty, return stores of all segments. + /// is `None`, return stores of all segments. /// /// Returns (segment id, layer name, store) tuples. async fn get_chunk_stores( &self, dataset_id: EntryId, - segment_ids: &[SegmentId], - ) -> tonic::Result> { + segment_ids: Option<&[SegmentId]>, + ) -> tonic::Result> { let store = self.store.read().await; let dataset = store.dataset(dataset_id)?; Ok(dataset - .segments_from_ids(segment_ids)? + .segments_from_ids(segment_ids) .flat_map(|(segment_id, segment)| { - segment.iter_layers().map(|(layer_name, layer)| { + segment.iter_sources().map(|(layer_name, source)| { ( segment_id.clone(), - layer_name.to_owned(), - layer.store_slot_id(), - layer.resolved_store().clone(), + layer_name.clone(), + source.store_slot_id(), + source.resolved_store().clone(), ) }) }) .collect()) } - fn resolve_data_sources(data_sources: &[DataSource]) -> tonic::Result> { + #[cfg_attr(target_arch = "wasm32", expect(clippy::unused_async))] + async fn resolve_data_sources(data_sources: &[DataSource]) -> tonic::Result> { let mut resolved = Vec::::with_capacity(data_sources.len()); for source in data_sources { if source.is_prefix { - if source.storage_url.scheme() == "memory" { + #[cfg(target_arch = "wasm32")] + { + // TODO(RR-5155): Support enumerating OPFS directories for prefix registration. return Err(tonic::Status::invalid_argument( - "memory:// URLs cannot be used as prefix data sources", + "prefix data sources are not supported on wasm", )); } - let path = source.storage_url.to_file_path().map_err(|_err| { - tonic::Status::invalid_argument(format!( - "getting file path from {:?}", - source.storage_url - )) - })?; - let meta = std::fs::metadata(&path).map_err(|err| match err.kind() { - std::io::ErrorKind::NotFound => { - tonic::Status::invalid_argument(format!("Directory not found: {:?}", &path)) - } - _ => tonic::Status::invalid_argument(format!( - "Failed to read directory metadata {path:?}: {err:#}" - )), - })?; - if !meta.is_dir() { - return Err(tonic::Status::invalid_argument(format!( - "expected prefix / directory but got an object ({path:?})" - ))); - } - - // Recursively walk the directory and grab all '.rrd' files - let mut dirs_to_visit = vec![path]; - let mut files = Vec::new(); - while let Some(current_dir) = dirs_to_visit.pop() { - let entries = std::fs::read_dir(¤t_dir).map_err(|err| { - tonic::Status::internal(format!( - "Failed to read directory {current_dir:?}: {err:#}" + #[cfg(not(target_arch = "wasm32"))] + { + if source.storage_url.scheme() == "memory" { + return Err(tonic::Status::invalid_argument( + "memory:// URLs cannot be used as prefix data sources", + )); + } + let path = source.storage_url.to_file_path().map_err(|_err| { + tonic::Status::invalid_argument(format!( + "getting file path from {:?}", + source.storage_url )) })?; + let meta = + tokio::fs::metadata(&path) + .await + .map_err(|err| match err.kind() { + std::io::ErrorKind::NotFound => tonic::Status::invalid_argument( + format!("Directory not found: {path:?}"), + ), + _ => tonic::Status::invalid_argument(format!( + "Failed to read directory metadata {path:?}: {err:#}" + )), + })?; + if !meta.is_dir() { + return Err(tonic::Status::invalid_argument(format!( + "expected prefix / directory but got an object ({path:?})" + ))); + } - for entry in entries { - let entry = entry.map_err(|err| { + // Recursively walk the directory and grab all '.rrd' files + let mut dirs_to_visit = vec![path]; + let mut files = Vec::new(); + + while let Some(current_dir) = dirs_to_visit.pop() { + let mut entries = + tokio::fs::read_dir(¤t_dir).await.map_err(|err| { + tonic::Status::internal(format!( + "Failed to read directory {current_dir:?}: {err:#}" + )) + })?; + + while let Some(entry) = entries.next_entry().await.map_err(|err| { tonic::Status::internal(format!( "Failed to read directory entry: {err:#}" )) - })?; - let entry_path = entry.path(); - - if entry_path.is_dir() { - dirs_to_visit.push(entry_path); - } else if let Some(extension) = entry_path.extension() - && extension == "rrd" - { - files.push(entry_path); + })? { + let entry_path = entry.path(); + let file_type = entry.file_type().await.map_err(|err| { + tonic::Status::internal(format!( + "Failed to read directory entry metadata: {err:#}" + )) + })?; + + if file_type.is_dir() { + dirs_to_visit.push(entry_path); + } else if let Some(extension) = entry_path.extension() + && extension == "rrd" + { + files.push(entry_path); + } } } - } - if files.is_empty() { - return Err(tonic::Status::invalid_argument(format!( - "no rrd files found in {:?}", - source.storage_url - ))); - } + if files.is_empty() { + return Err(tonic::Status::invalid_argument(format!( + "no rrd files found in {:?}", + source.storage_url + ))); + } - for file_path in files { - let mut file_url = source.storage_url.clone(); - file_url.set_path(&file_path.to_string_lossy()); - resolved.push(DataSource { - storage_url: file_url, - is_prefix: false, - ..source.clone() - }); + for file_path in files { + let mut file_url = source.storage_url.clone(); + file_url.set_path(&file_path.to_string_lossy()); + resolved.push(DataSource { + storage_url: file_url, + is_prefix: false, + ..source.clone() + }); + } } } else { resolved.push(source.clone()); @@ -313,14 +400,16 @@ macro_rules! decl_stream { }; } +decl_stream!(DoBandwidthTestResponseStream); +decl_stream!(WatchEventsResponseStream); decl_stream!(FetchChunksResponseStream); +decl_stream!(GetAssetsForSegmentResponseStream); decl_stream!(GetRrdManifestResponseStream); decl_stream!(QueryDatasetResponseStream); decl_stream!(QueryTasksOnCompletionResponseStream); decl_stream!(ScanDatasetManifestResponseStream); decl_stream!(ScanSegmentTableResponseStream); decl_stream!(ScanTableResponseStream); -decl_stream!(SearchDatasetResponseStream); decl_stream!(UnregisterFromDatasetResponseStream); impl RerunCloudHandler { @@ -421,6 +510,57 @@ impl RerunCloudHandler { } } +/// Verifies that the referenced blueprint dataset (if any) exists and is itself a blueprint dataset. +/// +/// Internal consistency of the `DatasetDetails`/`TableDetails` is checked separately via their +/// `validate_consistency` methods. +fn validate_blueprint_dataset( + store: &InMemoryStore, + blueprint_dataset: Option, + entry_kind: &str, +) -> tonic::Result<()> { + let Some(blueprint_dataset) = blueprint_dataset else { + return Ok(()); + }; + + let blueprint_dataset = store.dataset(blueprint_dataset).map_err(|err| { + tonic::Status::invalid_argument(format!( + "{entry_kind} blueprint dataset does not exist: {err}" + )) + })?; + + if blueprint_dataset.store_kind() != StoreKind::Blueprint { + return Err(tonic::Status::invalid_argument(format!( + "{entry_kind} blueprint dataset must be a blueprint dataset" + ))); + } + + Ok(()) +} + +/// Same as [`validate_blueprint_dataset`], for the asset dataset. +fn validate_asset_dataset( + store: &InMemoryStore, + asset_dataset: Option, +) -> tonic::Result<()> { + let Some(asset_dataset) = asset_dataset else { + return Ok(()); + }; + + let asset_dataset = store.dataset(asset_dataset).map_err(|err| { + tonic::Status::invalid_argument(format!("asset dataset does not exist: {err}")) + })?; + + let kind = asset_dataset.dataset_kind(); + if kind != DatasetKind::Asset { + return Err(tonic::Status::invalid_argument(format!( + "asset dataset reference must point to an asset dataset, this is a {kind:?} dataset" + ))); + } + + Ok(()) +} + #[tonic::async_trait] impl RerunCloudService for RerunCloudHandler { async fn version( @@ -438,6 +578,7 @@ impl RerunCloudService for RerunCloudHandler { version: re_build_info::exposed_version!().to_owned(), cloud_provider: None, cloud_region: None, + features: re_protos::cloud::v1alpha1::features::all_supported_features(), }, )) } @@ -456,83 +597,159 @@ impl RerunCloudService for RerunCloudHandler { )) } + type DoBandwidthTestStream = DoBandwidthTestResponseStream; + + async fn do_bandwidth_test( + &self, + request: tonic::Request, + ) -> tonic::Result> { + let re_protos::cloud::v1alpha1::DoBandwidthTestRequest { num_bytes } = request.into_inner(); + let max = ext::MAX_BANDWIDTH_TEST_BYTES; + if num_bytes > max { + return Err(Status::invalid_argument(format!( + "num_bytes ({num_bytes}) exceeds the maximum of {max}" + ))); + } + Ok(tonic::Response::new( + Box::pin(bandwidth_test_stream(num_bytes)) as Self::DoBandwidthTestStream, + )) + } + + type WatchEventsStream = WatchEventsResponseStream; + + async fn watch_events( + &self, + request: Request, + ) -> tonic::Result> { + let rx = self.events_tx.subscribe(); + + let kinds = request.into_inner().kinds; + + let stream = futures::stream::unfold((rx, kinds), |(mut rx, kinds)| async move { + loop { + match rx.recv().await { + Ok(event) => { + if kinds.is_empty() { + return Some((Ok(event), (rx, kinds))); + } + + let subscribed = event.kind.is_some_and(|kind| kind.is_entry_kind()) + && kinds.contains(&EventKind::entry()); + + if subscribed { + return Some((Ok(event), (rx, kinds))); + } + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {} + Err(tokio::sync::broadcast::error::RecvError::Closed) => return None, + } + } + }); + + Ok(tonic::Response::new( + Box::pin(stream) as Self::WatchEventsStream + )) + } + // --- Catalog --- async fn find_entries( &self, request: tonic::Request, ) -> tonic::Result> { - let filter = request.into_inner().filter; - let entry_id = filter - .as_ref() - .and_then(|filter| filter.id) - .map(TryInto::try_into) - .transpose()?; + let filter = request.into_inner().filter.unwrap_or_default(); + + let entry_id = filter.id.map(TryInto::try_into).transpose()?; let name = filter - .as_ref() - .and_then(|filter| filter.name.clone()) + .name .map(EntryName::new) .transpose() .map_err(|err| Status::invalid_argument(err.to_string()))?; - let kind = filter - .and_then(|filter| filter.entry_kind) - .map(EntryKind::try_from) - .transpose() - .map_err(|err| { + + // `entry_kinds` (new, repeated) always wins over the legacy singular `entry_kind` when + // both are set. `ENTRY_KIND_UNSPECIFIED` is rejected outright; unknown *positive* values + // (kinds newer than this server knows about) are intentionally allowed through and + // simply match no entry, so a client requesting them degrades gracefully instead of + // erroring out (forward compat, mirrors Rerun Hub). + if filter + .entry_kinds + .contains(&(EntryKind::Unspecified as i32)) + { + return Err(Status::invalid_argument( + "find_entries: entry_kinds must not contain ENTRY_KIND_UNSPECIFIED", + )); + } + + // The effective set of raw `EntryKind` values to match against. `None` for the + // kind-less default. + let effective_kinds: Option> = if !filter.entry_kinds.is_empty() { + Some(filter.entry_kinds) + } else if let Some(kind) = filter.entry_kind { + // Legacy singular field (pre hub 0.15) + let kind = EntryKind::try_from(kind).map_err(|err| { Status::invalid_argument(format!("find_entries: invalid entry kind {err}")) })?; - - let entries = match kind { - Some(EntryKind::Dataset) => { - self.find_datasets(entry_id, name, Some(StoreKind::Recording)) - .await? + if kind == EntryKind::Unspecified { + return Err(Status::invalid_argument( + "find_entries: entry kind unspecified", + )); } + Some(vec![kind as i32]) + } else { + None + }; - Some(EntryKind::BlueprintDataset) => { - self.find_datasets(entry_id, name, Some(StoreKind::Blueprint)) - .await? - } + let matches_kind = |raw_kind: i32| match &effective_kinds { + Some(kinds) => kinds.contains(&raw_kind), + // When neither the new `entry_kinds` nor legacy `entry_kind` (singular) + // are specified we fall back to the legacy default. + // + // See RR-5186. + None => EntryKind::try_from(raw_kind).is_ok_and(EntryKind::is_legacy_default_kind), + }; - Some(EntryKind::Table) => self.find_tables(entry_id, name).await?, + let soften_not_found = |result: tonic::Result>| match result { + Ok(entries) => Ok(entries), + // this is a find. Degrade a NotFound to an empty result set. + Err(err) if err.code() == Code::NotFound => Ok(vec![]), + Err(err) => Err(err), + }; - Some(EntryKind::DatasetView | EntryKind::TableView) => { - return Err(Status::unimplemented( - "find_entries: dataset and table views are not supported", - )); + let mut entries = if effective_kinds.is_some() { + // `Dataset` and `AssetDataset` are both backed by `StoreKind::Recording`, so a + // request for just one of them still has to fetch the whole recording family and + // filter by actual kind below (an asset dataset otherwise leaks into + // `entry_kind=Dataset` results). + let mut entries = Vec::new(); + if matches_kind(EntryKind::Dataset as i32) + || matches_kind(EntryKind::AssetDataset as i32) + { + let result = self + .find_datasets(entry_id, name.clone(), Some(StoreKind::Recording)) + .await; + entries.extend(soften_not_found(result)?); } - - Some(EntryKind::Unspecified) => { - return Err(Status::invalid_argument( - "find_entries: entry kind unspecified", - )); + if matches_kind(EntryKind::BlueprintDataset as i32) { + let result = self + .find_datasets(entry_id, name.clone(), Some(StoreKind::Blueprint)) + .await; + entries.extend(soften_not_found(result)?); } - - None => { - let mut datasets = match self.find_datasets(entry_id, name.clone(), None).await { - Ok(datasets) => datasets, - Err(err) => { - if err.code() == Code::NotFound { - vec![] - } else { - return Err(err); - } - } - }; - let tables = match self.find_tables(entry_id, name).await { - Ok(tables) => tables, - Err(err) => { - if err.code() == Code::NotFound { - vec![] - } else { - return Err(err); - } - } - }; - datasets.extend(tables); - datasets + if matches_kind(EntryKind::Table as i32) { + let result = self.find_tables(entry_id, name.clone()).await; + entries.extend(soften_not_found(result)?); } + entries + } else { + let datasets = self.find_datasets(entry_id, name.clone(), None).await; + let mut datasets = soften_not_found(datasets)?; + let tables = self.find_tables(entry_id, name.clone()).await; + datasets.extend(soften_not_found(tables)?); + datasets }; + entries.retain(|entry| matches_kind(entry.entry_kind)); + let response = re_protos::cloud::v1alpha1::FindEntriesResponse { entries }; Ok(tonic::Response::new(response)) @@ -552,6 +769,12 @@ impl RerunCloudService for RerunCloudHandler { let dataset_id = store.create_dataset(dataset_name, dataset_id)?; let dataset = store.dataset(dataset_id)?; + self.notify(watch_events_response::Kind::EntryCreated( + EntryCreatedEvent { + id: Some(dataset_id.into()), + }, + )); + Ok(tonic::Response::new( CreateDatasetEntryResponse { dataset: dataset.as_dataset_entry(), @@ -583,10 +806,38 @@ impl RerunCloudService for RerunCloudHandler { { let request: UpdateDatasetEntryRequest = request.into_inner().try_into()?; + request + .dataset_details + .validate_consistency() + .map_err(|err| tonic::Status::invalid_argument(err.to_string()))?; + let mut store = self.store.write().await; + validate_blueprint_dataset(&store, request.dataset_details.blueprint_dataset, "dataset")?; + + let mut dataset_details = request.dataset_details; + + // The asset dataset reference is server-managed: unless the client explicitly points it + // at a new asset dataset, keep the stored one. Recording datasets created before asset + // datasets were introduced have none, so create the missing one on demand, and replace a + // reference left dangling by a deleted asset dataset the same way. + let dataset = store.dataset(request.id)?; + let stored_asset_dataset = dataset.dataset_details().asset_dataset; + let dataset_kind = dataset.dataset_kind(); + let client_chosen_asset_dataset = dataset_details.asset_dataset.is_some() + && dataset_details.asset_dataset != stored_asset_dataset; + if client_chosen_asset_dataset { + validate_asset_dataset(&store, dataset_details.asset_dataset)?; + } else if dataset_kind == DatasetKind::Recording { + let existing = stored_asset_dataset.filter(|id| store.dataset(*id).is_ok()); + dataset_details.asset_dataset = Some(match existing { + Some(existing) => existing, + None => store.create_asset_dataset_for_entry(request.id)?, + }); + } + let dataset = store.dataset_mut(request.id)?; - dataset.set_dataset_details(request.dataset_details); + dataset.set_dataset_details(dataset_details); Ok(tonic::Response::new( UpdateDatasetEntryResponse { @@ -620,6 +871,53 @@ impl RerunCloudService for RerunCloudHandler { )) } + async fn update_table_entry( + &self, + request: tonic::Request, + ) -> tonic::Result> { + let request: UpdateTableEntryRequest = request.into_inner().try_into()?; + + let mut store = self.store.write().await; + store.table(request.id).ok_or_else(|| { + tonic::Status::not_found(format!("table with entry ID '{}' not found", request.id)) + })?; + + let mut table_details = request.table_details; + // Backwards compatibility: tables created before table blueprints had no associated + // blueprint dataset. If a client updates such a table without providing one, create the + // missing dataset on demand. + if table_details.blueprint_dataset.is_none() + && table_details.default_blueprint_segment.is_some() + { + table_details.blueprint_dataset = Some( + match store + .table(request.id) + .and_then(|table| table.table_details().blueprint_dataset) + { + Some(blueprint_dataset) => blueprint_dataset, + None => store.create_blueprint_dataset_for_entry(request.id)?, + }, + ); + } + + table_details + .validate_consistency() + .map_err(|err| tonic::Status::invalid_argument(err.to_string()))?; + validate_blueprint_dataset(&store, table_details.blueprint_dataset, "table")?; + + let table = store.table_mut(request.id).ok_or_else(|| { + tonic::Status::not_found(format!("table with entry ID '{}' not found", request.id)) + })?; + table.set_table_details(table_details); + + Ok(tonic::Response::new( + UpdateTableEntryResponse { + table_entry: table.as_table_entry(), + } + .try_into()?, + )) + } + async fn delete_entry( &self, request: tonic::Request, @@ -628,6 +926,12 @@ impl RerunCloudService for RerunCloudHandler { self.store.write().await.delete_entry(entry_id)?; + self.notify(watch_events_response::Kind::EntryDeleted( + EntryDeletedEvent { + id: Some(entry_id.into()), + }, + )); + Ok(tonic::Response::new(DeleteEntryResponse {})) } @@ -655,7 +959,6 @@ impl RerunCloudService for RerunCloudHandler { } // --- Manifest Registry --- - async fn register_with_dataset( &self, request: tonic::Request, @@ -670,272 +973,29 @@ impl RerunCloudService for RerunCloudHandler { on_duplicate, } = request.into_inner().try_into()?; - let data_sources = Self::resolve_data_sources(&data_sources)?; + let data_sources = Self::resolve_data_sources(&data_sources).await?; if data_sources.is_empty() { return Err(tonic::Status::invalid_argument( "no data sources to register", )); } - // Phase 1: Extract store IDs cheaply and check for intra-request duplicates. - // - // We extract store IDs from the RRD footer (fast) or by scanning messages - // for SetStoreInfo (fallback for older files without footers). This avoids - // full chunk loading on the unhappy path (duplicates found). - // - // The `on_duplicate` flag only affects cross-request duplicates (conflicts with - // already-registered segments), not intra-request duplicates. - enum ValidatedSource { - File { - rrd_path: PathBuf, - layer_name: String, - storage_url: url::Url, - }, - Memory { - store_slot_id: StoreSlotId, - resolved: ResolvedStore, - segment_id: SegmentId, - layer_name: String, - }, - } - - let mut seen: BTreeMap<(String, String), Vec> = BTreeMap::new(); - let mut validated_sources: Vec = Vec::new(); - - let store_kind = store.dataset(dataset_id)?.store_kind(); - - for source in data_sources { - let ext::DataSource { - storage_url, - is_prefix, - layer, - kind, - } = source; - - // TODO(ab): Should some or all of these errors be returned as task error instead? - // (No point in doing so unless this is tested in re_redap_tests.) - if is_prefix { - return Err(tonic::Status::internal( - "register_with_dataset: prefix data sources should have been resolved already", - )); - } - - if kind != ext::DataSourceKind::Rrd { - return Err(tonic::Status::unimplemented( - "register_with_dataset: only RRD data sources are implemented", - )); - } - - let layer = if layer.is_empty() { - DataSource::DEFAULT_LAYER.to_owned() - } else { - layer - }; - - // Handle memory:// URLs (re-registration of existing stores) - if storage_url.scheme() == "memory" { - let store_slot_id = parse_memory_url(&storage_url)?; - let resolved = store.resolve_store(&store_slot_id).ok_or_else(|| { - tonic::Status::not_found(format!( - "store not found for memory URL: {storage_url}" - )) - })?; - let store_id = resolved.store_id(); - if store_id.kind() != store_kind { - continue; - } - let segment_id = SegmentId::new(store_id.recording_id().to_string()); - let key = (segment_id.id.clone(), layer.clone()); - seen.entry(key).or_default().push(storage_url.clone()); - validated_sources.push(ValidatedSource::Memory { - store_slot_id, - resolved, - segment_id, - layer_name: layer, - }); - continue; - } - - let Ok(rrd_path) = storage_url.to_file_path() else { - return if storage_url.scheme() == "file" && storage_url.host().is_some() { - Err(tonic::Status::not_found(format!( - "RRD file not found, file URI should not have a host: {storage_url} (this may be caused by invalid relative-path URI)" - ))) - } else { - Err(tonic::Status::not_found(format!( - "RRD file not found, could not load URI: {storage_url}" - ))) - }; - }; - - if !rrd_path.exists() { - return Err(tonic::Status::not_found(format!( - "RRD file not found, file does not exists: {rrd_path:?}" - ))); - } - - if !rrd_path.is_file() { - return Err(tonic::Status::not_found(format!( - "RRD file not found, path is not a file: {rrd_path:?}" - ))); - } - - // Extract store IDs cheaply (footer or message scan, no chunk loading) - let store_ids = load_store_ids(&rrd_path)?; - - for store_id in store_ids { - if store_id.kind() != store_kind { - continue; - } - - let segment_id_str = store_id.recording_id().to_string(); - let key = (segment_id_str, layer.clone()); - - seen.entry(key).or_default().push(storage_url.clone()); - } - - validated_sources.push(ValidatedSource::File { - rrd_path, - layer_name: layer, - storage_url, - }); - } - - // Check for intra-request duplicates - let duplicates: Vec<_> = seen.iter().filter(|(_, urls)| urls.len() > 1).collect(); - - if !duplicates.is_empty() { - let details: Vec = duplicates - .iter() - .map(|((segment_id, layer), urls)| { - let uri_lines = urls - .iter() - .map(|u| format!(" {u}")) - .collect::>() - .join("\n"); - format!(" segment id: {segment_id}, layer name: {layer}\n{uri_lines}") - }) - .collect(); - return Err(tonic::Status::invalid_argument(format!( - "duplicate segment layers in request:\n{}", - details.join("\n") - ))); - } - - // Phase 2: Load file sources and unify with memory sources into a common form. - struct ReadySource { - store_slot_id: StoreSlotId, - resolved: ResolvedStore, - segment_id: SegmentId, - layer_name: String, - storage_url: String, - } - - let mut ready_sources: Vec = Vec::new(); - - for source in validated_sources { - match source { - ValidatedSource::Memory { - store_slot_id, - resolved, - segment_id, - layer_name, - } => { - ready_sources.push(ReadySource { - storage_url: format!("memory:///store/{store_slot_id}"), - store_slot_id, - resolved, - segment_id, - layer_name, - }); - } - - ValidatedSource::File { - rrd_path, - layer_name, - storage_url, - } => { - re_log::info!("Loading RRD: {}", rrd_path.display()); - - for (store_id, resolved) in ResolvedStore::load_rrd_file(&rrd_path, store_kind)? - { - ready_sources.push(ReadySource { - store_slot_id: StoreSlotId::new(), - resolved, - segment_id: SegmentId::new(store_id.recording_id().to_string()), - layer_name: layer_name.clone(), - storage_url: storage_url.to_string(), - }); - } - } - } - } - - // Phase 3: Register all stores in the pool, then add layers to dataset. - let mut segment_ids: Vec = vec![]; - let mut segment_layers: Vec = vec![]; - let mut segment_types: Vec = vec![]; - let mut storage_urls: Vec = vec![]; - let mut task_ids: Vec = vec![]; - let mut failed_task_results: Vec<(TaskId, TaskResult)> = vec![]; - - for source in &ready_sources { - store.register_store_with_id(source.store_slot_id, &source.resolved); - } - - { - let dataset = store.dataset_mut(dataset_id)?; - - for source in ready_sources { - let add_result = dataset - .add_layer( - source.segment_id.clone(), - source.layer_name.clone(), - source.store_slot_id, - source.resolved, - on_duplicate, - ) - .await; - - match add_result { - Ok(()) => { - segment_ids.push(source.segment_id.to_string()); - segment_layers.push(source.layer_name); - segment_types.push("rrd".to_owned()); - storage_urls.push(source.storage_url); - task_ids.push(TASK_ID_SUCCESS.to_owned()); - } - - Err(Error::SchemaConflict(msg)) => { - segment_ids.push(String::new()); - segment_layers.push(source.layer_name); - segment_types.push("rrd".to_owned()); - storage_urls.push(source.storage_url); - - let task_id = TaskId::new(); - task_ids.push(task_id.id.clone()); - failed_task_results.push((task_id, TaskResult::failed(&msg))); - } - - Err(other_err) => { - return Err(other_err.into()); - } - } - } - } - - // Register all task results now that the mutable borrow of dataset is done - for (task_id, result) in failed_task_results { - store.task_registry().register_failure(task_id, result); - } - - let record_batch = RegisterWithDatasetResponse::create_dataframe( + let RegisterWithDatasetResult { segment_ids, segment_layers, segment_types, storage_urls, task_ids, - ) + } = do_register_with_dataset(&mut store, dataset_id, data_sources, on_duplicate).await?; + + let record_batch = RegisterWithDatasetDataframe { + rerun_segment_id: segment_ids.into(), + rerun_segment_layer: segment_layers.into(), + rerun_segment_type: segment_types.into(), + rerun_storage_url: storage_urls.into(), + rerun_task_id: task_ids.into(), + } + .into_record_batch() .map_err(|err| tonic::Status::internal(format!("Failed to create dataframe: {err:#}")))?; Ok(tonic::Response::new( re_protos::cloud::v1alpha1::RegisterWithDatasetResponse { @@ -963,21 +1023,24 @@ impl RerunCloudService for RerunCloudHandler { force: _, // OSS doesn't even have statuses } = request.into_inner().try_into()?; - let segments_to_drop = segments_to_drop.iter().collect(); - let layers_to_drop = layers_to_drop.iter().map(|s| s.as_str()).collect(); - - let dataset_manifest_removed = - dataset.dataset_manifest_filtered(&segments_to_drop, &layers_to_drop)?; + // As per our proto conventions, an empty list means "all": + let segments_to_drop: Option> = + (!segments_to_drop.is_empty()).then(|| segments_to_drop.iter().collect()); + let layers_to_drop: Option> = + (!layers_to_drop.is_empty()).then(|| layers_to_drop.iter().collect()); _ = dataset - .remove_layers(&segments_to_drop, &layers_to_drop) + .remove_layers(segments_to_drop.as_ref(), layers_to_drop.as_ref()) .await?; store.cleanup_store_pool(); let stream = futures::stream::once(async move { Ok(re_protos::cloud::v1alpha1::UnregisterFromDatasetResponse { - data: Some(dataset_manifest_removed.into()), + data: Some(ScanDatasetManifestDataframe::empty_record_batch().into()), + task_id: Some(TaskId { + id: TASK_ID_SUCCESS.to_owned(), + }), }) }); @@ -1034,7 +1097,7 @@ impl RerunCloudService for RerunCloudHandler { StoreId::new( StoreKind::Recording, entry_id.to_string(), - segment_id.id.clone(), + segment_id.clone(), ), self.eager_chunk_store_config.clone(), ) @@ -1061,9 +1124,11 @@ impl RerunCloudService for RerunCloudHandler { for (entity_path, store_slot_id, resolved) in handles { dataset - .add_layer( + .add_source( entity_path, - DataSource::DEFAULT_LAYER.to_owned(), + Arc::new(LayerInfo { + name: LayerName::base(), + }), store_slot_id, resolved, IfDuplicateBehavior::Error, @@ -1102,21 +1167,33 @@ impl RerunCloudService for RerunCloudHandler { tonic::Status::internal(format!("Could not decode chunk: {err:#}")) })?; - let mut store = self.store.write().await; - let Some(table) = store.table_mut(entry_id) else { - return Err(tonic::Status::not_found("table not found")); - }; - let insert_op = match TableInsertMode::try_from(write_msg.insert_mode) - .map_err(|err| Status::invalid_argument(err.to_string()))? + let insert_op = TableInsertMode::try_from(write_msg.insert_mode) + .map_err(|err| Status::invalid_argument(err.to_string()))?; + + #[cfg(feature = "lance")] { - TableInsertMode::Append => InsertOp::Append, - TableInsertMode::Overwrite => InsertOp::Overwrite, - TableInsertMode::Replace => InsertOp::Replace, - }; + let mut store = self.store.write().await; + let Some(table) = store.table_mut(entry_id) else { + return Err(tonic::Status::not_found("table not found")); + }; + table.write_table(rb, insert_op).await.map_err(|err| { + tonic::Status::internal(format!("error writing to table: {err:#}")) + })?; + } - table.write_table(rb, insert_op).await.map_err(|err| { - tonic::Status::internal(format!("error writing to table: {err:#}")) - })?; + #[cfg(not(feature = "lance"))] + { + let mut table = { + let store = self.store.read().await; + store + .table(entry_id) + .cloned() + .ok_or_else(|| tonic::Status::not_found("table not found"))? + }; + table.write_table(rb, insert_op).await.map_err(|err| { + tonic::Status::internal(format!("error writing to table: {err:#}")) + })?; + } } Ok(tonic::Response::new( @@ -1135,7 +1212,7 @@ impl RerunCloudService for RerunCloudHandler { let entry_id = get_entry_id_from_headers(&store, &request)?; let dataset = store.dataset(entry_id)?; - let record_batch = dataset.segment_table().map_err(|err| { + let record_batch = dataset.segment_table().await.map_err(|err| { tonic::Status::internal(format!("Unable to read segment table: {err:#}")) })?; @@ -1160,15 +1237,17 @@ impl RerunCloudService for RerunCloudHandler { &self, request: tonic::Request, ) -> tonic::Result> { - let store = self.store.read().await; - let entry_id = get_entry_id_from_headers(&store, &request)?; - - let request = request.into_inner(); + let (mut record_batch, request) = { + let store = self.store.read().await; + let entry_id = get_entry_id_from_headers(&store, &request)?; + let dataset = store.dataset(entry_id)?; + let record_batch = dataset.segment_table().await.map_err(|err| { + tonic::Status::internal(format!("Unable to read segment table: {err:#}")) + })?; + (record_batch, request.into_inner()) + }; - let dataset = store.dataset(entry_id)?; - let mut record_batch = dataset.segment_table().map_err(|err| { - tonic::Status::internal(format!("Unable to read segment table: {err:#}")) - })?; + record_batch = apply_segment_id_filter(record_batch, request.segment_id_filter.as_ref())?; // project columns if !request.columns.is_empty() { @@ -1198,7 +1277,7 @@ impl RerunCloudService for RerunCloudHandler { let entry_id = get_entry_id_from_headers(&store, &request)?; let dataset = store.dataset(entry_id)?; - let record_batch = dataset.dataset_manifest()?; + let record_batch = dataset.dataset_manifest().await?; Ok(tonic::Response::new(GetDatasetManifestSchemaResponse { schema: Some( @@ -1221,13 +1300,15 @@ impl RerunCloudService for RerunCloudHandler { &self, request: Request, ) -> tonic::Result> { - let store = self.store.read().await; - let entry_id = get_entry_id_from_headers(&store, &request)?; - - let request = request.into_inner(); + let (mut record_batch, request) = { + let store = self.store.read().await; + let entry_id = get_entry_id_from_headers(&store, &request)?; + let dataset = store.dataset(entry_id)?; + let record_batch = dataset.dataset_manifest().await?; + (record_batch, request.into_inner()) + }; - let dataset = store.dataset(entry_id)?; - let mut record_batch = dataset.dataset_manifest()?; + record_batch = apply_segment_id_filter(record_batch, request.segment_id_filter.as_ref())?; // project columns if !request.columns.is_empty() { @@ -1303,89 +1384,57 @@ impl RerunCloudService for RerunCloudHandler { )) } - /* Indexing */ + type GetAssetsForSegmentStream = GetAssetsForSegmentResponseStream; - async fn create_index( + async fn get_assets_for_segment( &self, - request: tonic::Request, - ) -> tonic::Result> { - cfg_if! { - if #[cfg(feature = "lance")] { - let store = self.store.read().await; - let entry_id = get_entry_id_from_headers(&store, &request)?; - let dataset = store.dataset(entry_id)?; - - dataset.indexes().create_index(dataset, request.into_inner().try_into()?).await - } else { - let _ = request; - Err(tonic::Status::unimplemented("create_index requires the `lance` feature")) - } - } - } + request: tonic::Request, + ) -> tonic::Result> { + let store = self.store.read().await; - async fn list_indexes( - &self, - request: tonic::Request, - ) -> tonic::Result> { - cfg_if! { - if #[cfg(feature = "lance")] { - let store = self.store.read().await; - let entry_id = get_entry_id_from_headers(&store, &request)?; - let dataset = store.dataset(entry_id)?; - - dataset.indexes().list_indexes(request.into_inner()).await - } else { - let _ = request; - Err(tonic::Status::unimplemented("list_indexes requires the `lance` feature")) - } - } - } + let dataset_id = get_entry_id_from_headers(&store, &request)?; - async fn delete_indexes( - &self, - request: tonic::Request, - ) -> tonic::Result> { - cfg_if! { - if #[cfg(feature = "lance")] { - let store = self.store.read().await; - let entry_id = get_entry_id_from_headers(&store, &request)?; - let dataset = store.dataset(entry_id)?; - - let request = request.into_inner(); - let column = request.column.ok_or_else(|| { - missing_field!(re_protos::cloud::v1alpha1::DeleteIndexesRequest, "column") - })?; + let dataset = store.dataset(dataset_id)?; - dataset.indexes().delete_indexes(column.try_into()?).await - } else { - let _ = request; - Err(tonic::Status::unimplemented("delete_indexes requires the `lance` feature")) - } + let dataset_kind = dataset.dataset_kind(); + if dataset_kind != DatasetKind::Recording { + return Err(tonic::Status::invalid_argument(format!( + "assets can only be queried on recording datasets, this is a {dataset_kind:?} dataset" + ))); } - } - /* Queries */ + // Datasets created before asset datasets were introduced don't have one, which simply + // means no assets were ever registered. One is created on demand when the dataset entry + // is next updated. + let Some(asset_dataset) = dataset.dataset_details().asset_dataset else { + return Ok(tonic::Response::new( + Box::pin(futures::stream::empty()) as Self::GetAssetsForSegmentStream + )); + }; - type SearchDatasetStream = SearchDatasetResponseStream; + // TODO(RR-4979): Filter by properties here. + let asset_segment_ids = store + .dataset(asset_dataset)? + .segments() + .keys() + .cloned() + .map(Into::into) + .collect(); - async fn search_dataset( - &self, - request: tonic::Request, - ) -> tonic::Result> { - cfg_if! { - if #[cfg(feature = "lance")] { - let store = self.store.read().await; - let entry_id = get_entry_id_from_headers(&store, &request)?; - let dataset = store.dataset(entry_id)?; - - Ok(crate::chunk_index::DatasetChunkIndexes::search_dataset(dataset, request.into_inner().try_into()?).await?) - } else { - let _ = request; - Err(tonic::Status::unimplemented("search_dataset requires the `lance` feature")) - } - } + let response = futures::stream::once(futures::future::ok( + re_protos::cloud::v1alpha1::GetAssetsForSegmentResponse { + assets_entry: Some(asset_dataset.into()), + asset_segment_ids, + }, + )); + + Ok(tonic::Response::new( + Box::pin(response) as Self::GetAssetsForSegmentStream + )) } + /* Queries */ + type QueryDatasetStream = QueryDatasetResponseStream; async fn query_dataset( @@ -1427,11 +1476,40 @@ impl RerunCloudService for RerunCloudHandler { )); } - let chunk_stores = self.get_chunk_stores(entry_id, &segment_ids).await?; + // RR-4355: per-segment index value pushdown. + // + // If the request has `query.latest_at.per_segment_values`, build a + // map keyed by segment id so the per-segment chunk-fetch loop below + // can apply it. The ext `try_from` already validated that lengths + // match `segment_ids` and that there are no duplicates. + let per_segment_index_values: Option>> = + match query.as_ref().and_then(|q| q.latest_at.as_ref()) { + Some(la) if !la.per_segment_values.is_empty() => Some( + std::iter::zip(&segment_ids, &la.per_segment_values) + .map(|(sid, values)| { + ( + sid.clone(), + values + .iter() + .map(|v| re_log_types::TimeInt::new_temporal(*v)) + .collect(), + ) + }) + .collect(), + ), + _ => None, + }; + + // As per our proto conventions, an empty list means "all": + let segments_of_interest = (!segment_ids.is_empty()).then_some(segment_ids.as_slice()); + + let chunk_stores = self + .get_chunk_stores(entry_id, segments_of_interest) + .await?; if chunk_stores.is_empty() { let stream = futures::stream::iter([{ - let batch = QueryDatasetResponse::create_empty_dataframe(); + let batch = QueryDatasetDataframe::empty_record_batch(); let data = Some(batch.into()); Ok(QueryDatasetResponse { data }) }]); @@ -1462,8 +1540,62 @@ impl RerunCloudService for RerunCloudHandler { // Build metadata for all relevant chunks (physical + virtual). let metadata_vec: Vec = if let Some(query) = &query { - let (chunks, missing_virtual) = - get_chunks_for_query_results(&resolved, &entity_paths, query); + // RR-4355: per-segment index values pushdown. + // + // When the request carries `per_segment_values`, fan out + // `get_chunks_for_query_results` once per value for this + // segment with a synthesized latest-at, then dedup. Per + // the proto contract (`cloud.proto`): + // "An empty values list for a segment means no temporal + // chunks are returned for that segment (only static + // data)." + // For the empty case we run a single static-only query + // instead of returning nothing, so static chunks still + // surface. + let (chunks, missing_virtual) = if let Some(map) = &per_segment_index_values { + if let Some(values) = map.get(&segment_id) { + let synthesized: Vec = if values.is_empty() { + vec![re_log_types::TimeInt::STATIC] + } else { + values.clone() + }; + let mut all_chunks: Vec> = Vec::new(); + let mut all_missing: BTreeSet = BTreeSet::new(); + let mut seen: BTreeSet = BTreeSet::new(); + for v in &synthesized { + let mut q = query.clone(); + if let Some(la) = q.latest_at.as_mut() { + la.at = *v; + la.per_segment_values = Vec::new(); + } + let (cs, missing) = get_chunks_for_query_results( + &resolved, + &entity_paths, + select_all_entity_paths, + &q, + ); + for c in cs { + if seen.insert(c.id()) { + all_chunks.push(c); + } + } + all_missing.extend(missing); + } + for id in &seen { + all_missing.remove(id); + } + (all_chunks, all_missing.into_iter().collect()) + } else { + (Vec::new(), Vec::new()) + } + } else { + get_chunks_for_query_results( + &resolved, + &entity_paths, + select_all_entity_paths, + query, + ) + }; let mut metas: Vec<_> = chunks .iter() @@ -1535,9 +1667,7 @@ impl RerunCloudService for RerunCloudHandler { .collect(); for meta in &metadata_vec { - if !entity_paths.is_empty() - && !entity_paths.contains(&EntityPath::from(meta.entity_path.as_str())) - { + if !select_all_entity_paths && !entity_paths.contains(&meta.entity_path) { continue; } @@ -1557,8 +1687,8 @@ impl RerunCloudService for RerunCloudHandler { let mut missing_timelines: BTreeSet = timelines.keys().cloned().collect(); - for (timeline, range) in &meta.timelines { - let timeline_name = timeline.name().as_str(); + for (timeline_name, range) in &meta.timelines { + let timeline_name = timeline_name.as_str(); missing_timelines.remove(timeline_name); let timeline_data = timelines @@ -1575,7 +1705,7 @@ impl RerunCloudService for RerunCloudHandler { timeline_data.1.push(None); } - chunk_segment_ids.push(segment_id.id.clone()); + chunk_segment_ids.push(segment_id.clone()); chunk_ids.push(meta.chunk_id); chunk_entity_path.push(meta.entity_path.clone()); chunk_is_static.push(meta.is_static); @@ -1627,6 +1757,9 @@ impl RerunCloudService for RerunCloudHandler { type FetchChunksStream = FetchChunksResponseStream; + // NOTE: OSS server does not detect source drift (a registered rrd file + // being mutated after registration) which Rerun Hub implements. + // Consider if worth having parity (RR-4577). async fn fetch_chunks( &self, request: tonic::Request, @@ -1680,7 +1813,8 @@ impl RerunCloudService for RerunCloudHandler { .store .read() .await - .chunks_from_chunk_keys(&chunk_keys)?; + .chunks_from_chunk_keys(&chunk_keys) + .await?; let stream = futures::stream::iter(chunks).map(|(store_id, chunk)| { let arrow_msg = re_log_types::ArrowMsg { @@ -1715,48 +1849,65 @@ impl RerunCloudService for RerunCloudHandler { &self, request: tonic::Request, ) -> tonic::Result> { - #[cfg_attr(not(feature = "lance"), expect(unused_mut))] - let mut store = self.store.write().await; - let request = request.into_inner(); - let Some(provider_details) = request.provider_details else { - return Err(tonic::Status::invalid_argument("Missing provider details")); - }; - #[cfg_attr(not(feature = "lance"), expect(unused_variables))] - let lance_table = match ProviderDetails::try_from(&provider_details) { - Ok(ProviderDetails::LanceTable(lance_table)) => lance_table.table_url, - Ok(ProviderDetails::SystemTable(_)) => Err(Status::invalid_argument( - "System tables cannot be registered", - ))?, - Err(err) => return Err(err.into()), + #[cfg(target_arch = "wasm32")] + { + let _ = request; + return Err(tonic::Status::unimplemented( + "register_table is not supported on wasm", + )); } - .to_file_path() - .map_err(|()| tonic::Status::invalid_argument("Invalid lance table path"))?; - #[cfg(feature = "lance")] - let entry_id = { - let named_path = NamedPath { - name: Some(request.name.clone()), - path: lance_table, + #[cfg(not(target_arch = "wasm32"))] + { + #[cfg_attr(not(feature = "lance"), expect(unused_mut))] + let mut store = self.store.write().await; + let request = request.into_inner(); + let Some(provider_details) = request.provider_details else { + return Err(tonic::Status::invalid_argument("Missing provider details")); }; + #[cfg_attr(not(feature = "lance"), expect(unused_variables))] + let lance_table = match ProviderDetails::try_from(&provider_details) { + Ok(ProviderDetails::LanceTable(lance_table)) => lance_table.table_url, + Ok(ProviderDetails::SystemTable(_)) => Err(Status::invalid_argument( + "System tables cannot be registered", + ))?, + Err(err) => return Err(err.into()), + } + .to_file_path() + .map_err(|()| tonic::Status::invalid_argument("Invalid lance table path"))?; + + #[cfg(feature = "lance")] + let entry_id = { + let named_path = NamedPath { + name: Some(request.name.clone()), + path: lance_table, + }; - store - .load_directory_as_table(&named_path, IfDuplicateBehavior::Error) - .await? - }; + store + .load_directory_as_table(&named_path, IfDuplicateBehavior::Error) + .await? + }; - #[cfg(not(feature = "lance"))] - let entry_id = EntryId::new(); + #[cfg(not(feature = "lance"))] + let entry_id = EntryId::new(); - let table_entry = store - .table(entry_id) - .ok_or_else(|| Status::internal("table missing that was just registered"))? - .as_table_entry(); + let table_entry = store + .table(entry_id) + .ok_or_else(|| Status::internal("table missing that was just registered"))? + .as_table_entry(); - let response = RegisterTableResponse { - table_entry: Some(table_entry.try_into()?), - }; + let response = RegisterTableResponse { + table_entry: Some(table_entry.try_into()?), + }; + + self.notify(watch_events_response::Kind::EntryCreated( + EntryCreatedEvent { + id: Some(entry_id.into()), + }, + )); - Ok(response.into()) + Ok(response.into()) + } } async fn get_table_schema( @@ -1790,19 +1941,21 @@ impl RerunCloudService for RerunCloudHandler { &self, request: tonic::Request, ) -> tonic::Result> { - let store = self.store.read().await; let Some(entry_id) = request.into_inner().table_id else { return Err(Status::not_found("Table ID not specified in request")); }; let entry_id = entry_id.try_into()?; - let table = store - .table(entry_id) - .ok_or_else(|| Status::not_found(format!("Entry with ID {entry_id} not found")))?; + let provider = { + let store = self.store.read().await; + let table = store + .table(entry_id) + .ok_or_else(|| Status::not_found(format!("Entry with ID {entry_id} not found")))?; + table.provider() + }; let ctx = SessionContext::default(); - let plan = table - .provider() + let plan = provider .scan(&ctx.state(), None, &[], None) .await .map_err(|err| Status::internal(format!("failed to scan table: {err:#}")))?; @@ -1844,7 +1997,7 @@ impl RerunCloudService for RerunCloudHandler { .get(&task_id) .unwrap_or_else(TaskResult::success); - ids.push(task_id.id); + ids.push(task_id); exec_statuses.push(result.exec_status); msgs.push(if result.msgs.is_empty() { None @@ -1854,19 +2007,20 @@ impl RerunCloudService for RerunCloudHandler { } let num_tasks = ids.len(); - let rb = QueryTasksResponse::create_dataframe( - ids, - vec![None; num_tasks], // kind - vec![None; num_tasks], // data - exec_statuses, - msgs, - vec![None; num_tasks], // blob_len - vec![None; num_tasks], // lease_owner - vec![None; num_tasks], // lease_expiration - vec![1; num_tasks], // attempts - vec![None; num_tasks], // creation_time - vec![None; num_tasks], // last_update_time - ) + let rb = QueryTasksDataframe { + task_id: ids.into(), + kind: vec![None::; num_tasks].into(), + data: vec![None::; num_tasks].into(), + exec_status: exec_statuses.into(), + msgs: msgs.into(), + blob_len: vec![None::; num_tasks].into(), + lease_owner: vec![None::; num_tasks].into(), + lease_expiration: vec![None::; num_tasks].into(), + attempts: vec![1_u8; num_tasks].into(), + creation_time: vec![None::; num_tasks].into(), + last_update_time: vec![None::; num_tasks].into(), + } + .into_record_batch() .map_err(|err| tonic::Status::internal(format!("Failed to create dataframe: {err:#}")))?; // All tasks finish immediately in the OSS server @@ -1930,13 +2084,19 @@ impl RerunCloudService for RerunCloudHandler { &self, request: Request, ) -> tonic::Result> { - let mut store = self.store.write().await; - let request: CreateTableEntryRequest = request.into_inner().try_into()?; let table_name = request.name; let schema = Arc::new(request.schema); + #[cfg(target_arch = "wasm32")] + let Some(details) = request.provider_details else { + return Err(tonic::Status::unimplemented( + "filesystem-backed table creation is not supported on wasm", + )); + }; + + #[cfg(not(target_arch = "wasm32"))] let details = if let Some(details) = request.provider_details { details } else { @@ -1947,7 +2107,7 @@ impl RerunCloudService for RerunCloudHandler { .storage_dir .path() .join(format!("lance-{}", Tuid::new())); - ProviderDetails::LanceTable(LanceTable { + ProviderDetails::LanceTable(ext::LanceTable { table_url: url::Url::from_directory_path(table_path).map_err(|_err| { Status::internal(format!( "Failed to create table directory in {:?}", @@ -1957,62 +2117,42 @@ impl RerunCloudService for RerunCloudHandler { }) }; - let table = match details { - ProviderDetails::LanceTable(table) => { - store - .create_table_entry(table_name, &table.table_url, schema) - .await? - } - ProviderDetails::SystemTable(_) => { - return Err(tonic::Status::invalid_argument( - "Creating system tables is not supported", - )); - } - }; + #[cfg(target_arch = "wasm32")] + { + let _ = (table_name, schema, details); + return Err(tonic::Status::unimplemented( + "filesystem-backed table creation is not supported on wasm", + )); + } - Ok(Response::new( - CreateTableEntryResponse { table }.try_into()?, - )) - } -} + #[cfg(not(target_arch = "wasm32"))] + { + let table = match details { + ProviderDetails::LanceTable(table) => { + self.store + .write() + .await + .create_table_entry(table_name, &table.table_url, schema) + .await? + } + ProviderDetails::SystemTable(_) => { + return Err(tonic::Status::invalid_argument( + "Creating system tables is not supported", + )); + } + }; -/// Extracts unique store IDs from an RRD file without loading chunk data. -/// -/// Returns a deduplicated set because a single RRD can contain duplicate -/// `SetStoreInfo` messages for the same store. -fn load_store_ids(rrd_path: &std::path::Path) -> tonic::Result> { - let reader = std::io::BufReader::new( - std::fs::File::open(rrd_path) - .map_err(|err| tonic::Status::internal(format!("Failed to open RRD file: {err:#}")))?, - ); - let decoder = re_log_encoding::DecoderApp::decode_lazy(reader); - - let mut store_ids = BTreeSet::new(); - for msg_result in decoder { - let msg = msg_result.map_err(|err| { - tonic::Status::internal(format!("Failed to decode RRD message: {err:#}")) - })?; - if let re_log_types::LogMsg::SetStoreInfo(info) = msg { - store_ids.insert(info.info.store_id); + self.notify(watch_events_response::Kind::EntryCreated( + EntryCreatedEvent { + id: Some(table.details.id.into()), + }, + )); + + Ok(Response::new( + CreateTableEntryResponse { table }.try_into()?, + )) } } - - Ok(store_ids) -} - -/// Parses a `memory:///store/{store_slot_id}` URL and returns the [`StoreSlotId`]. -fn parse_memory_url(url: &url::Url) -> tonic::Result { - let path = url.path(); - let slot_id_str = path.strip_prefix("/store/").ok_or_else(|| { - tonic::Status::invalid_argument(format!( - "invalid memory URL format, expected memory:///store/{{store_slot_id}}: {url}" - )) - })?; - slot_id_str.parse::().map_err(|err| { - tonic::Status::invalid_argument(format!( - "invalid store slot ID in memory URL '{url}': {err}" - )) - }) } /// Retrieves the entry ID based on HTTP headers. @@ -2038,21 +2178,18 @@ fn get_entry_id_from_headers( /// Return the equivalent latest at query fn latest_at_or_static(latest_at: &ext::QueryLatestAt) -> LatestAtQuery { match &latest_at.index { - Some(index) => LatestAtQuery::new(index.clone().into(), latest_at.at), - None => { - // Static only data - LatestAtQuery::new("".into(), re_log_types::TimeInt::MIN) - } + Some(index) => LatestAtQuery::new(*index, latest_at.at), + None => LatestAtQuery::new_static(), } } /// Metadata for a single chunk, extractable from either a physical `Chunk` or a manifest. struct ChunkMetadata { chunk_id: ChunkId, - entity_path: String, + entity_path: EntityPath, is_static: bool, byte_size: u64, - timelines: IntMap, + timelines: IntMap, } impl ChunkMetadata { @@ -2060,11 +2197,11 @@ impl ChunkMetadata { let timelines = chunk .timelines() .values() - .map(|col| (*col.timeline(), col.time_range())) + .map(|col| (*col.timeline().name(), col.time_range())) .collect(); Self { chunk_id: chunk.id(), - entity_path: chunk.entity_path().to_string(), + entity_path: chunk.entity_path().clone(), is_static: chunk.is_static(), byte_size: re_byte_size::SizeBytes::total_size_bytes(chunk), timelines, @@ -2075,14 +2212,11 @@ impl ChunkMetadata { manifest: &re_log_encoding::RrdManifest, chunk_id: ChunkId, row_idx: usize, - chunk_timelines: Option<&IntMap>, + chunk_timelines: Option<&IntMap>, ) -> Self { Self { chunk_id, - entity_path: manifest - .col_chunk_entity_path_raw() - .value(row_idx) - .to_owned(), + entity_path: EntityPath::from(manifest.col_chunk_entity_path_raw().value(row_idx)), is_static: manifest.col_chunk_is_static_raw().value(row_idx), byte_size: manifest.col_chunk_byte_size_uncompressed()[row_idx], timelines: chunk_timelines.cloned().unwrap_or_default(), @@ -2094,6 +2228,7 @@ impl ChunkMetadata { fn get_chunks_for_query_results( resolved: &ResolvedStore, entity_paths: &IntSet, + select_all_entity_paths: bool, query: &ext::Query, ) -> (Vec>, Vec) { // Contract: a Query with neither `latest_at` nor `range` means "all chunks", regardless of @@ -2106,8 +2241,12 @@ fn get_chunks_for_query_results( }; } - let paths = if entity_paths.is_empty() { + let paths = if select_all_entity_paths { resolved.all_entities() + } else if entity_paths.is_empty() { + // Per `cloud.proto`: `(select_all_entity_paths=false, entity_paths=[])` + // is a valid query that selects no entities and yields no results. + return (Vec::new(), Vec::new()); } else { entity_paths.clone() }; @@ -2133,7 +2272,7 @@ fn get_chunks_for_query_results( all_missing.extend(results.missing_virtual); } if let Some(range) = &query.range { - let range_q = RangeQuery::new(range.index.clone().into(), range.index_range); + let range_q = RangeQuery::new(range.index, range.index_range); let results = resolved.range_relevant_chunks_for_all_components( ChunkTrackingMode::Report, &range_q, @@ -2145,7 +2284,31 @@ fn get_chunks_for_query_results( all_chunks.push(chunk); } } - all_missing.extend(results.missing_virtual); + // Range tightening for virtual chunks. `range_relevant_chunks_for_all_components` + // post-filters physical chunks against the per-chunk timeline range, but the + // start-time-indexed scan that produces `missing_virtual` can pull in chunks + // whose actual time range falls outside the query (the index lookup widens by + // the longest chunk interval). Without this drop, lazy stores leak those + // chunks to the client and rows outside the requested range show up in the + // result set. Latest-at is unaffected because it doesn't fan out via + // `missing_virtual` here. + for chunk_id in results.missing_virtual { + let keep = match resolved { + // Eager stores already went through the physical post-filter above. + ResolvedStore::Eager(_) => true, + ResolvedStore::Lazy(lazy) => match lazy.timeline_ranges().get(&chunk_id) { + // No temporal entry => static chunk; let it through (matches the + // `chunk.is_static() && include_static` branch of the physical filter). + None => true, + Some(per_timeline) => per_timeline + .get(&range.index) + .is_some_and(|time_range| time_range.intersects(range.index_range)), + }, + }; + if keep { + all_missing.insert(chunk_id); + } + } } } @@ -2156,3 +2319,81 @@ fn get_chunks_for_query_results( (all_chunks, all_missing.into_iter().collect()) } + +/// Streams `num_bytes` of pseudo-random (incompressible) bytes back to the client, +/// split into ~1 MiB chunks. +fn bandwidth_test_stream( + num_bytes: u64, +) -> impl futures::Stream> + Send { + futures::stream::iter(ext::BandwidthTestPayloadIter::new(num_bytes).map(Ok)) +} + +#[cfg(test)] +mod tests { + use super::*; + + use futures::TryStreamExt as _; + use re_protos::cloud::v1alpha1::GetAssetsForSegmentRequest; + use re_protos::headers::RerunHeadersInjectorExt as _; + + /// Datasets created before asset datasets were introduced don't have one. Querying assets on + /// such a dataset returns no assets, and updating its entry creates the missing asset dataset. + #[tokio::test] + async fn legacy_dataset_without_asset_dataset() { + let handler = RerunCloudHandlerBuilder::new().build(); + + let dataset_id = EntryId::new(); + handler + .store + .write() + .await + .create_dataset_impl( + EntryName::new("legacy_dataset").unwrap(), + dataset_id, + DatasetKind::Recording, + None, + ) + .unwrap(); + + let responses: Vec<_> = handler + .get_assets_for_segment( + tonic::Request::new(GetAssetsForSegmentRequest {}).with_entry_id(dataset_id), + ) + .await + .expect("querying assets should succeed without an asset dataset") + .into_inner() + .try_collect() + .await + .unwrap(); + assert!( + responses.is_empty(), + "a dataset without an asset dataset should have no assets" + ); + + let updated: ext::DatasetEntry = handler + .update_dataset_entry(tonic::Request::new( + UpdateDatasetEntryRequest { + id: dataset_id, + dataset_details: Default::default(), + } + .into(), + )) + .await + .expect("updating the entry should succeed") + .into_inner() + .dataset + .unwrap() + .try_into() + .unwrap(); + + let asset_dataset_id = updated + .dataset_details + .asset_dataset + .expect("updating the entry should create the missing asset dataset"); + let store = handler.store.read().await; + assert_eq!( + store.dataset(asset_dataset_id).unwrap().dataset_kind(), + DatasetKind::Asset, + ); + } +} diff --git a/crates/store/re_server/src/rerun_cloud/register_with_dataset.rs b/crates/store/re_server/src/rerun_cloud/register_with_dataset.rs new file mode 100644 index 000000000000..2fb1049a801d --- /dev/null +++ b/crates/store/re_server/src/rerun_cloud/register_with_dataset.rs @@ -0,0 +1,478 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use re_log_types::{EntryId, StoreId, StoreKind}; +use re_protos::cloud::v1alpha1::ext; +use re_protos::common::v1alpha1::TaskId; +use re_protos::common::v1alpha1::ext::{IfDuplicateBehavior, SegmentId}; +use re_types_core::LayerName; +use url::Url; + +#[cfg(not(target_arch = "wasm32"))] +use tokio_util::compat::TokioAsyncReadCompatExt as _; + +#[cfg(target_arch = "wasm32")] +use crate::opfs as fs; +use crate::store::{ + Error, InMemoryStore, LayerInfo, ResolvedStore, StoreSlotId, TASK_ID_SUCCESS, TaskResult, +}; +#[cfg(not(target_arch = "wasm32"))] +use tokio::fs; + +/// Return type of [`do_register_with_dataset`]. +#[derive(Default)] +pub struct RegisterWithDatasetResult { + /// Recording IDs from the registered RRDs, one per data source. + /// + /// Empty string for sources that failed with a schema conflict. + pub segment_ids: Vec, + + /// Layer name for each registered source. + pub segment_layers: Vec, + + /// File format of each source (e.g. `"rrd"`). + pub segment_types: Vec, + + /// Storage URL for each source. + pub storage_urls: Vec, + + /// Task ID for each source; [`crate::store::TASK_ID_SUCCESS`] for successes, + /// a unique ID for schema-conflict failures. + pub task_ids: Vec, +} + +/// A data source that has been validated (paths confirmed to exist, duplicates checked) +/// but not yet loaded into memory. +enum ValidatedSource { + File { + rrd_path: PathBuf, + layer_info: Arc, + storage_url: url::Url, + }, + Memory { + store_slot_id: StoreSlotId, + resolved: ResolvedStore, + segment_id: SegmentId, + layer_info: Arc, + }, +} + +/// A data source that has been fully loaded and is ready to be added to the dataset. +struct ReadySource { + store_slot_id: StoreSlotId, + resolved: ResolvedStore, + segment_id: SegmentId, + layer_info: Arc, + storage_url: Url, +} + +// --- + +pub async fn do_register_with_dataset( + store: &mut InMemoryStore, + dataset_id: EntryId, + data_sources: Vec, + on_duplicate: IfDuplicateBehavior, +) -> tonic::Result { + let (store_kind, validated) = validate_sources(store, dataset_id, data_sources).await?; + let ready = load_sources(validated, store_kind).await?; + register_sources(store, dataset_id, ready, on_duplicate).await +} + +// --- + +/// Phase 1: validate each data source, resolve memory URLs, and check for +/// intra-request duplicates. +/// +/// Returns the dataset's [`StoreKind`] alongside the validated sources, since +/// callers need it to filter stores when loading files. +async fn validate_sources( + store: &InMemoryStore, + dataset_id: EntryId, + data_sources: Vec, +) -> tonic::Result<(StoreKind, Vec)> { + // `seen` tracks (layer_name, segment_id) → URLs to detect intra-request dups. + // The `on_duplicate` flag only applies to cross-request conflicts. + let mut seen: BTreeMap<(LayerName, SegmentId), Vec> = BTreeMap::new(); + let mut validated: Vec = Vec::new(); + + let store_kind = store.dataset(dataset_id)?.store_kind(); + + for source in data_sources { + let ext::DataSource { + storage_url, + is_prefix, + layer, + kind, + } = source; + + // TODO(ab): Should some or all of these errors be returned as task error instead? + // (No point in doing so unless this is tested in re_redap_tests.) + if is_prefix { + return Err(tonic::Status::internal( + "register_with_dataset: prefix data sources should have been resolved already", + )); + } + + match kind { + ext::DataSourceKind::Rrd => {} + } + + let layer_name = if layer.is_empty() { + LayerName::base() + } else { + layer + }; + + let layer_info = Arc::new(LayerInfo { name: layer_name }); + + if storage_url.scheme() == "memory" { + validated.push(validate_memory_source( + store, + store_kind, + &storage_url, + layer_info, + &mut seen, + )?); + continue; + } + + if let Some(file_source) = + validate_file_source(store_kind, &storage_url, layer_info, &mut seen).await? + { + validated.push(file_source); + } + } + + check_intra_request_duplicates(&seen)?; + + Ok((store_kind, validated)) +} + +fn validate_memory_source( + store: &InMemoryStore, + expected_store_kind: StoreKind, + storage_url: &url::Url, + layer_info: Arc, + seen: &mut BTreeMap<(LayerName, SegmentId), Vec>, +) -> tonic::Result { + let store_slot_id = parse_memory_url(storage_url)?; + let resolved = store.resolve_store(&store_slot_id).ok_or_else(|| { + tonic::Status::not_found(format!("store not found for memory URL: {storage_url}")) + })?; + let store_id = resolved.store_id(); + if store_id.kind() != expected_store_kind { + return Err(tonic::Status::invalid_argument(format!( + "memory store has kind {:?}, expected {expected_store_kind:?}", + store_id.kind() + ))); + } + let segment_id = SegmentId::new(store_id.recording_id().to_string()); + seen.entry((layer_info.name.clone(), segment_id.clone())) + .or_default() + .push(storage_url.clone()); + Ok(ValidatedSource::Memory { + store_slot_id, + resolved, + segment_id, + layer_info, + }) +} + +/// Returns `None` if the file's store kind doesn't match (silently skipped). +async fn validate_file_source( + store_kind: StoreKind, + storage_url: &url::Url, + layer_info: Arc, + seen: &mut BTreeMap<(LayerName, SegmentId), Vec>, +) -> tonic::Result> { + let rrd_path = rrd_path_from_url(storage_url)?; + let metadata = fs::metadata(&rrd_path) + .await + .map_err(|err| match err.kind() { + std::io::ErrorKind::NotFound => tonic::Status::not_found(format!( + "RRD file not found, file does not exist: {rrd_path:?}" + )), + _ => tonic::Status::internal(format!( + "Failed to check whether RRD file exists: {err:#}\nFile path: {rrd_path:?}" + )), + })?; + if !metadata.is_file() { + return Err(tonic::Status::not_found(format!( + "RRD file not found, path is not a file: {rrd_path:?}" + ))); + } + + let store_ids = load_store_ids(&rrd_path).await?; + + let mut matched = false; + for store_id in store_ids { + if store_id.kind() != store_kind { + continue; + } + matched = true; + seen.entry(( + layer_info.name.clone(), + SegmentId::from(store_id.recording_id()), + )) + .or_default() + .push(storage_url.clone()); + } + + if !matched { + return Ok(None); + } + + Ok(Some(ValidatedSource::File { + rrd_path, + layer_info, + storage_url: storage_url.clone(), + })) +} + +fn rrd_path_from_url(storage_url: &url::Url) -> tonic::Result { + #[cfg(not(target_arch = "wasm32"))] + let rrd_path = storage_url.to_file_path(); + + #[cfg(target_arch = "wasm32")] + let rrd_path = { + // NOTE: `Url::to_file_path` is not available on browser Wasm targets, so keep the + // Wasm conversion here in sync with native file-URL semantics. + if storage_url.scheme() == "file" && storage_url.host().is_none() { + let path = storage_url.path().strip_prefix('/').ok_or(()); + path.and_then(|path| { + use percent_encoding::percent_decode; + let mut bytes = Vec::with_capacity(storage_url.path().len()); + for segment in path.split('/') { + bytes.push(b'/'); + bytes.extend(percent_decode(segment.as_bytes())); + } + + String::from_utf8(bytes) + .map(PathBuf::from) + .map_err(|_err| ()) + }) + } else { + Err(()) + } + }; + + let Ok(rrd_path) = rrd_path else { + return if storage_url.scheme() == "file" && storage_url.host().is_some() { + Err(tonic::Status::not_found(format!( + "RRD file not found, file URI should not have a host: {storage_url} \ + (this may be caused by invalid relative-path URI)" + ))) + } else { + Err(tonic::Status::not_found(format!( + "RRD file not found, could not load URI: {storage_url}" + ))) + }; + }; + + Ok(rrd_path) +} + +fn check_intra_request_duplicates( + seen: &BTreeMap<(LayerName, SegmentId), Vec>, +) -> tonic::Result<()> { + let duplicates: Vec<_> = seen.iter().filter(|(_, urls)| urls.len() > 1).collect(); + if duplicates.is_empty() { + return Ok(()); + } + + let details: Vec = duplicates + .iter() + .map(|((layer, segment_id), urls)| { + let uri_lines = urls + .iter() + .map(|u| format!(" {u}")) + .collect::>() + .join("\n"); + format!(" segment id: {segment_id}, layer name: {layer}\n{uri_lines}") + }) + .collect(); + + Err(tonic::Status::invalid_argument(format!( + "duplicate segment layers in request:\n{}", + details.join("\n") + ))) +} + +// --- + +/// Phase 2: load file-backed sources into memory and unify with already-in-memory sources. +async fn load_sources( + validated: Vec, + store_kind: StoreKind, +) -> tonic::Result> { + let mut ready: Vec = Vec::new(); + + for source in validated { + match source { + ValidatedSource::Memory { + store_slot_id, + resolved, + segment_id, + layer_info, + } => { + let storage_url = + Url::parse(&format!("memory:///store/{store_slot_id}")).map_err(|err| { + tonic::Status::internal(format!("failed to build memory URL: {err}")) + })?; + ready.push(ReadySource { + store_slot_id, + resolved, + segment_id, + layer_info, + storage_url, + }); + } + + ValidatedSource::File { + rrd_path, + layer_info, + storage_url, + } => { + re_log::info!("Loading {rrd_path:?}…"); + + let stores = ResolvedStore::load_rrd_file(&rrd_path, store_kind).await?; + + for (store_id, resolved) in stores { + ready.push(ReadySource { + store_slot_id: StoreSlotId::new(), + resolved, + segment_id: SegmentId::new(store_id.recording_id().to_string()), + layer_info: layer_info.clone(), + storage_url: storage_url.clone(), + }); + } + } + } + } + + Ok(ready) +} + +// --- + +/// Phase 3: register stores in the pool and add sources to the dataset. +async fn register_sources( + store: &mut InMemoryStore, + dataset_id: EntryId, + ready: Vec, + on_duplicate: IfDuplicateBehavior, +) -> tonic::Result { + let mut result = RegisterWithDatasetResult::default(); + let mut failed_task_results: Vec<(TaskId, TaskResult)> = vec![]; + + for source in &ready { + store.register_store_with_id(source.store_slot_id, &source.resolved); + } + + { + let dataset = store.dataset_mut(dataset_id)?; + + for source in ready { + let add_result = dataset + .add_source( + source.segment_id.clone(), + source.layer_info.clone(), + source.store_slot_id, + source.resolved, + on_duplicate, + ) + .await; + + match add_result { + Ok(()) => { + result.segment_ids.push(source.segment_id); + result.segment_layers.push(source.layer_info.name.clone()); + result.segment_types.push(ext::DataSourceKind::Rrd); + result.storage_urls.push(source.storage_url); + result.task_ids.push(TaskId { + id: TASK_ID_SUCCESS.to_owned(), + }); + } + + // Schema conflicts and asset-segment rejections fail just this source's task, + // matching how the cloud server reports them during registration. + Err(Error::SchemaConflict(msg) | Error::SegmentRejected(msg)) => { + result.segment_ids.push(SegmentId::new(String::new())); + result.segment_layers.push(source.layer_info.name.clone()); + result.segment_types.push(ext::DataSourceKind::Rrd); + result.storage_urls.push(source.storage_url); + + let task_id = TaskId::new(); + result.task_ids.push(task_id.clone()); + failed_task_results.push((task_id, TaskResult::failed(&msg))); + } + + // Everything else, including the synchronous segment-count limit, aborts the batch. + Err(other_err) => { + return Err(other_err.into()); + } + } + } + } + + // Register all task results now that the mutable borrow of dataset is done + for (task_id, task_result) in failed_task_results { + store.task_registry().register_failure(task_id, task_result); + } + + Ok(result) +} + +// --- + +/// Extracts unique store IDs from an RRD file without loading chunk data. +/// +/// Returns a deduplicated set because a single RRD can contain duplicate +/// `SetStoreInfo` messages for the same store. +async fn load_store_ids(rrd_path: &Path) -> tonic::Result> { + #[cfg(not(target_arch = "wasm32"))] + let mut file = fs::File::open(rrd_path) + .await + .map_err(|err| { + tonic::Status::internal(format!( + "Failed to open RRD file: {err:#}\nFile path: {rrd_path:?}" + )) + })? + .compat(); + + #[cfg(target_arch = "wasm32")] + let mut file = { + let bytes = fs::read(rrd_path).await.map_err(|err| { + tonic::Status::internal(format!( + "Failed to open RRD file: {err:#}\nFile path: {rrd_path:?}" + )) + })?; + // TODO(RR-5154): Avoid buffering the full OPFS file once footer enumeration can use range reads. + futures::io::Cursor::new(bytes) + }; + + let store_ids = re_log_encoding::enumerate_rrd_stores(&mut file) + .await + .map_err(|err| { + tonic::Status::internal(format!("Failed to enumerate RRD stores: {err:#}")) + })?; + + Ok(store_ids.into_iter().collect()) +} + +/// Parses a `memory:///store/{store_slot_id}` URL and returns the [`StoreSlotId`]. +fn parse_memory_url(url: &url::Url) -> tonic::Result { + let path = url.path(); + let slot_id_str = path.strip_prefix("/store/").ok_or_else(|| { + tonic::Status::invalid_argument(format!( + "invalid memory URL format, expected memory:///store/{{store_slot_id}}: {url}" + )) + })?; + slot_id_str.parse::().map_err(|err| { + tonic::Status::invalid_argument(format!( + "invalid store slot ID in memory URL '{url}': {err}" + )) + }) +} diff --git a/crates/store/re_server/src/server.rs b/crates/store/re_server/src/server.rs index 1e23dfd608be..bac598fad369 100644 --- a/crates/store/re_server/src/server.rs +++ b/crates/store/re_server/src/server.rs @@ -10,7 +10,7 @@ use tokio_stream::StreamExt as _; use tonic::service::{Routes, RoutesBuilder}; use tracing::{error, info}; -use crate::error_layer::InjectedErrors; +use crate::layers::{BandwidthLayer, ErrorInjectionLayer, InjectedErrors, LatencyLayer}; // --- @@ -141,8 +141,8 @@ impl Server { re_protos::headers::new_rerun_headers_layer(name, version, is_client) }) .layer(re_grpc_server::cors_layer(&cors_allowed_origins)) - .layer(crate::latency_layer::LatencyLayer::new(artificial_latency)) - .layer(crate::bandwidth_layer::BandwidthLayer::new(bandwidth_limit)) + .layer(LatencyLayer::new(artificial_latency)) + .layer(BandwidthLayer::new(bandwidth_limit)) .layer(re_protos::trace_id_layer::TraceIdLayer::new( std::sync::Arc::new(|| { // We inject a dummy trace-id here so that our e2e integration tests @@ -152,9 +152,7 @@ impl Server { Some(opentelemetry::TraceId::from(DUMMY_TRACE_ID)) }), )) - .layer(crate::error_layer::ErrorInjectionLayer::new( - injected_errors.clone(), - )) + .layer(ErrorInjectionLayer::new(injected_errors.clone())) // NOTE: GrpcWebLayer is applied directly to gRPC routes in ServerBuilder::build() // to avoid rejecting regular HTTP requests .into_inner(); diff --git a/crates/store/re_server/src/store/dataset.rs b/crates/store/re_server/src/store/dataset.rs index 7dcf61399d86..83eea721d6e1 100644 --- a/crates/store/re_server/src/store/dataset.rs +++ b/crates/store/re_server/src/store/dataset.rs @@ -1,4 +1,7 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +#[cfg(not(target_arch = "wasm32"))] +use std::collections::BTreeSet; +use std::collections::{BTreeMap, HashMap, HashSet}; +#[cfg(not(target_arch = "wasm32"))] use std::path::Path; use std::sync::Arc; @@ -10,28 +13,33 @@ use re_arrow_util::RecordBatchExt as _; use re_log_encoding::RawRrdManifest; use re_log_types::{EntryId, StoreId, StoreKind, TimeType}; use re_protos::EntryName; -use re_protos::cloud::v1alpha1::ext::{DataSource, DatasetDetails, DatasetEntry, EntryDetails}; -use re_protos::cloud::v1alpha1::{ - EntryKind, ScanDatasetManifestResponse, ScanSegmentTableResponse, +use re_protos::cloud::v1alpha1::ext as cloud_ext; +use re_protos::cloud::v1alpha1::ext::ScanDatasetManifestDataframe; +use re_protos::cloud::v1alpha1::ext::{DataSourceKind, DatasetDetails, DatasetEntry, EntryDetails}; +use re_protos::cloud::v1alpha1::{EntryKind, ScanSegmentTableResponse}; +use re_protos::common::v1alpha1::ext::{ + DatasetHandle, DatasetKind, IfDuplicateBehavior, SegmentId, }; -use re_protos::common::v1alpha1::ext::{DatasetHandle, IfDuplicateBehavior, SegmentId}; +use re_types_core::LayerName; +#[cfg(not(target_arch = "wasm32"))] +use crate::store::store_pool::StorePool; use crate::store::{ - Error, Layer, ResolvedStore, Segment, StoreSlotId, Tracked, store_pool::StorePool, + Error, LayerInfo, ResolvedStore, Segment, Source, SourceInsertOutcome, StoreSlotId, Tracked, }; /// The mutable inner state of a [`Dataset`], wrapped in [`Tracked`] for automatic timestamp updates. pub struct DatasetInner { name: EntryName, + details: DatasetDetails, + segments: HashMap, - #[cfg(feature = "lance")] - indexes: crate::chunk_index::DatasetChunkIndexes, } pub struct Dataset { id: EntryId, - store_kind: StoreKind, + dataset_kind: DatasetKind, created_at: jiff::Timestamp, inner: Tracked, @@ -44,19 +52,17 @@ impl Dataset { pub fn new( id: EntryId, name: EntryName, - store_kind: StoreKind, + dataset_kind: DatasetKind, details: DatasetDetails, ) -> Self { Self { id, - store_kind, + dataset_kind, created_at: jiff::Timestamp::now(), inner: Tracked::new(DatasetInner { name, details, - segments: HashMap::default(), - #[cfg(feature = "lance")] - indexes: crate::chunk_index::DatasetChunkIndexes::new(id), + segments: Default::default(), }), cached_schema: Mutex::new(None), } @@ -78,16 +84,22 @@ impl Dataset { } } + #[inline] + pub fn dataset_kind(&self) -> DatasetKind { + self.dataset_kind + } + #[inline] pub fn store_kind(&self) -> StoreKind { - self.store_kind + self.dataset_kind.store_kind() } #[inline] pub fn entry_kind(&self) -> EntryKind { - match self.store_kind() { - StoreKind::Recording => EntryKind::Dataset, - StoreKind::Blueprint => EntryKind::BlueprintDataset, + match self.dataset_kind { + DatasetKind::Recording => EntryKind::Dataset, + DatasetKind::Blueprint => EntryKind::BlueprintDataset, + DatasetKind::Asset => EntryKind::AssetDataset, } } @@ -96,11 +108,6 @@ impl Dataset { self.inner.updated_at() } - #[cfg(feature = "lance")] - pub fn indexes(&self) -> &crate::chunk_index::DatasetChunkIndexes { - &self.inner.indexes - } - pub fn segments(&self) -> &HashMap { &self.inner.segments } @@ -117,27 +124,28 @@ impl Dataset { /// Returns the segments from the given list of id. /// - /// As per our proto conventions, all segments are returned if none is listed. + /// All segments are returned if `segment_ids` is `None`. + /// + /// Unknown segment IDs are silently skipped rather than treated as errors: + /// callers (notably `QueryDataset`) may receive segment IDs from a DataFusion + /// filter pushdown such as `WHERE rerun_segment_id = 'foo'`, where the value + /// is data, not a referent. Erroring on a mismatch would turn ordinary SQL + /// filters into hand-grenades. The same `segment_ids` field is also used by + /// explicit API paths (e.g. `filter_segments`, `using_index_values`), which + /// accept the same silent-ignore semantics in exchange for not paying a + /// round-trip to validate IDs client-side. pub fn segments_from_ids<'a>( &'a self, - segment_ids: &'a [SegmentId], - ) -> Result, Error> { - if segment_ids.is_empty() { - Ok(Either::Left(self.inner.segments.iter())) + segment_ids: Option<&'a [SegmentId]>, + ) -> impl Iterator { + if let Some(segment_ids) = segment_ids { + Either::Left( + segment_ids + .iter() + .filter_map(|id| self.inner.segments.get(id).map(|segment| (id, segment))), + ) } else { - // Validate that all segment IDs exist - for id in segment_ids { - if !self.inner.segments.contains_key(id) { - return Err(Error::SegmentIdNotFound { - segment_id: id.clone(), - entry_id: self.id, - }); - } - } - - Ok(Either::Right(segment_ids.iter().filter_map(|id| { - self.inner.segments.get(id).map(|segment| (id, segment)) - }))) + Either::Right(self.inner.segments.iter()) } } @@ -175,17 +183,18 @@ impl Dataset { handle: DatasetHandle { id: Some(self.id), - store_kind: self.store_kind, + dataset_kind: self.dataset_kind, url: url::Url::parse(&format!("memory:///{}", self.id)).expect("valid url"), }, } } - pub fn iter_layers(&self) -> impl Iterator { + /// Iterate over all distinct sources of this dataset. + pub fn iter_sources(&self) -> impl Iterator { self.inner .segments .values() - .flat_map(|segment| segment.iter_layers().map(|(_, layer)| layer)) + .flat_map(|segment| segment.iter_sources().map(|(_, source)| source)) } // TODO(ab): now that we systematically check the merged schema upon registration, we could @@ -204,7 +213,7 @@ impl Dataset { } // Recompute schema - let schema = Schema::try_merge(self.iter_layers().map(|layer| layer.schema()))?; + let schema = Schema::try_merge(self.iter_sources().map(|source| source.schema()))?; let schema_arc = Arc::new(schema.clone()); *cache = Some((updated_at, Arc::clone(&schema_arc))); @@ -215,7 +224,7 @@ impl Dataset { self.inner.segments.keys().cloned() } - pub fn segment_table(&self) -> Result { + pub async fn segment_table(&self) -> Result { let row_count = self.inner.segments.len(); let mut all_segment_properties = Vec::with_capacity(row_count); @@ -230,18 +239,18 @@ impl Dataset { let mut all_index_ranges = Vec::with_capacity(row_count); for (segment_id, segment) in &self.inner.segments { - let layer_count = segment.layer_count(); + let layer_count = segment.source_count(); let mut layer_names_row = Vec::with_capacity(layer_count); let mut storage_urls_row = Vec::with_capacity(layer_count); let mut current_segment_properties = BTreeMap::default(); let mut current_segment_indexes = BTreeMap::default(); - for (layer_name, layer) in segment.iter_layers() { - layer_names_row.push(layer_name.to_owned()); + for (layer_name, layer) in segment.iter_sources() { + layer_names_row.push(layer_name.clone()); storage_urls_row.push(format!("memory:///store/{}", layer.store_slot_id())); - let layer_properties = layer.compute_properties()?; + let layer_properties = layer.compute_properties().await?; // Accumulate properties. // @@ -324,7 +333,7 @@ impl Dataset { all_segment_properties.push(properties_batch); all_index_ranges.push(indexes_batch); - segment_ids.push(segment_id.to_string()); + segment_ids.push(segment_id.clone()); layer_names.push(layer_names_row); storage_urls.push(storage_urls_row); last_updated_at.push(segment.last_updated_at().as_nanosecond() as i64); @@ -355,40 +364,44 @@ impl Dataset { .map_err(Into::into) } - pub fn dataset_manifest(&self) -> Result { - self.dataset_manifest_filtered(&Default::default(), &Default::default()) + pub async fn dataset_manifest(&self) -> Result { + self.dataset_manifest_filtered(None, None).await } /// Like [`Self::dataset_manifest`] but filtered down to just the segments/layers of interest. /// /// This method acts as a *product* filter: - /// * empty `segments_of_interest` + empty `layers_of_interest`: everything - /// * empty `segments_of_interest` + non-empty `layers_of_interest`: return specified layers for *all* segments - /// * non-empty `segments_of_interest` + empty `layers_of_interest`: return *all* layers for specified segments - /// * non-empty `segments_of_interest` + non-empty `layers_of_interest`: return *all* specified layers for *all* specified segments - pub fn dataset_manifest_filtered( + /// * `None` `segments_of_interest` + `None` `layers_of_interest`: everything + /// * `None` `segments_of_interest` + `Some` `layers_of_interest`: return specified layers for *all* segments + /// * `Some` `segments_of_interest` + `None` `layers_of_interest`: return *all* layers for specified segments + /// * `Some` `segments_of_interest` + `Some` `layers_of_interest`: return *all* specified layers for *all* specified segments + pub async fn dataset_manifest_filtered( &self, - segments_of_interest: &HashSet<&SegmentId>, - layers_of_interest: &HashSet<&str>, + segments_of_interest: Option<&HashSet<&SegmentId>>, + layers_of_interest: Option<&HashSet<&LayerName>>, ) -> Result { - let row_count = self + let segment_rows = self .inner .segments .iter() .filter(|(segment_id, _)| { - segments_of_interest.is_empty() || segments_of_interest.contains(segment_id) + segments_of_interest.is_none_or(|segments| segments.contains(segment_id)) }) - .flat_map(|(segment_id, layers)| { + .flat_map(|(segment_id, segment)| { itertools::izip!( std::iter::repeat(segment_id), - layers - .layers() - .keys() - .filter(|layer| layers_of_interest.is_empty() - || layers_of_interest.contains(layer.as_str())) + segment.iter_sources().filter(|(name, _layer)| { + layers_of_interest.is_none_or(|layers| layers.contains(name)) + }) ) }) - .count(); + .map(|(segment_id, (layer_name, source))| { + let segment_id = segment_id.to_string(); + (layer_name, segment_id, source) + }); + + let layers: Vec<(&LayerName, String, &Source)> = segment_rows.collect(); + let row_count = layers.len(); let mut layer_names = Vec::with_capacity(row_count); let mut segment_ids = Vec::with_capacity(row_count); @@ -403,51 +416,29 @@ impl Dataset { let mut properties = Vec::with_capacity(row_count); - let layers = self - .inner - .segments - .iter() - .filter(|(segment_id, _)| { - segments_of_interest.is_empty() || segments_of_interest.contains(segment_id) - }) - .flat_map(|(segment_id, layers)| { - itertools::izip!( - std::iter::repeat(segment_id), - layers - .iter_layers() - .filter(|(name, _layer)| layers_of_interest.is_empty() - || layers_of_interest.contains(name)) - ) - }) - .map(|(segment_id, (layer_name, layer))| { - let segment_id = segment_id.to_string(); - (layer_name, segment_id, layer) - }); - - for (layer_name, segment_id, layer) in layers { - layer_names.push(layer_name.to_owned()); - storage_urls.push(format!("memory:///store/{}", layer.store_slot_id())); - segment_ids.push(segment_id); - layer_types.push(layer.layer_type().to_owned()); - registration_times.push(layer.registration_time().as_nanosecond() as i64); - last_updated_at.push(layer.last_updated_at().as_nanosecond() as i64); - num_chunks.push(layer.num_chunks()); - size_bytes.push(layer.size_bytes()); + for (layer_name, segment_id, source) in layers { + layer_names.push(layer_name.clone()); + storage_urls.push(format!("memory:///store/{}", source.store_slot_id())); + segment_ids.push(segment_id.into()); + layer_types.push(source.data_source_kind().to_string()); + registration_times.push(source.registration_time().as_nanosecond() as i64); + last_updated_at.push(source.last_updated_at().as_nanosecond() as i64); + num_chunks.push(source.num_chunks()); + size_bytes.push(source.size_bytes()); schema_sha256s.push( - layer + source .schema_sha256() .map_err(Error::failed_to_extract_properties)?, ); // In re_server, only successful registrations exist (schema conflicts fail synchronously), // so all entries are always `Done`. - registration_statuses - .push(re_protos::cloud::v1alpha1::ext::LayerRegistrationStatus::Done.to_string()); + registration_statuses.push(cloud_ext::LayerRegistrationStatus::Done.to_string()); - properties.push(layer.compute_properties()?); + properties.push(source.compute_properties().await?); } - let base_record_batch = ScanDatasetManifestResponse::create_dataframe( + let base_record_batch = ScanDatasetManifestDataframe::new( layer_names, segment_ids, storage_urls, @@ -459,6 +450,7 @@ impl Dataset { schema_sha256s, registration_statuses, ) + .into_record_batch() .map_err(Error::failed_to_extract_properties)?; let properties_record_batch = @@ -479,25 +471,76 @@ impl Dataset { // Each layer produces its own manifest (Lazy clones its cached footer, Eager rebuilds // from chunks), then we merge them under the segment-scoped store id. let per_layer: Vec = partition - .iter_layers() - .map(|(_, layer)| layer.rrd_manifest()) - .collect::>()?; + .iter_sources() + .map(|(_, source)| source.rrd_manifest()) + .try_collect()?; RawRrdManifest::merge(segment_store_id, per_layer) .map_err(|err| Error::RrdLoadingError(err.into())) } + /// Enforce this dataset kind's [registration limits](DatasetKind::limits) for a new source. + /// + /// Returns [`Error::SegmentLimitReached`] or [`Error::SegmentRejected`] if adding `source` + /// under `segment_id` would exceed a limit. Recording and blueprint datasets are unlimited, so + /// this is a no-op for them. + fn enforce_limits(&self, segment_id: &SegmentId, source: &Source) -> Result<(), Error> { + let limits = self.dataset_kind.limits(); + + // Only new segments count against the limit. Adding a layer to an existing segment is fine. + // Like the cloud server, the segment-count cap is enforced synchronously up front. + if let Some(max) = limits.max_segment_count + && !self.inner.segments.contains_key(segment_id) + && self.inner.segments.len() as u64 >= max + { + return Err(Error::SegmentLimitReached(format!( + "this {} already holds the maximum of {max} {}s", + self.dataset_kind.name(), + self.dataset_kind.contained_name(), + ))); + } + + // The content checks below match the cloud server, which rejects them during the + // registration task. `register_with_dataset` reports `SegmentRejected` as a failed task. + if limits.static_chunks_only && source.has_temporal_chunks() { + return Err(Error::SegmentRejected(format!( + "{}s only accept static chunks, but {} '{segment_id}' contains temporal data", + self.dataset_kind.name(), + self.dataset_kind.contained_name(), + ))); + } + + if let Some(max) = limits.max_segment_size_bytes { + let existing = self + .inner + .segments + .get(segment_id) + .map_or(0, |segment| segment.size_bytes()); + let combined = existing + source.size_bytes(); + if combined > max { + return Err(Error::SegmentRejected(format!( + "{} '{segment_id}' would be {combined} bytes, exceeding the {max}-byte limit for {}s", + self.dataset_kind.contained_name(), + self.dataset_kind.name(), + ))); + } + } + + Ok(()) + } + // we can't expect there are no async calls without the lance feature #[allow(clippy::allow_attributes)] #[allow(clippy::unused_async)] - pub async fn add_layer( + pub async fn add_source( &mut self, segment_id: SegmentId, - layer_name: String, + layer_info: Arc, store_slot_id: StoreSlotId, resolved: ResolvedStore, on_duplicate: IfDuplicateBehavior, ) -> Result<(), Error> { + let layer_name = &layer_info.name; re_log::debug!(?segment_id, ?layer_name, "add_layer"); // Validate schema compatibility before inserting. @@ -520,31 +563,55 @@ impl Dataset { )?; } } - Schema::try_merge([current_schema, new_layer_schema]).map_err(|err| { - Error::SchemaConflict(format!( - "schema incompatibility on segment '{segment_id}', layer '{layer_name}': {err}" - )) - })?; - - let overwritten = self + // Keep the merged schema so we can refresh the cache below. + let merged_schema = + Schema::try_merge([current_schema.clone(), new_layer_schema]).map_err(|err| { + Error::SchemaConflict(format!( + "schema incompatibility on segment '{segment_id}', layer '{layer_name}': {err}" + )) + })?; + + let source = Arc::new(Source::new( + store_slot_id, + resolved, + DataSourceKind::Rrd, + layer_info, + )); + + self.enforce_limits(&segment_id, &source)?; + + let outcome = self .inner .modify() .segments .entry(segment_id.clone()) .or_default() - .insert_layer( - layer_name.clone(), - Layer::new(store_slot_id, resolved.clone()), - on_duplicate, - )?; - - #[cfg(feature = "lance")] - self.indexes() - .on_layer_added(segment_id, &resolved, &layer_name, overwritten) - .await?; - - #[cfg(not(feature = "lance"))] - let _ = overwritten; + .insert_source(source.clone(), on_duplicate)?; + + // Refresh the schema cache after each successful add_source to avoid + // the O(N²) recompute pattern when register_with_dataset adds many + // layers in a single batch. `self.inner.modify()` always bumps + // `updated_at`, which would otherwise invalidate the cache on every + // iteration. + // + // - Inserted: dataset schema is exactly `merged_schema`. + // - Skipped: insert_source was a no-op, so the schema is + // unchanged → reuse `current_schema`. + // - Overwritten: the old layer's exclusive fields may no longer be + // present anywhere, so the schema may shrink in ways + // we can't reconstruct here. Drop the cache; the next + // `schema()` call will pay the full recompute. + // (Overwrite is rare relative to fresh insert in + // registration batches.) + { + let mut cache = self.cached_schema.lock(); + let updated_at = self.updated_at(); + *cache = match outcome { + SourceInsertOutcome::Inserted => Some((updated_at, Arc::new(merged_schema))), + SourceInsertOutcome::Skipped => Some((updated_at, Arc::new(current_schema))), + SourceInsertOutcome::Overwritten => None, + }; + } Ok(()) } @@ -552,19 +619,19 @@ impl Dataset { /// Unregisters segments and layers from the dataset. /// /// This method acts as a *product* filter: - /// * empty `segments_to_drop` + empty `layers_to_drop`: remove everything - /// * empty `segments_to_drop` + non-empty `layers_to_drop`: remove specified layers for *all* segments - /// * non-empty `segments_to_drop` + empty `layers_to_drop`: remove *all* layers for specified segments - /// * non-empty `segments_to_drop` + non-empty `layers_to_drop`: delete *all* specified layers for *all* specified segments + /// * `None` `segments_to_drop` + `None` `layers_to_drop`: remove everything + /// * `None` `segments_to_drop` + `Some` `layers_to_drop`: remove specified layers for *all* segments + /// * `Some` `segments_to_drop` + `None` `layers_to_drop`: remove *all* layers for specified segments + /// * `Some` `segments_to_drop` + `Some` `layers_to_drop`: delete *all* specified layers for *all* specified segments // // we can't expect there are no async calls without the lance feature #[allow(clippy::allow_attributes)] #[allow(clippy::unused_async)] pub async fn remove_layers( &mut self, - segments_to_drop: &HashSet<&SegmentId>, - layers_to_drop: &HashSet<&str>, - ) -> Result, Error> { + segments_to_drop: Option<&HashSet<&SegmentId>>, + layers_to_drop: Option<&HashSet<&LayerName>>, + ) -> Result, Error> { re_log::debug!(?segments_to_drop, ?layers_to_drop, "remove_layers"); let mut removed_layers = Vec::new(); @@ -573,10 +640,9 @@ impl Dataset { // TODO(cmc): we could have fast paths if segments.is_empty() or layers.is_empty() or both. segments.retain(|segment_id, segment| { - if segments_to_drop.is_empty() || segments_to_drop.contains(segment_id) { - segment.retain_layers(|layer_name, _layer| { - if layers_to_drop.is_empty() || layers_to_drop.contains(layer_name.as_str()) - { + if segments_to_drop.is_none_or(|segments| segments.contains(segment_id)) { + segment.retain_sources(|layer_name, _source| { + if layers_to_drop.is_none_or(|layers| layers.contains(layer_name)) { removed_layers.push((segment_id.clone(), layer_name.clone())); false } else { @@ -584,16 +650,13 @@ impl Dataset { } }); - segment.layer_count() > 0 + segment.source_count() > 0 } else { true } }); } - #[cfg(feature = "lance")] - self.indexes().on_layers_removed(&removed_layers).await?; - Ok(removed_layers) } @@ -601,26 +664,30 @@ impl Dataset { /// /// Only stores with matching kinds will be loaded. The stores are registered in the provided /// [`StorePool`] automatically. + #[cfg(not(target_arch = "wasm32"))] pub async fn register_rrd( &mut self, pool: &mut StorePool, path: &Path, - layer_name: Option<&str>, + layer_name: Option, on_duplicate: IfDuplicateBehavior, store_kind: StoreKind, ) -> Result, Error> { - re_log::info!("Loading RRD: {}", path.display()); + re_log::info!("Loading {path:?}…"); - let layer_name = layer_name.unwrap_or(DataSource::DEFAULT_LAYER); + let layer_name = layer_name.unwrap_or_else(LayerName::base); + let layer_info = Arc::new(LayerInfo { + name: layer_name.clone(), + }); let mut new_segment_ids = BTreeSet::default(); - for (store_id, resolved) in ResolvedStore::load_rrd_file(path, store_kind)? { + for (store_id, resolved) in ResolvedStore::load_rrd_file(path, store_kind).await? { let segment_id = SegmentId::new(store_id.recording_id().to_string()); let slot_id = pool.register(&resolved); - self.add_layer( + self.add_source( segment_id.clone(), - layer_name.to_owned(), + layer_info.clone(), slot_id, resolved, on_duplicate, diff --git a/crates/store/re_server/src/store/error.rs b/crates/store/re_server/src/store/error.rs index 32131d8c7046..f49ad67501d7 100644 --- a/crates/store/re_server/src/store/error.rs +++ b/crates/store/re_server/src/store/error.rs @@ -1,6 +1,7 @@ use re_log_types::{ComponentPath, EntryId}; use re_protos::EntryName; -use re_protos::common::v1alpha1::ext::SegmentId; +use re_types_core::LayerName; +use re_types_core::SegmentId; #[derive(thiserror::Error, Debug)] #[expect(clippy::enum_variant_names)] @@ -34,33 +35,27 @@ pub enum Error { #[error("Layer '{layer_name}' not found in segment '{segment_id}' of dataset '{entry_id}'")] LayerNameNotFound { - layer_name: String, + layer_name: LayerName, segment_id: SegmentId, entry_id: EntryId, }, #[error("Layer '{0}' already exists")] - LayerAlreadyExists(String), + LayerAlreadyExists(LayerName), #[error("Component path '{0}' not found")] ComponentPathNotFound(ComponentPath), - #[error("Index '{0}' already exists")] - IndexAlreadyExists(String), - #[error(transparent)] DataFusionError(#[from] datafusion::error::DataFusionError), #[error(transparent)] ArrowError(#[from] arrow::error::ArrowError), - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] #[error(transparent)] LanceError(#[from] lance::Error), - #[error("Indexing error: {0}")] - IndexingError(String), - #[error("Error loading RRD: {0}")] RrdLoadingError(anyhow::Error), @@ -79,6 +74,16 @@ pub enum Error { #[error("{0}")] SchemaConflict(String), + /// A segment exceeds a per-segment limit of its dataset kind, such as + /// too large byte size, or non-static chunks. + #[error("{0}")] + SegmentRejected(String), + + /// Registration would push the dataset past its segment-count limit. Reported synchronously, + /// matching how the cloud server reports this. + #[error("{0}")] + SegmentLimitReached(String), + #[error("Table storage already exists at location: {0}")] TableStorageAlreadyExists(String), } @@ -105,7 +110,7 @@ impl From for tonic::Status { Error::DataFusionError(err) => Self::internal(format!("DataFusion error: {err:#}")), Error::ArrowError(err) => Self::internal(format!("Arrow error: {err:#}")), - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] Error::LanceError(err) => Self::internal(format!("Lance error: {err:#}")), Error::RrdLoadingError(err) => Self::internal(format!("{err:#}")), @@ -119,12 +124,13 @@ impl From for tonic::Status { Error::DuplicateEntryNameError(_) | Error::DuplicateEntryIdError(_) | Error::LayerAlreadyExists(_) - | Error::IndexAlreadyExists(_) | Error::TableStorageAlreadyExists(_) => Self::already_exists(format!("{err:#}")), - Error::IndexingError(_) => Self::internal(format!("Indexing error: {err:#}")), - Error::SchemaConflict(_) => Self::invalid_argument(format!("{err:#}")), + + Error::SegmentRejected(_) | Error::SegmentLimitReached(_) => { + Self::failed_precondition(format!("{err:#}")) + } } } } diff --git a/crates/store/re_server/src/store/in_memory_store.rs b/crates/store/re_server/src/store/in_memory_store.rs index da6dd58dc8c0..058a090c9820 100644 --- a/crates/store/re_server/src/store/in_memory_store.rs +++ b/crates/store/re_server/src/store/in_memory_store.rs @@ -9,16 +9,28 @@ use datafusion::catalog::MemTable; use datafusion::common::DataFusionError; use itertools::Itertools as _; use re_chunk_store::{Chunk, ChunkStoreConfig}; -use re_log_types::{EntryId, StoreId, StoreKind}; +#[cfg(not(target_arch = "wasm32"))] +use re_log_types::StoreKind; +use re_log_types::{EntryId, StoreId}; use re_protos::EntryName; use re_protos::cloud::v1alpha1::EntryKind; -use re_protos::cloud::v1alpha1::ext::{DatasetDetails, EntryDetails, ProviderDetails, TableEntry}; +#[cfg(all(feature = "lance", not(target_arch = "wasm32")))] // only used by the `lance` feature +use re_protos::cloud::v1alpha1::ext as cloud_ext; +use re_protos::cloud::v1alpha1::ext::{ + DatasetDetails, EntryDetails, ProviderDetails, TableDetails, TableEntry, +}; +use re_protos::common::v1alpha1::ext::DatasetKind; +#[cfg(not(target_arch = "wasm32"))] use re_protos::common::v1alpha1::ext::IfDuplicateBehavior; use re_tuid::Tuid; +#[cfg(not(target_arch = "wasm32"))] +use re_types_core::LayerName; use re_types_core::{ComponentBatch as _, Loggable as _}; +#[cfg(not(target_arch = "wasm32"))] +use crate::NamedPath; +#[cfg(not(target_arch = "wasm32"))] use crate::OnError; -use crate::entrypoint::NamedPath; use crate::store::store_pool::StorePool; use crate::store::table::TableType; use crate::store::task_registry::TaskRegistry; @@ -33,7 +45,7 @@ pub struct InMemoryStore { /// Config applied to eager (in-memory) chunk stores created by this server. /// - /// Lazy stores load their config from the RRD file they back and ignore this + /// Lazy stores load their config from their RRD manifest and ignore this /// value. Exposed via the builder as a testing hook so integration tests can /// tune eager chunk-store knobs without relying on global env vars. eager_chunk_store_config: ChunkStoreConfig, @@ -100,20 +112,19 @@ impl InMemoryStore { /// /// Important: there is no guarantee on the order of the returned chunks. /// - /// For Lazy stores, any not-yet-resident chunks are loaded in a single batched call per - /// distinct store, amortizing the file-mutex, IPC parse, and store-write-lock overhead that - /// per-key loading would pay N times. - pub fn chunks_from_chunk_keys( + /// For Lazy stores, chunks are loaded in a single batched call per distinct store, + /// amortizing the provider and IPC-parse overhead that per-key loading would pay N times. + /// The returned chunks are owned by the caller — no caching happens in the Lazy store. + pub async fn chunks_from_chunk_keys( &self, chunk_keys: &[ChunkKey], ) -> Result)>, Error> { use crate::store::ResolvedStore; - // Step 1: resolve every key's store once, and collect the set of missing chunk IDs per - // Lazy store. Eager stores that lack a chunk are fatal (no way to load them). + // Step 1: resolve every key's store once and group Lazy-store chunk IDs for batched I/O. let mut resolved_per_key: Vec<(ResolvedStore, &ChunkKey)> = Vec::with_capacity(chunk_keys.len()); - let mut missing_per_store: HashMap> = + let mut ids_per_lazy: HashMap> = HashMap::default(); for chunk_key in chunk_keys { @@ -126,50 +137,60 @@ impl InMemoryStore { )) })?; - if resolved.physical_chunk(&chunk_key.chunk_id).is_none() { - match &resolved { - ResolvedStore::Lazy(_) => { - missing_per_store - .entry(chunk_key.store_slot_id) - .or_default() - .push(chunk_key.chunk_id); - } - ResolvedStore::Eager(_) => { - return Err(Error::InvalidChunkKey(format!( - "chunk id {} not found", - chunk_key.chunk_id - ))); - } - } + if matches!(resolved, ResolvedStore::Lazy(_)) { + ids_per_lazy + .entry(chunk_key.store_slot_id) + .or_default() + .push(chunk_key.chunk_id); } resolved_per_key.push((resolved, chunk_key)); } - // Step 2: one batched `load_chunks` call per Lazy store, so I/O is more efficient (chunk - // spans are merged on read). `load_chunks` filters already-resident chunks internally, so - // this also handles the concurrent-load race: if another request loaded a chunk between - // step 1 and step 2, we simply load fewer chunks here. - for (slot_id, missing_ids) in &missing_per_store { - let Some(ResolvedStore::Lazy(lazy)) = self.resolve_store(slot_id) else { - // A store that was Lazy in step 1 should still be Lazy here; defensive guard. + // Step 2: one batched load per Lazy store. Preserves the existing provider and IPC-parse + // amortization. The Lazy store does not cache returned chunks. + let mut loaded: HashMap<(StoreSlotId, re_chunk_store::ChunkId), Arc> = + HashMap::default(); + for (slot_id, mut ids) in ids_per_lazy { + let Some(ResolvedStore::Lazy(lazy)) = self.resolve_store(&slot_id) else { continue; }; - lazy.load_chunks(missing_ids) + // Duplicate `ChunkKey`s in the input would otherwise have us decode the same chunk + // multiple times. + ids.sort_unstable(); + ids.dedup(); + let chunks = lazy + .load_chunks(&ids) + .await .map_err(|err| Error::InvalidChunkKey(format!("lazy load failed: {err:#}")))?; + for chunk in chunks { + loaded.insert((slot_id, chunk.id()), chunk); + } } - // Step 3: every chunk should now be physical. Pull them from memory. + // Step 3: assemble output. let mut result = Vec::with_capacity(chunk_keys.len()); for (resolved, chunk_key) in resolved_per_key { - let chunk = resolved - .physical_chunk(&chunk_key.chunk_id) - .ok_or_else(|| { - Error::InvalidChunkKey(format!( - "chunk id {} not found in manifest", - chunk_key.chunk_id - )) - })?; + let chunk = match &resolved { + // Duplicate `ChunkKey`s in the input clone the `Arc` — same as the previous + // `physical_chunk()`-based implementation. + ResolvedStore::Lazy(_) => loaded + .get(&(chunk_key.store_slot_id, chunk_key.chunk_id)) + .cloned() + .ok_or_else(|| { + Error::InvalidChunkKey(format!( + "chunk id {} not found in manifest", + chunk_key.chunk_id + )) + })?, + ResolvedStore::Eager(h) => h + .read() + .physical_chunk(&chunk_key.chunk_id) + .cloned() + .ok_or_else(|| { + Error::InvalidChunkKey(format!("chunk id {} not found", chunk_key.chunk_id)) + })?, + }; result.push((resolved.store_id(), chunk)); } @@ -177,15 +198,15 @@ impl InMemoryStore { } /// Load a single RRD into an existing dataset, registering stores in the pool. + #[cfg(not(target_arch = "wasm32"))] pub async fn register_rrd_to_dataset( &mut self, dataset_id: EntryId, path: &std::path::Path, - layer_name: Option<&str>, + layer_name: Option, on_duplicate: IfDuplicateBehavior, store_kind: StoreKind, - ) -> Result, Error> - { + ) -> Result, Error> { let dataset = self .datasets .get_mut(&dataset_id) @@ -203,13 +224,14 @@ impl InMemoryStore { /// Load a directory of RRDs. //TODO(ab): maybe we could be smart with .rbl and auto-setup a blueprint dataset? + #[cfg(not(target_arch = "wasm32"))] pub async fn load_directory_as_dataset( &mut self, named_path: &NamedPath, on_duplicate: IfDuplicateBehavior, on_error: OnError, ) -> Result<(), Error> { - let directory = named_path.path.canonicalize()?; + let directory = tokio::fs::canonicalize(&named_path.path).await?; if !directory.is_dir() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -232,9 +254,9 @@ impl InMemoryStore { .create_dataset(entry_name, None) .expect("Name cannot yet exist"); - for entry in std::fs::read_dir(&directory)? { - let entry = entry?; - if entry.file_type()?.is_file() { + let mut entries = tokio::fs::read_dir(&directory).await?; + while let Some(entry) = entries.next_entry().await? { + if entry.file_type().await?.is_file() { let is_rrd = entry .file_name() .to_str() @@ -275,7 +297,7 @@ impl InMemoryStore { Ok(()) } - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] pub async fn load_directory_as_table( &mut self, named_path: &NamedPath, @@ -285,7 +307,7 @@ impl InMemoryStore { use re_protos::cloud::v1alpha1::ext::LanceTable; - let directory = named_path.path.canonicalize()?; + let directory = tokio::fs::canonicalize(&named_path.path).await?; if !directory.is_dir() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -381,14 +403,16 @@ impl InMemoryStore { } } - #[cfg(feature = "lance")] // only used by the `lance` feature + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] // only used by the `lance` feature fn add_table_entry( &mut self, entry_name: EntryName, entry_id: EntryId, table: TableType, - provider_details: re_protos::cloud::v1alpha1::ext::LanceTable, + provider_details: cloud_ext::LanceTable, ) -> Result<(), Error> { + let blueprint_dataset_id = self.create_blueprint_dataset_for_entry(entry_id)?; + self.id_by_name.insert(entry_name.clone(), entry_id); self.tables.insert( entry_id, @@ -398,13 +422,17 @@ impl InMemoryStore { table, None, ProviderDetails::LanceTable(provider_details), + TableDetails { + blueprint_dataset: Some(blueprint_dataset_id), + default_blueprint_segment: None, + }, ), ); self.update_entries_table() } - /// Create a (regular) dataset with a matching blueprint dataset. + /// Create a recording dataset with a matching hidden blueprint dataset. /// /// The server is typically responsible for setting the dataset id, so use `Some` at your own /// risk for `dataset_id`. @@ -413,36 +441,86 @@ impl InMemoryStore { dataset_name: EntryName, dataset_id: Option, ) -> Result { - let dataset_id = dataset_id.unwrap_or_else(EntryId::new); + self.create_dataset_with_kind(dataset_name, dataset_id, DatasetKind::Recording) + } + + pub(crate) fn create_blueprint_dataset_for_entry( + &mut self, + entry_id: EntryId, + ) -> Result { let blueprint_dataset_id = EntryId::new(); - let blueprint_dataset_name = EntryName::blueprint_for(dataset_id); + let blueprint_dataset_name = EntryName::blueprint_for(entry_id); self.create_dataset_impl( blueprint_dataset_name, blueprint_dataset_id, - StoreKind::Blueprint, + DatasetKind::Blueprint, None, )?; - let dataset_details = DatasetDetails { - blueprint_dataset: Some(blueprint_dataset_id), - default_blueprint_segment: None, - }; + Ok(blueprint_dataset_id) + } + + pub(crate) fn create_asset_dataset_for_entry( + &mut self, + entry_id: EntryId, + ) -> Result { + let asset_dataset_id = EntryId::new(); + let asset_dataset_name = EntryName::asset_for(entry_id); self.create_dataset_impl( - dataset_name, - dataset_id, - StoreKind::Recording, - Some(dataset_details), - ) + asset_dataset_name, + asset_dataset_id, + DatasetKind::Asset, + None, + )?; + + Ok(asset_dataset_id) + } + + /// Create a dataset of the given kind. + /// + /// Recording datasets automatically get a matching hidden blueprint dataset. + /// Blueprint datasets are created standalone and do not get their own blueprint dataset. + pub fn create_dataset_with_kind( + &mut self, + dataset_name: EntryName, + dataset_id: Option, + dataset_kind: DatasetKind, + ) -> Result { + let dataset_id = dataset_id.unwrap_or_else(EntryId::new); + + match dataset_kind { + DatasetKind::Recording => { + let blueprint_dataset_id = self.create_blueprint_dataset_for_entry(dataset_id)?; + let asset_dataset_id = self.create_asset_dataset_for_entry(dataset_id)?; + + let dataset_details = DatasetDetails { + blueprint_dataset: Some(blueprint_dataset_id), + asset_dataset: Some(asset_dataset_id), + default_blueprint_segment: None, + default_segment_table_blueprint_segment: None, + }; + + self.create_dataset_impl( + dataset_name, + dataset_id, + DatasetKind::Recording, + Some(dataset_details), + ) + } + DatasetKind::Blueprint | DatasetKind::Asset => { + self.create_dataset_impl(dataset_name, dataset_id, dataset_kind, None) + } + } } /// Create a dataset of the given kind with the given details. - fn create_dataset_impl( + pub(crate) fn create_dataset_impl( &mut self, name: EntryName, entry_id: EntryId, - store_kind: StoreKind, + dataset_kind: DatasetKind, details: Option, ) -> Result { re_log::debug!(%name, "create_dataset"); @@ -458,7 +536,7 @@ impl InMemoryStore { self.datasets.insert( entry_id, - Dataset::new(entry_id, name, store_kind, details.unwrap_or_default()), + Dataset::new(entry_id, name, dataset_kind, details.unwrap_or_default()), ); self.update_entries_table()?; @@ -467,24 +545,38 @@ impl InMemoryStore { /// Delete the provided entry. /// - /// For dataset, the corresponding blueprint dataset will be deleted as well. + /// For dataset and table entries, the corresponding blueprint dataset will be deleted as well. pub fn delete_entry(&mut self, entry_id: EntryId) -> Result<(), Error> { re_log::debug!(?entry_id, "delete_entry"); if let Some(table) = self.tables.remove(&entry_id) { + let blueprint_dataset = table.table_details().blueprint_dataset; self.id_by_name.remove(table.name()); self.update_entries_table()?; - Ok(()) + + if let Some(blueprint_entry_id) = blueprint_dataset { + self.delete_entry(blueprint_entry_id) + } else { + Ok(()) + } } else if let Some(dataset) = self.datasets.remove(&entry_id) { self.id_by_name.remove(dataset.name()); self.update_entries_table()?; - let result = - if let Some(blueprint_entry_id) = dataset.dataset_details().blueprint_dataset { - self.delete_entry(blueprint_entry_id) - } else { - Ok(()) - }; + // Blueprint and asset datasets are owned by this dataset, so deleting it deletes them too. + let owned_datasets = [ + dataset.dataset_details().blueprint_dataset, + dataset.dataset_details().asset_dataset, + ]; + + // Attempt all deletions even if one fails, so we don't leave the others orphaned. + let mut result = Ok(()); + for owned_entry_id in owned_datasets.into_iter().flatten() { + let owned_result = self.delete_entry(owned_entry_id); + if result.is_ok() { + result = owned_result; + } + } self.cleanup_store_pool(); @@ -522,6 +614,7 @@ impl InMemoryStore { ProviderDetails::SystemTable(SystemTable { kind: SystemTableKind::Entries, }), + TableDetails::default(), ), ); @@ -594,8 +687,19 @@ impl InMemoryStore { } let entry_id = EntryId::new(); + let blueprint_dataset_id = self.create_blueprint_dataset_for_entry(entry_id)?; - let table = Table::create_table_entry(entry_id, name.clone(), url, schema).await?; + let table = Table::create_table_entry( + entry_id, + name.clone(), + url, + schema, + TableDetails { + blueprint_dataset: Some(blueprint_dataset_id), + default_blueprint_segment: None, + }, + ) + .await?; let table_entry = table.as_table_entry(); self.id_by_name.insert(name, entry_id); diff --git a/crates/store/re_server/src/store/layer_info.rs b/crates/store/re_server/src/store/layer_info.rs new file mode 100644 index 000000000000..e754de69685e --- /dev/null +++ b/crates/store/re_server/src/store/layer_info.rs @@ -0,0 +1,4 @@ +pub struct LayerInfo { + pub name: re_types_core::LayerName, + // In the future we could add date_created etc here. +} diff --git a/crates/store/re_server/src/store/mod.rs b/crates/store/re_server/src/store/mod.rs index ff8df1c4862e..96ddcb25c6e8 100644 --- a/crates/store/re_server/src/store/mod.rs +++ b/crates/store/re_server/src/store/mod.rs @@ -2,9 +2,10 @@ mod chunk_key; mod dataset; mod error; mod in_memory_store; -mod layer; +mod layer_info; mod resolved_store; mod segment; +mod source; mod store_pool; mod table; mod task_registry; @@ -14,9 +15,10 @@ pub use self::chunk_key::ChunkKey; pub use self::dataset::Dataset; pub use self::error::Error; pub use self::in_memory_store::InMemoryStore; -pub use self::layer::Layer; +pub use self::layer_info::LayerInfo; pub use self::resolved_store::ResolvedStore; -pub use self::segment::Segment; +pub use self::segment::{Segment, SourceInsertOutcome}; +pub use self::source::Source; pub use self::store_pool::StoreSlotId; pub use self::table::Table; pub use self::task_registry::{TASK_ID_SUCCESS, TaskResult}; diff --git a/crates/store/re_server/src/store/resolved_store.rs b/crates/store/re_server/src/store/resolved_store.rs index cf1269fc62ce..3074629b8562 100644 --- a/crates/store/re_server/src/store/resolved_store.rs +++ b/crates/store/re_server/src/store/resolved_store.rs @@ -2,15 +2,21 @@ use std::path::Path; use std::sync::Arc; use arrow::array::RecordBatch; +use futures::AsyncRead; +#[cfg(not(target_arch = "wasm32"))] +use futures::AsyncSeekExt as _; use nohash_hasher::IntSet; use re_chunk_store::{ - Chunk, ChunkId, ChunkStore, ChunkStoreHandle, ChunkStoreHandleWeak, ChunkTrackingMode, - LazyRrdStore, QueryResults, StoreSchema, + ChunkStoreHandle, ChunkStoreHandleWeak, ChunkTrackingMode, LazyStore, QueryResults, StoreSchema, }; +#[cfg(not(target_arch = "wasm32"))] +use re_log_encoding::RrdChunkProvider; use re_log_encoding::RrdManifest; use re_log_types::{EntityPath, StoreId, StoreKind}; +#[cfg(not(target_arch = "wasm32"))] +use tokio_util::compat::TokioAsyncReadCompatExt as _; -/// A store backend: either an in-memory eager store or a file-backed lazy store. +/// A store backend: either an in-memory eager store or a provider-backed lazy store. /// /// Both variants are `Arc`-based, so `Clone` is cheap. #[derive(Clone)] @@ -18,8 +24,8 @@ pub enum ResolvedStore { /// Fully in-memory store (e.g. from `write_chunks` or legacy RRD without footer). Eager(ChunkStoreHandle), - /// File-backed store with on-demand chunk loading. - Lazy(Arc), + /// Provider-backed store with on-demand chunk loading. + Lazy(Arc), } impl ResolvedStore { @@ -44,13 +50,6 @@ impl ResolvedStore { } } - pub fn physical_chunk(&self, id: &ChunkId) -> Option> { - match self { - Self::Eager(h) => h.read().physical_chunk(id).cloned(), - Self::Lazy(l) => l.physical_chunk(id), - } - } - pub fn latest_at_relevant_chunks_for_all_components( &self, report_mode: ChunkTrackingMode, @@ -104,10 +103,10 @@ impl ResolvedStore { } } - pub fn extract_properties(&self) -> Result { + pub async fn extract_properties(&self) -> Result { match self { Self::Eager(h) => h.read().extract_properties(), - Self::Lazy(l) => l.extract_properties(), + Self::Lazy(l) => l.extract_properties().await, } .map_err(super::Error::failed_to_extract_properties) } @@ -119,47 +118,80 @@ impl ResolvedStore { } } + /// Load an RRD reader as one or more _eager_ [`ResolvedStore`]s, one per store found in the stream. + /// + /// Stores whose kind does not match `store_kind` are filtered out. + async fn load_rrd_reader_eager( + reader: R, + store_kind: StoreKind, + config: &re_chunk_store::ChunkStoreConfig, + ) -> Result, super::Error> { + Ok( + re_chunk_store::ChunkStore::handle_from_rrd_reader_async(config, reader) + .await + .map_err(super::Error::RrdLoadingError)? + .into_iter() + .filter(|(store_id, _)| store_id.kind() == store_kind) + .map(|(store_id, handle)| (store_id, Self::Eager(handle))) + .collect(), + ) + } + /// Load an RRD file as one or more [`ResolvedStore`]s, one per store found in the file. /// /// Prefers the lazy path (chunks loaded on demand) when the RRD has a footer; falls back to /// eager loading (whole file read into memory) when the footer is missing or unreadable. /// Stores whose kind does not match `store_kind` are filtered out. - pub fn load_rrd_file( + pub async fn load_rrd_file( path: &Path, store_kind: StoreKind, ) -> Result, super::Error> { - let mut file = std::fs::File::open(path)?; - - if let Ok(Some(footer)) = re_log_encoding::read_rrd_footer(&mut file) { - // The footer-reading handle is no longer needed — each `LazyRrdStore` holds its own. - drop(file); + #[cfg(target_arch = "wasm32")] + { + let bytes = crate::opfs::read(path).await?; - let mut out = Vec::with_capacity(footer.manifests.len()); - for (store_id, raw_manifest) in footer.manifests { - if store_id.kind() != store_kind { - continue; - } - let store_file = std::fs::File::open(path)?; - let lazy = Arc::new( - LazyRrdStore::try_new(store_file, path.to_owned(), Arc::new(raw_manifest)) - .map_err(|err| super::Error::RrdLoadingError(err.into()))?, - ); - out.push((store_id, Self::Lazy(lazy))); - } - Ok(out) - } else { - // Legacy fallback: eager load (no footer, or footer read error). - let contents = ChunkStore::handle_from_rrd_filepath( + // TODO(RR-5086): Ultimately, we want to be able to load from an OPFS file into a lazy store too. + Self::load_rrd_reader_eager( + futures::io::Cursor::new(bytes), + store_kind, &super::InMemoryStore::default_eager_chunk_store_config(), - path, ) - .map_err(super::Error::RrdLoadingError)?; + .await + } - Ok(contents - .into_iter() - .filter(|(store_id, _)| store_id.kind() == store_kind) - .map(|(store_id, handle)| (store_id, Self::Eager(handle))) - .collect()) + #[cfg(not(target_arch = "wasm32"))] + { + let mut file = tokio::fs::File::open(path).await?.compat(); + + if let Ok(Some(footer)) = re_log_encoding::read_rrd_footer(&mut file).await { + let mut out = Vec::with_capacity(footer.manifests.len()); + for (store_id, raw_manifest) in footer.manifests { + if store_id.kind() != store_kind { + continue; + } + let store_file = tokio::fs::File::open(path).await?.compat(); + let provider = Arc::new( + RrdChunkProvider::from_reader( + store_file, + path.display().to_string(), + Arc::new(raw_manifest), + ) + .map_err(|err| super::Error::RrdLoadingError(err.into()))?, + ); + let lazy = Arc::new(LazyStore::new(provider)); + out.push((store_id, Self::Lazy(lazy))); + } + Ok(out) + } else { + // Legacy fallback: eager load (no footer, or footer read error). + file.seek(std::io::SeekFrom::Start(0)).await?; + Self::load_rrd_reader_eager( + file, + store_kind, + &super::InMemoryStore::default_eager_chunk_store_config(), + ) + .await + } } } } @@ -167,7 +199,7 @@ impl ResolvedStore { /// Weak counterpart of [`ResolvedStore`], held by [`StorePool`](super::store_pool::StorePool). pub(crate) enum ResolvedStoreWeak { Eager(ChunkStoreHandleWeak), - Lazy(std::sync::Weak), + Lazy(std::sync::Weak), } impl ResolvedStoreWeak { @@ -178,3 +210,109 @@ impl ResolvedStoreWeak { } } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use std::collections::BTreeSet; + use std::sync::Arc; + + use re_chunk::{Chunk, RowId, TimePoint, Timeline}; + use re_log_types::example_components::{MyPoint, MyPoints}; + use re_log_types::{ + EntityPath, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind, StoreSource, + }; + use tokio_util::compat::TokioAsyncReadCompatExt as _; + + use super::ResolvedStore; + + /// Authors a minimal RRD (one `SetStoreInfo` + a few chunks) at `path`, with or without a + /// footer, and returns the `StoreId` that was written. + fn write_rrd(path: &std::path::Path, store_id: &StoreId, with_footer: bool) { + let entity_path = EntityPath::from("/test/entity"); + let timeline = Timeline::new_sequence("frame"); + let chunks: Vec> = (0..3) + .map(|i| { + let points = MyPoint::from_iter(i as u32..i as u32 + 1); + Arc::new( + Chunk::builder(entity_path.clone()) + .with_sparse_component_batches( + RowId::new(), + TimePoint::default().with(timeline, i64::from(i)), + [(MyPoints::descriptor_points(), Some(&points as _))], + ) + .build() + .expect("test chunk should be valid"), + ) + }) + .collect(); + + let mut file = std::fs::File::create(path).expect("failed to create test RRD file"); + let mut encoder = re_log_encoding::Encoder::new_eager( + re_build_info::CrateVersion::LOCAL, + re_log_encoding::EncodingOptions::PROTOBUF_COMPRESSED, + &mut file, + ) + .expect("failed to create test RRD encoder"); + if !with_footer { + encoder.do_not_emit_footer(); + } + encoder + .append(&LogMsg::SetStoreInfo(SetStoreInfo { + row_id: *RowId::ZERO, + info: StoreInfo::new(store_id.clone(), StoreSource::Unknown), + })) + .expect("failed to write test store info"); + for chunk in &chunks { + encoder + .append(&LogMsg::ArrowMsg( + store_id.clone(), + chunk + .to_arrow_msg() + .expect("test chunk should encode as arrow"), + )) + .expect("failed to write test chunk"); + } + encoder.finish().expect("failed to finish test RRD"); + } + + /// The register VALIDATION phase enumerates store IDs via + /// [`re_log_encoding::enumerate_rrd_stores`], while the LOAD phase derives them from + /// [`ResolvedStore::load_rrd_file`]. These run different code (footer-keys vs lazy load, and + /// frame-scan vs eager decode for legacy RRDs) and MUST agree, or registration would validate + /// a different set of segments than it ends up loading. This pins that invariant for both the + /// modern (footer) and legacy (no-footer) representations. + #[tokio::test] + async fn enumerate_and_load_agree_on_store_ids() { + for with_footer in [true, false] { + let file = tempfile::NamedTempFile::new().expect("failed to create temp RRD file"); + let path = file.path(); + let store_id = StoreId::random(StoreKind::Recording, "test"); + write_rrd(path, &store_id, with_footer); + + let mut file = tokio::fs::File::open(path) + .await + .expect("failed to open test RRD file") + .compat(); + let validated: BTreeSet = re_log_encoding::enumerate_rrd_stores(&mut file) + .await + .expect("failed to enumerate test RRD stores") + .into_iter() + .filter(|id| id.kind() == StoreKind::Recording) + .collect(); + + let loaded: BTreeSet = + ResolvedStore::load_rrd_file(path, StoreKind::Recording) + .await + .expect("failed to load test RRD file") + .into_iter() + .map(|(id, _)| id) + .collect(); + + assert_eq!( + validated, loaded, + "validate/load store-id sets must agree (with_footer={with_footer})" + ); + assert_eq!(loaded, BTreeSet::from([store_id])); + } + } +} diff --git a/crates/store/re_server/src/store/segment.rs b/crates/store/re_server/src/store/segment.rs index 42a61fc6ccd4..6daceb5b6ed5 100644 --- a/crates/store/re_server/src/store/segment.rs +++ b/crates/store/re_server/src/store/segment.rs @@ -1,121 +1,140 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use itertools::Itertools as _; use re_protos::common::v1alpha1::ext::IfDuplicateBehavior; +use re_types_core::LayerName; -use crate::store::{Error, Layer, Tracked}; +use crate::store::{Error, Source, Tracked}; /// The mutable inner state of a [`Segment`], wrapped in [`Tracked`] for automatic timestamp updates. -#[derive(Clone)] +#[derive(Clone, Default)] pub struct SegmentInner { - /// The layers of this segment. - layers: HashMap, + /// The sources for all the layers this segment belongs to. + sources: HashMap>, } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct Segment { inner: Tracked, } -impl Default for Segment { - fn default() -> Self { - Self { - inner: Tracked::new(SegmentInner { - layers: HashMap::default(), - }), - } - } +/// What happened to a segment's layer map as a result of an +/// [`Segment::insert_source`] call. +#[must_use] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SourceInsertOutcome { + /// The layer name was not previously present; the new layer was added. + Inserted, + + /// The layer name was already present; the existing layer was replaced + /// (per [`IfDuplicateBehavior::Overwrite`]). + Overwritten, + + /// The layer name was already present and the existing layer was kept + /// (per [`IfDuplicateBehavior::Skip`]). No mutation occurred. + Skipped, } impl Segment { - pub fn layer_count(&self) -> usize { - self.inner.layers.len() + pub fn source_count(&self) -> usize { + self.inner.sources.len() } - pub fn layers(&self) -> &HashMap { - &self.inner.layers + pub fn sources(&self) -> &HashMap> { + &self.inner.sources } - /// Iterate over layers. + /// Iterate over the layers in this segments. /// - /// Layers are iterated in (registration time, layer name) order, as per how they should appear - /// in the segment table. - pub fn iter_layers(&self) -> impl Iterator { + /// Layers are iterated in (registration time, layer name) order, + /// as per how they should appear in the segment table. + pub fn iter_sources(&self) -> impl Iterator { self.inner - .layers + .sources .iter() - .sorted_by(|(name_a, layer_a), (name_b, layer_b)| { - (layer_a.registration_time(), name_a).cmp(&(layer_b.registration_time(), name_b)) + .sorted_by(|(name_a, source_a), (name_b, source_b)| { + (source_a.registration_time(), name_a).cmp(&(source_b.registration_time(), name_b)) }) - .map(|(layer_name, layer)| (layer_name.as_str(), layer)) + .map(|(name, source)| (name, source.as_ref())) } - pub fn layer(&self, layer_name: &str) -> Option<&Layer> { - self.inner.layers.get(layer_name) + pub fn source(&self, layer_name: &LayerName) -> Option<&Source> { + self.inner.sources.get(layer_name).map(|s| s.as_ref()) } pub fn last_updated_at(&self) -> jiff::Timestamp { self.inner.updated_at() } - /// Result: a successful result is `true` if the layer existed and was overwritten - pub fn insert_layer( + /// Insert a layer into this segment, observing `on_duplicate` if the + /// layer name is already present. + /// + /// Returns: + /// - `Ok(Inserted)` on fresh insert + /// - `Ok(Overwritten)` if the layer existed and `on_duplicate = Overwrite` + /// - `Ok(Skipped)` if the layer existed and `on_duplicate = Skip` + /// (no mutation occurs; the existing layer is unchanged) + /// - `Err(LayerAlreadyExists)` if the layer existed and + /// `on_duplicate = Error` + pub fn insert_source( &mut self, - layer_name: String, - layer: Layer, + source: Arc, on_duplicate: IfDuplicateBehavior, - ) -> Result { - // Check if the layer already exists first - if self.inner.layers.contains_key(&layer_name) { + ) -> Result { + let layer_name = source.layer_info().name.clone(); + if self.inner.sources.contains_key(&layer_name) { match on_duplicate { IfDuplicateBehavior::Overwrite => { // Will overwrite, so modify - self.inner.modify().layers.insert(layer_name, layer); + self.inner.modify().sources.insert(layer_name, source); // Timestamp updated when guard drops - Ok(true) + Ok(SourceInsertOutcome::Overwritten) } IfDuplicateBehavior::Skip => { re_log::info!("Ignoring layer '{layer_name}': already exists in segment"); // No modification, no timestamp update - Ok(true) + Ok(SourceInsertOutcome::Skipped) } IfDuplicateBehavior::Error => Err(Error::LayerAlreadyExists(layer_name)), } } else { - self.inner.modify().layers.insert(layer_name, layer); - Ok(false) + self.inner.modify().sources.insert(layer_name, source); + Ok(SourceInsertOutcome::Inserted) } } - /// Returns the removed [`Layer`], if any. - pub fn remove_layer(&mut self, layer_name: &str) -> Option { - self.inner.modify().layers.remove(layer_name) + /// Returns the removed [`Source`], if any. + pub fn remove_source(&mut self, layer_name: &LayerName) -> Option> { + self.inner.modify().sources.remove(layer_name) } - /// Retains only the layers specified by the predicate. + /// Retains only the sources specified by the predicate. /// - /// In other words, remove all pairs `(name, layer)` for which `f(&name, &mut layer)` returns `false`. - /// The layers are visited in unsorted (and unspecified) order. - pub fn retain_layers(&mut self, f: F) + /// In other words, remove all pairs `(name, source)` for which `f(&name, &mut source)` returns `false`. + /// The sources are visited in unsorted (and unspecified) order. + pub fn retain_sources(&mut self, mut f: F) where - F: FnMut(&String, &mut Layer) -> bool, + F: FnMut(&LayerName, &Source) -> bool, { - self.inner.modify().layers.retain(f); + self.inner + .modify() + .sources + .retain(|name, source| f(name, source.as_ref())); } pub fn num_chunks(&self) -> u64 { self.inner - .layers + .sources .values() - .map(|layer| layer.num_chunks()) + .map(|source| source.num_chunks()) .sum() } pub fn size_bytes(&self) -> u64 { self.inner - .layers + .sources .values() - .map(|layer| layer.size_bytes()) + .map(|source| source.size_bytes()) .sum() } } diff --git a/crates/store/re_server/src/store/layer.rs b/crates/store/re_server/src/store/source.rs similarity index 70% rename from crates/store/re_server/src/store/layer.rs rename to crates/store/re_server/src/store/source.rs index 191477f0ec5f..fe7422d596f5 100644 --- a/crates/store/re_server/src/store/layer.rs +++ b/crates/store/re_server/src/store/source.rs @@ -4,29 +4,59 @@ use std::sync::Arc; use arrow::array::{BinaryArray, RecordBatch, RecordBatchOptions}; use arrow::datatypes::Schema; use arrow::error::ArrowError; +use itertools::Itertools as _; use re_byte_size::SizeBytes as _; use re_log_encoding::RawRrdManifest; use re_log_types::{AbsoluteTimeRange, Timeline}; +use re_protos::cloud::v1alpha1::ext::DataSourceKind; + +use crate::store::LayerInfo; use super::StoreSlotId; use super::resolved_store::ResolvedStore; -#[derive(Clone)] -pub struct Layer { +/// The contents of a ([`re_types_core::SegmentId`], [`re_types_core::LayerName`]) pair. +/// +/// A dataset is a table, where the columns are segments, and the rows layers. +/// This is the content of a single cell in that table. +pub struct Source { store_slot_id: StoreSlotId, + resolved: ResolvedStore, + registration_time: jiff::Timestamp, + + /// .rrd, .mcap, … + data_source_kind: DataSourceKind, + + /// All sources in the same layer share the same [`LayerInfo`]. + layer_info: Arc, } -impl Layer { - pub fn new(store_slot_id: StoreSlotId, resolved: ResolvedStore) -> Self { +impl Source { + pub fn new( + store_slot_id: StoreSlotId, + resolved: ResolvedStore, + data_source_kind: DataSourceKind, + layer_info: Arc, + ) -> Self { Self { store_slot_id, resolved, registration_time: jiff::Timestamp::now(), + data_source_kind, + layer_info, } } + pub fn data_source_kind(&self) -> DataSourceKind { + self.data_source_kind + } + + pub fn layer_info(&self) -> &LayerInfo { + &self.layer_info + } + pub fn store_slot_id(&self) -> StoreSlotId { self.store_slot_id } @@ -44,12 +74,6 @@ impl Layer { self.registration_time } - #[expect(clippy::unused_self)] - pub fn layer_type(&self) -> &'static str { - //TODO(ab): what should that actually be? - "rrd" - } - pub fn num_chunks(&self) -> u64 { match &self.resolved { ResolvedStore::Eager(h) => h.read().num_physical_chunks() as u64, @@ -62,7 +86,7 @@ impl Layer { /// The unit differs by backing store and the two values are **not directly comparable**: /// /// - **Eager** layers report the in-memory heap size of the materialized chunks. - /// - **Lazy** layers report the on-disk IPC byte length from the RRD footer, including + /// - **Lazy** layers report the RRD-encoded IPC byte length from the manifest, including /// each chunk's message header. Chunks are not materialized. /// /// Treat this as a rough load indicator, not a precise accounting. @@ -85,6 +109,11 @@ impl Layer { } } + /// Whether this layer holds any temporal data rather than only static data. + pub fn has_temporal_chunks(&self) -> bool { + !self.index_ranges().is_empty() + } + pub fn schema(&self) -> Schema { let fields = self .resolved @@ -98,8 +127,8 @@ impl Layer { re_log_encoding::RawRrdManifest::compute_sorbet_schema_sha256(&self.schema()) } - pub fn compute_properties(&self) -> Result { - self.resolved.extract_properties() + pub async fn compute_properties(&self) -> Result { + self.resolved.extract_properties().await } /// Produce a [`RawRrdManifest`] for this layer, with a `chunk_key` column already populated. @@ -119,11 +148,11 @@ impl Layer { fn rrd_manifest_from_lazy_cache( &self, - lazy: &Arc, + lazy: &Arc, ) -> Result { let mut manifest = (**lazy.raw_manifest()).clone(); - let chunk_keys = manifest + let chunk_keys: Vec<_> = manifest .col_chunk_id() .map_err(|err| super::Error::RrdLoadingError(err.into()))? .map(|chunk_id| { @@ -133,7 +162,7 @@ impl Layer { } .encode() }) - .collect::, _>>()?; + .try_collect()?; append_chunk_key_column(&mut manifest, &chunk_keys)?; Ok(manifest) @@ -161,10 +190,8 @@ impl Layer { // There's no compression on the OSS server (no disk), so "compressed size" equals // uncompressed size. The chunk_key is what's used to actually fetch data. let byte_size_uncompressed = chunk.heap_size_bytes(); - let uncompressed_byte_span = re_span::Span { - start: offset, - len: byte_size_uncompressed, - }; + let uncompressed_byte_span = + re_span::Span::from_start_len(offset, byte_size_uncompressed); offset += byte_size_uncompressed; builder @@ -250,7 +277,7 @@ fn append_chunk_key_column( Ok(()) } -#[cfg(test)] +#[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use std::collections::BTreeSet; use std::path::Path; @@ -258,14 +285,16 @@ mod tests { use arrow::array::Array as _; use re_arrow_util::ArrowArrayDowncastRef as _; use re_chunk_store::external::re_chunk; - use re_chunk_store::{Chunk, ChunkStore, ChunkStoreConfig, ChunkStoreHandle, LazyRrdStore}; + use re_chunk_store::{Chunk, ChunkStore, ChunkStoreConfig, ChunkStoreHandle, LazyStore}; use re_log_encoding::EncodingOptions; + use re_log_encoding::RrdChunkProvider; use re_log_types::{ EntityPath, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind, StoreSource, TimePoint, Timeline, example_components::{MyPoint, MyPoints}, }; use re_types_core::ChunkId; + use tokio_util::compat::TokioAsyncReadCompatExt as _; use super::*; use crate::store::{ChunkKey, ResolvedStore}; @@ -290,7 +319,7 @@ mod tests { [(MyPoints::descriptor_points(), Some(&points as _))], ) .build() - .unwrap(); + .expect("test chunk should be valid"); chunks.push(Arc::new(chunk)); } } @@ -302,20 +331,24 @@ mod tests { row_id: *re_chunk::RowId::ZERO, info: StoreInfo::new(store_id.clone(), StoreSource::Unknown), }); - let mut file = std::fs::File::create(path).unwrap(); + let mut file = std::fs::File::create(path).expect("failed to create test RRD file"); let mut encoder = re_log_encoding::Encoder::new_eager( re_log_encoding::CrateVersion::LOCAL, EncodingOptions::PROTOBUF_COMPRESSED, &mut file, ) - .unwrap(); - encoder.append(&set_store_info).unwrap(); + .expect("failed to create test RRD encoder"); + encoder + .append(&set_store_info) + .expect("failed to write test store info"); for chunk in chunks { - let arrow_msg = chunk.to_arrow_msg().unwrap(); + let arrow_msg = chunk + .to_arrow_msg() + .expect("test chunk should encode as arrow"); let msg = LogMsg::ArrowMsg(store_id.clone(), arrow_msg); - encoder.append(&msg).unwrap(); + encoder.append(&msg).expect("failed to write test chunk"); } - encoder.finish().unwrap(); + encoder.finish().expect("failed to finish test RRD"); } /// Single-layer equivalence: a Lazy-backed layer and an Eager-backed layer holding the same @@ -323,40 +356,66 @@ mod tests { /// IDs, entity paths, staticness, row counts, schema shape, decodable `chunk_key`s). /// /// Byte-size/offset columns are intentionally NOT compared: per the `RawRrdManifest` - /// docstring, Lazy reports on-disk IPC sizes while Eager reports heap sizes. - #[test] - fn rrd_manifest_lazy_and_eager_produce_equivalent_output() { + /// docstring, Lazy reports RRD-encoded IPC sizes while Eager reports heap sizes. + #[tokio::test] + async fn rrd_manifest_lazy_and_eager_produce_equivalent_output() { let (store_id, chunks) = build_chunks(); - // Eager backend: in-memory `ChunkStore`. `ALL_DISABLED` matches `LazyRrdStore`'s internal + // Eager backend: in-memory `ChunkStore`. `ALL_DISABLED` matches `LazyStore`'s internal // config, so both sides hold the same chunk set (otherwise compaction on insert would // merge them and the manifests would no longer be row-wise comparable). let mut eager_store = ChunkStore::new(store_id.clone(), ChunkStoreConfig::ALL_DISABLED); for chunk in &chunks { - eager_store.insert_chunk(chunk).unwrap(); + eager_store + .insert_chunk(chunk) + .expect("failed to insert test chunk"); } - let eager_layer = Layer::new( + let test_layer_info = Arc::new(LayerInfo { + name: re_types_core::LayerName::base(), + }); + let eager_layer = Source::new( StoreSlotId::new(), ResolvedStore::Eager(ChunkStoreHandle::new(eager_store)), + DataSourceKind::Rrd, + test_layer_info.clone(), ); // Lazy backend: same chunks, written to an RRD file with footer, then loaded lazily. - let dir = tempfile::tempdir().unwrap(); + let dir = tempfile::tempdir().expect("failed to create temp dir"); let rrd_path = dir.path().join("test.rrd"); write_rrd(&rrd_path, &store_id, &chunks); - let mut footer_file = std::fs::File::open(&rrd_path).unwrap(); + let mut footer_file = tokio::fs::File::open(&rrd_path) + .await + .expect("failed to open test RRD") + .compat(); let footer = re_log_encoding::read_rrd_footer(&mut footer_file) - .unwrap() - .unwrap(); + .await + .expect("failed to read test RRD footer") + .expect("test RRD should have a footer"); let raw_manifest = Arc::new(footer.manifests[&store_id].clone()); - let store_file = std::fs::File::open(&rrd_path).unwrap(); - let lazy = - Arc::new(LazyRrdStore::try_new(store_file, rrd_path.clone(), raw_manifest).unwrap()); - let lazy_layer = Layer::new(StoreSlotId::new(), ResolvedStore::Lazy(lazy)); + let store_file = tokio::fs::File::open(&rrd_path) + .await + .expect("failed to open test RRD") + .compat(); + let provider = Arc::new( + RrdChunkProvider::from_reader(store_file, rrd_path.display().to_string(), raw_manifest) + .expect("failed to create test RRD chunk provider"), + ); + let lazy = Arc::new(LazyStore::new(provider)); + let lazy_layer = Source::new( + StoreSlotId::new(), + ResolvedStore::Lazy(lazy), + DataSourceKind::Rrd, + test_layer_info, + ); - let lazy_manifest = lazy_layer.rrd_manifest().unwrap(); - let eager_manifest = eager_layer.rrd_manifest().unwrap(); + let lazy_manifest = lazy_layer + .rrd_manifest() + .expect("lazy layer should produce a manifest"); + let eager_manifest = eager_layer + .rrd_manifest() + .expect("eager layer should produce a manifest"); // Row counts match. assert_eq!( @@ -366,29 +425,50 @@ mod tests { ); // Chunk IDs match as sets (per-row order is not part of the contract). - let lazy_ids: BTreeSet = lazy_manifest.col_chunk_id().unwrap().collect(); - let eager_ids: BTreeSet = eager_manifest.col_chunk_id().unwrap().collect(); + let lazy_ids: BTreeSet = lazy_manifest + .col_chunk_id() + .expect("lazy manifest should contain chunk IDs") + .collect(); + let eager_ids: BTreeSet = eager_manifest + .col_chunk_id() + .expect("eager manifest should contain chunk IDs") + .collect(); assert_eq!(lazy_ids, eager_ids, "chunk IDs differ"); // Compare per-chunk metadata. Both manifests may list chunks in different orders, so // sort by chunk_id first. let sort_by_chunk_id = |manifest: &RawRrdManifest| -> Vec { - let mut indexed: Vec<(usize, ChunkId)> = - manifest.col_chunk_id().unwrap().enumerate().collect(); + let mut indexed: Vec<(usize, ChunkId)> = manifest + .col_chunk_id() + .expect("manifest should contain chunk IDs") + .enumerate() + .collect(); indexed.sort_by_key(|(_, id)| *id); indexed.into_iter().map(|(i, _)| i).collect() }; let lazy_order = sort_by_chunk_id(&lazy_manifest); let eager_order = sort_by_chunk_id(&eager_manifest); - let lazy_entity_paths = lazy_manifest.col_chunk_entity_path_raw().unwrap(); - let eager_entity_paths = eager_manifest.col_chunk_entity_path_raw().unwrap(); - let lazy_is_static = lazy_manifest.col_chunk_is_static_raw().unwrap(); - let eager_is_static = eager_manifest.col_chunk_is_static_raw().unwrap(); - let lazy_num_rows = lazy_manifest.col_chunk_num_rows_raw().unwrap(); - let eager_num_rows = eager_manifest.col_chunk_num_rows_raw().unwrap(); - - for (li, ei) in lazy_order.iter().zip(eager_order.iter()) { + let lazy_entity_paths = lazy_manifest + .col_chunk_entity_path_raw() + .expect("lazy manifest should contain entity paths"); + let eager_entity_paths = eager_manifest + .col_chunk_entity_path_raw() + .expect("eager manifest should contain entity paths"); + let lazy_is_static = lazy_manifest + .col_chunk_is_static_raw() + .expect("lazy manifest should contain static flags"); + let eager_is_static = eager_manifest + .col_chunk_is_static_raw() + .expect("eager manifest should contain static flags"); + let lazy_num_rows = lazy_manifest + .col_chunk_num_rows_raw() + .expect("lazy manifest should contain row counts"); + let eager_num_rows = eager_manifest + .col_chunk_num_rows_raw() + .expect("eager manifest should contain row counts"); + + for (li, ei) in std::iter::zip(&lazy_order, &eager_order) { assert_eq!( lazy_entity_paths.value(*li), eager_entity_paths.value(*ei), @@ -428,9 +508,13 @@ mod tests { .column_by_name(RawRrdManifest::FIELD_CHUNK_KEY) .expect("chunk_key column missing") .downcast_array_ref::() - .unwrap(); + .expect("chunk_key column should be binary"); (0..keys.len()) - .map(|i| ChunkKey::decode(keys.value(i)).unwrap().chunk_id) + .map(|i| { + ChunkKey::decode(keys.value(i)) + .expect("chunk_key should decode") + .chunk_id + }) .collect() }; assert_eq!(decode_keys(&lazy_manifest), lazy_ids); diff --git a/crates/store/re_server/src/store/store_pool.rs b/crates/store/re_server/src/store/store_pool.rs index 8f2dff7d043f..50a9e49fe9cb 100644 --- a/crates/store/re_server/src/store/store_pool.rs +++ b/crates/store/re_server/src/store/store_pool.rs @@ -41,7 +41,7 @@ impl std::str::FromStr for StoreSlotId { /// A lookup index of [`ResolvedStore`]s keyed by [`StoreSlotId`]. /// /// The pool holds **weak** references. The strong (owning) references live in -/// [`Layer`](super::Layer)s. When all layers drop a store, the weak entry +/// [`crate::store::source::Source`]. When all layers drop a store, the weak entry /// expires naturally. Call [`StorePool::cleanup`] to sweep expired entries. #[derive(Default)] pub struct StorePool { diff --git a/crates/store/re_server/src/store/table.rs b/crates/store/re_server/src/store/table.rs index 7592ab3c7911..660c6e708d09 100644 --- a/crates/store/re_server/src/store/table.rs +++ b/crates/store/re_server/src/store/table.rs @@ -12,12 +12,14 @@ use futures::StreamExt as _; use re_log_types::EntryId; use re_protos::EntryName; use re_protos::cloud::v1alpha1::EntryKind; -use re_protos::cloud::v1alpha1::ext::{EntryDetails, ProviderDetails, TableEntry}; +use re_protos::cloud::v1alpha1::ext::{ + EntryDetails, ProviderDetails, TableDetails, TableEntry, TableInsertMode, +}; #[derive(Clone)] pub enum TableType { DataFusionTable(Arc), - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] LanceDataset(Arc), } @@ -31,6 +33,7 @@ pub struct Table { updated_at: jiff::Timestamp, provider_details: ProviderDetails, + table_details: TableDetails, } impl Table { @@ -40,6 +43,7 @@ impl Table { table: TableType, created_at: Option, provider_details: ProviderDetails, + table_details: TableDetails, ) -> Self { Self { id, @@ -48,6 +52,7 @@ impl Table { created_at: created_at.unwrap_or_else(jiff::Timestamp::now), updated_at: jiff::Timestamp::now(), provider_details, + table_details, } } @@ -89,13 +94,23 @@ impl Table { }, provider_details: self.provider_details.clone(), + table_details: self.table_details.clone(), } } + pub fn table_details(&self) -> &TableDetails { + &self.table_details + } + + pub fn set_table_details(&mut self, table_details: TableDetails) { + self.table_details = table_details; + self.updated_at = jiff::Timestamp::now(); + } + pub fn schema(&self) -> SchemaRef { match &self.table { TableType::DataFusionTable(t) => t.schema(), - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] TableType::LanceDataset(dataset) => { Arc::new(arrow::datatypes::Schema::from(dataset.schema())) } @@ -105,7 +120,7 @@ impl Table { pub fn provider(&self) -> Arc { match &self.table { TableType::DataFusionTable(t) => Arc::clone(t), - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] TableType::LanceDataset(dataset) => { Arc::new(lance::datafusion::LanceTableProvider::new( Arc::new(dataset.as_ref().clone()), @@ -119,18 +134,32 @@ impl Table { async fn write_table_provider( &self, rb: RecordBatch, - insert_op: InsertOp, + insert_op: TableInsertMode, ) -> Result<(), DataFusionError> { let schema = rb.schema(); - #[cfg_attr(not(feature = "lance"), expect(irrefutable_let_patterns))] + #[cfg_attr( + not(all(feature = "lance", not(target_arch = "wasm32"))), + expect(irrefutable_let_patterns) + )] let TableType::DataFusionTable(provider) = &self.table else { return exec_err!("Expected DataFusion Table Provider"); }; + let df_op = match insert_op { + TableInsertMode::Append => InsertOp::Append, + TableInsertMode::Overwrite => InsertOp::Overwrite, + TableInsertMode::Replace => InsertOp::Replace, + TableInsertMode::Update => { + return exec_err!( + "TableInsertMode::Update is not supported for DataFusion table providers" + ); + } + }; + let input = MemorySourceConfig::try_new_from_batches(schema, vec![rb])?; let session = SessionStateBuilder::default().build(); - let result = provider.insert_into(&session, input, insert_op).await?; + let result = provider.insert_into(&session, input, df_op).await?; let mut output = result.execute(0, session.task_ctx())?; while let Some(r) = output.next().await { @@ -139,11 +168,11 @@ impl Table { Ok(()) } - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] async fn write_table_lance_dataset( &mut self, rb: RecordBatch, - insert_op: InsertOp, + insert_op: TableInsertMode, ) -> Result<(), DataFusionError> { use lance::dataset::{ MergeInsertBuilder, WhenMatched, WhenNotMatched, WriteMode, WriteParams, @@ -157,8 +186,32 @@ impl Table { let reader = arrow::record_batch::RecordBatchIterator::new(vec![Ok(rb)], schema); + let merge_with = |when_not_matched: WhenNotMatched| { + let key_columns: Vec<_> = dataset + .schema() + .fields + .iter() + .filter_map(|field| { + if field + .metadata + .get(re_sorbet::metadata::SORBET_IS_TABLE_INDEX) + .is_some_and(|v| v.to_lowercase() == "true") + { + Some(field.name.clone()) + } else { + None + } + }) + .collect(); + let mut builder = MergeInsertBuilder::try_new(Arc::clone(dataset), key_columns)?; + builder + .when_not_matched(when_not_matched) + .when_matched(WhenMatched::UpdateAll) + .try_build() + }; + match insert_op { - InsertOp::Append => { + TableInsertMode::Append => { params.mode = WriteMode::Append; dataset @@ -168,36 +221,20 @@ impl Table { .await .map_err(|err| DataFusionError::External(err.into()))?; } - InsertOp::Replace => { - let key_columns: Vec<_> = dataset - .schema() - .fields - .iter() - .filter_map(|field| { - if field - .metadata - .get(re_sorbet::metadata::SORBET_IS_TABLE_INDEX) - .is_some_and(|v| v.to_lowercase() == "true") - { - Some(field.name.clone()) - } else { - None - } - }) - .collect(); - - let mut builder = MergeInsertBuilder::try_new(Arc::clone(dataset), key_columns)?; - - let op = builder - .when_not_matched(WhenNotMatched::InsertAll) - .when_matched(WhenMatched::UpdateAll) - .try_build()?; - + TableInsertMode::Replace => { + let op = merge_with(WhenNotMatched::InsertAll)?; + let (merge_dataset, _merge_stats) = op.execute_reader(reader).await?; + *dataset = merge_dataset; + } + TableInsertMode::Update => { + // Partial-schema upsert: update existing rows only, drop unmatched. + // Lance 7 rejects `WhenNotMatched::InsertAll` when the source + // omits any non-nullable target column. + let op = merge_with(WhenNotMatched::DoNothing)?; let (merge_dataset, _merge_stats) = op.execute_reader(reader).await?; - *dataset = merge_dataset; } - InsertOp::Overwrite => { + TableInsertMode::Overwrite => { params.mode = WriteMode::Overwrite; let _ = @@ -217,25 +254,29 @@ impl Table { Ok(()) } - #[cfg_attr(not(feature = "lance"), expect(clippy::needless_pass_by_ref_mut))] + #[cfg_attr( + not(all(feature = "lance", not(target_arch = "wasm32"))), + expect(clippy::needless_pass_by_ref_mut) + )] pub async fn write_table( &mut self, rb: RecordBatch, - insert_op: InsertOp, + insert_op: TableInsertMode, ) -> Result<(), DataFusionError> { match &self.table { - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] TableType::LanceDataset(_) => self.write_table_lance_dataset(rb, insert_op).await, TableType::DataFusionTable(_) => self.write_table_provider(rb, insert_op).await, } } - #[cfg(feature = "lance")] + #[cfg(all(feature = "lance", not(target_arch = "wasm32")))] pub async fn create_table_entry( id: EntryId, name: EntryName, url: &url::Url, schema: SchemaRef, + table_details: TableDetails, ) -> Result { use re_protos::cloud::v1alpha1::ext::LanceTable; @@ -260,16 +301,18 @@ impl Table { TableType::LanceDataset(ds), created_at, ProviderDetails::LanceTable(provider_details), + table_details, )) } - #[cfg(not(feature = "lance"))] + #[cfg(not(all(feature = "lance", not(target_arch = "wasm32"))))] #[expect(clippy::unused_async)] pub async fn create_table_entry( _id: EntryId, _name: EntryName, _url: &url::Url, _schema: SchemaRef, + _table_details: TableDetails, ) -> Result { Err(DataFusionError::NotImplemented( "Create table not implemented for bare DataFusion table".to_owned(), diff --git a/crates/store/re_server/src/store/tracked.rs b/crates/store/re_server/src/store/tracked.rs index 30b1d4ef727a..6a2b0986fd06 100644 --- a/crates/store/re_server/src/store/tracked.rs +++ b/crates/store/re_server/src/store/tracked.rs @@ -4,6 +4,7 @@ use std::ops::{Deref, DerefMut}; /// /// Provides immutable access via `Deref`, and mutable access via `modify()`, /// which returns a guard that automatically updates the timestamp when dropped. +#[derive(Default)] pub struct Tracked { value: T, updated_at: jiff::Timestamp, diff --git a/crates/store/re_server/tests/memory_url.rs b/crates/store/re_server/tests/memory_url.rs index 38026a7fa64a..d610a1505019 100644 --- a/crates/store/re_server/tests/memory_url.rs +++ b/crates/store/re_server/tests/memory_url.rs @@ -6,14 +6,14 @@ #![cfg(feature = "lance")] #![expect(clippy::unwrap_used)] -use arrow::array::StringArray; use futures::TryStreamExt as _; use itertools::Itertools as _; +use re_protos::cloud::v1alpha1::DeleteEntryRequest; use re_protos::cloud::v1alpha1::ScanDatasetManifestRequest; use re_protos::cloud::v1alpha1::ext; +use re_protos::cloud::v1alpha1::ext::ScanDatasetManifestDataframe; use re_protos::cloud::v1alpha1::rerun_cloud_service_server::RerunCloudService as _; -use re_protos::cloud::v1alpha1::{DeleteEntryRequest, ScanDatasetManifestResponse}; use re_protos::headers::RerunHeadersInjectorExt as _; use re_redap_tests::{ DataSourcesDefinition, LayerDefinition, RerunCloudServiceExt as _, entry_name, @@ -49,13 +49,10 @@ async fn register_memory_url_cross_dataset() { // Extract the memory:// URL from the manifest let manifest_a = scan_manifest(&service, "dataset_a").await; - let urls = manifest_a - .column_by_name(ScanDatasetManifestResponse::FIELD_STORAGE_URL) - .unwrap() - .as_any() - .downcast_ref::() + let urls = ScanDatasetManifestDataframe::COLUMN_RERUN_STORAGE_URL + .extract(&manifest_a) .unwrap(); - let memory_url = urls.value(0).to_owned(); + let memory_url = urls.value_owned(0); assert!( memory_url.starts_with("memory:///store/"), "expected memory URL, got: {memory_url}" @@ -64,20 +61,14 @@ async fn register_memory_url_cross_dataset() { // --- Step 2: Create dataset B, register using the memory:// URL --- let dataset_b = service.create_dataset_entry_with_name("dataset_b").await; - let memory_data_source: re_protos::cloud::v1alpha1::DataSource = ext::DataSource { - storage_url: url::Url::parse(&memory_url).unwrap(), - is_prefix: false, - layer: ext::DataSource::DEFAULT_LAYER.to_owned(), - kind: ext::DataSourceKind::Rrd, - } - .into(); + let memory_data_source: re_protos::cloud::v1alpha1::DataSource = + ext::DataSource::new_rrd(&memory_url).unwrap().into(); let request = tonic::Request::new(re_protos::cloud::v1alpha1::RegisterWithDatasetRequest { data_sources: vec![memory_data_source.clone()], on_duplicate: Default::default(), }) - .with_entry_name(entry_name("dataset_b")) - .unwrap(); + .with_entry_name(entry_name("dataset_b")); let task_results = register_and_wait(&service, request).await; assert!( @@ -122,8 +113,7 @@ async fn register_memory_url_cross_dataset() { data_sources: vec![memory_data_source], on_duplicate: Default::default(), }) - .with_entry_name(entry_name("dataset_c")) - .unwrap(); + .with_entry_name(entry_name("dataset_c")); let result = service.register_with_dataset(request).await; assert!( @@ -148,20 +138,14 @@ async fn register_memory_url_not_found() { let fake_tuid = re_tuid::Tuid::new(); let fake_memory_url = format!("memory:///store/{fake_tuid}"); - let memory_data_source: re_protos::cloud::v1alpha1::DataSource = ext::DataSource { - storage_url: url::Url::parse(&fake_memory_url).unwrap(), - is_prefix: false, - layer: ext::DataSource::DEFAULT_LAYER.to_owned(), - kind: ext::DataSourceKind::Rrd, - } - .into(); + let memory_data_source: re_protos::cloud::v1alpha1::DataSource = + ext::DataSource::new_rrd(&fake_memory_url).unwrap().into(); let request = tonic::Request::new(re_protos::cloud::v1alpha1::RegisterWithDatasetRequest { data_sources: vec![memory_data_source], on_duplicate: Default::default(), }) - .with_entry_name(entry_name("dataset_nf")) - .unwrap(); + .with_entry_name(entry_name("dataset_nf")); let result = service.register_with_dataset(request).await; assert!( @@ -183,9 +167,8 @@ async fn scan_manifest( ) -> arrow::array::RecordBatch { let responses: Vec<_> = service .scan_dataset_manifest( - tonic::Request::new(ScanDatasetManifestRequest { columns: vec![] }) - .with_entry_name(entry_name(dataset_name)) - .unwrap(), + tonic::Request::new(ScanDatasetManifestRequest::all()) + .with_entry_name(entry_name(dataset_name)), ) .await .unwrap() diff --git a/crates/store/re_server/tests/opfs.rs b/crates/store/re_server/tests/opfs.rs new file mode 100644 index 000000000000..3fc638ce1dc2 --- /dev/null +++ b/crates/store/re_server/tests/opfs.rs @@ -0,0 +1,153 @@ +#![cfg(target_arch = "wasm32")] + +// NOTE: The end-goal here should be to run the `wasm32` build of the server +// against the `re_redap_tests` conformance suite. + +use re_chunk::{Chunk, RowId, TimePoint, Timeline}; +use re_log_types::example_components::{MyPoint, MyPoints}; +use re_log_types::{ + EntityPath, EntryName, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind, StoreSource, +}; +use re_protos::cloud::v1alpha1::ext::RegisterWithDatasetDataframe; +use re_protos::cloud::v1alpha1::rerun_cloud_service_server::RerunCloudService as _; +use re_protos::cloud::v1alpha1::{ + CreateDatasetEntryRequest, DataSource, DataSourceKind, GetDatasetSchemaRequest, + RegisterWithDatasetRequest, VersionRequest, +}; +use re_protos::headers::RerunHeadersInjectorExt as _; +use re_server::RerunCloudHandlerBuilder; +use wasm_bindgen_test::wasm_bindgen_test; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +async fn version() { + let service = RerunCloudHandlerBuilder::new().build(); + + let response = service + .version(tonic::Request::new(VersionRequest {})) + .await + .expect("version request should succeed") + .into_inner(); + + assert_eq!(response.version, re_build_info::exposed_version!()); + assert!(response.build_info.is_some()); +} + +#[wasm_bindgen_test] +async fn register_rrd_from_file_url_in_opfs() { + let service = RerunCloudHandlerBuilder::new().build(); + let dataset_name = EntryName::new("opfs_dataset").expect("valid dataset name"); + let file_name = format!("{}.rrd", re_tuid::Tuid::new()); + let url = format!("file:///{file_name}"); + + re_server::opfs::write(&file_name, encode_rrd().into()) + .await + .expect("failed to write OPFS file"); + + service + .create_dataset_entry(tonic::Request::new(CreateDatasetEntryRequest { + name: Some(dataset_name.as_str().to_owned()), + id: None, + })) + .await + .expect("failed to create dataset"); + + let response = service + .register_with_dataset( + tonic::Request::new(RegisterWithDatasetRequest { + data_sources: vec![DataSource { + storage_url: Some(url.clone()), + layer: None, + prefix: false, + typ: DataSourceKind::Rrd as i32, + }], + on_duplicate: Default::default(), + }) + .with_entry_name(dataset_name.clone()), + ) + .await + .expect("failed to register OPFS RRD") + .into_inner(); + + let registered: arrow::array::RecordBatch = response + .data + .expect("registration response should contain data") + .try_into() + .expect("registration response should contain a record batch"); + let registered = RegisterWithDatasetDataframe::try_from(registered) + .expect("registration response should match its declared schema"); + assert_eq!( + registered + .rerun_storage_url + .into_iter_owned() + .collect::>(), + [url] + ); + assert_eq!( + registered + .rerun_segment_type + .into_iter_owned() + .collect::>(), + ["rrd"] + ); + + let schema = service + .get_dataset_schema( + tonic::Request::new(GetDatasetSchemaRequest {}).with_entry_name(dataset_name), + ) + .await + .expect("failed to get dataset schema") + .into_inner() + .schema() + .expect("dataset schema should decode"); + + assert!(schema.fields().iter().any(|field| { + let metadata = field.metadata(); + metadata + .get("rerun:entity_path") + .is_some_and(|path| path == "/test/entity") + && metadata + .get("rerun:component") + .is_some_and(|component| component == "example.MyPoints:points") + })); +} + +fn encode_rrd() -> Vec { + let store_id = StoreId::random(StoreKind::Recording, "opfs_test"); + let timeline = Timeline::new_sequence("frame"); + let points = MyPoint::from_iter(0..1); + let chunk = Chunk::builder(EntityPath::from("/test/entity")) + .with_sparse_component_batches( + RowId::new(), + TimePoint::default().with(timeline, 0), + [(MyPoints::descriptor_points(), Some(&points as _))], + ) + .build() + .expect("test chunk should be valid"); + + let mut bytes = Vec::new(); + let mut encoder = re_log_encoding::Encoder::new_eager( + re_build_info::CrateVersion::LOCAL, + re_log_encoding::EncodingOptions::PROTOBUF_COMPRESSED, + &mut bytes, + ) + .expect("failed to create test RRD encoder"); + encoder + .append(&LogMsg::SetStoreInfo(SetStoreInfo { + row_id: *RowId::ZERO, + info: StoreInfo::new(store_id.clone(), StoreSource::Unknown), + })) + .expect("failed to write test store info"); + encoder + .append(&LogMsg::ArrowMsg( + store_id, + chunk + .to_arrow_msg() + .expect("test chunk should encode as arrow"), + )) + .expect("failed to write test chunk"); + encoder.finish().expect("failed to finish test RRD"); + drop(encoder); + bytes +} diff --git a/crates/store/re_server/tests/redap_tests.rs b/crates/store/re_server/tests/redap_tests.rs index 2ec58ac84e35..421b25993b7e 100644 --- a/crates/store/re_server/tests/redap_tests.rs +++ b/crates/store/re_server/tests/redap_tests.rs @@ -2,12 +2,14 @@ use re_server::{RerunCloudHandler, RerunCloudHandlerBuilder}; -#[expect(clippy::unused_async)] // needed by the macro +// The lint fires locally but not on CI, so we use `allow` instead of `expect`: +#[allow(clippy::unused_async, clippy::allow_attributes)] // needed by the macro async fn build() -> RerunCloudHandler { RerunCloudHandlerBuilder::new().build() } re_redap_tests::generate_redap_tests!(build); +re_redap_tests::generate_oss_only_redap_tests!(build); #[tokio::test(flavor = "multi_thread")] async fn version() { diff --git a/crates/store/re_sorbet/src/chunk_batch.rs b/crates/store/re_sorbet/src/chunk_batch.rs index 1bf67c04d8eb..1343c84ff565 100644 --- a/crates/store/re_sorbet/src/chunk_batch.rs +++ b/crates/store/re_sorbet/src/chunk_batch.rs @@ -152,7 +152,6 @@ impl TryFrom<&ArrowRecordBatch> for ChunkBatch { /// * Will automatically wrap data columns in `ListArrays` if they are not already /// * Will reorder columns so that Row ID comes before timelines, which come before data /// * Will migrate component descriptors to colon-based notation - #[tracing::instrument(level = "trace", skip_all)] fn try_from(batch: &ArrowRecordBatch) -> Result { re_tracing::profile_function!(); @@ -167,7 +166,6 @@ impl TryFrom for ChunkBatch { type Error = SorbetError; /// Will automatically wrap data columns in `ListArrays` if they are not already. - #[tracing::instrument(level = "trace", skip_all)] fn try_from(sorbet_batch: SorbetBatch) -> Result { re_tracing::profile_function!(); diff --git a/crates/store/re_sorbet/src/chunk_columns.rs b/crates/store/re_sorbet/src/chunk_columns.rs index 01fe4c84a219..7dd03c720d1f 100644 --- a/crates/store/re_sorbet/src/chunk_columns.rs +++ b/crates/store/re_sorbet/src/chunk_columns.rs @@ -1,4 +1,5 @@ use arrow::datatypes::{Field as ArrowField, Fields as ArrowFields}; +use itertools::chain; use re_log_types::EntityPath; use crate::{ @@ -74,14 +75,14 @@ impl ChunkColumnDescriptors { } pub fn arrow_fields(&self) -> Vec { - std::iter::once(self.row_id.to_arrow_field()) - .chain(self.indices.iter().map(|c| c.to_arrow_field())) - .chain( - self.components - .iter() - .map(|c| c.to_arrow_field(BatchType::Dataframe)), - ) - .collect() + chain!( + std::iter::once(self.row_id.to_arrow_field()), + self.indices.iter().map(|c| c.to_arrow_field()), + self.components + .iter() + .map(|c| c.to_arrow_field(BatchType::Dataframe)), + ) + .collect() } } diff --git a/crates/store/re_sorbet/src/column_descriptor.rs b/crates/store/re_sorbet/src/column_descriptor.rs index c1bb1132cf05..45646ada0303 100644 --- a/crates/store/re_sorbet/src/column_descriptor.rs +++ b/crates/store/re_sorbet/src/column_descriptor.rs @@ -19,7 +19,10 @@ pub enum ColumnError { UnsupportedColumnKind { kind: ColumnKind }, #[error(transparent)] - UnsupportedTimeType(#[from] crate::UnsupportedTimeType), + IndexColumn(#[from] crate::IndexColumnError), + + #[error(transparent)] + InvalidComponentIdentifier(#[from] re_types_core::InvalidComponentIdentifierError), } /// Describes any kind of column. @@ -28,7 +31,7 @@ pub enum ColumnError { /// * [`RowIdColumnDescriptor`] /// * [`IndexColumnDescriptor`] /// * [`ComponentColumnDescriptor`] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, re_byte_size::SizeBytes)] pub enum ColumnDescriptor { /// The primary row id column. /// @@ -158,7 +161,7 @@ impl ColumnDescriptor { ColumnKind::Index => Ok(Self::Time(IndexColumnDescriptor::try_from(field)?)), ColumnKind::Component => Ok(Self::Component( - ComponentColumnDescriptor::from_arrow_field(chunk_entity_path, field), + ComponentColumnDescriptor::from_arrow_field(chunk_entity_path, field)?, )), } } @@ -175,9 +178,9 @@ fn test_schema_over_ipc() { )), ColumnDescriptor::Component(ComponentColumnDescriptor { entity_path: re_log_types::EntityPath::from("/some/path"), - archetype: Some("archetype".to_owned().into()), - component: "component".to_owned().into(), - component_type: Some(re_types_core::ComponentType::new("component_type")), + archetype: Some("archetype".into()), + component: "component".into(), + component_type: Some(re_types_core::ComponentType::from("component_type")), store_datatype: arrow::datatypes::DataType::Int64, is_static: true, is_tombstone: false, diff --git a/crates/store/re_sorbet/src/component_column_descriptor.rs b/crates/store/re_sorbet/src/component_column_descriptor.rs index e1d7fb969e8b..c6e58674ee7a 100644 --- a/crates/store/re_sorbet/src/component_column_descriptor.rs +++ b/crates/store/re_sorbet/src/component_column_descriptor.rs @@ -1,11 +1,14 @@ use arrow::datatypes::{DataType as ArrowDatatype, Field as ArrowField}; use re_log_types::{ComponentPath, EntityPath}; -use re_types_core::{ArchetypeName, ComponentDescriptor, ComponentIdentifier, ComponentType}; +use re_types_core::{ + ArchetypeName, ComponentDescriptor, ComponentIdentifier, ComponentType, + InvalidComponentIdentifierError, +}; use crate::{ArrowFieldMetadata, BatchType, ColumnKind, ComponentColumnSelector, MetadataExt as _}; /// This is an [`ArrowField`] that contains specific meta-data. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, re_byte_size::SizeBytes)] pub struct ComponentColumnDescriptor { /// The Arrow datatype of the stored column. /// @@ -66,27 +69,6 @@ pub struct ComponentColumnDescriptor { pub is_semantically_empty: bool, } -impl re_byte_size::SizeBytes for ComponentColumnDescriptor { - #[inline] - fn heap_size_bytes(&self) -> u64 { - let Self { - entity_path, - archetype, - component, - component_type, - store_datatype, - is_static: _, - is_tombstone: _, - is_semantically_empty: _, - } = self; - entity_path.heap_size_bytes() - + archetype.heap_size_bytes() - + component.heap_size_bytes() - + component_type.heap_size_bytes() - + store_datatype.heap_size_bytes() - } -} - impl PartialOrd for ComponentColumnDescriptor { #[inline] fn partial_cmp(&self, other: &Self) -> Option { @@ -341,7 +323,10 @@ impl ComponentColumnDescriptor { impl ComponentColumnDescriptor { /// `chunk_entity_path`: if this column is part of a chunk batch, /// what is its entity path (so we can set [`ComponentColumnDescriptor::entity_path`])? - pub fn from_arrow_field(chunk_entity_path: Option<&EntityPath>, field: &ArrowField) -> Self { + pub fn from_arrow_field( + chunk_entity_path: Option<&EntityPath>, + field: &ArrowField, + ) -> Result { let entity_path = if let Some(entity_path) = field.get_opt(crate::metadata::SORBET_ENTITY_PATH) { EntityPath::parse_forgiving(entity_path) @@ -351,23 +336,24 @@ impl ComponentColumnDescriptor { EntityPath::root() // NOTE: should be optional for general sorbet batches }; - let component = - if let Some(component) = field.get_opt(re_types_core::FIELD_METADATA_KEY_COMPONENT) { - ComponentIdentifier::from(component) - } else { - ComponentIdentifier::new(field.name()) // fallback - }; + // Prefer the `rerun:component` metadata, falling back to the field name. + // An empty `rerun:component` is treated as missing. + let component = field + .get_opt(re_types_core::FIELD_METADATA_KEY_COMPONENT) + .filter(|component| !component.is_empty()) + .unwrap_or_else(|| field.name()); + let component = ComponentIdentifier::try_new(component)?; let schema = Self { store_datatype: field.data_type().clone(), entity_path, archetype: field .get_opt(re_types_core::FIELD_METADATA_KEY_ARCHETYPE) - .map(Into::into), + .and_then(|s| ArchetypeName::try_new(s).ok()), component, component_type: field .get_opt(re_types_core::FIELD_METADATA_KEY_COMPONENT_TYPE) - .map(Into::into), + .and_then(|s| ComponentType::try_new(s).ok()), is_static: field.get_bool("rerun:is_static"), is_tombstone: field.get_bool("rerun:is_tombstone"), is_semantically_empty: field.get_bool("rerun:is_semantically_empty"), @@ -375,6 +361,6 @@ impl ComponentColumnDescriptor { schema.sanity_check(); - schema + Ok(schema) } } diff --git a/crates/store/re_sorbet/src/dataframe_to_chunks.rs b/crates/store/re_sorbet/src/dataframe_to_chunks.rs new file mode 100644 index 000000000000..7a793225cdff --- /dev/null +++ b/crates/store/re_sorbet/src/dataframe_to_chunks.rs @@ -0,0 +1,1323 @@ +//! Interpret an arbitrary Arrow record batch as Rerun chunk data. +//! +//! This is the core implementation for the Arrow → chunk interpretation that the SDKs and the +//! platform share. It is surfaced in Python through `Chunk.from_record_batch` and +//! `rr.send_dataframe`. + +use std::collections::HashMap; +use std::sync::Arc; + +use arrow::array::{ + Array as _, ArrayRef as ArrowArrayRef, RecordBatch as ArrowRecordBatch, RecordBatchOptions, +}; +use arrow::datatypes::{Field as ArrowField, Schema as ArrowSchema}; + +use re_arrow_util::RecordBatchExt as _; +use re_log_types::{EntityPath, TimelineName}; +use re_types_core::{ChunkId, FIELD_METADATA_KEY_COMPONENT, Loggable as _, RowId}; + +use crate::{ + BatchType, ChunkBatch, ComponentColumnDescriptor, IndexColumnDescriptor, MetadataExt as _, + RowIdColumnDescriptor, SorbetBatch, SorbetError, SorbetSchema, + metadata::{RERUN_CHUNK_ID, RERUN_KIND, SORBET_ENTITY_PATH, SORBET_INDEX_NAME}, +}; + +/// The Arrow field metadata key that holds the extension name (e.g. the TUID extension). +const ARROW_EXTENSION_NAME: &str = "ARROW:extension:name"; + +/// How index (timeline) columns are chosen when interpreting a dataframe batch. +#[derive(Clone, Debug)] +pub enum DataframeIndex { + /// Derive index columns from `rerun:kind`/`rerun:index_name` metadata; error if none found. + Auto, + + /// Promote exactly these columns to timelines; all other non-row-id columns become components. + Columns(Vec), + + /// No timelines (static data). + Static, +} + +/// Errors raised while interpreting a dataframe record batch as [`ChunkBatch`]es. +#[derive(thiserror::Error, Debug)] +pub enum DataframeToChunksError { + /// An underlying sorbet-level failure: classification, list-wrapping, assembly, or an + /// unsupported index datatype ([`crate::IndexColumnError::UnsupportedTimeType`]). + #[error(transparent)] + Sorbet(#[from] SorbetError), + + /// `index` was left at the default but the batch carries no index metadata, so it cannot be + /// told apart from static data. + #[error( + "The record batch carries no index column, so it cannot be unambiguously interpreted as \ + temporal or static. Pass `index=` for temporal data or `index=None` for static \ + data." + )] + AmbiguousStaticData, + + /// `index=None` (static) was requested, but the batch contradicts it with index metadata. + #[error( + "`index=None` (static) was requested, but the record batch also carries index metadata or \ + names an index column. Drop the index metadata, or request a temporal interpretation \ + instead." + )] + StaticWithIndex, + + /// A named index column does not exist in the batch. + #[error("The requested index column {0:?} is not present in the record batch.")] + MissingIndexColumn(String), + + /// A column promoted to an index contains null values. + #[error( + "The index column {0:?} contains null values. Time columns must be dense — express static \ + data with `index=None` rather than null times. (Mixing static and temporal rows in a \ + single batch is not supported.)" + )] + NullIndexColumn(String), + + /// The batch has no component columns, so there is nothing to log. + #[error("The record batch contains no component columns, so there is nothing to log.")] + NoComponentColumns, + + /// An identified chunk (row-id column + chunk id) resolves to more than one entity path. + #[error( + "The record batch is an identified chunk (it carries a row-id column and a chunk id), but \ + resolves to more than one entity path. An identified chunk is preserved as-is and must be \ + a single chunk for a single entity. To reinterpret it into one chunk per entity (with \ + freshly-minted ids), drop the chunk-id metadata and/or the row-id column." + )] + IdentifiedChunkWithMultipleEntities, +} + +/// Interpret an arbitrary Arrow record batch as one [`ChunkBatch`] per entity path. +/// +/// Each column is classified as a row-id column, an index (timeline) column, or a component column. +/// Component columns are grouped by entity path, and one [`ChunkBatch`] is emitted per distinct +/// entity path, in first-seen column order. +/// +/// `rerun:*` Arrow metadata, when present, drives the classification of each column, as well as the +/// entity path / archetype / component / component-type of component columns. +/// +/// # Chunk identity +/// +/// A row-id column together with a `rerun:id` chunk id mark the batch as a *fully identified* chunk +/// (e.g. one produced from a [`ChunkBatch`]). Both the row ids and chunk id are preserved when: +/// - both are present in the input batch, +/// - `index` is [`DataframeIndex::Auto`] (the default), and +/// - `entity_path` is not set. +/// +/// Such an identified chunk round-trips into a single chunk, and must resolve to a single entity +/// path (otherwise [`DataframeToChunksError::IdentifiedChunkWithMultipleEntities`]). A row-id column +/// is recognized by a `rerun:kind` of `row_id`/`control`, or the `rerun.datatypes.TUID` Arrow +/// extension. +/// +/// If any of these conditions is not met, the batch is either not fully identified, or its data is +/// being reinterpreted (`index`) and/or relocated (`entity_path`). In that case, fresh row ids and +/// a fresh chunk id are minted and the input ones discarded to avoid unwanted reuse of UUID. Also, +/// the data may be spread into one chunk per entity path. +/// +/// # Index (timeline) columns +/// +/// The `index` argument ([`DataframeIndex`]) selects which columns become timelines. By default +/// ([`DataframeIndex::Auto`]), index columns are derived from metadata. The timeline type is +/// derived from the column's datatype: +/// - `Int64` → sequence, +/// - `Timestamp(ns)` → timestamp +/// - `Duration(ns)` → duration +/// +/// Any other datatype is rejected. +/// +/// Index columns are shared across every emitted chunk, and must be dense: a null index value is +/// rejected, since it would make a row neither temporal nor static (see *Limitations*). +/// +/// # Component columns and entity paths +/// +/// Every non-row-id, non-index column is a component column. Component arrays may be either lists +/// (one component batch per row) or plain arrays; plain arrays are automatically wrapped as +/// single-element lists. +/// +/// A component's entity path is resolved, in order, from: +/// - its own `rerun:entity_path` metadata, +/// - the batch-level `rerun:entity_path` metadata, +/// - the column-name convention (see below), +/// - the `entity_path` argument, if provided, +/// - the root entity (`/`). +/// +/// ## Column-name convention +/// +/// When a component column has no `rerun:entity_path` metadata and its name starts with `/` and +/// contains a `:`, the part before the first `:` is taken as the entity path and the remainder as +/// the component identifier (e.g. `/points:Points3D:positions` → entity `/points`, component +/// `Points3D:positions`). Names without a leading `/` are not split and land on the resolved +/// default entity. +/// +/// # Static data +/// +/// With [`DataframeIndex::Static`] — or under [`DataframeIndex::Auto`] when the batch is an +/// already-identified static chunk that round-trips as-is (see *Chunk identity*) — the resulting +/// chunks have no timeline. (A non-identified batch with no index metadata cannot be assumed static +/// under [`DataframeIndex::Auto`]; it is ambiguous and rejected with +/// [`DataframeToChunksError::AmbiguousStaticData`] — pass `index=None` to force a static reading.) +/// +/// Static chunks with more than one row can be legitimate in some cases, but latest-at queries only +/// surface the last row, so an info-level message is emitted in that case. (Only when a chunk is +/// freshly assembled — an already-identified chunk that is preserved as-is is passed through without +/// this check.) +/// +/// # Limitations +/// +/// * A batch that mixes static and temporal rows — i.e. one with some `null` index values — is not +/// split into a mix of static and temporal chunks. Such a batch is rejected outright (a null +/// index value yields [`DataframeToChunksError::NullIndexColumn`]). +/// * Recording-property columns (named `property:…`, mapping to the `/__properties` entity) are not +/// recognized by the column-name convention. +// NOTE: Agent, keep this in sync with `Chunk.from_record_batch`. +pub fn chunk_batches_from_dataframe_record_batch( + batch: &ArrowRecordBatch, + index: &DataframeIndex, + entity_path: Option<&EntityPath>, +) -> Result, DataframeToChunksError> { + re_tracing::profile_function!(); + + // Step 0: chunk-identity dispatch. + let has_row_id = batch + .schema_ref() + .fields() + .iter() + .any(|f| is_row_id_field(f)); + let has_chunk_id = batch.schema_ref().metadata().contains_key(RERUN_CHUNK_ID); + let preserve_requested = matches!(index, DataframeIndex::Auto) && entity_path.is_none(); + + if has_row_id && has_chunk_id && preserve_requested { + // Identified chunk: preserve its identity, round-tripping into a single chunk. + return preserve_identified_chunk(batch).map(|cb| vec![cb]); + } + + // Otherwise we mint a fresh identity. Drop any provided row-id column so it is neither carried + // as a component nor mistaken for the minted one (the input chunk id is likewise ignored — the + // assembly step stamps a freshly-minted `rerun:id` per chunk). + let batch = drop_row_id_columns(batch)?; + + // Step 1: pre-stamp a working copy of the schema metadata. + let stamped = stamp_dataframe_metadata(&batch, index, entity_path)?; + + // Step 2: classify (this is where a bad index dtype raises `UnsupportedTimeType`). + let sorbet_batch = SorbetBatch::try_from_record_batch(&stamped, BatchType::Dataframe)?; + + // Step 3: policy. + let index_columns: Vec<(&IndexColumnDescriptor, &ArrowArrayRef)> = + sorbet_batch.index_columns().collect(); + let component_columns: Vec<(&ComponentColumnDescriptor, &ArrowArrayRef)> = + sorbet_batch.component_columns().collect(); + + if matches!(index, DataframeIndex::Auto) && index_columns.is_empty() { + return Err(DataframeToChunksError::AmbiguousStaticData); + } + if component_columns.is_empty() { + return Err(DataframeToChunksError::NoComponentColumns); + } + // The index columns are shared across every emitted chunk, so validate them once here. + reject_null_index_columns(index_columns.iter().map(|&(descr, array)| (descr, array)))?; + + // Step 4: group component columns by entity path, preserving first-seen order. + let mut entity_order: Vec = Vec::new(); + for (descr, _) in &component_columns { + if !entity_order.contains(&descr.entity_path) { + entity_order.push(descr.entity_path.clone()); + } + } + + // Step 5: assemble one chunk batch per entity group. + let num_rows = batch.num_rows(); + let mut chunk_batches = Vec::with_capacity(entity_order.len()); + for entity in entity_order { + let group: Vec<(&ComponentColumnDescriptor, &ArrowArrayRef)> = component_columns + .iter() + .filter(|(descr, _)| descr.entity_path == entity) + .copied() + .collect(); + + let chunk_batch = assemble_chunk_batch(&entity, num_rows, &index_columns, &group)?; + + // Step 6: note static chunks with more than one row. Occasionally legit (tf-transforms), + // but worth a heads-up + if chunk_batch.is_static() && chunk_batch.num_rows() > 1 { + re_log::info!( + "Building a static chunk for entity {entity} from {} rows (latest-at queries only \ + surface the last row)", + chunk_batch.num_rows() + ); + } + + chunk_batches.push(chunk_batch); + } + + Ok(chunk_batches) +} + +/// Reject any index (time) column that contains nulls. +/// +/// A null index value belongs to a row that is neither temporal nor static — exactly the mixed +/// static/temporal case we do not (yet) handle. Rather than emit an unsound chunk batch and defer +/// the failure to chunk construction, we refuse it here. +fn reject_null_index_columns<'a>( + index_columns: impl IntoIterator, +) -> Result<(), DataframeToChunksError> { + for (descr, array) in index_columns { + if array.null_count() > 0 { + return Err(DataframeToChunksError::NullIndexColumn( + descr.column_name().to_owned(), + )); + } + } + Ok(()) +} + +/// Is this field a row-id column? +/// +/// A field with `rerun:kind ∈ {row_id, control}` or the TUID Arrow extension. +fn is_row_id_field(field: &ArrowField) -> bool { + matches!(field.get_opt(RERUN_KIND), Some("row_id" | "control")) + || field.get_opt(ARROW_EXTENSION_NAME) == Some(re_tuid::Tuid::ARROW_EXTENSION_NAME) +} + +/// Was this field detected as a row-id column *only* via the TUID extension (no `rerun:kind`)? +fn is_row_id_via_extension_only(field: &ArrowField) -> bool { + !matches!(field.get_opt(RERUN_KIND), Some("row_id" | "control")) + && field.get_opt(ARROW_EXTENSION_NAME) == Some(re_tuid::Tuid::ARROW_EXTENSION_NAME) +} + +/// Is this field an index (timeline) column under `Auto` classification? +fn is_index_field_auto(field: &ArrowField) -> bool { + matches!(field.get_opt(RERUN_KIND), Some("index" | "time")) + || field.get_opt(SORBET_INDEX_NAME).is_some() +} + +/// Split a column name following the `/entity:component` convention. +/// +/// Returns `(entity, component)` if `name` starts with `/` and contains a `:`. +fn split_name_convention(name: &str) -> Option<(&str, &str)> { + if name.starts_with('/') { + name.split_once(':') + } else { + None + } +} + +/// Resolve a component column's entity path *for the multi-entity guard* on the preserve path. +/// +/// Deliberately ignores the batch-level entity path and the `entity_path` argument: it only flags +/// genuinely-different per-column entities, which `ChunkBatch::try_from` (keyed by batch-level +/// entity) would otherwise silently collapse. +fn guard_component_entity(field: &ArrowField) -> EntityPath { + if let Some(entity) = field.get_opt(SORBET_ENTITY_PATH) { + EntityPath::parse_forgiving(entity) + } else if let Some((entity, _component)) = split_name_convention(field.name()) { + EntityPath::parse_forgiving(entity) + } else { + EntityPath::root() + } +} + +/// Return a copy of `batch` with any row-id column(s) removed. +fn drop_row_id_columns( + batch: &ArrowRecordBatch, +) -> Result { + batch + .clone() + .filter_columns_by(|field| !is_row_id_field(field)) + .map_err(|err| DataframeToChunksError::Sorbet(err.into())) +} + +/// The preserve path: round-trip an *identified chunk* (row-id column + chunk id) as-is. +/// +/// The caller guarantees the batch is fully identified and that no reinterpretation/relocation was +/// requested ([`DataframeIndex::Auto`], no `entity_path`); the only remaining requirement is that +/// it resolves to a single entity path. +fn preserve_identified_chunk( + batch: &ArrowRecordBatch, +) -> Result { + // Single-entity requirement: resolve per-column entities of the component columns (everything + // that is neither a row-id nor an index column). + let mut entities: Vec = Vec::new(); + for field in batch.schema_ref().fields() { + if is_row_id_field(field) || is_index_field_auto(field) { + continue; + } + let entity = guard_component_entity(field); + if !entities.contains(&entity) { + entities.push(entity); + } + } + if entities.len() > 1 { + return Err(DataframeToChunksError::IdentifiedChunkWithMultipleEntities); + } + + // Single entity (or no components): preserve. Stamp the metadata-safety bits when absent. + let mut fields: Vec = Vec::with_capacity(batch.num_columns()); + for field in batch.schema_ref().fields() { + let mut field = field.as_ref().clone(); + // If the row-id column was detected only via the TUID extension, give it an explicit kind + // so the chunk classifier recognizes it. + if is_row_id_via_extension_only(&field) { + field + .metadata_mut() + .insert(RERUN_KIND.to_owned(), "control".to_owned()); + } + fields.push(field); + } + + let mut batch_metadata = batch.schema_ref().metadata().clone(); + // Stamp the entity path when absent (the single resolved entity, else root). + batch_metadata + .entry(SORBET_ENTITY_PATH.to_owned()) + .or_insert_with(|| { + entities + .first() + .cloned() + .unwrap_or_else(EntityPath::root) + .to_string() + }); + // Stamp the version so we skip the migration chain (and its metadata rewrites). + batch_metadata + .entry(SorbetSchema::METADATA_KEY_VERSION.to_owned()) + .or_insert_with(|| SorbetSchema::METADATA_VERSION.to_string()); + + let stamped = rebuild_record_batch(batch, fields, batch_metadata)?; + let chunk_batch = ChunkBatch::try_from(&stamped)?; + reject_null_index_columns(chunk_batch.index_columns())?; + Ok(chunk_batch) +} + +/// Rebuild a record batch with new field metadata / batch metadata but the same arrays. +fn rebuild_record_batch( + batch: &ArrowRecordBatch, + fields: Vec, + batch_metadata: HashMap, +) -> Result { + let schema = Arc::new(ArrowSchema::new_with_metadata(fields, batch_metadata)); + Ok(ArrowRecordBatch::try_new_with_options( + schema, + batch.columns().to_vec(), + &RecordBatchOptions::default().with_row_count(Some(batch.num_rows())), + )?) +} + +/// Build a working copy of the batch with stamped Rerun metadata, ready for Dataframe classification. +fn stamp_dataframe_metadata( + batch: &ArrowRecordBatch, + index: &DataframeIndex, + entity_path: Option<&EntityPath>, +) -> Result { + let batch_entity = batch + .schema_ref() + .metadata() + .get(SORBET_ENTITY_PATH) + .cloned(); + + // For `Columns`, verify every named column exists. + if let DataframeIndex::Columns(names) = index { + for name in names { + if !batch + .schema_ref() + .fields() + .iter() + .any(|f| f.name() == name.as_str()) + { + return Err(DataframeToChunksError::MissingIndexColumn(name.to_string())); + } + } + } + + let mut fields: Vec = Vec::with_capacity(batch.num_columns()); + for field in batch.schema_ref().fields() { + let mut field = field.as_ref().clone(); + let name = field.name().clone(); + + // Static contradiction check (on the raw field metadata). + if matches!(index, DataframeIndex::Static) + && (matches!(field.get_opt(RERUN_KIND), Some("index" | "time")) + || field.get_opt(SORBET_INDEX_NAME).is_some()) + { + return Err(DataframeToChunksError::StaticWithIndex); + } + + let is_index = match index { + DataframeIndex::Auto => is_index_field_auto(&field), + DataframeIndex::Columns(names) => names.iter().any(|n| n.as_str() == name), + DataframeIndex::Static => false, + }; + + if is_index { + field + .metadata_mut() + .insert(RERUN_KIND.to_owned(), "index".to_owned()); + field + .metadata_mut() + .entry(SORBET_INDEX_NAME.to_owned()) + .or_insert_with(|| name.clone()); + } else { + // Component column. Make the kind explicit. + field + .metadata_mut() + .insert(RERUN_KIND.to_owned(), "data".to_owned()); + + // Resolve the entity path (field metadata → batch metadata → name convention → arg → + // root) and stamp it so per-column classification picks it up. + if !field.metadata().contains_key(SORBET_ENTITY_PATH) { + let resolved = if let Some(batch_entity) = &batch_entity { + batch_entity.clone() + } else if let Some((entity, component)) = split_name_convention(&name) { + if !field.metadata().contains_key(FIELD_METADATA_KEY_COMPONENT) { + field.metadata_mut().insert( + FIELD_METADATA_KEY_COMPONENT.to_owned(), + component.to_owned(), + ); + } + entity.to_owned() + } else if let Some(entity_path) = entity_path { + entity_path.to_string() + } else { + EntityPath::root().to_string() + }; + field + .metadata_mut() + .insert(SORBET_ENTITY_PATH.to_owned(), resolved); + } + } + + fields.push(field); + } + + let mut batch_metadata = batch.schema_ref().metadata().clone(); + // Stamp the version so the migration chain early-outs instead of rewriting reserved metadata. + batch_metadata + .entry(SorbetSchema::METADATA_KEY_VERSION.to_owned()) + .or_insert_with(|| SorbetSchema::METADATA_VERSION.to_string()); + + Ok(rebuild_record_batch(batch, fields, batch_metadata)?) +} + +/// Assemble a single chunk batch from a minted row-id column, the shared index columns, and one +/// entity group's component columns. +fn assemble_chunk_batch( + entity: &EntityPath, + num_rows: usize, + index_columns: &[(&IndexColumnDescriptor, &ArrowArrayRef)], + components: &[(&ComponentColumnDescriptor, &ArrowArrayRef)], +) -> Result { + let mut fields: Vec = + Vec::with_capacity(1 + index_columns.len() + components.len()); + let mut arrays: Vec = Vec::with_capacity(fields.capacity()); + + // Minted row-id column (sequential ids are sorted by construction). + fields.push(RowIdColumnDescriptor::from_sorted(true).to_arrow_field()); + arrays.push(Arc::new(mint_row_ids(num_rows))); + + // Shared index columns. + for (descr, array) in index_columns { + fields.push(descr.to_arrow_field()); + arrays.push((*array).clone()); + } + + // This group's component columns (carrying their classified descriptors). + for (descr, array) in components { + fields.push(descr.to_arrow_field(BatchType::Chunk)); + arrays.push((*array).clone()); + } + + let batch_metadata = HashMap::from([ + (RERUN_CHUNK_ID.to_owned(), ChunkId::new().to_string()), + (SORBET_ENTITY_PATH.to_owned(), entity.to_string()), + ( + SorbetSchema::METADATA_KEY_VERSION.to_owned(), + SorbetSchema::METADATA_VERSION.to_string(), + ), + ]); + + let schema = Arc::new(ArrowSchema::new_with_metadata(fields, batch_metadata)); + let record_batch = ArrowRecordBatch::try_new_with_options( + schema, + arrays, + &RecordBatchOptions::default().with_row_count(Some(num_rows)), + )?; + + // `try_from` (not `try_new`) so plain component arrays get auto list-wrapped + reordered. + ChunkBatch::try_from(&record_batch) +} + +/// Mint `count` fresh, sequential (hence sorted) row ids as a `FixedSizeBinary(16)` array. +fn mint_row_ids(count: usize) -> arrow::array::FixedSizeBinaryArray { + let mut ids = Vec::with_capacity(count); + let mut next = RowId::new(); + for _ in 0..count { + ids.push(next); + next = next.next(); + } + re_log::debug_assert_eq!( + RowId::arrow_datatype(), + arrow::datatypes::DataType::FixedSizeBinary(16) + ); + RowId::arrow_from_slice(&ids) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use arrow::array::{ + ArrayRef as ArrowArrayRef, DurationNanosecondArray, Float32Array, Int64Array, + RecordBatch as ArrowRecordBatch, RecordBatchOptions, TimestampMicrosecondArray, + TimestampNanosecondArray, + }; + use arrow::datatypes::{ + DataType as ArrowDatatype, Field as ArrowField, Schema as ArrowSchema, TimeUnit, + }; + use re_log_types::{EntityPath, TimelineName}; + use re_types_core::{Loggable as _, RowId}; + + use super::{DataframeIndex, chunk_batches_from_dataframe_record_batch}; + use crate::{ + ChunkBatch, DataframeToChunksError, RowIdColumnDescriptor, SorbetError, SorbetSchema, + metadata::{RERUN_CHUNK_ID, RERUN_KIND, SORBET_ENTITY_PATH, SORBET_INDEX_NAME}, + }; + + fn field(name: &str, dt: ArrowDatatype, meta: &[(&str, &str)]) -> ArrowField { + ArrowField::new(name, dt, true).with_metadata( + meta.iter() + .map(|(k, v)| ((*k).to_owned(), (*v).to_owned())) + .collect(), + ) + } + + fn batch( + fields: Vec, + arrays: Vec, + batch_meta: &[(&str, &str)], + ) -> ArrowRecordBatch { + let num_rows = arrays.first().map_or(0, |a| a.len()); + let schema = Arc::new(ArrowSchema::new_with_metadata( + fields, + batch_meta + .iter() + .map(|(k, v)| ((*k).to_owned(), (*v).to_owned())) + .collect(), + )); + ArrowRecordBatch::try_new_with_options( + schema, + arrays, + &RecordBatchOptions::default().with_row_count(Some(num_rows)), + ) + .unwrap() + } + + fn int64(values: &[i64]) -> ArrowArrayRef { + Arc::new(Int64Array::from(values.to_vec())) + } + + fn floats(values: &[f32]) -> ArrowArrayRef { + Arc::new(Float32Array::from(values.to_vec())) + } + + /// `kind=index` + `kind=data` → one temporal chunk. + #[test] + fn auto_temporal() { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + ), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + assert_eq!(chunks.len(), 1); + let chunk = &chunks[0]; + assert!(!chunk.is_static()); + assert_eq!(chunk.entity_path(), &EntityPath::from("/e")); + assert_eq!(chunk.index_columns().count(), 1); + assert_eq!( + chunk.index_columns().next().unwrap().0.timeline_name(), + TimelineName::from("frame") + ); + } + + /// An `index_name`-only column (no `rerun:kind`) is still promoted to a timeline under Auto. + /// Guards the `reader()` round-trip. + #[test] + fn auto_index_name_only() { + let rb = batch( + vec![ + field( + "frame", + ArrowDatatype::Int64, + &[(SORBET_INDEX_NAME, "frame")], + ), + field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + ), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + assert_eq!(chunks.len(), 1); + assert!(!chunks[0].is_static()); + assert_eq!(chunks[0].index_columns().count(), 1); + } + + /// A plain (non-list) component array is auto list-wrapped. + #[test] + fn plain_component_array_is_list_wrapped() { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + ), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + let (_descr, array) = chunks[0].component_columns().next().unwrap(); + assert!( + matches!(array.data_type(), ArrowDatatype::List(_)), + "component column should be list-wrapped, got {:?}", + array.data_type() + ); + } + + /// Auto + zero index columns → `AmbiguousStaticData`. + #[test] + fn auto_no_index_is_ambiguous() { + let rb = batch( + vec![field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + )], + vec![floats(&[1.0, 2.0])], + &[], + ); + let err = chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None) + .unwrap_err(); + assert!(matches!(err, DataframeToChunksError::AmbiguousStaticData)); + } + + /// `Columns` promotes the named columns, with their time type taken from the Arrow dtype. + #[test] + fn columns_promotion_time_types() { + let cases: Vec<(ArrowDatatype, ArrowArrayRef)> = vec![ + (ArrowDatatype::Int64, int64(&[0, 1])), + ( + ArrowDatatype::Timestamp(TimeUnit::Nanosecond, None), + Arc::new(TimestampNanosecondArray::from(vec![0, 1])), + ), + ( + ArrowDatatype::Duration(TimeUnit::Nanosecond), + Arc::new(DurationNanosecondArray::from(vec![0, 1])), + ), + ]; + for (dt, array) in cases { + let rb = batch( + vec![ + field("t", dt.clone(), &[]), + field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + ), + ], + vec![array, floats(&[1.0, 2.0])], + &[], + ); + let chunks = chunk_batches_from_dataframe_record_batch( + &rb, + &DataframeIndex::Columns(vec![TimelineName::from("t")]), + None, + ) + .unwrap(); + assert_eq!(chunks.len(), 1, "dtype {dt:?}"); + assert_eq!(chunks[0].index_columns().count(), 1, "dtype {dt:?}"); + } + } + + /// `Columns` on an unsupported time dtype fails at classification with `UnsupportedTimeType`. + #[test] + fn columns_bad_time_type() { + let rb = batch( + vec![ + field( + "t", + ArrowDatatype::Timestamp(TimeUnit::Microsecond, None), + &[], + ), + field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + ), + ], + vec![ + Arc::new(TimestampMicrosecondArray::from(vec![0, 1])), + floats(&[1.0, 2.0]), + ], + &[], + ); + let err = chunk_batches_from_dataframe_record_batch( + &rb, + &DataframeIndex::Columns(vec![TimelineName::from("t")]), + None, + ) + .unwrap_err(); + assert!( + matches!( + err, + DataframeToChunksError::Sorbet(SorbetError::IndexColumn(_)) + ), + "got {err}" + ); + } + + /// A named index column that does not exist → `MissingIndexColumn`. + #[test] + fn columns_missing() { + let rb = batch( + vec![field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + )], + vec![floats(&[1.0, 2.0])], + &[], + ); + let err = chunk_batches_from_dataframe_record_batch( + &rb, + &DataframeIndex::Columns(vec![TimelineName::from("nope")]), + None, + ) + .unwrap_err(); + assert!( + matches!(err, DataframeToChunksError::MissingIndexColumn(_)), + "got {err}" + ); + } + + /// A null value in a promoted index column → `NullIndexColumn` (rejected eagerly). + #[test] + fn null_index_value_rejected() { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + ), + ], + vec![ + Arc::new(Int64Array::from(vec![Some(0_i64), None])), + floats(&[1.0, 2.0]), + ], + &[], + ); + let err = chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None) + .unwrap_err(); + assert!( + matches!(err, DataframeToChunksError::NullIndexColumn(_)), + "got {err}" + ); + } + + /// `Static` produces a static chunk; an index contradiction is rejected. + #[test] + fn static_and_contradiction() { + let rb = batch( + vec![field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + )], + vec![floats(&[1.0])], + &[], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Static, None).unwrap(); + assert!(chunks[0].is_static()); + assert_eq!(chunks[0].index_columns().count(), 0); + + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + ), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[], + ); + let err = chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Static, None) + .unwrap_err(); + assert!( + matches!(err, DataframeToChunksError::StaticWithIndex), + "got {err}" + ); + } + + /// Multiple entities split into multiple chunks, preserving first-seen column order. + #[test] + fn multi_entity_split_order() { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field( + "/b:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/b")], + ), + field( + "/a:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/a")], + ), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0]), floats(&[3.0, 4.0])], + &[], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + let entities: Vec<_> = chunks.iter().map(|c| c.entity_path().to_string()).collect(); + assert_eq!(entities, vec!["/b".to_owned(), "/a".to_owned()]); + + // The shared index column is carried by every emitted chunk. + for chunk in &chunks { + assert_eq!(chunk.index_columns().count(), 1); + assert_eq!( + chunk.index_columns().next().unwrap().0.timeline_name(), + TimelineName::from("frame") + ); + } + } + + /// Batch-level `rerun:entity_path` wins over the column-name convention (resolution-order guard). + #[test] + fn batch_level_entity_path_wins_over_name_convention() { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + // A conventional name that *would* resolve to `/a`, but no own entity metadata. + field("/a:c", ArrowDatatype::Float32, &[]), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[(SORBET_ENTITY_PATH, "/batch")], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].entity_path(), &EntityPath::from("/batch")); + } + + /// The name convention sets the component identifier to the part after the first `:`. + #[test] + fn name_convention_extracts_component_id() { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field("/points:Points3D:positions", ArrowDatatype::Float32, &[]), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + assert_eq!(chunks[0].entity_path(), &EntityPath::from("/points")); + let (descr, _) = chunks[0].component_columns().next().unwrap(); + assert_eq!(descr.component.to_string(), "Points3D:positions"); + } + + /// The column-name convention: leading-`/` required, otherwise root. + #[test] + fn name_convention() { + let cases = [ + ("/e:c", "/e"), + ("/e:Arch:c", "/e"), + ("foo:bar", "/"), // no leading slash → root + ("property:foo", "/"), // not recognized → root + ]; + for (name, expected_entity) in cases { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field(name, ArrowDatatype::Float32, &[]), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None) + .unwrap(); + assert_eq!( + chunks[0].entity_path(), + &EntityPath::from(expected_entity), + "name {name:?}" + ); + } + } + + /// `entity_path` arg is used as the default for un-located component columns. + #[test] + fn entity_path_arg_default() { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field("bare", ArrowDatatype::Float32, &[]), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[], + ); + let chunks = chunk_batches_from_dataframe_record_batch( + &rb, + &DataframeIndex::Auto, + Some(&EntityPath::from("/world")), + ) + .unwrap(); + assert_eq!(chunks[0].entity_path(), &EntityPath::from("/world")); + } + + /// Zero component columns → `NoComponentColumns`. + #[test] + fn no_component_columns() { + let rb = batch( + vec![field( + "frame", + ArrowDatatype::Int64, + &[(RERUN_KIND, "index")], + )], + vec![int64(&[0, 1])], + &[], + ); + let err = chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None) + .unwrap_err(); + assert!( + matches!(err, DataframeToChunksError::NoComponentColumns), + "got {err}" + ); + } + + /// Build a chunk-shaped batch carrying a row-id column with a known chunk id + row ids. + fn row_id_batch( + component_meta: &[(&str, &str)], + with_version: bool, + entity: &str, + ) -> (ArrowRecordBatch, re_types_core::ChunkId, RowId) { + let chunk_id = re_types_core::ChunkId::new(); + let first_row_id = RowId::new(); + let row_ids = RowId::arrow_from_slice(&[first_row_id, first_row_id.next()]); + + let mut batch_meta = vec![ + (RERUN_CHUNK_ID.to_owned(), chunk_id.to_string()), + (SORBET_ENTITY_PATH.to_owned(), entity.to_owned()), + ]; + if with_version { + batch_meta.push(( + SorbetSchema::METADATA_KEY_VERSION.to_owned(), + SorbetSchema::METADATA_VERSION.to_string(), + )); + } + + let schema = Arc::new(ArrowSchema::new_with_metadata( + vec![ + RowIdColumnDescriptor::from_sorted(true).to_arrow_field(), + field("c", ArrowDatatype::Float32, component_meta), + ], + batch_meta.into_iter().collect(), + )); + let rb = ArrowRecordBatch::try_new_with_options( + schema, + vec![Arc::new(row_ids), floats(&[1.0, 2.0])], + &RecordBatchOptions::default().with_row_count(Some(2)), + ) + .unwrap(); + (rb, chunk_id, first_row_id) + } + + /// The preserve path keeps the chunk id and row ids. + #[test] + fn preserve_keeps_ids() { + let (rb, chunk_id, first_row_id) = row_id_batch(&[(RERUN_KIND, "data")], true, "/foo"); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].chunk_id(), chunk_id); + assert_eq!(chunks[0].entity_path(), &EntityPath::from("/foo")); + let (_descr, ids) = chunks[0].row_id_column(); + let preserved = RowId::from_arrow(&(Arc::new(ids.clone()) as ArrowArrayRef)).unwrap(); + assert_eq!(preserved[0], first_row_id); + } + + /// A TUID-extension-only row-id column (no `rerun:kind`) preserves correctly. + #[test] + fn preserve_tuid_extension_only() { + let chunk_id = re_types_core::ChunkId::new(); + let mut row_id_field = RowIdColumnDescriptor::from_sorted(true).to_arrow_field(); + // Drop the `rerun:kind` so only the TUID extension marks it. + row_id_field.metadata_mut().remove(RERUN_KIND); + let schema = Arc::new(ArrowSchema::new_with_metadata( + vec![ + row_id_field, + field("c", ArrowDatatype::Float32, &[(RERUN_KIND, "data")]), + ], + [ + (RERUN_CHUNK_ID.to_owned(), chunk_id.to_string()), + (SORBET_ENTITY_PATH.to_owned(), "/foo".to_owned()), + ( + SorbetSchema::METADATA_KEY_VERSION.to_owned(), + SorbetSchema::METADATA_VERSION.to_string(), + ), + ] + .into_iter() + .collect(), + )); + let rb = ArrowRecordBatch::try_new_with_options( + schema, + vec![ + Arc::new(RowId::arrow_from_slice(&[RowId::new(), RowId::new()])), + floats(&[1.0, 2.0]), + ], + &RecordBatchOptions::default().with_row_count(Some(2)), + ) + .unwrap(); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].chunk_id(), chunk_id); + } + + /// Reinterpreting an identified chunk (e.g. `index=None`) mints a fresh identity and discards + /// the provided chunk id / row ids. + #[test] + fn reinterpretation_mints_fresh_identity() { + let (rb, chunk_id, first_row_id) = row_id_batch(&[(RERUN_KIND, "data")], true, "/foo"); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Static, None).unwrap(); + assert_eq!(chunks.len(), 1); + assert!(chunks[0].is_static()); + assert_eq!(chunks[0].entity_path(), &EntityPath::from("/foo")); + + // The provided identity was discarded: fresh chunk id and fresh row ids. + assert_ne!(chunks[0].chunk_id(), chunk_id); + let (_descr, ids) = chunks[0].row_id_column(); + let minted = RowId::from_arrow(&(Arc::new(ids.clone()) as ArrowArrayRef)).unwrap(); + assert_ne!(minted[0], first_row_id); + } + + /// Providing `entity_path` defeats identity preservation: even a fully-identified chunk is + /// reinterpreted, minting a fresh chunk id and fresh row ids. + #[test] + fn entity_path_arg_forces_mint() { + let chunk_id = re_types_core::ChunkId::new(); + let first_row_id = RowId::new(); + let schema = Arc::new(ArrowSchema::new_with_metadata( + vec![ + RowIdColumnDescriptor::from_sorted(true).to_arrow_field(), + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field( + "c", + ArrowDatatype::Float32, + &[(RERUN_KIND, "data"), (SORBET_ENTITY_PATH, "/foo")], + ), + ], + [ + (RERUN_CHUNK_ID.to_owned(), chunk_id.to_string()), + (SORBET_ENTITY_PATH.to_owned(), "/foo".to_owned()), + ( + SorbetSchema::METADATA_KEY_VERSION.to_owned(), + SorbetSchema::METADATA_VERSION.to_string(), + ), + ] + .into_iter() + .collect(), + )); + let rb = ArrowRecordBatch::try_new_with_options( + schema, + vec![ + Arc::new(RowId::arrow_from_slice(&[ + first_row_id, + first_row_id.next(), + ])), + int64(&[0, 1]), + floats(&[1.0, 2.0]), + ], + &RecordBatchOptions::default().with_row_count(Some(2)), + ) + .unwrap(); + + // Sanity: with no `entity_path` this is the preserve path and keeps the chunk id. + let preserved = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + assert_eq!(preserved[0].chunk_id(), chunk_id); + + // With `entity_path`, identity preservation is defeated → fresh chunk id and row ids. + let chunks = chunk_batches_from_dataframe_record_batch( + &rb, + &DataframeIndex::Auto, + Some(&EntityPath::from("/relocated")), + ) + .unwrap(); + assert_eq!(chunks.len(), 1); + assert_ne!(chunks[0].chunk_id(), chunk_id); + let (_descr, ids) = chunks[0].row_id_column(); + let minted = RowId::from_arrow(&(Arc::new(ids.clone()) as ArrowArrayRef)).unwrap(); + assert_ne!(minted[0], first_row_id); + } + + /// A row-id column but no chunk id is only *partially* identified, so fresh ids are minted and + /// the batch may split into one chunk per entity path. + #[test] + fn partial_identity_mints_and_splits() { + let schema = Arc::new(ArrowSchema::new_with_metadata( + vec![ + RowIdColumnDescriptor::from_sorted(true).to_arrow_field(), + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field( + "x", + ArrowDatatype::Float32, + &[(RERUN_KIND, "data"), (SORBET_ENTITY_PATH, "/a")], + ), + field( + "y", + ArrowDatatype::Float32, + &[(RERUN_KIND, "data"), (SORBET_ENTITY_PATH, "/b")], + ), + ], + // Note: no `rerun:id` → only partially identified. + std::iter::once(( + SorbetSchema::METADATA_KEY_VERSION.to_owned(), + SorbetSchema::METADATA_VERSION.to_string(), + )) + .collect(), + )); + let rb = ArrowRecordBatch::try_new_with_options( + schema, + vec![ + Arc::new(RowId::arrow_from_slice(&[RowId::new(), RowId::new()])), + int64(&[0, 1]), + floats(&[1.0, 2.0]), + floats(&[3.0, 4.0]), + ], + &RecordBatchOptions::default().with_row_count(Some(2)), + ) + .unwrap(); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + let entities: Vec<_> = chunks.iter().map(|c| c.entity_path().to_string()).collect(); + assert_eq!(entities, vec!["/a".to_owned(), "/b".to_owned()]); + } + + /// An identified chunk (row-id + chunk id) with components on more than one entity → error. + #[test] + fn identified_chunk_with_multiple_entities() { + let chunk_id = re_types_core::ChunkId::new(); + let schema = Arc::new(ArrowSchema::new_with_metadata( + vec![ + RowIdColumnDescriptor::from_sorted(true).to_arrow_field(), + field( + "x", + ArrowDatatype::Float32, + &[(RERUN_KIND, "data"), (SORBET_ENTITY_PATH, "/a")], + ), + field( + "y", + ArrowDatatype::Float32, + &[(RERUN_KIND, "data"), (SORBET_ENTITY_PATH, "/b")], + ), + ], + [ + (RERUN_CHUNK_ID.to_owned(), chunk_id.to_string()), + ( + SorbetSchema::METADATA_KEY_VERSION.to_owned(), + SorbetSchema::METADATA_VERSION.to_string(), + ), + ] + .into_iter() + .collect(), + )); + let rb = ArrowRecordBatch::try_new_with_options( + schema, + vec![ + Arc::new(RowId::arrow_from_slice(&[RowId::new(), RowId::new()])), + floats(&[1.0, 2.0]), + floats(&[3.0, 4.0]), + ], + &RecordBatchOptions::default().with_row_count(Some(2)), + ) + .unwrap(); + let err = chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None) + .unwrap_err(); + assert!( + matches!( + err, + DataframeToChunksError::IdentifiedChunkWithMultipleEntities + ), + "got {err}" + ); + } + + /// The preserve path stamps `sorbet:version`, so a version-less row-id batch carrying a + /// `Pose*` component type survives the migration chain unchanged. + #[test] + fn preserve_migration_safety() { + let (rb, _, _) = row_id_batch( + &[ + (RERUN_KIND, "data"), + ("rerun:component", "translation"), + ("rerun:component_type", "rerun.components.PoseTranslation3D"), + ], + /* with_version = */ false, + "/foo", + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + let (descr, _) = chunks[0].component_columns().next().unwrap(); + assert_eq!( + descr.component_type.map(|c| c.to_string()), + Some("rerun.components.PoseTranslation3D".to_owned()), + "the Pose component type should not have been migrated away" + ); + } + + /// Sanity: a `ChunkBatch` round-trips back through `ArrowRecordBatch` (smoke test for assembly). + #[test] + fn assembly_is_a_valid_chunk_batch() { + let rb = batch( + vec![ + field("frame", ArrowDatatype::Int64, &[(RERUN_KIND, "index")]), + field( + "/e:c", + ArrowDatatype::Float32, + &[(SORBET_ENTITY_PATH, "/e")], + ), + ], + vec![int64(&[0, 1]), floats(&[1.0, 2.0])], + &[], + ); + let chunks = + chunk_batches_from_dataframe_record_batch(&rb, &DataframeIndex::Auto, None).unwrap(); + let round_trip = ArrowRecordBatch::from(&chunks[0]); + assert!(ChunkBatch::try_from(&round_trip).is_ok()); + } +} diff --git a/crates/store/re_sorbet/src/error.rs b/crates/store/re_sorbet/src/error.rs index 3eca38e3511a..90846124f32f 100644 --- a/crates/store/re_sorbet/src/error.rs +++ b/crates/store/re_sorbet/src/error.rs @@ -12,11 +12,14 @@ pub enum SorbetError { MissingFieldMetadata(#[from] crate::MissingFieldMetadata), #[error(transparent)] - UnsupportedTimeType(#[from] crate::UnsupportedTimeType), + IndexColumn(#[from] crate::IndexColumnError), #[error(transparent)] WrongDatatypeError(#[from] re_arrow_util::WrongDatatypeError), + #[error(transparent)] + InvalidComponentIdentifier(#[from] re_types_core::InvalidComponentIdentifierError), + #[error(transparent)] ArrowError(#[from] ArrowError), diff --git a/crates/store/re_sorbet/src/index_column_descriptor.rs b/crates/store/re_sorbet/src/index_column_descriptor.rs index 3973c37156b8..d70c5f917d4c 100644 --- a/crates/store/re_sorbet/src/index_column_descriptor.rs +++ b/crates/store/re_sorbet/src/index_column_descriptor.rs @@ -4,13 +4,16 @@ use re_log_types::{Timeline, TimelineName}; use crate::MetadataExt as _; #[derive(thiserror::Error, Debug)] -#[error("Unsupported time type: {datatype}")] -pub struct UnsupportedTimeType { - pub datatype: ArrowDatatype, +pub enum IndexColumnError { + #[error("Unsupported time type: {datatype}")] + UnsupportedTimeType { datatype: ArrowDatatype }, + + #[error(transparent)] + InvalidTimelineName(#[from] re_types_core::InvalidTimelineNameError), } /// Describes a time column, such as `log_time`. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, re_byte_size::SizeBytes)] pub struct IndexColumnDescriptor { /// The timeline this column is associated with. pub timeline: Timeline, @@ -24,17 +27,6 @@ pub struct IndexColumnDescriptor { pub is_sorted: bool, } -impl re_byte_size::SizeBytes for IndexColumnDescriptor { - fn heap_size_bytes(&self) -> u64 { - let Self { - timeline, - datatype, - is_sorted: _, - } = self; - timeline.heap_size_bytes() + datatype.heap_size_bytes() - } -} - impl PartialOrd for IndexColumnDescriptor { #[inline] fn partial_cmp(&self, other: &Self) -> Option { @@ -143,7 +135,7 @@ impl From for IndexColumnDescriptor { } impl TryFrom<&ArrowField> for IndexColumnDescriptor { - type Error = UnsupportedTimeType; + type Error = IndexColumnError; fn try_from(field: &ArrowField) -> Result { let name = if let Some(name) = field.metadata().get(crate::metadata::SORBET_INDEX_NAME) { @@ -159,10 +151,10 @@ impl TryFrom<&ArrowField> for IndexColumnDescriptor { let datatype = field.data_type().clone(); let Some(time_type) = re_log_types::TimeType::from_arrow_datatype(&datatype) else { - return Err(UnsupportedTimeType { datatype }); + return Err(IndexColumnError::UnsupportedTimeType { datatype }); }; - let timeline = Timeline::new(name, time_type); + let timeline = Timeline::new(TimelineName::try_new(name)?, time_type); Ok(Self { timeline, diff --git a/crates/store/re_sorbet/src/lib.rs b/crates/store/re_sorbet/src/lib.rs index 14ec581487bb..6438d7fa69f9 100644 --- a/crates/store/re_sorbet/src/lib.rs +++ b/crates/store/re_sorbet/src/lib.rs @@ -18,6 +18,7 @@ mod column_descriptor; mod column_descriptor_ref; mod column_kind; mod component_column_descriptor; +mod dataframe_to_chunks; mod error; mod index_column_descriptor; mod ipc; @@ -40,8 +41,11 @@ pub use self::column_descriptor::{ColumnDescriptor, ColumnError}; pub use self::column_descriptor_ref::ColumnDescriptorRef; pub use self::column_kind::{ColumnKind, UnknownColumnKind}; pub use self::component_column_descriptor::ComponentColumnDescriptor; +pub use self::dataframe_to_chunks::{ + DataframeIndex, DataframeToChunksError, chunk_batches_from_dataframe_record_batch, +}; pub use self::error::SorbetError; -pub use self::index_column_descriptor::{IndexColumnDescriptor, UnsupportedTimeType}; +pub use self::index_column_descriptor::{IndexColumnDescriptor, IndexColumnError}; pub use self::ipc::{ipc_from_schema, migrated_schema_from_ipc, raw_schema_from_ipc}; pub use self::metadata::{ ArrowBatchMetadata, ArrowFieldMetadata, MetadataExt, MissingFieldMetadata, MissingMetadataKey, @@ -80,7 +84,7 @@ pub fn chunk_id_of_schema( ) -> Result { let metadata = schema.metadata(); if let Some(chunk_id_str) = metadata - .get("rerun:id") + .get(crate::metadata::RERUN_CHUNK_ID) .or_else(|| metadata.get("rerun.id")) { chunk_id_str.parse().map_err(|err| { diff --git a/crates/store/re_sorbet/src/metadata.rs b/crates/store/re_sorbet/src/metadata.rs index b70741b3adef..71091f5af943 100644 --- a/crates/store/re_sorbet/src/metadata.rs +++ b/crates/store/re_sorbet/src/metadata.rs @@ -5,6 +5,9 @@ use arrow::datatypes::Field as ArrowField; // The following constants are used as metadata keys. See also // [`re_types_core::component_descriptor`] for additional constants. +/// The key used to identify the chunk ID in batch-level metadata. +pub const RERUN_CHUNK_ID: &str = "rerun:id"; + /// The key used to identify the index name in field-level metadata. pub const SORBET_INDEX_NAME: &str = "rerun:index_name"; diff --git a/crates/store/re_sorbet/src/migrations/mod.rs b/crates/store/re_sorbet/src/migrations/mod.rs index 871d17ee4b2c..e90a7663a077 100644 --- a/crates/store/re_sorbet/src/migrations/mod.rs +++ b/crates/store/re_sorbet/src/migrations/mod.rs @@ -59,7 +59,7 @@ fn get_or_guess_version(batch: &RecordBatch) -> Result { }) } else { // The record batch does not have a sorbet version metadata. - // Rerun cloud schemas currently come without metadata, + // Rerun Hub schemas currently come without metadata, // so we need to run the full migration just in case. // TODO(RR-2175): Always include version metadata in redap @@ -106,7 +106,7 @@ fn maybe_apply( } /// Migrate a sorbet record batch of unknown version to the latest version. -#[tracing::instrument(level = "debug", skip_all)] +#[tracing::instrument(level = "trace", skip_all)] pub fn migrate_record_batch(mut batch: RecordBatch, batch_type: BatchType) -> RecordBatch { batch = migrate_record_batch_impl(batch); diff --git a/crates/store/re_sorbet/src/migrations/v0_0_1__to__v0_0_2.rs b/crates/store/re_sorbet/src/migrations/v0_0_1__to__v0_0_2.rs index 859d7bcccd35..5f24e0d8aafc 100644 --- a/crates/store/re_sorbet/src/migrations/v0_0_1__to__v0_0_2.rs +++ b/crates/store/re_sorbet/src/migrations/v0_0_1__to__v0_0_2.rs @@ -81,7 +81,6 @@ impl TryFrom<&ArrowField> for ColumnKind { } /// Migrate TUID:s with the pre-0.23 encoding. -#[tracing::instrument(level = "trace", skip_all)] fn migrate_tuids(batch: &ArrowRecordBatch) -> ArrowRecordBatch { re_tracing::profile_function!(); @@ -125,7 +124,6 @@ fn migrate_tuids(batch: &ArrowRecordBatch) -> ArrowRecordBatch { } /// Migrate TUID:s with the pre-0.23 encoding. -#[tracing::instrument(level = "trace", skip_all)] fn migrate_tuid_column( field: ArrowFieldRef, array: ArrowArrayRef, diff --git a/crates/store/re_sorbet/src/row_id_column_descriptor.rs b/crates/store/re_sorbet/src/row_id_column_descriptor.rs index 7e3f23675449..b1a94ae96989 100644 --- a/crates/store/re_sorbet/src/row_id_column_descriptor.rs +++ b/crates/store/re_sorbet/src/row_id_column_descriptor.rs @@ -5,7 +5,7 @@ use re_types_core::{Loggable as _, RowId}; use crate::MetadataExt as _; /// Describes the schema of the primary [`RowId`] column. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, re_byte_size::SizeBytes)] pub struct RowIdColumnDescriptor { /// Are the values in this column sorted? /// diff --git a/crates/store/re_sorbet/src/schema_builder.rs b/crates/store/re_sorbet/src/schema_builder.rs index 4b99051d0978..643a92f2128a 100644 --- a/crates/store/re_sorbet/src/schema_builder.rs +++ b/crates/store/re_sorbet/src/schema_builder.rs @@ -43,7 +43,7 @@ impl SchemaBuilder { let chunk_schema = chunk_batch.chunk_schema(); for (column_descriptor, array_ref) in - (*chunk_schema.columns).iter().zip(chunk_batch.columns()) + std::iter::zip(chunk_schema.columns.iter(), chunk_batch.columns()) { let this_metadata = match column_descriptor { ColumnDescriptor::RowId(_) | ColumnDescriptor::Time(_) => ColumnMetadata { diff --git a/crates/store/re_sorbet/src/selectors.rs b/crates/store/re_sorbet/src/selectors.rs index 64756a8dad6e..41ad3578e9c3 100644 --- a/crates/store/re_sorbet/src/selectors.rs +++ b/crates/store/re_sorbet/src/selectors.rs @@ -1,5 +1,5 @@ use re_log_types::{EntityPath, Timeline, TimelineName}; -use re_types_core::ComponentDescriptor; +use re_types_core::{ComponentDescriptor, ComponentIdentifier}; use crate::{ColumnDescriptor, ComponentColumnDescriptor, IndexColumnDescriptor}; @@ -69,24 +69,6 @@ impl From for TimeColumnSelector { } } -impl From<&str> for TimeColumnSelector { - #[inline] - fn from(timeline: &str) -> Self { - Self { - timeline: timeline.into(), - } - } -} - -impl From for TimeColumnSelector { - #[inline] - fn from(timeline: String) -> Self { - Self { - timeline: timeline.into(), - } - } -} - impl From for TimeColumnSelector { #[inline] fn from(desc: IndexColumnDescriptor) -> Self { @@ -127,6 +109,15 @@ impl ComponentColumnSelector { pub fn column_name(&self) -> String { format!("{}:{}", self.entity_path, self.component) } + + /// The [`ComponentIdentifier`] this selector refers to. + /// + /// Fails if the stored component string is invalid (e.g. empty). + pub fn component_identifier( + &self, + ) -> Result { + ComponentIdentifier::try_new(&self.component) + } } impl std::str::FromStr for ComponentColumnSelector { diff --git a/crates/store/re_sorbet/src/sorbet_batch.rs b/crates/store/re_sorbet/src/sorbet_batch.rs index f68497aeb124..51d4b41db7c3 100644 --- a/crates/store/re_sorbet/src/sorbet_batch.rs +++ b/crates/store/re_sorbet/src/sorbet_batch.rs @@ -118,7 +118,7 @@ impl SorbetBatch { /// The columns of the indices (timelines). pub fn index_columns(&self) -> impl Iterator { - itertools::izip!(self.schema.columns.iter(), self.batch.columns().iter()).filter_map( + itertools::izip!(self.schema.columns.iter(), self.batch.columns()).filter_map( |(descr, array)| { if let ColumnDescriptor::Time(descr) = descr { Some((descr, array)) @@ -133,7 +133,7 @@ impl SorbetBatch { pub fn component_columns( &self, ) -> impl Iterator { - itertools::izip!(self.schema.columns.iter(), self.batch.columns().iter()).filter_map( + itertools::izip!(self.schema.columns.iter(), self.batch.columns()).filter_map( |(descr, array)| { if let ColumnDescriptor::Component(descr) = descr { Some((descr, array)) @@ -189,7 +189,6 @@ impl SorbetBatch { /// /// Non-Rerun metadata will be preserved (both at batch-level and column-level). /// Rerun metadata will be updated and added to the batch if needed. - #[tracing::instrument(level = "trace", skip_all)] pub fn try_from_record_batch( batch: &ArrowRecordBatch, batch_type: crate::BatchType, diff --git a/crates/store/re_sorbet/src/sorbet_columns.rs b/crates/store/re_sorbet/src/sorbet_columns.rs index d16f1d23b2e6..d11cbb188a98 100644 --- a/crates/store/re_sorbet/src/sorbet_columns.rs +++ b/crates/store/re_sorbet/src/sorbet_columns.rs @@ -160,7 +160,7 @@ impl SorbetColumnDescriptors { } ColumnKind::Component => ColumnDescriptor::Component( - ComponentColumnDescriptor::from_arrow_field(chunk_entity_path, field), + ComponentColumnDescriptor::from_arrow_field(chunk_entity_path, field)?, ), }; diff --git a/crates/store/re_sorbet/src/sorbet_schema.rs b/crates/store/re_sorbet/src/sorbet_schema.rs index 727a6957fb0c..174d2c534740 100644 --- a/crates/store/re_sorbet/src/sorbet_schema.rs +++ b/crates/store/re_sorbet/src/sorbet_schema.rs @@ -2,7 +2,7 @@ use std::collections::BTreeSet; use arrow::datatypes::{Schema as ArrowSchema, SchemaRef as ArrowSchemaRef}; use re_log_types::EntityPath; -use re_types_core::ChunkId; +use re_types_core::{ChunkId, SegmentId}; use crate::{ ArrowBatchMetadata, SorbetColumnDescriptors, SorbetError, TimestampMetadata, migrate_schema_ref, @@ -26,9 +26,12 @@ pub struct SorbetSchema { pub entity_path: Option, /// The segment id that this chunk belongs to. - pub segment_id: Option, + pub segment_id: Option, /// Timing statistics. + /// + /// NOT related to timelines. + /// This is about measuring the latency of the data pipeline, from SDK to viewer. pub timestamps: TimestampMetadata, } @@ -46,25 +49,28 @@ impl SorbetSchema { } impl SorbetSchema { - pub fn chunk_id_metadata(chunk_id: &ChunkId) -> (String, String) { - ("rerun:id".to_owned(), chunk_id.to_string()) - } + pub fn arrow_batch_metadata(&self) -> ArrowBatchMetadata { + fn chunk_id_metadata(chunk_id: &ChunkId) -> (String, String) { + ( + crate::metadata::RERUN_CHUNK_ID.to_owned(), + chunk_id.to_string(), + ) + } - pub fn entity_path_metadata(entity_path: &EntityPath) -> (String, String) { - ( - crate::metadata::SORBET_ENTITY_PATH.to_owned(), - entity_path.to_string(), - ) - } + fn entity_path_metadata(entity_path: &EntityPath) -> (String, String) { + ( + crate::metadata::SORBET_ENTITY_PATH.to_owned(), + entity_path.to_string(), + ) + } - pub fn segment_id_metadata(segment_id: impl AsRef) -> (String, String) { - ( - "rerun:segment_id".to_owned(), - segment_id.as_ref().to_owned(), - ) - } + fn segment_id_metadata(segment_id: impl AsRef) -> (String, String) { + ( + "rerun:segment_id".to_owned(), + segment_id.as_ref().to_owned(), + ) + } - pub fn arrow_batch_metadata(&self) -> ArrowBatchMetadata { let Self { columns: _, chunk_id, @@ -73,28 +79,30 @@ impl SorbetSchema { timestamps, } = self; - [ - Some(( - Self::METADATA_KEY_VERSION.to_owned(), - Self::METADATA_VERSION.to_string(), - )), - chunk_id.as_ref().map(Self::chunk_id_metadata), - entity_path.as_ref().map(Self::entity_path_metadata), - segment_id.as_ref().map(Self::segment_id_metadata), - ] - .into_iter() - .flatten() - .chain(timestamps.to_metadata()) + std::iter::chain( + [ + Some(( + Self::METADATA_KEY_VERSION.to_owned(), + Self::METADATA_VERSION.to_string(), + )), + chunk_id.as_ref().map(chunk_id_metadata), + entity_path.as_ref().map(entity_path_metadata), + segment_id.as_ref().map(segment_id_metadata), + ] + .into_iter() + .flatten(), + timestamps.to_metadata(), + ) .collect() } /// All the entities referenced by any column. pub fn all_entities(&self) -> BTreeSet<&EntityPath> { - self.columns - .iter() - .filter_map(|c| c.entity_path()) - .chain(self.entity_path.iter()) - .collect() + std::iter::chain( + self.columns.iter().filter_map(|c| c.entity_path()), + self.entity_path.iter(), + ) + .collect() } } @@ -138,7 +146,7 @@ impl SorbetSchema { let columns = SorbetColumnDescriptors::try_from_arrow_fields(entity_path.as_ref(), fields)?; - let chunk_id = if let Some(chunk_id_str) = metadata.get("rerun:id") { + let chunk_id = if let Some(chunk_id_str) = metadata.get(crate::metadata::RERUN_CHUNK_ID) { Some(chunk_id_str.parse().map_err(|err| { SorbetError::ChunkIdDeserializationError(format!( "Failed to deserialize chunk id {chunk_id_str:?}: {err}" @@ -152,7 +160,7 @@ impl SorbetSchema { let segment_id = metadata .get("rerun:segment_id") .or_else(|| metadata.get("rerun:partition_id")) - .map(|s| s.to_owned()); + .map(|s| SegmentId::from(s.as_str())); // Verify version if let Some(batch_version) = metadata.get(Self::METADATA_KEY_VERSION) @@ -206,7 +214,7 @@ mod tests { // Verify that segment_id is correctly populated from the legacy partition_id assert_eq!( sorbet_schema.segment_id, - Some(partition_id_value.to_owned()), + Some(partition_id_value.into()), "Legacy rerun:partition_id should be read as segment_id" ); } @@ -239,7 +247,7 @@ mod tests { // Verify that segment_id takes precedence assert_eq!( sorbet_schema.segment_id, - Some(segment_id_value.to_owned()), + Some(segment_id_value.into()), "rerun:segment_id should take precedence over rerun:partition_id" ); } diff --git a/crates/store/re_tf/benches/transform_resolution_cache_bench.rs b/crates/store/re_tf/benches/transform_resolution_cache_bench.rs index a2b1dc02df75..15b5213bdaf6 100644 --- a/crates/store/re_tf/benches/transform_resolution_cache_bench.rs +++ b/crates/store/re_tf/benches/transform_resolution_cache_bench.rs @@ -25,7 +25,12 @@ fn setup_store() -> (EntityDb, Vec) { )); let timelines = (0..NUM_TIMELINES) - .map(|i| Timeline::new(format!("timeline{i}"), re_log_types::TimeType::Sequence)) + .map(|i| { + Timeline::new( + TimelineName::try_new(format!("timeline{i}")).unwrap(), + re_log_types::TimeType::Sequence, + ) + }) .collect_vec(); let mut events = Vec::new(); @@ -67,7 +72,7 @@ fn transform_resolution_cache_query(c: &mut Criterion) { for i in 0..NUM_TIMELINES { cache.ensure_timeline_is_initialized( chunk_store, - TimelineName::new(&format!("timeline{i}")), + TimelineName::try_new(format!("timeline{i}")).unwrap(), ); } cache @@ -81,7 +86,7 @@ fn transform_resolution_cache_query(c: &mut Criterion) { b.iter(create_cache_with_all_timelines); }); - let query = re_chunk_store::LatestAtQuery::new(TimelineName::new("timeline2"), 123); + let query = re_chunk_store::LatestAtQuery::new(TimelineName::from("timeline2"), 123); let queried_frame = TransformFrameIdHash::from_entity_path(&EntityPath::from("entity2")); c.bench_function("query_uncached_frame", |b| { @@ -128,7 +133,7 @@ fn transform_resolution_cache_query(c: &mut Criterion) { b.iter_batched( || { let mut cache = TransformResolutionCache::new(&entity_db); - cache.ensure_timeline_is_initialized(chunk_store, query.timeline()); + cache.ensure_timeline_is_initialized(chunk_store, query.timeline().unwrap()); cache }, |mut cache| { diff --git a/crates/store/re_tf/src/frame_id_registry.rs b/crates/store/re_tf/src/frame_id_registry.rs index 2923632156e1..c69bf41f4a96 100644 --- a/crates/store/re_tf/src/frame_id_registry.rs +++ b/crates/store/re_tf/src/frame_id_registry.rs @@ -7,6 +7,7 @@ use re_sdk_types::components::TransformFrameId; use re_sdk_types::{TransformFrameIdHash, archetypes}; /// Provides context around frame id hashes. +#[derive(SizeBytes)] pub struct FrameIdRegistry { /// A lookup table for resolving frame id hashes back to frame ids. frame_id_lookup_table: IntMap, @@ -34,17 +35,6 @@ impl Default for FrameIdRegistry { } } -impl SizeBytes for FrameIdRegistry { - fn heap_size_bytes(&self) -> u64 { - let Self { - frame_id_lookup_table, - child_frames_per_entity, - } = self; - - frame_id_lookup_table.total_size_bytes() + child_frames_per_entity.total_size_bytes() - } -} - impl FrameIdRegistry { /// Looks up a frame ID by its hash. /// @@ -156,6 +146,21 @@ impl FrameIdRegistry { self.frame_id_lookup_table.iter() } + /// Iterates over frame id pairs formed by implicit parent-child entity path relationships, + /// where each pair represents an edge in a transform tree. + /// + /// Doesn't include explicit (named) frame ids. + pub fn iter_entity_path_hierarchy_edges( + &self, + ) -> impl Iterator + '_ { + self.frame_id_lookup_table + .iter() + .filter_map(|(child, frame_id)| { + let parent = frame_id.as_entity_path()?.parent()?; + Some((TransformFrameIdHash::from_entity_path(&parent), *child)) + }) + } + /// Iterates over all known entities with child frame components. pub fn iter_entities_with_child_frames( &self, diff --git a/crates/store/re_tf/src/lib.rs b/crates/store/re_tf/src/lib.rs index c232528512fc..33e94259236a 100644 --- a/crates/store/re_tf/src/lib.rs +++ b/crates/store/re_tf/src/lib.rs @@ -154,6 +154,7 @@ pub use self::transform_queries::{ }; pub use self::transform_resolution_cache::{ CachedTransformsForTimeline, ResolvedPinholeProjection, TransformResolutionCache, + transform_cache_snapshot, }; /// Returns the view coordinates used for 2D (image) views. diff --git a/crates/store/re_tf/src/transform_forest.rs b/crates/store/re_tf/src/transform_forest.rs index 021b8a578a1b..cc1a1a5ada76 100644 --- a/crates/store/re_tf/src/transform_forest.rs +++ b/crates/store/re_tf/src/transform_forest.rs @@ -1,5 +1,5 @@ use nohash_hasher::{IntMap, IntSet}; -use re_byte_size::SizeBytes; +use re_byte_size::SizeBytes as _; use re_chunk_store::{LatestAtQuery, MissingChunkReporter}; use re_entity_db::EntityDb; use re_log::debug_assert; @@ -12,7 +12,7 @@ use crate::{ }; /// Details on how to transform from a source to a target frame. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] pub struct TreeTransform { /// Root frame this transform belongs to. /// @@ -59,17 +59,6 @@ impl TreeTransform { } } -impl SizeBytes for TreeTransform { - fn heap_size_bytes(&self) -> u64 { - let Self { - root, - target_from_source, - } = self; - - root.heap_size_bytes() + target_from_source.heap_size_bytes() - } -} - impl re_byte_size::MemUsageTreeCapture for TreeTransform { fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree { re_tracing::profile_function!(); @@ -135,7 +124,7 @@ struct SourceInfo<'a> { /// Each pinhole forms its own subtree which may be embedded into a 3D space. /// Everything at and below the pinhole tree root is considered to be 2D, /// everything above is considered to be 3D. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] pub struct PinholeTreeRoot { /// The tree root of the parent of this pinhole. pub parent_tree_root: TransformFrameIdHash, @@ -151,24 +140,10 @@ pub struct PinholeTreeRoot { pub parent_root_from_pinhole_root: glam::DAffine3, } -impl SizeBytes for PinholeTreeRoot { - fn heap_size_bytes(&self) -> u64 { - let Self { - parent_tree_root, - pinhole_projection, - parent_root_from_pinhole_root, - } = self; - - parent_tree_root.heap_size_bytes() - + pinhole_projection.heap_size_bytes() - + parent_root_from_pinhole_root.heap_size_bytes() - } -} - /// Properties of a transform root. /// /// [`TransformForest`] tries to identify all roots. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] pub enum TransformTreeRootInfo { /// Regular root without any extra meta information. TransformFrameRoot, @@ -178,23 +153,15 @@ pub enum TransformTreeRootInfo { Pinhole(PinholeTreeRoot), } -impl SizeBytes for TransformTreeRootInfo { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::TransformFrameRoot => 0, - Self::Pinhole(pinhole_tree_root) => pinhole_tree_root.heap_size_bytes(), - } - } -} - /// Analyzes & propagates the transform graph of a recording at a given time & timeline. /// /// Identifies different transform trees present in the recording and computes transforms relative to their roots, /// such that arbitrary transforms within the tree can be resolved (relatively) quickly. -#[derive(Default, Clone)] +#[derive(Default, Clone, re_byte_size::SizeBytes)] pub struct TransformForest { /// Are there any chunks missing from the chunk store, /// leading to an incomplete forest? + #[size_bytes(ignore)] missing_chunk_reporter: MissingChunkReporter, /// All known tree roots. @@ -396,20 +363,6 @@ impl TransformForest { } } -impl SizeBytes for TransformForest { - fn heap_size_bytes(&self) -> u64 { - re_tracing::profile_function!(); - - let Self { - missing_chunk_reporter: _, - roots, - root_from_frame, - } = self; - - roots.heap_size_bytes() + root_from_frame.heap_size_bytes() - } -} - impl re_byte_size::MemUsageTreeCapture for TransformForest { fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree { re_tracing::profile_function!(); @@ -429,7 +382,6 @@ impl re_byte_size::MemUsageTreeCapture for TransformForest { /// Starting from a `current_frame`, walks towards the parent and accumulates transforms into `transform_stack`. /// Stops until not more connection is found or an already processed `frame_id` is hit. -#[expect(clippy::too_many_arguments)] fn walk_towards_parent( entity_db: &EntityDb, missing_chunk_reporter: &MissingChunkReporter, @@ -1357,14 +1309,14 @@ mod tests { let mut transform_cache = TransformResolutionCache::new(&test_scene); transform_cache.ensure_timeline_is_initialized( test_scene.storage_engine().store(), - query.timeline(), + query.timeline().unwrap(), ); // Add a connection the cache doesn't know about. test_scene.add_chunk(&Arc::new( Chunk::builder(EntityPath::from("transforms")) .with_archetype_auto_row( - [(query.timeline(), TimeCell::from_sequence(0))], + [(query.timeline().unwrap(), TimeCell::from_sequence(0))], &archetypes::Transform3D::from_translation([4.0, 0.0, 0.0]) .with_child_frame("child2") .with_parent_frame("top"), @@ -1401,7 +1353,7 @@ mod tests { test_scene.add_chunk(&Arc::new( Chunk::builder(EntityPath::from("transforms")) .with_archetype_auto_row( - [(query.timeline(), TimeCell::from_sequence(0))], + [(query.timeline().unwrap(), TimeCell::from_sequence(0))], &archetypes::Transform3D::from_translation([4.0, 0.0, 0.0]) .with_child_frame("child2") .with_parent_frame("top"), @@ -1411,14 +1363,14 @@ mod tests { let mut transform_cache = TransformResolutionCache::new(&test_scene); transform_cache.ensure_timeline_is_initialized( test_scene.storage_engine().store(), - query.timeline(), + query.timeline().unwrap(), ); test_scene.add_chunk(&Arc::new( // Add a connection the cache doesn't know about. Chunk::builder(EntityPath::from("transforms")) .with_archetype_auto_row( - [(query.timeline(), TimeCell::from_sequence(0))], // Same time before, different parent frame! + [(query.timeline().unwrap(), TimeCell::from_sequence(0))], // Same time before, different parent frame! &archetypes::Transform3D::from_translation([5.0, 0.0, 0.0]) .with_child_frame("child2") .with_parent_frame("new_top"), @@ -1471,8 +1423,7 @@ mod tests { fn test_implicit_transform_at_root_being_ignored_with_warning() -> Result<(), Box> { re_log::setup_logging(); - let (logger, log_rx) = re_log::ChannelLogger::new(re_log::LevelFilter::Warn); - re_log::add_boxed_logger(Box::new(logger)).expect("Failed to add logger"); + let log_rx = re_log::add_log_msg_receiver(re_log::LevelFilter::WARN); let mut entity_db = EntityDb::new(StoreInfo::testing().store_id); @@ -1496,8 +1447,10 @@ mod tests { let query = LatestAtQuery::latest(TimelineName::log_tick()); let mut transform_cache = TransformResolutionCache::new(&entity_db); - transform_cache - .ensure_timeline_is_initialized(entity_db.storage_engine().store(), query.timeline()); + transform_cache.ensure_timeline_is_initialized( + entity_db.storage_engine().store(), + query.timeline().unwrap(), + ); let transform_forest = TransformForest::new(&entity_db, &transform_cache, &query); assert!(!transform_forest.any_missing_chunks()); @@ -1522,13 +1475,13 @@ mod tests { ); let received_log = log_rx.try_recv()?; - assert_eq!(received_log.level, re_log::Level::Warn); + assert_eq!(received_log.level, re_log::Level::WARN); assert!( received_log - .msg + .message .contains("Ignoring transform at root entity"), "Expected warning about ignoring implicit root parent frame, got: {}", - received_log.msg + received_log.message ); Ok(()) diff --git a/crates/store/re_tf/src/transform_queries.rs b/crates/store/re_tf/src/transform_queries.rs index e7ac6e036553..0c17c2363e23 100644 --- a/crates/store/re_tf/src/transform_queries.rs +++ b/crates/store/re_tf/src/transform_queries.rs @@ -4,13 +4,16 @@ use std::sync::OnceLock; use glam::DAffine3; use itertools::Either; +use re_chunk_store::external::re_chunk::ChunkError; use re_chunk_store::{ChunkShared, LatestAtQuery, MissingChunkReporter}; use re_entity_db::EntityDb; use re_entity_db::external::re_query::StorageEngineReadGuard; use re_log_types::EntityPath; use re_sdk_types::archetypes::{self, InstancePoses3D}; use re_sdk_types::external::arrow::array::Array as _; -use re_sdk_types::{ChunkId, ComponentIdentifier, RowId, TransformFrameIdHash, components}; +use re_sdk_types::{ + ChunkId, Component, ComponentIdentifier, RowId, TransformFrameIdHash, components, +}; use crate::convert; use crate::transform_resolution_cache::{ @@ -28,6 +31,21 @@ pub enum TransformError { #[error("missing transform on entity `{entity_path}`")] MissingTransform { entity_path: EntityPath }, + #[error( + "Entity `{entity_path}` has multiple values for component `{component}` per row. Only one per row is supported." + )] + MultipleComponentsPerRow { + entity_path: EntityPath, + component: ComponentIdentifier, + }, + + #[error("Couldn't read component `{component}` on entity `{entity_path}`: {source}")] + ReadComponent { + entity_path: EntityPath, + component: ComponentIdentifier, + source: ChunkError, + }, + #[error( "Ignoring transform due to empty parent frame name for component `{component}` on entity `{entity_path}`." )] @@ -54,11 +72,7 @@ fn lookup_chunk_row<'a>( return None; }; - let index = if chunk.is_sorted() { - chunk.row_ids_slice().binary_search(&row_id).ok()? - } else { - chunk.row_ids_slice().iter().position(|r| *r == row_id)? - }; + let index = chunk.row_index_of(row_id)?; Some((chunk, index)) } @@ -114,6 +128,31 @@ pub fn atomic_component_set_for_pinhole_projection() -> &'static [ComponentIdent }) } +/// Reads one `Transform3D` component value and reports non-mono rows as transform errors. +fn mono_transform3d_component( + chunk: &ChunkShared, + component: ComponentIdentifier, + row_index: usize, + entity_path: &EntityPath, +) -> Result, TransformError> { + match chunk.component_mono::(component, row_index) { + None => Ok(None), + Some(Ok(value)) => Ok(Some(value)), + Some(Err(ChunkError::IndexOutOfBounds { kind, len: 0, .. })) if kind == "mono" => Ok(None), + Some(Err(ChunkError::IndexOutOfBounds { kind, .. })) if kind == "mono" => { + Err(TransformError::MultipleComponentsPerRow { + entity_path: entity_path.clone(), + component, + }) + } + Some(Err(source)) => Err(TransformError::ReadComponent { + entity_path: entity_path.clone(), + component, + source, + }), + } +} + /// Queries & processes all components that are part of a transform, returning the transform from child to parent. /// /// If any of the components yields an invalid transform, returns `None`. @@ -156,24 +195,32 @@ pub fn query_and_resolve_tree_transform_at_entity( }); }; - // TODO(andreas): silently ignores deserialization error right now. - - let parent = get_parent_frame(chunk, row_index, entity_path, identifier_parent_frame)?; + let parent_frame = mono_transform3d_component::( + chunk, + identifier_parent_frame, + row_index, + entity_path, + )?; + let parent = resolve_parent_frame(parent_frame, entity_path, identifier_parent_frame)?; #[expect(clippy::useless_let_if_seq)] let mut transform = DAffine3::IDENTITY; // The order of the components here is important. - if let Some(translation) = chunk - .component_mono::(identifier_translations, row_index) - .and_then(|v| v.ok()) - { + if let Some(translation) = mono_transform3d_component::( + chunk, + identifier_translations, + row_index, + entity_path, + )? { transform = convert::translation_3d_to_daffine3(translation); } - if let Some(axis_angle) = chunk - .component_mono::(identifier_rotation_axis_angles, row_index) - .and_then(|v| v.ok()) - { + if let Some(axis_angle) = mono_transform3d_component::( + chunk, + identifier_rotation_axis_angles, + row_index, + entity_path, + )? { let axis_angle = convert::rotation_axis_angle_to_daffine3(axis_angle).map_err(|_err| { TransformError::InvalidTransform { entity_path: entity_path.clone(), @@ -182,10 +229,12 @@ pub fn query_and_resolve_tree_transform_at_entity( })?; transform *= axis_angle; } - if let Some(quaternion) = chunk - .component_mono::(identifier_quaternions, row_index) - .and_then(|v| v.ok()) - { + if let Some(quaternion) = mono_transform3d_component::( + chunk, + identifier_quaternions, + row_index, + entity_path, + )? { let quaternion = convert::rotation_quat_to_daffine3(quaternion).map_err(|_err| { TransformError::InvalidTransform { entity_path: entity_path.clone(), @@ -194,10 +243,12 @@ pub fn query_and_resolve_tree_transform_at_entity( })?; transform *= quaternion; } - if let Some(scale) = chunk - .component_mono::(identifier_scales, row_index) - .and_then(|v| v.ok()) - { + if let Some(scale) = mono_transform3d_component::( + chunk, + identifier_scales, + row_index, + entity_path, + )? { if scale.x() == 0.0 && scale.y() == 0.0 && scale.z() == 0.0 { return Err(TransformError::InvalidTransform { entity_path: entity_path.clone(), @@ -206,10 +257,12 @@ pub fn query_and_resolve_tree_transform_at_entity( } transform *= convert::scale_3d_to_daffine3(scale); } - if let Some(mat3x3) = chunk - .component_mono::(identifier_mat3x3, row_index) - .and_then(|v| v.ok()) - { + if let Some(mat3x3) = mono_transform3d_component::( + chunk, + identifier_mat3x3, + row_index, + entity_path, + )? { let affine_transform = convert::transform_mat3x3_to_daffine3(mat3x3); if affine_transform.matrix3.determinant() == 0.0 { return Err(TransformError::InvalidTransform { @@ -220,10 +273,12 @@ pub fn query_and_resolve_tree_transform_at_entity( transform *= affine_transform; } - if chunk - .component_mono::(identifier_relation, row_index) - .and_then(|v| v.ok()) - == Some(components::TransformRelation::ChildFromParent) + if mono_transform3d_component::( + chunk, + identifier_relation, + row_index, + entity_path, + )? == Some(components::TransformRelation::ChildFromParent) { let determinant = transform.matrix3.determinant(); if determinant != 0.0 && determinant.is_finite() { @@ -302,12 +357,7 @@ pub fn query_and_resolve_instance_poses_at_entity( return Either::Left(std::iter::empty()); }; let last = last.clone(); - Either::Right( - values - .into_iter() - .chain(std::iter::repeat(last)) - .take(clamped_len), - ) + Either::Right(std::iter::chain(values, std::iter::repeat(last)).take(clamped_len)) } let batch_translation = chunk @@ -449,27 +499,35 @@ fn get_parent_frame( entity_path: &EntityPath, identifier_parent_frame: ComponentIdentifier, ) -> Result { - chunk + let parent_frame = chunk .component_mono::(identifier_parent_frame, row_index) - .and_then(|v| v.ok()) - .map_or_else( - || { - entity_path - .parent() - .ok_or(TransformError::ImplicitRootParentFrame) - .map(|parent| TransformFrameIdHash::from_entity_path(&parent)) - }, - |frame_id| { - if frame_id.as_str().is_empty() { - Err(TransformError::EmptyParentFrame { - entity_path: entity_path.clone(), - component: identifier_parent_frame, - }) - } else { - Ok(TransformFrameIdHash::new(&frame_id)) - } - }, - ) + .and_then(|v| v.ok()); + resolve_parent_frame(parent_frame, entity_path, identifier_parent_frame) +} + +fn resolve_parent_frame( + parent_frame: Option, + entity_path: &EntityPath, + identifier_parent_frame: ComponentIdentifier, +) -> Result { + parent_frame.map_or_else( + || { + entity_path + .parent() + .ok_or(TransformError::ImplicitRootParentFrame) + .map(|parent| TransformFrameIdHash::from_entity_path(&parent)) + }, + |frame_id| { + if frame_id.as_str().is_empty() { + Err(TransformError::EmptyParentFrame { + entity_path: entity_path.clone(), + component: identifier_parent_frame, + }) + } else { + Ok(TransformFrameIdHash::new(&frame_id)) + } + }, + ) } /// Queries view coordinates from either the [`archetypes::Pinhole`] or [`archetypes::ViewCoordinates`] archetype. @@ -530,7 +588,10 @@ mod tests { use re_chunk_store::Chunk; use re_entity_db::{EntityDb, EntityPath}; use re_log_types::Timeline; - use re_sdk_types::{archetypes::InstancePoses3D, components::RotationQuat}; + use re_sdk_types::{ + archetypes::{InstancePoses3D, Transform3D}, + components::RotationQuat, + }; use super::*; @@ -569,4 +630,44 @@ mod tests { Ok(()) } + + /// Tests that `Transform3D` with multiple transform components per row are treated as error. + #[test] + fn non_mono_transform3d_component_errors() -> Result<(), Box> { + let mut entity_db = EntityDb::new(re_log_types::StoreInfo::testing().store_id); + + let timeline = Timeline::new_sequence("t"); + let entity_path = EntityPath::from("my_entity"); + let chunk = Chunk::builder(entity_path.clone()) + .with_archetype_auto_row( + [(timeline, 1)], + &Transform3D::new().with_many_translation([[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]), + ) + .build()?; + let chunk_id = chunk.id(); + let row_id = chunk.row_ids_slice()[0]; + entity_db.add_chunk(&Arc::new(chunk))?; + + let err = query_and_resolve_tree_transform_at_entity( + &entity_db, + &MissingChunkReporter::default(), + &entity_path, + chunk_id, + row_id, + ) + .expect_err("Transform3D with multiple transform components per row should fail"); + + let TransformError::MultipleComponentsPerRow { + entity_path: err_entity_path, + component, + } = err + else { + panic!("unexpected error: {err}"); + }; + + assert_eq!(err_entity_path, entity_path); + assert_eq!(component, Transform3D::descriptor_translation().component); + + Ok(()) + } } diff --git a/crates/store/re_tf/src/transform_resolution_cache/cache.rs b/crates/store/re_tf/src/transform_resolution_cache/cache.rs index 0d7dde0b9fcc..3cc6edecd0ed 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/cache.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/cache.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use ahash::HashMap; use parking_lot::{ArcRwLockReadGuard, RawRwLock, RwLock}; -use re_byte_size::SizeBytes; +use re_byte_size::SizeBytes as _; use re_chunk_store::ChunkStore; use re_entity_db::EntityDb; use re_log::{debug_assert, debug_assert_eq}; @@ -36,6 +36,7 @@ type ArcRwLock = Arc>; /// * [`archetypes::InstancePoses3D`] /// Instance poses that should be applied to the tree transforms (via [`crate::TransformForest`]) but not propagate. /// Also unlike tree transforms, these are not associated with transform frames but rather with entity paths. +#[derive(re_byte_size::SizeBytes)] pub struct TransformResolutionCache { /// The frame id registry is co-located in the resolution cache for convenience: /// the resolution cache is often the lowest level of transform access and @@ -97,22 +98,6 @@ impl TransformResolutionCache { } } -impl SizeBytes for TransformResolutionCache { - fn heap_size_bytes(&self) -> u64 { - re_tracing::profile_function!(); - - let Self { - frame_id_registry, - per_timeline, - static_timeline, - } = self; - - frame_id_registry.heap_size_bytes() - + per_timeline.heap_size_bytes() - + static_timeline.heap_size_bytes() - } -} - impl re_byte_size::MemUsageTreeCapture for TransformResolutionCache { fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree { re_tracing::profile_function!(); @@ -147,12 +132,17 @@ impl TransformResolutionCache { } /// Accesses the transform component tracking data for a given timeline. + /// + /// A `None` timeline (a static-only query) yields the static transforms. #[inline] pub fn transforms_for_timeline( &self, - timeline: TimelineName, + timeline: impl Into>, ) -> ArcRwLockReadGuard { - if let Some(per_timeline) = self.per_timeline.get(&timeline) { + if let Some(per_timeline) = timeline + .into() + .and_then(|timeline| self.per_timeline.get(&timeline)) + { per_timeline.read_arc() } else { self.static_timeline.read_arc() @@ -294,7 +284,7 @@ impl TransformResolutionCache { debug_assert!(chunk.is_static()); let entity_path = chunk.entity_path(); - let place_holder_timeline = TimelineName::new("ignored for static chunk"); + let place_holder_timeline = TimelineName::from("ignored for static chunk"); let transform_child_frame_component = archetypes::Transform3D::descriptor_child_frame().component; diff --git a/crates/store/re_tf/src/transform_resolution_cache/cached_transform_value.rs b/crates/store/re_tf/src/transform_resolution_cache/cached_transform_value.rs index cfd593f5e551..6f7b2e45d159 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/cached_transform_value.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/cached_transform_value.rs @@ -2,7 +2,7 @@ use re_byte_size::{BookkeepingBTreeMap, SizeBytes}; use re_log_types::TimeInt; use re_sdk_types::{ChunkId, RowId}; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, SizeBytes)] pub enum CachedTransformValue { /// Cache is invalidated, we don't know what state we're in. Invalidated { @@ -26,15 +26,6 @@ impl CachedTransformValue { } } -impl SizeBytes for CachedTransformValue { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::Resident { value, .. } => value.heap_size_bytes(), - Self::Invalidated { .. } | Self::Cleared => 0, - } - } -} - pub fn add_invalidated_entry_if_not_already_cleared( transforms: &mut BookkeepingBTreeMap>, time: TimeInt, diff --git a/crates/store/re_tf/src/transform_resolution_cache/cached_transforms_for_timeline.rs b/crates/store/re_tf/src/transform_resolution_cache/cached_transforms_for_timeline.rs index e7793eef7008..0931bbc32446 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/cached_transforms_for_timeline.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/cached_transforms_for_timeline.rs @@ -1,8 +1,9 @@ use std::collections::BTreeSet; use nohash_hasher::IntMap; -use re_byte_size::SizeBytes; -use re_chunk_store::ChunkStore; +use re_byte_size::SizeBytes as _; +use re_chunk_store::{ChunkStore, LatestAtQuery, MissingChunkReporter}; +use re_entity_db::EntityDb; use re_log_types::{EntityPath, EntityPathHash, TimeInt, TimelineName}; use re_sdk_types::ChunkId; @@ -17,12 +18,14 @@ use crate::transform_resolution_cache::iter_relevant_rows_in_chunk; use super::iter_relevant_rows_in_chunk_with_child_frames; use super::pose_transform_for_entity::PoseTransformForEntity; +use super::transform_cache_snapshot; use super::tree_transforms_for_child_frame::TreeTransformsForChildFrame; /// Cached transforms for a single timeline. /// /// Includes any static transforms that may apply globally. /// Therefore, this can't be trivially constructed. +#[derive(re_byte_size::SizeBytes)] pub struct CachedTransformsForTimeline { /// Transforms information for each child frame to a parent frame over time. // Note that these are potentially a lot of mutexes, but `parking_lot`-Mutex are incredibly lightweight on all platforms, so not a memory concern. @@ -176,10 +179,10 @@ impl CachedTransformsForTimeline { if aspects.contains(TransformAspect::Clear) { let component = re_sdk_types::archetypes::Clear::descriptor_is_recursive().component; - for ((time, _row_id), is_recursive_slice) in chunk - .iter_component_indices(timeline, component) - .zip(chunk.iter_slices::(component)) - { + for ((time, _row_id), is_recursive_slice) in std::iter::zip( + chunk.iter_component_indices(timeline, component), + chunk.iter_slices::(component), + ) { if let Some(is_recursive) = is_recursive_slice.values().first() && *is_recursive != 0 { @@ -211,10 +214,10 @@ impl CachedTransformsForTimeline { if aspects.contains(TransformAspect::Clear) { let component = re_sdk_types::archetypes::Clear::descriptor_is_recursive().component; - for ((time, _row_id), is_recursive_slice) in chunk - .iter_component_indices(timeline, component) - .zip(chunk.iter_slices::(component)) - { + for ((time, _row_id), is_recursive_slice) in std::iter::zip( + chunk.iter_component_indices(timeline, component), + chunk.iter_slices::(component), + ) { if let Some(is_recursive) = is_recursive_slice.values().first() && *is_recursive != 0 { @@ -503,23 +506,24 @@ impl CachedTransformsForTimeline { pub fn all_child_frames(&self) -> impl Iterator { self.per_child_frame_transforms.keys().copied() } -} - -impl SizeBytes for CachedTransformsForTimeline { - fn heap_size_bytes(&self) -> u64 { - re_tracing::profile_function!(); - - let Self { - per_child_frame_transforms, - non_recursive_clears, - recursive_clears, - per_entity_poses, - } = self; - per_child_frame_transforms.heap_size_bytes() - + non_recursive_clears.heap_size_bytes() - + recursive_clears.heap_size_bytes() - + per_entity_poses.heap_size_bytes() + /// Returns a snapshot of this timeline's transform cache for a single latest-at time. + pub fn latest_at_transform_cache_snapshot( + &self, + frame_id_registry: &FrameIdRegistry, + entity_db: &EntityDb, + missing_chunk_reporter: &MissingChunkReporter, + query: &LatestAtQuery, + filter: transform_cache_snapshot::SnapshotFilter, + ) -> transform_cache_snapshot::Snapshot { + transform_cache_snapshot::latest_at( + self, + frame_id_registry, + entity_db, + missing_chunk_reporter, + query, + filter, + ) } } diff --git a/crates/store/re_tf/src/transform_resolution_cache/mod.rs b/crates/store/re_tf/src/transform_resolution_cache/mod.rs index 4c266d809762..61f391408ec5 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/mod.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/mod.rs @@ -4,6 +4,7 @@ mod cached_transforms_for_timeline; mod parent_from_child_transform; mod pose_transform_for_entity; mod resolved_pinhole_projection; +pub mod transform_cache_snapshot; mod transforms_for_child_frame_events; mod tree_transforms_for_child_frame; diff --git a/crates/store/re_tf/src/transform_resolution_cache/parent_from_child_transform.rs b/crates/store/re_tf/src/transform_resolution_cache/parent_from_child_transform.rs index af9103f496b7..b56194c9c320 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/parent_from_child_transform.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/parent_from_child_transform.rs @@ -1,10 +1,9 @@ use glam::DAffine3; -use re_byte_size::SizeBytes; use crate::TransformFrameIdHash; /// A transform from a child frame to a parent frame. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] pub struct ParentFromChildTransform { /// The frame we're transforming into. pub parent: TransformFrameIdHash, @@ -12,13 +11,3 @@ pub struct ParentFromChildTransform { /// The transform from the child frame to the parent frame. pub transform: DAffine3, } - -impl SizeBytes for ParentFromChildTransform { - fn heap_size_bytes(&self) -> u64 { - re_tracing::profile_function!(); - - let Self { parent, transform } = self; - - parent.heap_size_bytes() + transform.heap_size_bytes() - } -} diff --git a/crates/store/re_tf/src/transform_resolution_cache/pose_transform_for_entity.rs b/crates/store/re_tf/src/transform_resolution_cache/pose_transform_for_entity.rs index 7543b4efd07e..975b8d79f291 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/pose_transform_for_entity.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/pose_transform_for_entity.rs @@ -18,7 +18,7 @@ use super::cached_transforms_for_timeline::CachedTransformsForTimeline; /// All instance poses for a given entity over time. /// /// Similar to [`super::tree_transforms_for_child_frame::TreeTransformsForChildFrame`], but for poses associated with an entity path. -#[derive(Debug)] +#[derive(Debug, SizeBytes)] pub struct PoseTransformForEntity { pub entity_path: EntityPath, pub poses_per_time: Mutex>>>, @@ -33,17 +33,6 @@ impl Clone for PoseTransformForEntity { } } -impl SizeBytes for PoseTransformForEntity { - fn heap_size_bytes(&self) -> u64 { - let Self { - entity_path, - poses_per_time, - } = self; - - entity_path.heap_size_bytes() + poses_per_time.lock().heap_size_bytes() - } -} - impl PoseTransformForEntity { pub fn new( entity_path: EntityPath, diff --git a/crates/store/re_tf/src/transform_resolution_cache/resolved_pinhole_projection.rs b/crates/store/re_tf/src/transform_resolution_cache/resolved_pinhole_projection.rs index e6d8e1d0de21..cd7d89ad80a4 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/resolved_pinhole_projection.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/resolved_pinhole_projection.rs @@ -1,11 +1,10 @@ use std::ops::Deref; -use re_byte_size::SizeBytes; use re_sdk_types::components; use crate::TransformFrameIdHash; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] pub struct ResolvedPinholeProjection { /// All components that are updated atomically are cached. pub(crate) cached: ResolvedPinholeProjectionCached, @@ -27,17 +26,7 @@ impl Deref for ResolvedPinholeProjection { } } -impl SizeBytes for ResolvedPinholeProjection { - fn is_pod() -> bool { - true - } - - fn heap_size_bytes(&self) -> u64 { - 0 - } -} - -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] pub struct ResolvedPinholeProjectionCached { /// The parent frame of the pinhole projection. pub parent: TransformFrameIdHash, @@ -46,13 +35,3 @@ pub struct ResolvedPinholeProjectionCached { pub resolution: Option, } - -impl SizeBytes for ResolvedPinholeProjectionCached { - fn is_pod() -> bool { - true - } - - fn heap_size_bytes(&self) -> u64 { - 0 - } -} diff --git a/crates/store/re_tf/src/transform_resolution_cache/tests.rs b/crates/store/re_tf/src/transform_resolution_cache/tests.rs index ec824655161f..f6c19f19f423 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/tests.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/tests.rs @@ -16,7 +16,10 @@ use re_sdk_types::{ }; use crate::convert; -use crate::{TransformFrameIdHash, transform_resolution_cache::ResolvedPinholeProjectionCached}; +use crate::{ + TransformFrameIdHash, transform_cache_snapshot, + transform_resolution_cache::ResolvedPinholeProjectionCached, +}; use super::pose_transform_for_entity::PoseTransformForEntity; use super::tree_transforms_for_child_frame::TreeTransformsForChildFrame; @@ -264,6 +267,210 @@ fn test_transforms_per_timeline_access() -> Result<(), Box Result<(), Box> { + let mut entity_db = new_entity_db_with_subscriber_registered(); + let mut cache = TransformResolutionCache::default(); + let timeline = Timeline::new_sequence("t"); + + // Set up one implicit entity-path transform ("world/camera") and one pinhole transform with + // named parent & child on the same entity. + let image_from_camera = + PinholeProjection::from_focal_length_and_principal_point([1.0, 2.0], [1.0, 2.0]); + let chunk = Chunk::builder(EntityPath::from("world/camera")) + .with_archetype_auto_row( + [(timeline, 1)], + &Transform3D::from_translation([1.0, 2.0, 3.0]), + ) + .with_archetype_auto_row( + [(timeline, 1)], + &Pinhole::new(image_from_camera) + .with_child_frame("image_frame") + .with_parent_frame("camera_frame"), + ) + .build()?; + entity_db.add_chunk(&Arc::new(chunk))?; + + apply_store_subscriber_events(&mut cache, &entity_db); + + // Query the unfiltered snapshot first. Filtered snapshots below are compared against this one. + let missing_chunk_reporter = MissingChunkReporter::default(); + let query = LatestAtQuery::new(*timeline.name(), 1); + let frame_id_registry = cache.frame_id_registry(); + let transforms = cache.transforms_for_timeline(*timeline.name()); + let snapshot = transforms.latest_at_transform_cache_snapshot( + &frame_id_registry, + &entity_db, + &missing_chunk_reporter, + &query, + transform_cache_snapshot::SnapshotFilter::default(), + ); + assert!( + missing_chunk_reporter.is_empty(), + "Test expected no missing chunks, but some were missing." + ); + + let expected_root_frame = TransformFrameIdHash::entity_path_hierarchy_root(); + let expected_world_frame = TransformFrameIdHash::from_entity_path(&EntityPath::from("world")); + let expected_camera_frame = + TransformFrameIdHash::from_entity_path(&EntityPath::from("world/camera")); + let expected_named_camera_frame = TransformFrameIdHash::from_str("camera_frame"); + let expected_image_frame = TransformFrameIdHash::from_str("image_frame"); + + // Check that the entity-path-based camera frame is included and marked as 3D subspace. + let world_camera_frame = snapshot + .frames + .iter() + .find(|frame| frame.id == expected_camera_frame) + .expect("camera entity-path frame should be registered"); + assert_eq!( + world_camera_frame.kind, + transform_cache_snapshot::FrameKind::EntityPath + ); + assert_eq!( + world_camera_frame.subspace_kind, + transform_cache_snapshot::SubspaceKind::ThreeD + ); + assert!(world_camera_frame.has_transform); + + // Check that the pinhole child frame is marked as 2D subspace. + let image_frame_snapshot = snapshot + .frames + .iter() + .find(|frame| frame.id == expected_image_frame) + .expect("image frame should be registered"); + assert_eq!( + image_frame_snapshot.kind, + transform_cache_snapshot::FrameKind::Named + ); + assert_eq!( + image_frame_snapshot.subspace_kind, + transform_cache_snapshot::SubspaceKind::TwoD + ); + assert!(image_frame_snapshot.has_transform); + + // Check that the implicit edge from the entity path hierarchy root to the parent `world` frame + // is included. + let implicit_edge = snapshot + .edges + .iter() + .find(|edge| { + edge.parent == expected_root_frame + && edge.child == expected_world_frame + && edge.time.is_static() + }) + .expect("implicit hierarchy edge should be present"); + + // Check that the implicit edge has no logged transform payload. + assert!(matches!( + implicit_edge.source, + transform_cache_snapshot::EdgeSource::ImplicitHierarchy + )); + + // Check that the logged transform shows up as such in the snapshot. + let transform_edge = snapshot + .edges + .iter() + .find(|edge| { + edge.parent == expected_world_frame + && edge.child == expected_camera_frame + && edge.time == TimeInt::new_temporal(1) + }) + .expect("transform edge should be present"); + let transform_cache_snapshot::EdgeSource::Transform { + entity_path, + transform, + } = &transform_edge.source + else { + unreachable!("transform edge source should be a transform"); + }; + assert_eq!(entity_path, &EntityPath::from("world/camera")); + assert_eq!(transform.transform.translation.to_array(), [1.0, 2.0, 3.0]); + + // Check that the Pinhole archetype produces a direct pinhole edge between named frames. + let pinhole_edge = snapshot + .edges + .iter() + .find(|edge| { + edge.parent == expected_named_camera_frame + && edge.child == expected_image_frame + && edge.time == TimeInt::new_temporal(1) + }) + .expect("pinhole edge should be present"); + let transform_cache_snapshot::EdgeSource::Pinhole { + entity_path, + pinhole, + } = &pinhole_edge.source + else { + unreachable!("pinhole edge source should be a pinhole"); + }; + assert_eq!(entity_path, &EntityPath::from("world/camera")); + assert_eq!(pinhole.parent, expected_named_camera_frame); + assert_eq!( + pinhole.image_from_camera, + PinholeProjection::from_focal_length_and_principal_point([1.0, 2.0], [1.0, 2.0]) + ); + + // Check that a static edge filter drops temporal logged transforms. + let static_snapshot = transforms.latest_at_transform_cache_snapshot( + &frame_id_registry, + &entity_db, + &missing_chunk_reporter, + &query, + transform_cache_snapshot::SnapshotFilter { + edges: transform_cache_snapshot::EdgeFilter::Static, + ..Default::default() + }, + ); + assert!( + static_snapshot + .edges + .iter() + .all(|edge| edge.time.is_static()) + ); + + // Check that a temporal edge filter drops static transforms. + let temporal_snapshot = transforms.latest_at_transform_cache_snapshot( + &frame_id_registry, + &entity_db, + &missing_chunk_reporter, + &query, + transform_cache_snapshot::SnapshotFilter { + edges: transform_cache_snapshot::EdgeFilter::Temporal, + ..Default::default() + }, + ); + assert!( + temporal_snapshot + .edges + .iter() + .all(|edge| !edge.time.is_static()) + ); + + // Check that a `Named` frame filter drops entity-path derived frames. + // With our test data, only the named frames from the pinhole edge remain. + let named_snapshot = transforms.latest_at_transform_cache_snapshot( + &frame_id_registry, + &entity_db, + &missing_chunk_reporter, + &query, + transform_cache_snapshot::SnapshotFilter { + frames: transform_cache_snapshot::FrameFilter::Named, + ..Default::default() + }, + ); + assert_eq!(named_snapshot.edges.len(), 1); + assert_eq!(named_snapshot.edges[0].parent, expected_named_camera_frame); + assert_eq!(named_snapshot.edges[0].child, expected_image_frame); + assert_eq!(named_snapshot.frames.len(), 2); + for frame in named_snapshot.frames { + assert!([expected_named_camera_frame, expected_image_frame].contains(&frame.id)); + } + + Ok(()) +} + #[test] fn test_static_tree_transforms() -> Result<(), Box> { for flavor in &ALL_STATIC_TEST_FLAVOURS { @@ -346,7 +553,7 @@ fn test_static_tree_transforms() -> Result<(), Box> { ); // Timelines that the cache has never seen should still have the static transform. - let transforms_per_timeline = cache.transforms_for_timeline(TimelineName::new("other")); + let transforms_per_timeline = cache.transforms_for_timeline(TimelineName::from("other")); let transforms = transforms_per_timeline .frame_transforms(TransformFrameIdHash::from_entity_path(&EntityPath::from( "my_entity", @@ -356,7 +563,7 @@ fn test_static_tree_transforms() -> Result<(), Box> { latest_at_transform_test( transforms, &entity_db, - &LatestAtQuery::new(TimelineName::new("other"), 123) + &LatestAtQuery::new(TimelineName::from("other"), 123) ), Some(ParentFromChildTransform { parent: TransformFrameIdHash::entity_path_hierarchy_root(), @@ -444,7 +651,7 @@ fn test_static_pose_transforms() -> Result<(), Box> { ); // Timelines that the cache has never seen should still have the static poses. - let transforms_per_timeline = cache.transforms_for_timeline(TimelineName::new("other")); + let transforms_per_timeline = cache.transforms_for_timeline(TimelineName::from("other")); let transforms = transforms_per_timeline .pose_transforms(EntityPath::from("my_entity").hash()) .unwrap(); @@ -452,7 +659,7 @@ fn test_static_pose_transforms() -> Result<(), Box> { latest_at_instance_poses_test( transforms, &entity_db, - &LatestAtQuery::new(TimelineName::new("other"), 123) + &LatestAtQuery::new(TimelineName::from("other"), 123) ), vec![ DAffine3::from_translation(glam::dvec3(1.0, 2.0, 3.0)), @@ -555,7 +762,7 @@ fn test_static_pinhole_projection() -> Result<(), Box> { ); // Timelines that the cache has never seen should still have the static pinhole. - let transforms_per_timeline = cache.transforms_for_timeline(TimelineName::new("other")); + let transforms_per_timeline = cache.transforms_for_timeline(TimelineName::from("other")); let transforms = transforms_per_timeline .frame_transforms(TransformFrameIdHash::from_entity_path(&EntityPath::from( "my_entity", @@ -565,7 +772,7 @@ fn test_static_pinhole_projection() -> Result<(), Box> { latest_at_pinhole_test( transforms, &entity_db, - &LatestAtQuery::new(TimelineName::new("other"), 123) + &LatestAtQuery::new(TimelineName::from("other"), 123) ), Some(ResolvedPinholeProjection { cached: ResolvedPinholeProjectionCached { @@ -1607,7 +1814,7 @@ fn test_different_associated_paths_for_static_and_temporal() ); // Test on a different timeline that never saw the temporal data - let other_timeline = TimelineName::new("other"); + let other_timeline = TimelineName::from("other"); let transforms_per_timeline = cache.transforms_for_timeline(other_timeline); let transforms = transforms_per_timeline .frame_transforms(child_frame) @@ -1638,8 +1845,7 @@ fn ensure_no_logged_error(rx: &re_log::Receiver) { fn test_error_on_changing_associated_path(time: TimeInt) -> Result<(), Box> { re_log::setup_logging(); - let (logger, log_rx) = re_log::ChannelLogger::new(re_log::LevelFilter::Error); - re_log::add_boxed_logger(Box::new(logger)).expect("Failed to add logger"); + let log_rx = re_log::add_log_msg_receiver(re_log::LevelFilter::ERROR); let mut entity_db = EntityDb::new(StoreInfo::testing().store_id); let mut cache = TransformResolutionCache::default(); @@ -1676,21 +1882,21 @@ fn test_error_on_changing_associated_path(time: TimeInt) -> Result<(), Box bool { + match self { + Self::All => true, + Self::Static => time.is_static(), + Self::Temporal => !time.is_static(), + } + } +} + +/// Which transform-cache snapshot frames should be returned. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum FrameFilter { + /// Include all registered frames. + #[default] + All, + + /// Include only frames derived from entity paths. + EntityPath, + + /// Include only explicitly named frames. + Named, +} + +impl FrameFilter { + #[inline] + pub fn includes(self, kind: FrameKind) -> bool { + match self { + Self::All => true, + Self::EntityPath => kind == FrameKind::EntityPath, + Self::Named => kind == FrameKind::Named, + } + } +} + +/// Filter for a transform-cache snapshot. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SnapshotFilter { + pub frames: FrameFilter, + pub edges: EdgeFilter, +} + +/// The source category of a registered transform frame. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameKind { + EntityPath, + Named, +} + +/// Whether a transform frame belongs to a 2D or 3D subspace. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SubspaceKind { + TwoD, + ThreeD, +} + +/// Information about a registered transform frame. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Frame { + pub id: TransformFrameIdHash, + pub label: TransformFrameId, + pub kind: FrameKind, + pub subspace_kind: SubspaceKind, + + /// Whether this frame participates in any latest-at transform. + pub has_transform: bool, +} + +/// Where does the parent-child transform edge originate from? +#[derive(Clone, Debug, PartialEq)] +pub enum EdgeSource { + ImplicitHierarchy, + Transform { + entity_path: EntityPath, + transform: ParentFromChildTransform, + }, + Pinhole { + entity_path: EntityPath, + pinhole: ResolvedPinholeProjection, + }, +} + +/// A transform-cache snapshot edge between a child frame and its parent frame. +#[derive(Clone, Debug, PartialEq)] +pub struct Edge { + pub parent: TransformFrameIdHash, + pub child: TransformFrameIdHash, + pub time: TimeInt, + pub source: EdgeSource, +} + +/// Snapshot of the transform cache at a single latest-at time. +#[derive(Clone, Debug, PartialEq)] +pub struct Snapshot { + pub frames: Vec, + pub edges: Vec, +} + +/// Returns a snapshot of the transform cache for a single latest-at time. +/// +/// The snapshot contains registered frames matching the frame filter plus latest direct transform +/// edges between them. +/// +/// `filter` defines which frame and edge kinds shall be included in the result. +pub fn latest_at( + transforms: &CachedTransformsForTimeline, + frame_id_registry: &FrameIdRegistry, + entity_db: &EntityDb, + missing_chunk_reporter: &MissingChunkReporter, + query: &LatestAtQuery, + filter: SnapshotFilter, +) -> Snapshot { + // Collect all logged transform edges. + let logged_edges = + latest_at_logged_transform_edges(transforms, entity_db, missing_chunk_reporter, query); + let children_with_logged_transforms = logged_edges + .iter() + .map(|edge| edge.child) + .collect::>(); + + // First, collect all frames and edges of logged transforms that are compatible with the edge + // filter. Frame filtering happens in a second step later. + let mut two_d_frames = HashSet::default(); + let mut frames_with_transforms = HashSet::default(); + let mut edges = Vec::new(); + for edge in logged_edges { + frames_with_transforms.insert(edge.parent); + frames_with_transforms.insert(edge.child); + if matches!(edge.source, EdgeSource::Pinhole { .. }) { + two_d_frames.insert(edge.child); + } + if filter.edges.includes(edge.time) { + edges.push(edge); + } + } + + // Entity-path-derived frames are implicit relationships that don't necessarily have a logged + // transform (identity transform as default). + // Collect these entity-path-derived frames that we haven't yet seen as logged transforms. + for (parent, child) in frame_id_registry.iter_entity_path_hierarchy_edges() { + if children_with_logged_transforms.contains(&child) { + continue; + } + + frames_with_transforms.insert(parent); + frames_with_transforms.insert(child); + + // Implicit identity transforms are static. + if filter.edges.includes(TimeInt::STATIC) { + edges.push(Edge { + parent, + child, + time: TimeInt::STATIC, + source: EdgeSource::ImplicitHierarchy, + }); + } + } + + // Retrieve the frame information for all frames that match the frame filter. + let mut returned_frames = HashSet::default(); + let frames = frame_id_registry + .iter_frame_ids() + .filter_map(|(id, label)| { + let kind = if label.as_entity_path().is_some() { + FrameKind::EntityPath + } else { + FrameKind::Named + }; + + if !filter.frames.includes(kind) { + return None; + } + + returned_frames.insert(*id); + Some(Frame { + id: *id, + label: label.clone(), + kind, + subspace_kind: if two_d_frames.contains(id) { + SubspaceKind::TwoD + } else { + SubspaceKind::ThreeD + }, + has_transform: frames_with_transforms.contains(id), + }) + }) + .collect::>(); + + // Frame filtering can hide edge endpoints, so prune edges after collecting returned frames. + edges.retain(|edge| { + returned_frames.contains(&edge.parent) && returned_frames.contains(&edge.child) + }); + + Snapshot { frames, edges } +} + +/// Returns the latest logged transform and pinhole edges for the requested time. +fn latest_at_logged_transform_edges( + transforms: &CachedTransformsForTimeline, + entity_db: &EntityDb, + missing_chunk_reporter: &MissingChunkReporter, + query: &LatestAtQuery, +) -> Vec { + let mut child_transforms = transforms + .per_child_frame_transforms + .iter() + .collect::>(); + child_transforms.sort_unstable_by_key(|(child, _)| **child); + + child_transforms + .into_iter() + .flat_map(|(_, transforms)| { + [ + latest_at_transform_edge(transforms, entity_db, missing_chunk_reporter, query), + latest_at_pinhole_edge(transforms, entity_db, missing_chunk_reporter, query), + ] + .into_iter() + .flatten() + }) + .collect() +} + +/// Returns the latest logged transform edge for the child frame. +fn latest_at_transform_edge( + transforms: &TreeTransformsForChildFrame, + entity_db: &EntityDb, + missing_chunk_reporter: &MissingChunkReporter, + query: &LatestAtQuery, +) -> Option { + let (time, transform) = + transforms.latest_at_transform_with_metadata(entity_db, missing_chunk_reporter, query)?; + + Some(Edge { + parent: transform.parent, + child: transforms.child_frame, + time, + source: EdgeSource::Transform { + entity_path: transforms.associated_entity_path(time).clone(), + transform, + }, + }) +} + +/// Returns the latest logged pinhole edge for the child frame. +fn latest_at_pinhole_edge( + transforms: &TreeTransformsForChildFrame, + entity_db: &EntityDb, + missing_chunk_reporter: &MissingChunkReporter, + query: &LatestAtQuery, +) -> Option { + let (time, pinhole) = + transforms.latest_at_pinhole_with_metadata(entity_db, missing_chunk_reporter, query)?; + + Some(Edge { + parent: pinhole.parent, + child: transforms.child_frame, + time, + source: EdgeSource::Pinhole { + entity_path: transforms.associated_entity_path(time).clone(), + pinhole, + }, + }) +} diff --git a/crates/store/re_tf/src/transform_resolution_cache/transforms_for_child_frame_events.rs b/crates/store/re_tf/src/transform_resolution_cache/transforms_for_child_frame_events.rs index ff328b41796e..e9216b023549 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/transforms_for_child_frame_events.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/transforms_for_child_frame_events.rs @@ -1,6 +1,6 @@ use std::collections::BTreeSet; -use re_byte_size::{BookkeepingBTreeMap, SizeBytes}; +use re_byte_size::BookkeepingBTreeMap; use re_log_types::TimeInt; use super::cached_transform_value::CachedTransformValue; @@ -19,7 +19,7 @@ pub type FrameTransformTimeMap = pub type PinholeProjectionMap = BookkeepingBTreeMap>; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] pub struct TransformsForChildFrameEvents { /// There can be only a single parent at any point in time, but it may change over time. /// Whenever it changes, the previous parent frame is no longer reachable. @@ -74,14 +74,3 @@ impl TransformsForChildFrameEvents { frame_transforms.is_empty() && pinhole_projections.is_empty() } } - -impl SizeBytes for TransformsForChildFrameEvents { - fn heap_size_bytes(&self) -> u64 { - let Self { - frame_transforms, - pinhole_projections, - } = self; - - frame_transforms.heap_size_bytes() + pinhole_projections.heap_size_bytes() - } -} diff --git a/crates/store/re_tf/src/transform_resolution_cache/tree_transforms_for_child_frame.rs b/crates/store/re_tf/src/transform_resolution_cache/tree_transforms_for_child_frame.rs index 8b318182d599..76f51f2dc1f9 100644 --- a/crates/store/re_tf/src/transform_resolution_cache/tree_transforms_for_child_frame.rs +++ b/crates/store/re_tf/src/transform_resolution_cache/tree_transforms_for_child_frame.rs @@ -24,7 +24,7 @@ use super::transforms_for_child_frame_events::TransformsForChildFrameEvents; /// Cached transforms from a single child frame to a (potentially changing) parent frame over time. /// /// Incorporates any static transforms that may apply to this entity. -#[derive(Debug)] +#[derive(Debug, SizeBytes)] pub struct TreeTransformsForChildFrame { // Is None if this is about static time. #[cfg(debug_assertions)] @@ -78,25 +78,6 @@ impl PartialEq for TreeTransformsForChildFrame { } } -impl SizeBytes for TreeTransformsForChildFrame { - fn heap_size_bytes(&self) -> u64 { - let Self { - associated_entity_path_temporal, - associated_entity_path_static, - child_frame, - events, - - #[cfg(debug_assertions)] - timeline: _, - } = self; - - associated_entity_path_temporal.heap_size_bytes() - + associated_entity_path_static.heap_size_bytes() - + child_frame.heap_size_bytes() - + events.read().heap_size_bytes() - } -} - impl TreeTransformsForChildFrame { pub fn new_temporal( associated_entity_path: EntityPath, @@ -216,8 +197,20 @@ impl TreeTransformsForChildFrame { missing_chunk_reporter: &MissingChunkReporter, query: &LatestAtQuery, ) -> Option { + self.latest_at_transform_with_metadata(entity_db, missing_chunk_reporter, query) + .map(|(_, transform)| transform) + } + + /// Like [`Self::latest_at_transform`], but also returns the time of the resolved entry. + #[inline] + pub(crate) fn latest_at_transform_with_metadata( + &self, + entity_db: &EntityDb, + missing_chunk_reporter: &MissingChunkReporter, + query: &LatestAtQuery, + ) -> Option<(TimeInt, ParentFromChildTransform)> { #[cfg(debug_assertions)] // `self.timeline` is only present with `debug_assertions` enabled. - debug_assert!(Some(query.timeline()) == self.timeline || self.timeline.is_none()); + debug_assert!(query.timeline() == self.timeline || self.timeline.is_none()); let mut events = self.events.write(); @@ -252,13 +245,14 @@ impl TreeTransformsForChildFrame { }; } - match frame_transform { - CachedTransformValue::Resident { value, .. } => Some(value.clone()), - CachedTransformValue::Cleared => None, + let value = match frame_transform { + CachedTransformValue::Resident { value, .. } => value.clone(), + CachedTransformValue::Cleared => return None, CachedTransformValue::Invalidated { .. } => { unreachable!("Just made transform cache-resident") } - } + }; + Some((*time_of_last_update_to_this_frame, value)) }, ) .flatten() @@ -271,8 +265,20 @@ impl TreeTransformsForChildFrame { missing_chunk_reporter: &MissingChunkReporter, query: &LatestAtQuery, ) -> Option { + self.latest_at_pinhole_with_metadata(entity_db, missing_chunk_reporter, query) + .map(|(_, pinhole)| pinhole) + } + + /// Like [`Self::latest_at_pinhole`], but also returns the time of the resolved entry. + #[inline] + pub(crate) fn latest_at_pinhole_with_metadata( + &self, + entity_db: &EntityDb, + missing_chunk_reporter: &MissingChunkReporter, + query: &LatestAtQuery, + ) -> Option<(TimeInt, ResolvedPinholeProjection)> { #[cfg(debug_assertions)] // `self.timeline` is only present with `debug_assertions` enabled. - debug_assert!(Some(query.timeline()) == self.timeline || self.timeline.is_none()); + debug_assert!(query.timeline() == self.timeline || self.timeline.is_none()); let mut events = self.events.write(); @@ -310,26 +316,25 @@ impl TreeTransformsForChildFrame { }; } - match pinhole_projection { - CachedTransformValue::Resident { value, .. } => { - Some(ResolvedPinholeProjection { - cached: value.clone(), - - // TODO(andreas): view coordinates are in a weird limbo state in more than one way. - // Not only are they only _partially_ relevant for the camera's transform (they both name axis & orient cameras), - // we also rely on them too much being latest-at driven and to make matters worse query them from two different archetypes. - view_coordinates: { - query_view_coordinates(entity_path, entity_db, query).unwrap_or( - re_sdk_types::archetypes::Pinhole::DEFAULT_CAMERA_XYZ, - ) - }, - }) - } - CachedTransformValue::Cleared => None, + let value = match pinhole_projection { + CachedTransformValue::Resident { value, .. } => value.clone(), + CachedTransformValue::Cleared => return None, CachedTransformValue::Invalidated { .. } => { unreachable!("Just made transform cache-resident") } - } + }; + Some(( + *time_of_last_update_to_this_frame, + ResolvedPinholeProjection { + cached: value, + + // TODO(andreas): view coordinates are in a weird limbo state in more than one way. + // Not only are they only _partially_ relevant for the camera's transform (they both name axis & orient cameras), + // we also rely on them too much being latest-at driven and to make matters worse query them from two different archetypes. + view_coordinates: query_view_coordinates(entity_path, entity_db, query) + .unwrap_or(re_sdk_types::archetypes::Pinhole::DEFAULT_CAMERA_XYZ), + }, + )) }, ) .flatten() diff --git a/crates/store/re_types_core/Cargo.toml b/crates/store/re_types_core/Cargo.toml index 9dc897363e2b..4496a0c7a183 100644 --- a/crates/store/re_types_core/Cargo.toml +++ b/crates/store/re_types_core/Cargo.toml @@ -22,9 +22,6 @@ all-features = true [features] default = [] -## Enable (de)serialization using serde. -serde = ["dep:serde", "re_string_interner/serde", "re_tuid/serde"] - [dependencies] # Rerun @@ -46,15 +43,14 @@ document-features.workspace = true half.workspace = true itertools.workspace = true nohash-hasher.workspace = true +quiver.workspace = true +serde = { workspace = true, features = ["derive"] } thiserror.workspace = true -# Optional dependencies -serde = { workspace = true, optional = true } - - [dev-dependencies] criterion.workspace = true +serde_json.workspace = true similar-asserts.workspace = true [lib] diff --git a/crates/store/re_types_core/benches/bench_tuid.rs b/crates/store/re_types_core/benches/bench_tuid.rs index d3d40f08e024..a56ec2369e8e 100644 --- a/crates/store/re_types_core/benches/bench_tuid.rs +++ b/crates/store/re_types_core/benches/bench_tuid.rs @@ -16,7 +16,7 @@ fn bench_arrow(c: &mut Criterion) { group.bench_function("arrow", |b| { b.iter(|| { let data = re_tuid::Tuid::to_arrow(tuids.clone()).unwrap(); - criterion::black_box(data) + std::hint::black_box(data) }); }); } @@ -31,7 +31,7 @@ fn bench_arrow(c: &mut Criterion) { group.bench_function("arrow", |b| { b.iter(|| { let tuids = re_tuid::Tuid::from_arrow(data.as_ref()).unwrap(); - criterion::black_box(tuids) + std::hint::black_box(tuids) }); }); } diff --git a/crates/store/re_types_core/src/archetype.rs b/crates/store/re_types_core/src/archetype.rs index 2fe3f783da44..67a9244657e3 100644 --- a/crates/store/re_types_core/src/archetype.rs +++ b/crates/store/re_types_core/src/archetype.rs @@ -86,10 +86,15 @@ pub trait Archetype { where Self: Sized, { - Self::from_arrow_components( - data.into_iter() - .map(|(field, array)| (ComponentDescriptor::from(field), array)), - ) + let components = data + .into_iter() + .map(|(field, array)| { + let descr = ComponentDescriptor::try_from(field) + .map_err(|err| crate::DeserializationError::ValidationError(err.to_string()))?; + Ok((descr, array)) + }) + .collect::>>()?; + Self::from_arrow_components(components) } /// Given an iterator of Arrow arrays and their respective [`ComponentDescriptor`]s, deserializes them @@ -117,9 +122,8 @@ pub trait ArchetypeReflectionMarker {} // --- -re_string_interner::declare_new_type!( +re_string_interner::declare_new_type_nonempty!( /// The fully-qualified name of an [`Archetype`], e.g. `rerun.archetypes.Points3D`. - #[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))] pub struct ArchetypeName; ); @@ -180,8 +184,29 @@ impl ArchetypeName { // --- -re_string_interner::declare_new_type!( - /// An identifier for a component, i.e. a field in an [`Archetype`]. - #[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))] +re_string_interner::declare_new_type_nonempty!( + /// Uniquely identifies a component (a field of data) within an entity. + /// + /// It comes in one of two shapes: + /// * **Archetype-qualified**: the archetype's [short name][`ArchetypeName::short_name`] and the + /// field name joined by a colon, e.g. `Points3D:positions`, `Scalars:scalars`, or + /// `user.CustomPoints:colors` for a custom archetype. This is the common case for data logged + /// through an archetype; construct it with [`ComponentIdentifier::from_archetype_field`]. + /// * **Bare field name**: just the field name, e.g. `positions`, used for data logged without an + /// archetype (see [`crate::DynamicArchetype`] and `AnyValues`). + /// + /// The empty string is not a valid identifier. pub struct ComponentIdentifier; ); + +impl ComponentIdentifier { + /// Construct from an archetype name and a field name, e.g. `Points3D:positions`. + /// + /// Uses the archetype's [short name][`ArchetypeName::short_name`]. + #[inline] + pub fn from_archetype_field(archetype: ArchetypeName, field: &str) -> Self { + // The result always contains a `:`, so it can never be empty: + Self::try_new(format!("{}:{field}", archetype.short_name())) + .expect("`archetype:field` is never empty") + } +} diff --git a/crates/store/re_types_core/src/archetypes/clear.rs b/crates/store/re_types_core/src/archetypes/clear.rs index c413a6c62a3a..937adb288062 100644 --- a/crates/store/re_types_core/src/archetypes/clear.rs +++ b/crates/store/re_types_core/src/archetypes/clear.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -40,7 +41,8 @@ use crate::{DeserializationError, DeserializationResult}; /// use rerun::external::glam; /// /// fn main() -> Result<(), Box> { -/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_clear").spawn()?; +/// let rec = +/// rerun::RecordingStreamBuilder::new("rerun_example_clear").spawn()?; /// /// #[rustfmt::skip] /// let (vectors, origins, colors) = ( @@ -50,12 +52,16 @@ use crate::{DeserializationError, DeserializationResult}; /// ); /// /// // Log a handful of arrows. -/// for (i, ((vector, origin), color)) in vectors.into_iter().zip(origins).zip(colors).enumerate() { +/// for (i, (vector, origin, color)) in +/// itertools::izip!(vectors, origins, colors).enumerate() +/// { /// rec.log( /// format!("arrows/{i}"), /// &rerun::Arrows3D::from_vectors([vector]) /// .with_origins([origin]) -/// .with_colors([rerun::Color::from_rgb(color.0, color.1, color.2)]), +/// .with_colors([rerun::Color::from_rgb( +/// color.0, color.1, color.2, +/// )]), /// )?; /// } /// @@ -76,7 +82,7 @@ use crate::{DeserializationError, DeserializationResult}; /// /// /// -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, ::re_byte_size::SizeBytes)] pub struct Clear { pub is_recursive: Option, } @@ -87,11 +93,13 @@ impl Clear { /// The corresponding component is [`crate::components::ClearIsRecursive`]. #[inline] pub fn descriptor_is_recursive() -> ComponentDescriptor { - ComponentDescriptor { - archetype: Some("rerun.archetypes.Clear".into()), - component: "Clear:is_recursive".into(), - component_type: Some("rerun.components.ClearIsRecursive".into()), - } + static DESCRIPTOR: std::sync::LazyLock = + std::sync::LazyLock::new(|| ComponentDescriptor { + archetype: Some("rerun.archetypes.Clear".into()), + component: "Clear:is_recursive".into(), + component_type: Some("rerun.components.ClearIsRecursive".into()), + }); + (*DESCRIPTOR).clone() } } @@ -115,7 +123,10 @@ impl Clear { impl crate::Archetype for Clear { #[inline] fn name() -> crate::ArchetypeName { - "rerun.archetypes.Clear".into() + crate::external::re_string_interner::intern_static_nonempty!( + crate::ArchetypeName, + "rerun.archetypes.Clear" + ) } #[inline] @@ -258,10 +269,3 @@ impl Clear { self } } - -impl ::re_byte_size::SizeBytes for Clear { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.is_recursive.heap_size_bytes() - } -} diff --git a/crates/store/re_types_core/src/arrow_zip_validity.rs b/crates/store/re_types_core/src/arrow_zip_validity.rs index 1b566cafa479..0f6842be4438 100644 --- a/crates/store/re_types_core/src/arrow_zip_validity.rs +++ b/crates/store/re_types_core/src/arrow_zip_validity.rs @@ -45,9 +45,7 @@ where fn next(&mut self) -> Option { let value = self.values.next(); let is_valid = self.validity.next(); - is_valid - .zip(value) - .map(|(is_valid, value)| is_valid.then_some(value)) + Option::zip(is_valid, value).map(|(is_valid, value)| is_valid.then_some(value)) } #[inline] @@ -59,9 +57,7 @@ where fn nth(&mut self, n: usize) -> Option { let value = self.values.nth(n); let is_valid = self.validity.nth(n); - is_valid - .zip(value) - .map(|(is_valid, value)| is_valid.then_some(value)) + Option::zip(is_valid, value).map(|(is_valid, value)| is_valid.then_some(value)) } } @@ -74,9 +70,7 @@ where fn next_back(&mut self) -> Option { let value = self.values.next_back(); let is_valid = self.validity.next_back(); - is_valid - .zip(value) - .map(|(is_valid, value)| is_valid.then_some(value)) + Option::zip(is_valid, value).map(|(is_valid, value)| is_valid.then_some(value)) } } diff --git a/crates/store/re_types_core/src/as_components.rs b/crates/store/re_types_core/src/as_components.rs index bff1f204b2fa..b06828b35e20 100644 --- a/crates/store/re_types_core/src/as_components.rs +++ b/crates/store/re_types_core/src/as_components.rs @@ -208,7 +208,16 @@ mod tests { use crate::{Component as _, ComponentDescriptor}; - #[derive(Clone, Copy, Debug, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + bytemuck::Pod, + bytemuck::Zeroable, + re_byte_size::SizeBytes, + )] #[repr(transparent)] pub struct MyColor(pub u32); @@ -224,14 +233,6 @@ mod tests { crate::macros::impl_into_cow!(MyColor); - impl re_byte_size::SizeBytes for MyColor { - #[inline] - fn heap_size_bytes(&self) -> u64 { - let Self(_) = self; - 0 - } - } - impl crate::Loggable for MyColor { fn arrow_datatype() -> arrow::datatypes::DataType { arrow::datatypes::DataType::UInt32 diff --git a/crates/store/re_types_core/src/chunk_id.rs b/crates/store/re_types_core/src/chunk_id.rs index 64dc4b7b6bea..93661c4b2903 100644 --- a/crates/store/re_types_core/src/chunk_id.rs +++ b/crates/store/re_types_core/src/chunk_id.rs @@ -37,9 +37,19 @@ use crate::Loggable as _; /// think carefully about your `RowId`s in these cases. #[repr(C, align(1))] #[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, bytemuck::AnyBitPattern, bytemuck::NoUninit, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + bytemuck::AnyBitPattern, + bytemuck::NoUninit, + re_byte_size::SizeBytes, + serde::Deserialize, + serde::Serialize, )] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct ChunkId(pub(crate) re_tuid::Tuid); impl std::fmt::Debug for ChunkId { @@ -139,18 +149,23 @@ impl ChunkId { } } -impl re_byte_size::SizeBytes for ChunkId { +impl From<[u8; 16]> for ChunkId { #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 + fn from(bytes: [u8; 16]) -> Self { + Self(re_tuid::Tuid::from_bytes(bytes)) } +} +impl From for [u8; 16] { #[inline] - fn is_pod() -> bool { - true + fn from(id: ChunkId) -> Self { + id.0.as_bytes() } } +// Make `quiver::Column` work (backed by a big-endian `FixedSizeBinary(16)` column): +quiver::newtype_datatype!(ChunkId, quiver::FixedSizeBinary<16>); + impl std::ops::Deref for ChunkId { type Target = re_tuid::Tuid; @@ -167,4 +182,4 @@ impl std::ops::DerefMut for ChunkId { } } -crate::delegate_arrow_tuid!(ChunkId as "rerun.controls.ChunkId"); // Used in the Data Platform +crate::delegate_arrow_tuid!(ChunkId as "rerun.controls.ChunkId"); // Used in the catalog server diff --git a/crates/store/re_types_core/src/component_batch.rs b/crates/store/re_types_core/src/component_batch.rs index 1bcf8353f170..912c5fc32f6d 100644 --- a/crates/store/re_types_core/src/component_batch.rs +++ b/crates/store/re_types_core/src/component_batch.rs @@ -116,7 +116,7 @@ fn assert_component_batch_object_safe() { /// * See [`AsComponents`] for logging serialized data. /// /// [`AsComponents`]: [crate::AsComponents] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, re_byte_size::SizeBytes)] pub struct SerializedComponentBatch { // TODO(cmc): Maybe Cow<> this one if it grows bigger. Or intern descriptors altogether, most likely. pub descriptor: ComponentDescriptor, @@ -124,14 +124,6 @@ pub struct SerializedComponentBatch { pub array: arrow::array::ArrayRef, } -impl re_byte_size::SizeBytes for SerializedComponentBatch { - #[inline] - fn heap_size_bytes(&self) -> u64 { - let Self { array, descriptor } = self; - array.heap_size_bytes() + descriptor.heap_size_bytes() - } -} - impl PartialEq for SerializedComponentBatch { #[inline] fn eq(&self, other: &Self) -> bool { @@ -188,7 +180,7 @@ impl SerializedComponentBatch { /// A column's worth of component data. /// /// If a [`SerializedComponentBatch`] represents one row's worth of data -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, re_byte_size::SizeBytes)] pub struct SerializedComponentColumn { pub list_array: arrow::array::ListArray, @@ -226,13 +218,6 @@ impl SerializedComponentColumn { } } -impl re_byte_size::SizeBytes for SerializedComponentColumn { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.list_array.heap_size_bytes() + self.descriptor.heap_size_bytes() - } -} - impl From for SerializedComponentColumn { #[inline] fn from(batch: SerializedComponentBatch) -> Self { diff --git a/crates/store/re_types_core/src/component_descriptor.rs b/crates/store/re_types_core/src/component_descriptor.rs index e71fc4f3f198..280c1728e9b6 100644 --- a/crates/store/re_types_core/src/component_descriptor.rs +++ b/crates/store/re_types_core/src/component_descriptor.rs @@ -7,8 +7,17 @@ use crate::{ArchetypeName, ComponentIdentifier, ComponentType}; /// Every component at a given entity path is uniquely identified by the /// `component` field of the descriptor. The `archetype` and `component_type` /// fields provide additional information about the semantics of the data. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + re_byte_size::SizeBytes, + serde::Deserialize, + serde::Serialize, +)] pub struct ComponentDescriptor { /// Optional name of the `Archetype` associated with this data. /// @@ -87,20 +96,6 @@ impl ComponentDescriptor { } } -impl re_byte_size::SizeBytes for ComponentDescriptor { - #[inline] - fn heap_size_bytes(&self) -> u64 { - let Self { - archetype: archetype_name, - component, - component_type, - } = self; - archetype_name.heap_size_bytes() - + component_type.heap_size_bytes() - + component.heap_size_bytes() - } -} - impl ComponentDescriptor { /// Creates a new component descriptor that only has the `component` set. /// @@ -166,26 +161,31 @@ pub const FIELD_METADATA_KEY_COMPONENT: &str = "rerun:component"; /// The key used to identify the [`crate::ComponentType`] in field-level metadata. pub const FIELD_METADATA_KEY_COMPONENT_TYPE: &str = "rerun:component_type"; -impl From for ComponentDescriptor { +impl TryFrom for ComponentDescriptor { + type Error = crate::InvalidComponentIdentifierError; + #[inline] - fn from(field: arrow::datatypes::Field) -> Self { + fn try_from(field: arrow::datatypes::Field) -> Result { let md = field.metadata(); + let component = md.get(FIELD_METADATA_KEY_COMPONENT).cloned().unwrap_or_else(|| { + re_log::debug!( + "Missing metadata field {FIELD_METADATA_KEY_COMPONENT}, resorting to field name: {}", + field.name() + ); + field.name().clone() + }); + let descr = Self { archetype: md .get(FIELD_METADATA_KEY_ARCHETYPE) - .cloned() - .map(Into::into), - component: md.get(FIELD_METADATA_KEY_COMPONENT).cloned().unwrap_or_else(|| { - re_log::debug!("Missing metadata field {FIELD_METADATA_KEY_COMPONENT}, resorting to field name: {}", field.name()); - field.name().clone() - }).into(), + .and_then(|s| ArchetypeName::try_new(s).ok()), + component: ComponentIdentifier::try_new(component)?, component_type: md .get(FIELD_METADATA_KEY_COMPONENT_TYPE) - .cloned() - .map(Into::into), + .and_then(|s| ComponentType::try_new(s).ok()), }; descr.sanity_check(); - descr + Ok(descr) } } diff --git a/crates/store/re_types_core/src/components/clear_is_recursive.rs b/crates/store/re_types_core/src/components/clear_is_recursive.rs index e58a5bbc5da9..d5210be77e3e 100644 --- a/crates/store/re_types_core/src/components/clear_is_recursive.rs +++ b/crates/store/re_types_core/src/components/clear_is_recursive.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Component**: Configures how a clear operation should behave - recursive or not. -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct ClearIsRecursive( /// If true, also clears all recursive children entities. pub crate::datatypes::Bool, @@ -72,15 +73,3 @@ impl std::ops::DerefMut for ClearIsRecursive { &mut self.0 } } - -impl ::re_byte_size::SizeBytes for ClearIsRecursive { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/absolute_time_range.rs b/crates/store/re_types_core/src/datatypes/absolute_time_range.rs index 694a7fd95fcc..742fe4fe7b24 100644 --- a/crates/store/re_types_core/src/datatypes/absolute_time_range.rs +++ b/crates/store/re_types_core/src/datatypes/absolute_time_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: Two [`datatypes::TimeInt`][crate::datatypes::TimeInt] describing a range of time. -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] pub struct AbsoluteTimeRange { /// Start of the range. pub min: crate::datatypes::TimeInt, @@ -142,11 +143,11 @@ impl crate::Loggable for AbsoluteTimeRange { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let min = { if !arrays_by_name.contains_key("min") { return Err(DeserializationError::missing_struct_field( @@ -209,15 +210,3 @@ impl crate::Loggable for AbsoluteTimeRange { }) } } - -impl ::re_byte_size::SizeBytes for AbsoluteTimeRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.min.heap_size_bytes() + self.max.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/bool.rs b/crates/store/re_types_core/src/datatypes/bool.rs index 395daeff5402..85447be0f54a 100644 --- a/crates/store/re_types_core/src/datatypes/bool.rs +++ b/crates/store/re_types_core/src/datatypes/bool.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A single boolean. -#[derive(Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Clone, Debug, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Bool(pub bool); @@ -108,15 +111,3 @@ impl From for bool { value.0 } } - -impl ::re_byte_size::SizeBytes for Bool { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/entity_path.rs b/crates/store/re_types_core/src/datatypes/entity_path.rs index 5b817b5056e8..d6a8f8c45c7c 100644 --- a/crates/store/re_types_core/src/datatypes/entity_path.rs +++ b/crates/store/re_types_core/src/datatypes/entity_path.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A path to an entity in the `ChunkStore`. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)] +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct EntityPath(pub crate::ArrowString); @@ -145,15 +148,3 @@ impl From for crate::ArrowString { value.0 } } - -impl ::re_byte_size::SizeBytes for EntityPath { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/float32.rs b/crates/store/re_types_core/src/datatypes/float32.rs index c1e86f2a5443..4397115e3c93 100644 --- a/crates/store/re_types_core/src/datatypes/float32.rs +++ b/crates/store/re_types_core/src/datatypes/float32.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,17 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A single-precision 32-bit IEEE 754 floating point number. -#[derive(Clone, Debug, Default, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Float32(pub f32); @@ -136,15 +147,3 @@ impl From for f32 { value.0 } } - -impl ::re_byte_size::SizeBytes for Float32 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/float64.rs b/crates/store/re_types_core/src/datatypes/float64.rs index 0cadfba16b41..111410caeb66 100644 --- a/crates/store/re_types_core/src/datatypes/float64.rs +++ b/crates/store/re_types_core/src/datatypes/float64.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,17 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A double-precision 64-bit IEEE 754 floating point number. -#[derive(Clone, Debug, Default, Copy, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[derive( + Clone, + Debug, + Default, + Copy, + PartialEq, + PartialOrd, + bytemuck::Pod, + bytemuck::Zeroable, + ::re_byte_size::SizeBytes, +)] #[repr(transparent)] pub struct Float64(pub f64); @@ -136,15 +147,3 @@ impl From for f64 { value.0 } } - -impl ::re_byte_size::SizeBytes for Float64 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/time_int.rs b/crates/store/re_types_core/src/datatypes/time_int.rs index f2e91568e80d..730dfb793814 100644 --- a/crates/store/re_types_core/src/datatypes/time_int.rs +++ b/crates/store/re_types_core/src/datatypes/time_int.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A 64-bit number describing either nanoseconds OR sequence numbers. -#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes)] pub struct TimeInt(pub i64); crate::macros::impl_into_cow!(TimeInt); @@ -135,15 +136,3 @@ impl From for i64 { value.0 } } - -impl ::re_byte_size::SizeBytes for TimeInt { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/time_range.rs b/crates/store/re_types_core/src/datatypes/time_range.rs index a0e27f9e6bb8..3f7f80900155 100644 --- a/crates/store/re_types_core/src/datatypes/time_range.rs +++ b/crates/store/re_types_core/src/datatypes/time_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: Visible time range bounds for a specific timeline. -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct TimeRange { /// Low time boundary for sequence timeline. pub start: crate::datatypes::TimeRangeBoundary, @@ -150,11 +151,11 @@ impl crate::Loggable for TimeRange { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let start = { if !arrays_by_name.contains_key("start") { return Err(DeserializationError::missing_struct_field( @@ -201,16 +202,3 @@ impl crate::Loggable for TimeRange { }) } } - -impl ::re_byte_size::SizeBytes for TimeRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.start.heap_size_bytes() + self.end.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - && ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/time_range_boundary.rs b/crates/store/re_types_core/src/datatypes/time_range_boundary.rs index 94277df97567..18fb70f561bd 100644 --- a/crates/store/re_types_core/src/datatypes/time_range_boundary.rs +++ b/crates/store/re_types_core/src/datatypes/time_range_boundary.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: Left or right boundary of a time range. -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub enum TimeRangeBoundary { /// Boundary is a value relative to the time cursor. CursorRelative(crate::datatypes::TimeInt), @@ -323,20 +324,3 @@ impl crate::Loggable for TimeRangeBoundary { }) } } - -impl ::re_byte_size::SizeBytes for TimeRangeBoundary { - #[inline] - fn heap_size_bytes(&self) -> u64 { - #![allow(clippy::match_same_arms)] - match self { - Self::CursorRelative(v) => v.heap_size_bytes(), - Self::Absolute(v) => v.heap_size_bytes(), - Self::Infinite => 0, - } - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/uint16.rs b/crates/store/re_types_core/src/datatypes/uint16.rs index a8d7a99fefda..1a6920d49a3c 100644 --- a/crates/store/re_types_core/src/datatypes/uint16.rs +++ b/crates/store/re_types_core/src/datatypes/uint16.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A 16bit unsigned integer. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] pub struct UInt16(pub u16); crate::macros::impl_into_cow!(UInt16); @@ -135,15 +138,3 @@ impl From for u16 { value.0 } } - -impl ::re_byte_size::SizeBytes for UInt16 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/uint32.rs b/crates/store/re_types_core/src/datatypes/uint32.rs index 3eb5bb2cb1c2..d1f1be711d5e 100644 --- a/crates/store/re_types_core/src/datatypes/uint32.rs +++ b/crates/store/re_types_core/src/datatypes/uint32.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A 32bit unsigned integer. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] pub struct UInt32(pub u32); crate::macros::impl_into_cow!(UInt32); @@ -135,15 +138,3 @@ impl From for u32 { value.0 } } - -impl ::re_byte_size::SizeBytes for UInt32 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/uint64.rs b/crates/store/re_types_core/src/datatypes/uint64.rs index f2affcef6cfe..06d072f670ef 100644 --- a/crates/store/re_types_core/src/datatypes/uint64.rs +++ b/crates/store/re_types_core/src/datatypes/uint64.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,9 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A 64bit unsigned integer. -#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive( + Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord, ::re_byte_size::SizeBytes, +)] pub struct UInt64(pub u64); crate::macros::impl_into_cow!(UInt64); @@ -135,15 +138,3 @@ impl From for u64 { value.0 } } - -impl ::re_byte_size::SizeBytes for UInt64 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/utf8.rs b/crates/store/re_types_core/src/datatypes/utf8.rs index 58132017905a..ef6056180ff0 100644 --- a/crates/store/re_types_core/src/datatypes/utf8.rs +++ b/crates/store/re_types_core/src/datatypes/utf8.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: A string of text, encoded as UTF-8. -#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, ::re_byte_size::SizeBytes)] #[repr(transparent)] pub struct Utf8(pub crate::ArrowString); @@ -145,15 +146,3 @@ impl From for crate::ArrowString { value.0 } } - -impl ::re_byte_size::SizeBytes for Utf8 { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/datatypes/visible_time_range.rs b/crates/store/re_types_core/src/datatypes/visible_time_range.rs index 7bc4df0af864..a13b9895ddca 100644 --- a/crates/store/re_types_core/src/datatypes/visible_time_range.rs +++ b/crates/store/re_types_core/src/datatypes/visible_time_range.rs @@ -7,6 +7,7 @@ #![allow(clippy::allow_attributes)] #![allow(clippy::clone_on_copy)] #![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::eq_op)] #![allow(clippy::map_flatten)] #![allow(clippy::needless_question_mark)] #![allow(clippy::new_without_default)] @@ -22,7 +23,7 @@ use crate::{ComponentDescriptor, ComponentType}; use crate::{DeserializationError, DeserializationResult}; /// **Datatype**: Visible time range bounds for a specific timeline. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ::re_byte_size::SizeBytes)] pub struct VisibleTimeRange { /// Name of the timeline this applies to. pub timeline: crate::datatypes::Utf8, @@ -166,11 +167,11 @@ impl crate::Loggable for VisibleTimeRange { } else { let (arrow_data_fields, arrow_data_arrays) = (arrow_data.fields(), arrow_data.columns()); - let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields - .iter() - .map(|field| field.name().as_str()) - .zip(arrow_data_arrays) - .collect(); + let arrays_by_name: ::std::collections::HashMap<_, _> = ::std::iter::zip( + arrow_data_fields.iter().map(|field| field.name().as_str()), + arrow_data_arrays, + ) + .collect(); let timeline = { if !arrays_by_name.contains_key("timeline") { return Err(DeserializationError::missing_struct_field( @@ -257,15 +258,3 @@ impl crate::Loggable for VisibleTimeRange { }) } } - -impl ::re_byte_size::SizeBytes for VisibleTimeRange { - #[inline] - fn heap_size_bytes(&self) -> u64 { - self.timeline.heap_size_bytes() + self.range.heap_size_bytes() - } - - #[inline] - fn is_pod() -> bool { - ::is_pod() && ::is_pod() - } -} diff --git a/crates/store/re_types_core/src/dynamic_archetype.rs b/crates/store/re_types_core/src/dynamic_archetype.rs index 95c01ef5910b..be96e6d16fc8 100644 --- a/crates/store/re_types_core/src/dynamic_archetype.rs +++ b/crates/store/re_types_core/src/dynamic_archetype.rs @@ -42,10 +42,9 @@ impl DynamicArchetype { #[inline] pub fn with_component_from_data( mut self, - field: impl AsRef, + field: impl Into, array: arrow::array::ArrayRef, ) -> Self { - let field = field.as_ref(); let component = field.into(); self.batches.insert( @@ -68,7 +67,7 @@ impl DynamicArchetype { #[inline] pub fn with_component( self, - field: impl AsRef, + field: impl Into, loggable: impl IntoIterator>, ) -> Self { self.with_component_override(field, C::name(), loggable) @@ -80,11 +79,10 @@ impl DynamicArchetype { #[inline] pub fn with_component_override( mut self, - field: impl AsRef, + field: impl Into, component_type: impl Into, loggable: impl IntoIterator>, ) -> Self { - let field = field.as_ref(); let component = field.into(); let mut desc = ComponentDescriptor::partial(component).with_component_type(component_type.into()); diff --git a/crates/store/re_types_core/src/layer_name.rs b/crates/store/re_types_core/src/layer_name.rs new file mode 100644 index 000000000000..f05b804d71fc --- /dev/null +++ b/crates/store/re_types_core/src/layer_name.rs @@ -0,0 +1,242 @@ +/// The name of a layer (e.g. `"base"`). +/// +/// Layers partition a segment's chunks into named groups that can be +/// registered, queried, and deleted independently. +// +// NOTE: Intentionally does not implement `Default` — a blank layer name is +// almost always a bug. It cannot be constructed empty at all: use the fallible +// [`LayerName::try_new`], or [`LayerName::base`] when you really want `"base"`. +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, ::serde::Serialize)] +pub struct LayerName(String); + +/// Error returned when constructing an invalid [`LayerName`]. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct InvalidLayerNameError { + /// Why the string was rejected, e.g. `"must not be empty"`. + reason: &'static str, +} + +impl std::fmt::Display for InvalidLayerNameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Invalid `LayerName`: {}", self.reason) + } +} + +impl std::fmt::Debug for InvalidLayerNameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "InvalidLayerNameError({:?})", self.reason) + } +} + +impl std::error::Error for InvalidLayerNameError {} + +impl LayerName { + /// The default layer name (`"base"`) used when no explicit layer is specified. + pub const DEFAULT_STR: &'static str = "base"; + + /// Create a new layer name, failing if the string is invalid (e.g. empty). + #[inline] + pub fn try_new(name: impl Into) -> Result { + let name = name.into(); + + if name.is_empty() { + return Err(InvalidLayerNameError { + reason: "must not be empty", + }); + } + + Ok(Self(name)) + } + + /// Create from a trusted compile-time string literal. + /// + /// # Panics + /// Panics if `string` is invalid (e.g. empty). + #[inline] + pub fn from_static_str(string: &'static str) -> Self { + Self::try_new(string).unwrap_or_else(|err| panic!("{err} (got {string:?})")) + } + + /// The default layer (`"base"`). + #[inline] + pub fn base() -> Self { + Self(Self::DEFAULT_STR.to_owned()) + } + + #[inline] + pub fn as_str(&self) -> &str { + &self.0 + } + + #[inline] + pub fn into_string(self) -> String { + self.0 + } +} + +// NOTE: no `TryFrom<&str>` / `TryFrom<&String>`: those would collide with the blanket +// `impl> TryFrom for T` in `core` once we implement `From<&'static str>` +// below. Use the inherent `try_new` for fallible construction from borrowed strings. +impl TryFrom for LayerName { + type Error = InvalidLayerNameError; + + #[inline] + fn try_from(name: String) -> Result { + Self::try_new(name) + } +} + +// Only `&'static str` (string literals / consts), so `impl Into` parameters stay +// ergonomic for trusted compile-time values. Dynamic `&str`/`String` must go through +// the fallible `try_new`/`TryFrom` instead. +impl From<&'static str> for LayerName { + /// # Panics + /// Panics if `string` is empty. + #[inline] + fn from(string: &'static str) -> Self { + Self::from_static_str(string) + } +} + +impl From for String { + #[inline] + fn from(name: LayerName) -> Self { + name.0 + } +} + +// Fallible, so an empty string is rejected here too (used by e.g. `clap` value parsing). +impl std::str::FromStr for LayerName { + type Err = InvalidLayerNameError; + + #[inline] + fn from_str(name: &str) -> Result { + Self::try_new(name) + } +} + +// Make `quiver::Column` work (backed by a `Utf8` column). +// `try_*` because reading validates non-emptiness (via `TryFrom`) at +// column construction, so an empty layer name can't sneak in from storage either. +quiver::try_newtype_datatype!(LayerName, quiver::Utf8); + +impl AsRef for LayerName { + #[inline] + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl std::ops::Deref for LayerName { + type Target = str; + + #[inline] + fn deref(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for LayerName { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl PartialEq for LayerName { + #[inline] + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl PartialEq<&str> for LayerName { + #[inline] + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + +impl PartialEq for str { + #[inline] + fn eq(&self, other: &LayerName) -> bool { + self == other.0 + } +} + +impl PartialEq for &str { + #[inline] + fn eq(&self, other: &LayerName) -> bool { + *self == other.0 + } +} + +impl<'de> serde::Deserialize<'de> for LayerName { + #[inline] + fn deserialize>(deserializer: D) -> Result { + use serde::de::Error as _; + let string = ::deserialize(deserializer)?; + Self::try_new(string).map_err(D::Error::custom) + } +} + +impl re_byte_size::SizeBytes for LayerName { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.0.heap_size_bytes() + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr as _; + + use super::*; + + #[test] + fn empty_is_rejected_everywhere() { + assert!(LayerName::try_new("").is_err()); + assert!(LayerName::try_new(String::new()).is_err()); + assert!(LayerName::from_str("").is_err()); + assert!(LayerName::try_from(String::new()).is_err()); + } + + #[test] + fn non_empty_round_trips() { + assert_eq!(LayerName::try_new("base").unwrap().as_str(), "base"); + assert_eq!(LayerName::from_static_str("base").as_str(), "base"); + assert_eq!(LayerName::from("base").as_str(), "base"); // `From<&'static str>` + assert_eq!("base".parse::().unwrap().as_str(), "base"); + assert_eq!(LayerName::base().as_str(), LayerName::DEFAULT_STR); + } + + #[test] + #[should_panic(expected = "must not be empty")] + fn from_static_str_panics_on_empty() { + let _ = LayerName::from_static_str(""); + } + + #[test] + fn serde_rejects_empty() { + let json = serde_json::to_string(&LayerName::base()).unwrap(); + assert_eq!(json, "\"base\""); + assert_eq!( + serde_json::from_str::(&json).unwrap(), + LayerName::base() + ); + assert!(serde_json::from_str::("\"\"").is_err()); + } + + #[test] + fn quiver_column_rejects_empty() { + use arrow::array::StringArray; + + // A non-empty column round-trips. + let column = quiver::Column::::from_values([LayerName::base()]); + assert_eq!(column.to_vec(), [LayerName::base()]); + + // A column containing an empty string is rejected at construction. + let array = std::sync::Arc::new(StringArray::from(vec!["base", ""])); + assert!(quiver::Column::::try_new(array).is_err()); + } +} diff --git a/crates/store/re_types_core/src/lib.rs b/crates/store/re_types_core/src/lib.rs index 5c0b17f467e8..8b2835a48355 100644 --- a/crates/store/re_types_core/src/lib.rs +++ b/crates/store/re_types_core/src/lib.rs @@ -29,10 +29,12 @@ mod chunk_id; mod component_batch; mod component_descriptor; mod dynamic_archetype; +mod layer_name; mod loggable; pub mod reflection; mod result; mod row_id; +mod segment_id; mod timeline_name; mod tuid; mod view; @@ -40,6 +42,7 @@ mod wrapper_component; pub use self::archetype::{ Archetype, ArchetypeName, ArchetypeReflectionMarker, ComponentIdentifier, + InvalidComponentIdentifierError, }; pub use self::arrow_string::ArrowString; pub use self::as_components::AsComponents; @@ -52,18 +55,18 @@ pub use self::component_descriptor::{ FIELD_METADATA_KEY_COMPONENT_TYPE, }; pub use self::dynamic_archetype::DynamicArchetype; -pub use self::loggable::{ - Component, ComponentSet, ComponentType, DatatypeName, Loggable, UnorderedComponentSet, -}; +pub use self::layer_name::{InvalidLayerNameError, LayerName}; +pub use self::loggable::{Component, ComponentSet, ComponentType, Loggable, UnorderedComponentSet}; pub use self::result::{ _Backtrace, DeserializationError, DeserializationResult, ResultExt, SerializationError, SerializationResult, }; pub use self::row_id::RowId; +pub use self::segment_id::SegmentId; pub use self::tuid::tuids_to_arrow; pub use self::view::{View, ViewClassIdentifier}; pub use self::wrapper_component::WrapperComponent; -pub use timeline_name::TimelineName; +pub use timeline_name::{InvalidTimelineNameError, TimelineName}; /// Fundamental [`Archetype`]s that are implemented in `re_types_core` directly for convenience and /// dependency optimization. @@ -93,7 +96,7 @@ pub mod macros { } pub mod external { - pub use {anyhow, arrow, re_tuid}; + pub use {anyhow, arrow, re_string_interner, re_tuid}; } /// Useful macro for statically asserting that a `struct` contains some specific fields. diff --git a/crates/store/re_types_core/src/loggable.rs b/crates/store/re_types_core/src/loggable.rs index 2fd9e6743432..5cd973790b6d 100644 --- a/crates/store/re_types_core/src/loggable.rs +++ b/crates/store/re_types_core/src/loggable.rs @@ -95,9 +95,8 @@ pub type UnorderedComponentSet = IntSet; pub type ComponentSet = std::collections::BTreeSet; -re_string_interner::declare_new_type!( +re_string_interner::declare_new_type_nonempty!( /// The fully-qualified name of a [`Component`], e.g. `rerun.components.Position2D`. - #[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))] pub struct ComponentType; ); @@ -188,41 +187,3 @@ impl ComponentType { self.0.as_str().starts_with("rerun.") } } - -// --- - -re_string_interner::declare_new_type!( - /// The fully-qualified name of a [`Datatype`], e.g. `rerun.datatypes.Vec2D`. - #[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))] - pub struct DatatypeName; -); - -impl DatatypeName { - /// Returns the fully-qualified name, e.g. `rerun.datatypes.Vec2D`. - /// - /// This is the default `Display` implementation for [`DatatypeName`]. - #[inline] - pub fn full_name(&self) -> &'static str { - self.0.as_str() - } - - /// Returns the unqualified name, e.g. `Vec2D`. - /// - /// Used for most UI elements. - /// - /// ``` - /// # use re_types_core::DatatypeName; - /// assert_eq!(DatatypeName::from("rerun.datatypes.Vec2D").short_name(), "Vec2D"); - /// ``` - #[inline] - pub fn short_name(&self) -> &'static str { - let full_name = self.0.as_str(); - if let Some(short_name) = full_name.strip_prefix("rerun.datatypes.") { - short_name - } else if let Some(short_name) = full_name.strip_prefix("rerun.") { - short_name - } else { - full_name - } - } -} diff --git a/crates/store/re_types_core/src/reflection.rs b/crates/store/re_types_core/src/reflection.rs index 7476cd830f0a..1daad11ef35a 100644 --- a/crates/store/re_types_core/src/reflection.rs +++ b/crates/store/re_types_core/src/reflection.rs @@ -448,7 +448,7 @@ impl ArchetypeFieldReflection { /// Returns the component identifier for this field. #[inline] pub fn component(&self, archetype_name: ArchetypeName) -> ComponentIdentifier { - format!("{}:{}", archetype_name.short_name(), self.name).into() + ComponentIdentifier::from_archetype_field(archetype_name, self.name) } } @@ -480,16 +480,6 @@ pub trait ComponentDescriptorExt { fn or_with_builtin_archetype(self, archetype: impl Fn() -> ArchetypeName) -> Self; } -/// Constructs a [`ComponentIdentifier`] from this archetype by supplying a field name. -/// -/// Mainly used as a convenience function to create [`ComponentDescriptor`]s for -/// Rerun-builtin types. In general, the [`ArchetypeName`] does not place any restrictions -/// on the contents of [`ComponentIdentifier`]. -#[inline] -fn with_field(archetype: ArchetypeName, field_name: impl AsRef) -> ComponentIdentifier { - format!("{}:{}", archetype.short_name(), field_name.as_ref()).into() -} - impl ComponentDescriptorExt for ComponentDescriptor { fn archetype_field_name(&self) -> &str { self.archetype @@ -505,7 +495,7 @@ impl ComponentDescriptorExt for ComponentDescriptor { let archetype = archetype.into(); { let field_name = self.archetype_field_name(); - self.component = with_field(archetype, field_name); + self.component = ComponentIdentifier::from_archetype_field(archetype, field_name); } self.archetype = Some(archetype); self @@ -515,7 +505,8 @@ impl ComponentDescriptorExt for ComponentDescriptor { fn or_with_builtin_archetype(mut self, archetype: impl Fn() -> ArchetypeName) -> Self { if self.archetype.is_none() { let archetype = archetype(); - self.component = with_field(archetype, self.component); + self.component = + ComponentIdentifier::from_archetype_field(archetype, self.component.as_str()); self.archetype = Some(archetype); } self @@ -524,15 +515,15 @@ impl ComponentDescriptorExt for ComponentDescriptor { #[cfg(test)] mod test { - use super::{ComponentDescriptor, ComponentDescriptorExt as _, with_field}; - use crate::ArchetypeName; + use super::{ComponentDescriptor, ComponentDescriptorExt as _}; + use crate::{ArchetypeName, ComponentIdentifier}; #[test] fn component_descriptor_manipulation() { let archetype_name: ArchetypeName = "rerun.archetypes.MyExample".into(); let descr = ComponentDescriptor { archetype: Some(archetype_name), - component: with_field(archetype_name, "test"), + component: ComponentIdentifier::from_archetype_field(archetype_name, "test"), component_type: Some("user.Whatever".into()), }; assert_eq!(descr.archetype_field_name(), "test"); diff --git a/crates/store/re_types_core/src/row_id.rs b/crates/store/re_types_core/src/row_id.rs index 5ee6551bf424..a8d9ca6dbbe9 100644 --- a/crates/store/re_types_core/src/row_id.rs +++ b/crates/store/re_types_core/src/row_id.rs @@ -60,8 +60,10 @@ use crate::Loggable as _; Hash, bytemuck::AnyBitPattern, bytemuck::NoUninit, + re_byte_size::SizeBytes, + serde::Deserialize, + serde::Serialize, )] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct RowId(pub(crate) re_tuid::Tuid); impl std::fmt::Display for RowId { @@ -156,18 +158,6 @@ impl RowId { } } -impl re_byte_size::SizeBytes for RowId { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} - impl std::ops::Deref for RowId { type Target = re_tuid::Tuid; diff --git a/crates/store/re_types_core/src/segment_id.rs b/crates/store/re_types_core/src/segment_id.rs new file mode 100644 index 000000000000..628aa41a4567 --- /dev/null +++ b/crates/store/re_types_core/src/segment_id.rs @@ -0,0 +1,92 @@ +use std::borrow::Cow; + +/// Identifies a single segment within a dataset. +/// +/// Wraps a string id so the type system distinguishes segment identifiers from +/// arbitrary strings. +/// +/// Each segment is an episode, potentially consisting of many layers, +/// each backed by its own .rrd file. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + re_byte_size::SizeBytes, + serde::Serialize, + serde::Deserialize, +)] +pub struct SegmentId { + id: String, +} + +impl SegmentId { + #[inline] + pub fn new(id: String) -> Self { + Self { id } + } + + pub fn as_str(&self) -> &str { + &self.id + } + + pub fn into_inner(self) -> String { + self.id + } +} + +impl From for String { + fn from(value: SegmentId) -> Self { + value.id + } +} + +impl From for SegmentId { + fn from(id: String) -> Self { + Self { id } + } +} + +// Make `quiver::Column` work (backed by a `Utf8` column): +quiver::newtype_datatype!(SegmentId, quiver::Utf8); + +impl From<&str> for SegmentId { + fn from(id: &str) -> Self { + Self { id: id.to_owned() } + } +} + +impl<'a> From> for SegmentId { + fn from(id: Cow<'a, str>) -> Self { + Self { + id: id.into_owned(), + } + } +} + +impl std::fmt::Display for SegmentId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.id.fmt(f) + } +} + +impl AsRef for SegmentId { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +/// Allows `&str` lookups in maps keyed by [`SegmentId`]. +/// +/// Sound because the derived `Eq`/`Ord`/`Hash` all delegate to the inner `String`, +/// matching `str` semantics — same contract as `String: Borrow`. +impl std::borrow::Borrow for SegmentId { + #[inline] + fn borrow(&self) -> &str { + self.as_str() + } +} diff --git a/crates/store/re_types_core/src/timeline_name.rs b/crates/store/re_types_core/src/timeline_name.rs index d4a2fcd36103..9fcb6cd5c922 100644 --- a/crates/store/re_types_core/src/timeline_name.rs +++ b/crates/store/re_types_core/src/timeline_name.rs @@ -1,8 +1,7 @@ -re_string_interner::declare_new_type!( +re_string_interner::declare_new_type_nonempty!( /// The name of a timeline. Often something like `"log_time"` or `"frame_nr"`. /// /// This uniquely identifies a timeline. - #[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))] pub struct TimelineName; ); @@ -13,7 +12,7 @@ impl TimelineName { /// which point the data was logged (according to the client's wall-clock). #[inline] pub fn log_time() -> Self { - Self::new("log_time") + re_string_interner::intern_static_nonempty!(TimelineName, "log_time") } /// The log tick timeline to which all API functions will always log. @@ -24,6 +23,6 @@ impl TimelineName { /// methods on a `RecordingStream`. #[inline] pub fn log_tick() -> Self { - Self::new("log_tick") + re_string_interner::intern_static_nonempty!(TimelineName, "log_tick") } } diff --git a/crates/store/re_types_core/src/tuid.rs b/crates/store/re_types_core/src/tuid.rs index 05567c21c5aa..8ea44e6cf97a 100644 --- a/crates/store/re_types_core/src/tuid.rs +++ b/crates/store/re_types_core/src/tuid.rs @@ -1,16 +1,10 @@ -use std::sync::Arc; - -use arrow::array::{ArrayRef, AsArray as _, FixedSizeBinaryArray, FixedSizeBinaryBuilder}; -use arrow::datatypes::DataType; +use arrow::array::{ArrayRef, AsArray as _, FixedSizeBinaryArray}; use re_tuid::Tuid; use crate::{DeserializationError, Loggable}; // --- -#[expect(clippy::cast_possible_wrap)] -const BYTE_WIDTH: i32 = std::mem::size_of::() as i32; - pub fn tuids_to_arrow(tuids: &[Tuid]) -> FixedSizeBinaryArray { #[expect(clippy::unwrap_used)] // Can't fail ::to_arrow(tuids.iter()) @@ -22,7 +16,7 @@ pub fn tuids_to_arrow(tuids: &[Tuid]) -> FixedSizeBinaryArray { impl Loggable for Tuid { #[inline] fn arrow_datatype() -> arrow::datatypes::DataType { - DataType::FixedSizeBinary(BYTE_WIDTH) + quiver::Column::::datatype() } fn to_arrow_opt<'a>( @@ -44,15 +38,10 @@ impl Loggable for Tuid { where Self: 'a, { - let iter = iter.into_iter(); - - let mut builder = FixedSizeBinaryBuilder::with_capacity(iter.size_hint().0, BYTE_WIDTH); - for tuid in iter { - #[expect(clippy::unwrap_used)] // Can't fail because `BYTE_WIDTH` is correct. - builder.append_value(tuid.into().as_bytes()).unwrap(); - } - - Ok(Arc::new(builder.finish())) + let column = quiver::Column::::from_values( + iter.into_iter().map(|tuid| tuid.into().into_owned()), + ); + Ok(column.into_arrow()) } fn from_arrow(array: &dyn ::arrow::array::Array) -> crate::DeserializationResult> { diff --git a/crates/store/re_types_core/src/view.rs b/crates/store/re_types_core/src/view.rs index a97098388081..c6e9afb0d878 100644 --- a/crates/store/re_types_core/src/view.rs +++ b/crates/store/re_types_core/src/view.rs @@ -1,13 +1,12 @@ // --- -re_string_interner::declare_new_type!( +re_string_interner::declare_new_type_nonempty!( /// The unique name of a view - #[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))] pub struct ViewClassIdentifier; ); impl ViewClassIdentifier { pub fn invalid() -> Self { - Self::from("invalid") + re_string_interner::intern_static_nonempty!(ViewClassIdentifier, "invalid") } } diff --git a/crates/store/re_uri/Cargo.toml b/crates/store/re_uri/Cargo.toml index 1fcb75f144aa..38fbfa18b183 100644 --- a/crates/store/re_uri/Cargo.toml +++ b/crates/store/re_uri/Cargo.toml @@ -15,9 +15,11 @@ version.workspace = true workspace = true [dependencies] +re_byte_size.workspace = true re_log.workspace = true -re_log_types = { workspace = true, features = ["serde"] } +re_log_types.workspace = true re_tuid.workspace = true +re_types_core.workspace = true # External percent-encoding.workspace = true diff --git a/crates/store/re_uri/src/dataset_hierarchy.rs b/crates/store/re_uri/src/dataset_hierarchy.rs index cbf4bc9a7dd7..3fdba7ad0a23 100644 --- a/crates/store/re_uri/src/dataset_hierarchy.rs +++ b/crates/store/re_uri/src/dataset_hierarchy.rs @@ -18,14 +18,14 @@ pub fn split_dataset_hierarchy_path(path: &str) -> impl Iterator { (None, path) }; - parents - .into_iter() - .flat_map(|parents| { + std::iter::chain( + parents.into_iter().flat_map(|parents| { parents .split(DATASET_HIERARCHY_SEPARATOR) .filter(|s| !s.is_empty()) - }) - .chain(std::iter::once(leaf)) + }), + std::iter::once(leaf), + ) } /// Returns the leaf segment of an entry name using [`split_dataset_hierarchy_path`] semantics. diff --git a/crates/store/re_uri/src/endpoints/dataset.rs b/crates/store/re_uri/src/endpoints/dataset.rs index 44a32ef51f3f..f788d12901cf 100644 --- a/crates/store/re_uri/src/endpoints/dataset.rs +++ b/crates/store/re_uri/src/endpoints/dataset.rs @@ -1,4 +1,5 @@ use re_log_types::StoreId; +use re_types_core::SegmentId; use crate::{Error, Fragment, Origin, RedapUri}; @@ -9,14 +10,14 @@ use crate::{Error, Fragment, Origin, RedapUri}; /// /// `segment_id` is currently mandatory, and `time_range` is optional. /// In the future we will add richer queries. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, re_byte_size::SizeBytes)] pub struct DatasetSegmentUri { pub origin: Origin, pub dataset_id: re_tuid::Tuid, // Query parameters: these affect what data is returned. /// Currently mandatory. - pub segment_id: String, + pub segment_id: SegmentId, // Fragment parameters: these affect what the viewer focuses on: pub fragment: Fragment, @@ -57,11 +58,11 @@ impl DatasetSegmentUri { match key.as_ref() { // Accept legacy `partition_id` query parameter. "partition_id" => { - legacy_partition_id = Some(value.to_string()); + legacy_partition_id = Some(SegmentId::from(value)); } "segment_id" => { - segment_id = Some(value.to_string()); + segment_id = Some(SegmentId::from(value)); } _ => { // We ignore unknown query keys that may be from urls from prior/newer versions. diff --git a/crates/store/re_uri/src/fragment.rs b/crates/store/re_uri/src/fragment.rs index e612a43e5106..a81dd88a21b6 100644 --- a/crates/store/re_uri/src/fragment.rs +++ b/crates/store/re_uri/src/fragment.rs @@ -17,7 +17,7 @@ use crate::TimeSelection; /// # assert!(test.parse::().unwrap() != Fragment::default()); /// # } /// ``` -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, re_byte_size::SizeBytes)] pub struct Fragment { pub selection: Option, @@ -47,7 +47,7 @@ impl std::fmt::Display for Fragment { if did_write { write!(f, "&")?; } - write!(f, "when={timeline}@",)?; + write!(f, "when={timeline}@")?; time_cell.format_url(f)?; did_write = true; } @@ -56,7 +56,7 @@ impl std::fmt::Display for Fragment { if did_write { write!(f, "&")?; } - write!(f, "time_selection=",)?; + write!(f, "time_selection=")?; time_selection.format_url(f)?; } @@ -87,7 +87,8 @@ impl std::str::FromStr for Fragment { }, "when" => { if let Some((timeline, time)) = value.split_once('@') { - let timeline = TimelineName::from(timeline); + let timeline = TimelineName::try_new(timeline) + .map_err(|err| format!("Bad timeline name {timeline:?}: {err}"))?; match time.parse::() { Ok(time_cell) => { // If there were when fragments before this we ignore them. diff --git a/crates/store/re_uri/src/lib.rs b/crates/store/re_uri/src/lib.rs index 20d7ebdd8844..76b37d59fa29 100644 --- a/crates/store/re_uri/src/lib.rs +++ b/crates/store/re_uri/src/lib.rs @@ -16,7 +16,7 @@ //! //! ``` //! for uri in [ -//! // Access the Data Platform catalog. +//! // Access the catalog server. //! "rerun://rerun.io", //! "rerun://rerun.io:51234/catalog", //! "rerun+http://localhost:51234/catalog", @@ -25,7 +25,7 @@ //! // Proxy to send messages to another viewer. //! "rerun+http://localhost:51234/proxy", //! -//! // Links to recording on the Data Platform (optionally with timestamp). +//! // Links to a recording on the catalog server (optionally with timestamp). //! "rerun://127.0.0.1:1234/dataset/1830B33B45B963E7774455beb91701ae/data?segment_id=sid&time_range=timeline@1.23s..72s", //! //! // Links to a folder (dataset-name prefix) within the catalog. diff --git a/crates/store/re_uri/src/origin.rs b/crates/store/re_uri/src/origin.rs index 04c61ea0fcbb..eaa6a5e2f779 100644 --- a/crates/store/re_uri/src/origin.rs +++ b/crates/store/re_uri/src/origin.rs @@ -1,5 +1,7 @@ use std::net::SocketAddr; +use re_byte_size::SizeBytes; + use crate::{Error, Scheme}; /// `scheme://hostname:port` @@ -12,6 +14,21 @@ pub struct Origin { pub port: u16, } +impl SizeBytes for Origin { + fn heap_size_bytes(&self) -> u64 { + let Self { + scheme: _, + host, + port: _, + } = self; + + match host { + url::Host::Domain(s) => s.heap_size_bytes(), + url::Host::Ipv4(_) | url::Host::Ipv6(_) => 0, + } + } +} + impl Origin { pub fn from_scheme_and_socket_addr(scheme: Scheme, socket_addr: SocketAddr) -> Self { Self { diff --git a/crates/store/re_uri/src/redap_uri.rs b/crates/store/re_uri/src/redap_uri.rs index 2c2aa638506a..f42d7717f61a 100644 --- a/crates/store/re_uri/src/redap_uri.rs +++ b/crates/store/re_uri/src/redap_uri.rs @@ -54,11 +54,11 @@ impl RedapUri { impl std::fmt::Display for RedapUri { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Catalog(uri) => write!(f, "{uri}",), - Self::Entry(uri) => write!(f, "{uri}",), - Self::Folder(uri) => write!(f, "{uri}",), - Self::DatasetData(uri) => write!(f, "{uri}",), - Self::Proxy(uri) => write!(f, "{uri}",), + Self::Catalog(uri) => write!(f, "{uri}"), + Self::Entry(uri) => write!(f, "{uri}"), + Self::Folder(uri) => write!(f, "{uri}"), + Self::DatasetData(uri) => write!(f, "{uri}"), + Self::Proxy(uri) => write!(f, "{uri}"), } } } @@ -227,7 +227,7 @@ mod tests { dataset_id, "1830B33B45B963E7774455beb91701ae".parse().unwrap(), ); - assert_eq!(segment_id, "sid"); + assert_eq!(segment_id.as_str(), "sid"); assert_eq!(fragment, Default::default()); } @@ -243,7 +243,7 @@ mod tests { }; // Legacy `partition_id` is parsed into `segment_id`. - assert_eq!(segment_id, "pid"); + assert_eq!(segment_id.as_str(), "pid"); } /// Test that `segment_id` and `partition_id` together do not work. @@ -277,7 +277,7 @@ mod tests { dataset_id, "1830B33B45B963E7774455beb91701ae".parse().unwrap(), ); - assert_eq!(segment_id, "sid"); + assert_eq!(segment_id.as_str(), "sid"); assert_eq!( fragment, Fragment { @@ -313,7 +313,7 @@ mod tests { dataset_id, "1830B33B45B963E7774455beb91701ae".parse().unwrap(), ); - assert_eq!(segment_id, "sid"); + assert_eq!(segment_id.as_str(), "sid"); assert_eq!(fragment, Fragment::default()); } diff --git a/crates/store/re_uri/src/time_selection.rs b/crates/store/re_uri/src/time_selection.rs index 3c7e2adff727..bf9f2e327916 100644 --- a/crates/store/re_uri/src/time_selection.rs +++ b/crates/store/re_uri/src/time_selection.rs @@ -1,9 +1,19 @@ -use re_log_types::{AbsoluteTimeRange, AbsoluteTimeRangeF, TimeCell, Timeline}; +use re_log_types::{AbsoluteTimeRange, AbsoluteTimeRangeF, TimeCell, Timeline, TimelineName}; use crate::Error; /// A time range selection as used in URIs, qualified with a timeline. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + serde::Serialize, + serde::Deserialize, + re_byte_size::SizeBytes, +)] pub struct TimeSelection { pub timeline: Timeline, pub range: AbsoluteTimeRange, @@ -93,6 +103,8 @@ impl std::str::FromStr for TimeSelection { ))); } + let timeline = TimelineName::try_new(timeline) + .map_err(|err| Error::InvalidTimeRange(format!("Bad timeline name: {err}")))?; let timeline = Timeline::new(timeline, min.typ()); let range = AbsoluteTimeRange::new(min, max); diff --git a/crates/top/re_sdk/Cargo.toml b/crates/top/re_sdk/Cargo.toml index 91c13fb11cd5..a1c61532a319 100644 --- a/crates/top/re_sdk/Cargo.toml +++ b/crates/top/re_sdk/Cargo.toml @@ -26,7 +26,7 @@ default = [] ## Support for using Rerun's importers directly from the SDK. ## -## See our `log_file` example and +## See our `log_file` example and ## for more information. importers = ["dep:re_importer", "dep:re_log_channel"] diff --git a/crates/top/re_sdk/src/blueprint/container.rs b/crates/top/re_sdk/src/blueprint/container.rs index 40eba7b41a99..25ccf19b3cf1 100644 --- a/crates/top/re_sdk/src/blueprint/container.rs +++ b/crates/top/re_sdk/src/blueprint/container.rs @@ -365,6 +365,36 @@ impl From for ContainerLike { } } +impl From for ContainerLike { + fn from(view: crate::blueprint::TextLogView) -> Self { + Self::View(view.0) + } +} + +impl From for ContainerLike { + fn from(view: crate::blueprint::BarChartView) -> Self { + Self::View(view.0) + } +} + +impl From for ContainerLike { + fn from(view: crate::blueprint::DataframeView) -> Self { + Self::View(view.0) + } +} + +impl From for ContainerLike { + fn from(view: crate::blueprint::StateTimelineView) -> Self { + Self::View(view.0) + } +} + +impl From for ContainerLike { + fn from(view: crate::blueprint::TensorView) -> Self { + Self::View(view.0) + } +} + impl From for ContainerLike { fn from(view: crate::blueprint::Spatial2DView) -> Self { Self::View(view.0) @@ -376,3 +406,9 @@ impl From for ContainerLike { Self::View(view.0) } } + +impl From for ContainerLike { + fn from(view: crate::blueprint::GraphView) -> Self { + Self::View(view.0) + } +} diff --git a/crates/top/re_sdk/src/blueprint/mod.rs b/crates/top/re_sdk/src/blueprint/mod.rs index 3be83f771d7f..ed4520dec118 100644 --- a/crates/top/re_sdk/src/blueprint/mod.rs +++ b/crates/top/re_sdk/src/blueprint/mod.rs @@ -8,7 +8,10 @@ mod view; pub use api::{Blueprint, BlueprintActivation, BlueprintOpts}; pub use container::{ContainerLike, Grid, Horizontal, Tabs, Vertical}; pub use panel::{BlueprintPanel, SelectionPanel, TimePanel}; -pub use view::{MapView, Spatial2DView, Spatial3DView, TextDocumentView, TimeSeriesView, View}; +pub use view::{ + BarChartView, DataframeView, GraphView, MapView, Spatial2DView, Spatial3DView, + StateTimelineView, TensorView, TextDocumentView, TextLogView, TimeSeriesView, View, +}; // Re-export types for working with visualizers and component mappings pub use re_sdk_types::blueprint::datatypes::{ComponentSourceKind, VisualizerComponentMapping}; diff --git a/crates/top/re_sdk/src/blueprint/view.rs b/crates/top/re_sdk/src/blueprint/view.rs index 9680c76d459c..7a7ad16ee43b 100644 --- a/crates/top/re_sdk/src/blueprint/view.rs +++ b/crates/top/re_sdk/src/blueprint/view.rs @@ -5,7 +5,9 @@ use uuid::Uuid; use re_log_types::EntityPath; use re_sdk_types::blueprint::archetypes::{ - ActiveVisualizers, MapBackground, ViewBlueprint, ViewContents, VisualizerInstruction, + ActiveVisualizers, ForceCenter, ForceCollisionRadius, ForceLink, ForceManyBody, ForcePosition, + GraphBackground, MapBackground, ViewBlueprint, ViewContents, VisualBounds2D, + VisualizerInstruction, }; use re_sdk_types::blueprint::components::{QueryExpression, ViewClass}; use re_sdk_types::components::{Name, Visible}; @@ -401,6 +403,170 @@ impl MapView { } } +/// Graph view for visualizing directed or undirected graphs. +pub struct GraphView(pub(crate) View); + +impl GraphView { + /// Create a new graph view. + pub fn new(name: impl Into) -> Self { + Self(View { + class_identifier: "Graph".into(), + name: Some(name.into()), + ..Default::default() + }) + } + + /// Set the origin entity path. + pub fn with_origin(mut self, origin: impl Into) -> Self { + self.0.origin = origin.into(); + self + } + + /// Set the contents query expressions. + pub fn with_contents(mut self, queries: impl IntoIterator>) -> Self { + self.0.contents = queries.into_iter().map(Into::into).collect(); + self + } + + /// Set visibility. + pub fn with_visible(mut self, visible: bool) -> Self { + self.0.visible = Some(visible); + self + } + + /// Add a default archetype that applies to all entities in the view. + pub fn with_defaults(mut self, archetype: &dyn AsComponents) -> Self { + self.0.add_defaults(archetype); + self + } + + /// Add a visualizer override for a specific entity. + pub fn with_override( + self, + entity_path: impl Into, + visualizers: impl Into, + ) -> Self { + self.with_overrides(entity_path, [visualizers]) + } + + /// Add visualizer overrides for a specific entity. + pub fn with_overrides( + mut self, + entity_path: impl Into, + visualizers: impl IntoIterator>, + ) -> Self { + self.0.add_overrides(entity_path, visualizers); + self + } + + /// Configure the background of the graph. + pub fn with_background(mut self, background: &GraphBackground) -> Self { + self.0.add_property("GraphBackground", background); + self + } + + /// Set the visual bounds of the graph. + /// + /// Everything within these bounds is guaranteed to be visible. Some things outside of + /// these bounds may also be visible due to letterboxing. + pub fn with_visual_bounds(mut self, visual_bounds: &VisualBounds2D) -> Self { + self.0.add_property("VisualBounds2D", visual_bounds); + self + } + + /// Configure the link force, which controls the interaction between two nodes connected by an edge. + pub fn with_force_link(mut self, force_link: &ForceLink) -> Self { + self.0.add_property("ForceLink", force_link); + self + } + + /// Configure the many-body force, a force between each pair of nodes that resembles an electrical charge. + pub fn with_force_many_body(mut self, force_many_body: &ForceManyBody) -> Self { + self.0.add_property("ForceManyBody", force_many_body); + self + } + + /// Configure the position force, which pulls nodes towards a specific position (similar to gravity). + pub fn with_force_position(mut self, force_position: &ForcePosition) -> Self { + self.0.add_property("ForcePosition", force_position); + self + } + + /// Configure the collision radius force, which resolves collisions between bounding circles + /// according to the radius of the nodes. + pub fn with_force_collision_radius( + mut self, + force_collision_radius: &ForceCollisionRadius, + ) -> Self { + self.0 + .add_property("ForceCollisionRadius", force_collision_radius); + self + } + + /// Configure the center force, which tries to move the center of mass of the graph to the origin. + pub fn with_force_center(mut self, force_center: &ForceCenter) -> Self { + self.0.add_property("ForceCenter", force_center); + self + } +} + +/// Text log view, for use with [`re_sdk_types::archetypes::TextLog`]. +pub struct TextLogView(pub(crate) View); + +impl TextLogView { + /// Create a new text log view. + pub fn new(name: impl Into) -> Self { + Self(View { + class_identifier: "TextLog".into(), + name: Some(name.into()), + ..Default::default() + }) + } + + /// Set the origin entity path. + pub fn with_origin(mut self, origin: impl Into) -> Self { + self.0.origin = origin.into(); + self + } + + /// Set the contents query expressions. + pub fn with_contents(mut self, queries: impl IntoIterator>) -> Self { + self.0.contents = queries.into_iter().map(Into::into).collect(); + self + } + + /// Set visibility. + pub fn with_visible(mut self, visible: bool) -> Self { + self.0.visible = Some(visible); + self + } + + /// Add a default archetype that applies to all entities in the view. + pub fn with_defaults(mut self, archetype: &dyn AsComponents) -> Self { + self.0.add_defaults(archetype); + self + } + + /// Add a visualizer override for a specific entity. + pub fn with_override( + self, + entity_path: impl Into, + visualizers: impl Into, + ) -> Self { + self.with_overrides(entity_path, [visualizers]) + } + + /// Add visualizer overrides for a specific entity. + pub fn with_overrides( + mut self, + entity_path: impl Into, + visualizers: impl IntoIterator>, + ) -> Self { + self.0.add_overrides(entity_path, visualizers); + self + } +} + /// Text document view for markdown rendering. pub struct TextDocumentView(pub(crate) View); @@ -457,3 +623,231 @@ impl TextDocumentView { self } } + +/// Bar chart view, for use with [`re_sdk_types::archetypes::BarChart`]. +pub struct BarChartView(pub(crate) View); + +impl BarChartView { + /// Create a new bar chart view. + pub fn new(name: impl Into) -> Self { + Self(View { + class_identifier: "BarChart".into(), + name: Some(name.into()), + ..Default::default() + }) + } + + /// Set the origin entity path. + pub fn with_origin(mut self, origin: impl Into) -> Self { + self.0.origin = origin.into(); + self + } + + /// Set the contents query expressions. + pub fn with_contents(mut self, queries: impl IntoIterator>) -> Self { + self.0.contents = queries.into_iter().map(Into::into).collect(); + self + } + + /// Set visibility. + pub fn with_visible(mut self, visible: bool) -> Self { + self.0.visible = Some(visible); + self + } + + /// Add a default archetype that applies to all entities in the view. + pub fn with_defaults(mut self, archetype: &dyn AsComponents) -> Self { + self.0.add_defaults(archetype); + self + } + + /// Add a visualizer override for a specific entity. + pub fn with_override( + self, + entity_path: impl Into, + visualizers: impl Into, + ) -> Self { + self.with_overrides(entity_path, [visualizers]) + } + + /// Add visualizer overrides for a specific entity. + pub fn with_overrides( + mut self, + entity_path: impl Into, + visualizers: impl IntoIterator>, + ) -> Self { + self.0.add_overrides(entity_path, visualizers); + self + } +} + +/// Dataframe view, for displaying entities in a tabular form. +pub struct DataframeView(pub(crate) View); + +impl DataframeView { + /// Create a new dataframe view. + pub fn new(name: impl Into) -> Self { + Self(View { + class_identifier: "Dataframe".into(), + name: Some(name.into()), + ..Default::default() + }) + } + + /// Set the origin entity path. + pub fn with_origin(mut self, origin: impl Into) -> Self { + self.0.origin = origin.into(); + self + } + + /// Set the contents query expressions. + pub fn with_contents(mut self, queries: impl IntoIterator>) -> Self { + self.0.contents = queries.into_iter().map(Into::into).collect(); + self + } + + /// Set visibility. + pub fn with_visible(mut self, visible: bool) -> Self { + self.0.visible = Some(visible); + self + } + + /// Add a default archetype that applies to all entities in the view. + pub fn with_defaults(mut self, archetype: &dyn AsComponents) -> Self { + self.0.add_defaults(archetype); + self + } + + /// Add a visualizer override for a specific entity. + pub fn with_override( + self, + entity_path: impl Into, + visualizers: impl Into, + ) -> Self { + self.with_overrides(entity_path, [visualizers]) + } + + /// Add visualizer overrides for a specific entity. + pub fn with_overrides( + mut self, + entity_path: impl Into, + visualizers: impl IntoIterator>, + ) -> Self { + self.0.add_overrides(entity_path, visualizers); + self + } +} + +/// State timeline view, for visualizing discrete state changes over time. +pub struct StateTimelineView(pub(crate) View); + +impl StateTimelineView { + /// Create a new state timeline view. + pub fn new(name: impl Into) -> Self { + Self(View { + class_identifier: "StateTimeline".into(), + name: Some(name.into()), + ..Default::default() + }) + } + + /// Set the origin entity path. + pub fn with_origin(mut self, origin: impl Into) -> Self { + self.0.origin = origin.into(); + self + } + + /// Set the contents query expressions. + pub fn with_contents(mut self, queries: impl IntoIterator>) -> Self { + self.0.contents = queries.into_iter().map(Into::into).collect(); + self + } + + /// Set visibility. + pub fn with_visible(mut self, visible: bool) -> Self { + self.0.visible = Some(visible); + self + } + + /// Add a default archetype that applies to all entities in the view. + pub fn with_defaults(mut self, archetype: &dyn AsComponents) -> Self { + self.0.add_defaults(archetype); + self + } + + /// Add a visualizer override for a specific entity. + pub fn with_override( + self, + entity_path: impl Into, + visualizers: impl Into, + ) -> Self { + self.with_overrides(entity_path, [visualizers]) + } + + /// Add visualizer overrides for a specific entity. + pub fn with_overrides( + mut self, + entity_path: impl Into, + visualizers: impl IntoIterator>, + ) -> Self { + self.0.add_overrides(entity_path, visualizers); + self + } +} + +/// Tensor view, for use with [`re_sdk_types::archetypes::Tensor`]. +pub struct TensorView(pub(crate) View); + +impl TensorView { + /// Create a new tensor view. + pub fn new(name: impl Into) -> Self { + Self(View { + class_identifier: "Tensor".into(), + name: Some(name.into()), + ..Default::default() + }) + } + + /// Set the origin entity path. + pub fn with_origin(mut self, origin: impl Into) -> Self { + self.0.origin = origin.into(); + self + } + + /// Set the contents query expressions. + pub fn with_contents(mut self, queries: impl IntoIterator>) -> Self { + self.0.contents = queries.into_iter().map(Into::into).collect(); + self + } + + /// Set visibility. + pub fn with_visible(mut self, visible: bool) -> Self { + self.0.visible = Some(visible); + self + } + + /// Add a default archetype that applies to all entities in the view. + pub fn with_defaults(mut self, archetype: &dyn AsComponents) -> Self { + self.0.add_defaults(archetype); + self + } + + /// Add a visualizer override for a specific entity. + pub fn with_override( + self, + entity_path: impl Into, + visualizers: impl Into, + ) -> Self { + self.with_overrides(entity_path, [visualizers]) + } + + /// Add visualizer overrides for a specific entity. + pub fn with_overrides( + mut self, + entity_path: impl Into, + visualizers: impl IntoIterator>, + ) -> Self { + self.0.add_overrides(entity_path, visualizers); + self + } +} diff --git a/crates/top/re_sdk/src/lenses/mod.rs b/crates/top/re_sdk/src/lenses/mod.rs index bd46f4ac5c06..589e98f1a0a7 100644 --- a/crates/top/re_sdk/src/lenses/mod.rs +++ b/crates/top/re_sdk/src/lenses/mod.rs @@ -9,8 +9,8 @@ mod sink; // Re-exports from re_lenses. // We should be careful not to expose too much implementation details here. pub use re_lenses::{ - ChunkExt, Lens, LensBuilder, LensBuilderError, LensRuntimeError, Lenses, OutputBuilder, - OutputMode, PartialChunk, op, + CastTo, ChunkExt, DeriveLensBuilder, Lens, LensBuilderError, LensError, LensRuntimeError, + Lenses, MutateLensBuilder, OutputMode, default_runtime, op, }; pub use re_lenses_core::Selector; diff --git a/crates/top/re_sdk/src/lenses/sink.rs b/crates/top/re_sdk/src/lenses/sink.rs index bae3db3dad5a..47a57e3d656f 100644 --- a/crates/top/re_sdk/src/lenses/sink.rs +++ b/crates/top/re_sdk/src/lenses/sink.rs @@ -52,7 +52,8 @@ impl LogSink for LensesSink { } LogMsg::ArrowMsg(store_id, arrow_msg) => match Chunk::from_arrow_msg(arrow_msg) { Ok(original_chunk) => { - let new_chunks = self.lenses.apply(&original_chunk); + let runtime = re_lenses::default_runtime(); + let new_chunks = self.lenses.apply(&original_chunk, &runtime); for maybe_chunk in new_chunks { match maybe_chunk { Ok(new_chunk) => self.send_or_log_error(store_id.clone(), &new_chunk), @@ -61,7 +62,7 @@ impl LogSink for LensesSink { // TODO(grtlr): Make this even more contextualized in the future! re_log::error_once!("Error encountered for lens: {error}"); } - if let Some(chunk) = partial_chunk.take() + if let Some(chunk) = partial_chunk.partial_chunk() && self.strict { self.send_or_log_error(store_id.clone(), &chunk); diff --git a/crates/top/re_sdk/src/lib.rs b/crates/top/re_sdk/src/lib.rs index 49b4144eea33..615069639637 100644 --- a/crates/top/re_sdk/src/lib.rs +++ b/crates/top/re_sdk/src/lib.rs @@ -67,6 +67,11 @@ impl crate::sink::LogSink for re_log_encoding::FileSink { FileFlushError::Timeout => sink::SinkFlushError::Timeout, }) } + + #[inline] + fn defers_finalization_to_shutdown(&self) -> bool { + true + } } // --------------- @@ -78,7 +83,7 @@ impl crate::sink::LogSink for re_log_encoding::FileSink { /// sent over gRPC, written to file, etc. pub mod sink { #[cfg(not(target_arch = "wasm32"))] - pub use re_log_encoding::{FileSink, FileSinkError}; + pub use re_log_encoding::{FileSink, FileSinkError, FileSinkOptions}; pub use crate::binary_stream_sink::{BinaryStreamSink, BinaryStreamStorage}; pub use crate::log_sink::{ @@ -98,16 +103,17 @@ pub mod log { /// Time-related types. pub mod time { - pub use re_log_types::{Duration, TimeCell, TimeInt, TimePoint, TimeType, Timeline, Timestamp}; + pub use re_log_types::{ + Duration, TimeCell, TimeInt, TimePoint, TimeType, Timeline, TimelineName, Timestamp, + }; } pub use re_sdk_types::{ Archetype, ArchetypeName, AsComponents, Component, ComponentBatch, ComponentDescriptor, - ComponentIdentifier, ComponentType, DatatypeName, DeserializationError, DeserializationResult, - Loggable, SerializationError, SerializationResult, SerializedComponentBatch, - SerializedComponentColumn, + ComponentIdentifier, ComponentType, DeserializationError, DeserializationResult, Loggable, + SerializationError, SerializationResult, SerializedComponentBatch, SerializedComponentColumn, }; -pub use time::{TimeCell, TimePoint, Timeline}; +pub use time::{TimeCell, TimePoint, Timeline, TimelineName}; /// Transformation and reinterpretation of components. /// diff --git a/crates/top/re_sdk/src/log_sink.rs b/crates/top/re_sdk/src/log_sink.rs index 5973eb90007f..849f7334204d 100644 --- a/crates/top/re_sdk/src/log_sink.rs +++ b/crates/top/re_sdk/src/log_sink.rs @@ -102,6 +102,25 @@ pub trait LogSink: Send + Sync + 'static + std::any::Any { fn default_batcher_config(&self) -> ChunkBatcherConfig { ChunkBatcherConfig::DEFAULT } + + /// True if this sink can only finalize its on-disk format at process shutdown (i.e. file-like + /// sinks that write a footer at the end). + /// + /// Used to surface warnings when a batcher config would cause unbounded memory growth, since + /// these sinks have to keep per-chunk metadata around until the footer can be written. + fn defers_finalization_to_shutdown(&self) -> bool { + false + } + + /// Best-effort finalization of any deferred-finalization children that can be retired in + /// place without tearing down the rest of this sink. + /// + /// Composite sinks (e.g. [`MultiSink`]) override this to drop their file-like children while + /// keeping streaming children alive. Returns `true` if the sink handled finalization itself, + /// or `false` if the caller must replace the entire sink to write footers. + fn finalize_deferred_in_place(&self) -> bool { + false + } } // ---------------------------------------------------------------------------- @@ -156,6 +175,23 @@ impl LogSink for MultiSink { Vec::new() } + fn defers_finalization_to_shutdown(&self) -> bool { + self.0 + .lock() + .iter() + .any(|sink| sink.defers_finalization_to_shutdown()) + } + + fn finalize_deferred_in_place(&self) -> bool { + // Drop any children that need a footer-style finalization. Their `Drop` impls join their + // writer threads, which is what actually emits the footer. Non-deferring children (e.g. + // a long-lived gRPC sink) stay live in this MultiSink. + self.0 + .lock() + .retain(|sink| !sink.defers_finalization_to_shutdown()); + true + } + fn default_batcher_config(&self) -> ChunkBatcherConfig { let ChunkBatcherConfig { mut flush_tick, diff --git a/crates/top/re_sdk/src/recording_stream.rs b/crates/top/re_sdk/src/recording_stream.rs index 452d617724f3..8d25a6c753df 100644 --- a/crates/top/re_sdk/src/recording_stream.rs +++ b/crates/top/re_sdk/src/recording_stream.rs @@ -1,6 +1,6 @@ use std::fmt; use std::io::IsTerminal as _; -use std::sync::atomic::AtomicI64; +use std::sync::atomic::{AtomicBool, AtomicI64}; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -13,6 +13,7 @@ use re_chunk::{ BatcherFlushError, BatcherHooks, Chunk, ChunkBatcher, ChunkBatcherConfig, ChunkBatcherError, ChunkComponents, ChunkError, ChunkId, PendingRow, RowId, TimeColumn, }; +use re_log::env_var_flag; use re_log_types::{ ApplicationId, ArrowRecordBatchReleaseCallback, BlueprintActivationCommand, EntityPath, LogMsg, RecordingId, StoreId, StoreInfo, StoreKind, StoreSource, TimeCell, TimeInt, TimePoint, @@ -44,6 +45,59 @@ pub fn forced_sink_path() -> Option { std::env::var(ENV_FORCE_SAVE).ok() } +/// Environment variable controlling whether the `log_tick` timeline column is injected. +/// +/// Opt-in: disabled unless set to a truthy value. +const ENV_LOG_TICK: &str = "RERUN_LOG_TICK"; + +/// Environment variable controlling whether the `log_time` timeline column is injected. +/// +/// Opt-out: enabled unless set to a falsy value. +const ENV_LOG_TIME: &str = "RERUN_LOG_TIME"; + +/// Which of the default timelines (`log_tick` and `log_time`) are injected into logged data? +/// +/// The initial values come from the [`ENV_LOG_TICK`] / [`ENV_LOG_TIME`] env-vars +/// (`log_tick` opt-in, `log_time` opt-out), but can be toggled at runtime via +/// [`RecordingStream::set_log_tick_enabled`] / [`RecordingStream::set_log_time_enabled`]. +#[derive(Debug)] +struct DefaultTimelines { + log_tick: AtomicBool, + log_time: AtomicBool, +} + +impl DefaultTimelines { + fn from_env() -> Self { + static LOG_TICK: std::sync::LazyLock = + std::sync::LazyLock::new(|| env_var_flag(ENV_LOG_TICK).unwrap_or(false)); + static LOG_TIME: std::sync::LazyLock = + std::sync::LazyLock::new(|| env_var_flag(ENV_LOG_TIME).unwrap_or(true)); + + Self { + log_tick: AtomicBool::new(*LOG_TICK), + log_time: AtomicBool::new(*LOG_TIME), + } + } + + fn log_tick(&self) -> bool { + self.log_tick.load(std::sync::atomic::Ordering::Relaxed) + } + + fn log_time(&self) -> bool { + self.log_time.load(std::sync::atomic::Ordering::Relaxed) + } + + fn set_log_tick(&self, enabled: bool) { + self.log_tick + .store(enabled, std::sync::atomic::Ordering::Relaxed); + } + + fn set_log_time(&self, enabled: bool) { + self.log_time + .store(enabled, std::sync::atomic::Ordering::Relaxed); + } +} + /// Errors that can occur when creating/manipulating a [`RecordingStream`]. #[derive(thiserror::Error, Debug)] pub enum RecordingStreamError { @@ -588,7 +642,7 @@ impl RecordingStreamBuilder { // Spawn viewer and connect normally. // spawn() returns the actual port used, which may differ from opts.port when --new picks a free port. - let actual_port = crate::spawn(opts)?; + let actual_port = crate::spawn(opts)?.port; let addr = std::net::SocketAddr::new( std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), actual_port, @@ -790,6 +844,8 @@ struct RecordingStreamInner { recording_info: Option, tick: AtomicI64, + default_timelines: DefaultTimelines, + /// The one and only entrypoint into the pipeline: this is _never_ cloned nor publicly exposed, /// therefore the `Drop` implementation is guaranteed that no more data can come in while it's /// running. @@ -798,6 +854,9 @@ struct RecordingStreamInner { batcher: ChunkBatcher, batcher_to_sink_handle: Option>, + /// Mirror of the batcher's currently active configuration. + current_batcher_config: Mutex, + /// It true, any new sink will update the batcher's configuration (as far as possible). sink_dependent_batcher_config: bool, @@ -859,6 +918,39 @@ fn resolve_batcher_config( } } +/// Warns once if `sink` defers finalization to shutdown (e.g. a file sink) and is paired with a +/// flush-on-every-row batcher config such as [`ChunkBatcherConfig::ALWAYS_TEST_ONLY`]. +/// +/// These sinks have to keep per-chunk metadata in memory until the footer can be written at +/// process exit. A flush-on-every-row config produces one chunk per row, so for long-running +/// recordings this can drive memory usage through the roof. +fn warn_if_problematic_file_sink_config(config: &ChunkBatcherConfig, sink: &dyn LogSink) { + if !sink.defers_finalization_to_shutdown() { + return; + } + + if !config.always_flushes() { + return; + } + + // Snippet-roundtrip tests intentionally pair this config with a file sink to exercise the + // per-row serialization path. The warning is correct in principle but noisy (and fatal under + // `RERUN_PANIC_ON_WARN`) for that controlled setup, so suppress it when the test harness + // signals strict-test mode. + if re_log::env_var_is_truthy("RERUN_STRICT") { + return; + } + + re_log::warn_once!( + "ChunkBatcherConfig::ALWAYS_TEST_ONLY (or an equivalent flush-on-every-row config) is \ + being used with a file sink. This produces one chunk per row, and the file's footer \ + cannot be written until the SDK process exits — so per-chunk metadata accumulates in \ + memory for the entire lifetime of the recording, which can blow up memory usage. \ + Use the default `ChunkBatcherConfig` for production workloads, or \ + `ChunkBatcherConfig::LOW_LATENCY` if you need fast flushing." + ); +} + impl RecordingStreamInner { fn new( store_info: StoreInfo, @@ -870,6 +962,8 @@ impl RecordingStreamInner { let sink_dependent_batcher_config = batcher_config.is_none(); let batcher_config = resolve_batcher_config(batcher_config, &*sink); + warn_if_problematic_file_sink_config(&batcher_config, &*sink); + let on_release = batcher_hooks.on_release.clone(); let batcher = ChunkBatcher::new(batcher_config, batcher_hooks)?; @@ -924,9 +1018,11 @@ impl RecordingStreamInner { store_info, recording_info, tick: AtomicI64::new(0), + default_timelines: DefaultTimelines::from_env(), cmds_tx, batcher, batcher_to_sink_handle: Some(batcher_to_sink_handle), + current_batcher_config: Mutex::new(batcher_config), sink_dependent_batcher_config, importer_handles: Mutex::new(Vec::new()), pid_at_creation: std::process::id(), @@ -955,16 +1051,32 @@ type InspectSinkFn = Box; type FlushResult = Result<(), SinkFlushError>; +#[derive(re_byte_size::SizeBytes)] enum Command { RecordMsg(LogMsg), SwapSink { + #[size_bytes(ignore)] new_sink: Box, + #[size_bytes(ignore)] timeout: Duration, }, // TODO(#10444): This should go away with more explicit sinks. - InspectSink(InspectSinkFn), + InspectSink(#[size_bytes(ignore)] InspectSinkFn), Flush { + #[size_bytes(ignore)] on_done: Sender, + #[size_bytes(ignore)] + timeout: Duration, + }, + + /// Drop any sinks whose on-disk format only finalizes at shutdown (i.e. file-like sinks with + /// footers), while leaving streaming sinks (e.g. gRPC) untouched. Used by Python's + /// `RecordingStream.__exit__` to ensure file-backed recordings are consumable as soon as + /// the `with`-block exits, without waiting for GC. + FinalizeDeferredSinks { + #[size_bytes(ignore)] + on_done: Sender<()>, + #[size_bytes(ignore)] timeout: Duration, }, PopPendingChunks, @@ -978,30 +1090,25 @@ impl std::fmt::Debug for Command { Self::SwapSink { .. } => f.debug_struct("SwapSink").finish_non_exhaustive(), Self::InspectSink(_) => f.debug_tuple("InspectSink").finish_non_exhaustive(), Self::Flush { .. } => f.debug_struct("Flush").finish_non_exhaustive(), + Self::FinalizeDeferredSinks { .. } => f + .debug_struct("FinalizeDeferredSinks") + .finish_non_exhaustive(), Self::PopPendingChunks => write!(f, "PopPendingChunks"), Self::Shutdown => write!(f, "Shutdown"), } } } -impl re_byte_size::SizeBytes for Command { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::RecordMsg(msg) => msg.heap_size_bytes(), - Self::SwapSink { .. } - | Self::InspectSink(_) - | Self::Flush { .. } - | Self::PopPendingChunks - | Self::Shutdown => 0, - } - } -} - impl Command { fn flush(timeout: Duration) -> (Self, Receiver) { let (on_done, rx) = crossbeam::channel::bounded(1); // oneshot (Self::Flush { on_done, timeout }, rx) } + + fn finalize_deferred_sinks(timeout: Duration) -> (Self, Receiver<()>) { + let (on_done, rx) = crossbeam::channel::bounded(1); // oneshot + (Self::FinalizeDeferredSinks { on_done, timeout }, rx) + } } impl RecordingStream { @@ -1339,7 +1446,7 @@ impl RecordingStream { /// This method blocks until either at least one [`re_importer::Importer`] starts /// streaming data in or all of them fail. /// - /// See for more information. + /// See for more information. #[cfg(feature = "importers")] pub fn log_file_from_path( &self, @@ -1357,7 +1464,7 @@ impl RecordingStream { /// This method blocks until either at least one [`re_importer::Importer`] starts /// streaming data in or all of them fail. /// - /// See for more information. + /// See for more information. #[cfg(feature = "importers")] pub fn log_file_from_contents( &self, @@ -1394,7 +1501,6 @@ impl RecordingStream { let (tx, rx) = re_log_channel::log_channel(re_log_channel::LogSource::File { path: filepath.into(), - follow: false, }); let mut settings = crate::ImporterSettings { @@ -1403,18 +1509,20 @@ impl RecordingStream { opened_store_id: None, force_store_info: false, entity_path_prefix, - follow: false, timepoint: (!static_).then(|| { self.with(|inner| { // Get the current time on all timelines, for the current recording, on the current // thread… let mut now = self.now(); - // …and then also inject the current recording tick into it. + // Note: we always increment, even if `log_tick` is disabled let tick = inner .tick .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - now.insert_cell(TimelineName::log_tick(), TimeCell::from_sequence(tick)); + if inner.default_timelines.log_tick() { + // …and then also inject the current recording tick into it. + now.insert_cell(TimelineName::log_tick(), TimeCell::from_sequence(tick)); + } now }) @@ -1543,6 +1651,30 @@ fn forwarding_thread( re_log::error!("Failed to flush sink: {err}"); } } + Command::FinalizeDeferredSinks { on_done, timeout } => { + if sink.defers_finalization_to_shutdown() && !sink.finalize_deferred_in_place() { + // Composite sinks handle finalization themselves; otherwise the entire + // sink defers finalization (e.g. a bare FileSink), so we have to swap it + // out for a BufferedSink — dropping the old sink runs its writer thread + // to completion and emits the footer. + let backlog = sink.drain_backlog(); + if let Err(err) = sink.flush_blocking(timeout) { + re_log::error!("Failed to flush previous sink during finalize: {err}"); + } + + let new_sink: Box = Box::new(crate::log_sink::BufferedSink::new()); + new_sink.send( + re_log_types::SetStoreInfo { + row_id: *RowId::new(), + info: store_info.clone(), + } + .into(), + ); + new_sink.send_all(backlog); + *sink = new_sink; + } + send_crossbeam(&on_done, ()).ok(); + } Command::PopPendingChunks => { // Wake up and skip the current iteration so that we can drain all pending chunks // before handling the next command. @@ -1664,12 +1796,16 @@ impl RecordingStream { let tick = inner .tick .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + if inject_time { // Get the current time on all timelines, for the current recording, on the current // thread… let mut now = self.now(); + // …and then also inject the current recording tick into it. - now.insert_cell(TimelineName::log_tick(), TimeCell::from_sequence(tick)); + if inner.default_timelines.log_tick() { + now.insert_cell(TimelineName::log_tick(), TimeCell::from_sequence(tick)); + } // Inject all these times into the row, overriding conflicting times, if any. for (timeline, cell) in now { @@ -1687,15 +1823,15 @@ impl RecordingStream { /// Logs a single [`Chunk`]. /// - /// Will inject `log_tick` and `log_time` timeline columns into the chunk. + /// Will inject the `log_time` timeline column (and `log_tick` if enabled) into the chunk. /// If you don't want to inject these, use [`Self::send_chunk`] instead. #[inline] pub fn log_chunk(&self, mut chunk: Chunk) { let f = move |inner: &RecordingStreamInner| { // TODO(cmc): Repeating these values is pretty wasteful. Would be nice to have a way of // indicating these are fixed across the whole chunk. - // Inject the log time - { + if inner.default_timelines.log_time() { + // Inject the log time let time_timeline = Timeline::log_time(); let time = TimeInt::new_temporal(re_log_types::Timestamp::now().nanos_since_epoch()); @@ -1713,13 +1849,15 @@ impl RecordingStream { return; } } - // Inject the log tick - { - let tick_timeline = Timeline::log_tick(); - let tick = inner - .tick - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + // Note: we always increment, even if `log_tick` is disabled + let tick = inner + .tick + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + if inner.default_timelines.log_tick() { + // Inject the log tick + let tick_timeline = Timeline::log_tick(); let repeated_tick = std::iter::repeat_n(tick, chunk.num_rows()).collect(); @@ -1755,8 +1893,8 @@ impl RecordingStream { /// Records a single [`Chunk`]. /// - /// Will inject `log_tick` and `log_time` timeline columns into the chunk. - /// If you don't want to inject these, use [`Self::send_chunks`] instead. + /// This will _not_ inject `log_tick` and `log_time` timeline columns into the chunk, + /// for that use [`Self::log_chunk`]. #[inline] pub fn send_chunk(&self, chunk: Chunk) { let f = move |inner: &RecordingStreamInner| { @@ -1821,8 +1959,11 @@ impl RecordingStream { if inner.sink_dependent_batcher_config { let batcher_config = resolve_batcher_config(None, &*new_sink); inner.batcher.update_config(batcher_config); + *inner.current_batcher_config.lock() = batcher_config; } + warn_if_problematic_file_sink_config(&inner.current_batcher_config.lock(), &*new_sink); + // Swap the sink, which will internally make sure to re-ingest the backlog if needed inner .cmds_tx @@ -2129,7 +2270,7 @@ impl RecordingStream { return Ok(()); } - let actual_port = crate::spawn(opts)?; + let actual_port = crate::spawn(opts)?.port; let addr = std::net::SocketAddr::new( std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), actual_port, @@ -2260,6 +2401,43 @@ impl RecordingStream { } } + /// Finalize any sinks whose on-disk format only completes at shutdown (i.e. file-like sinks + /// that write a footer at the end), while leaving streaming sinks (e.g. gRPC) intact. + /// + /// For a bare deferring sink (e.g. `FileSink` from `save()`), this is equivalent to + /// [`Self::disconnect`]: the sink is replaced with a [`crate::sink::BufferedSink`] and its + /// `Drop` impl runs the writer thread to completion, which is what emits the footer. + /// + /// For a [`crate::log_sink::MultiSink`] containing a mix of deferring and streaming children, + /// only the deferring children are dropped. The streaming children remain live and the + /// `MultiSink` continues to receive new messages. + /// + /// For all other sinks this is a no-op. + /// + /// Used by Python's `RecordingStream.__exit__` so file-backed recordings are consumable as + /// soon as the `with`-block exits, without waiting for `__del__` / GC. + pub fn finalize_deferred_sinks(&self) { + let timeout = Duration::MAX; + let f = move |inner: &RecordingStreamInner| { + inner.wait_for_importers(); + + // Flush the batcher down the chunk channel so any pending data lands in the sink + // before we tear it down. + if let Err(err) = inner.batcher.flush_blocking(timeout) { + re_log::warn!("Failed to flush batcher in `finalize_deferred_sinks`: {err}"); + } + inner.cmds_tx.send(Command::PopPendingChunks).ok(); + + let (cmd, oneshot) = Command::finalize_deferred_sinks(timeout); + inner.cmds_tx.send(cmd).ok(); + oneshot.recv().ok(); + }; + + if self.with(f).is_none() { + re_log::warn_once!("Recording disabled - call to finalize_deferred_sinks() ignored"); + } + } + /// Send a blueprint through this recording stream. pub fn send_blueprint( &self, @@ -2309,9 +2487,11 @@ impl fmt::Debug for RecordingStream { store_info, recording_info, tick, + default_timelines: _, cmds_tx: _, batcher: _, batcher_to_sink_handle: _, + current_batcher_config, sink_dependent_batcher_config, importer_handles, pid_at_creation, @@ -2321,6 +2501,7 @@ impl fmt::Debug for RecordingStream { .field("store_info", &store_info) .field("recording_info", &recording_info) .field("tick", &tick) + .field("current_batcher_config", &*current_batcher_config.lock()) .field( "sink_dependent_batcher_config", &sink_dependent_batcher_config, @@ -2347,8 +2528,10 @@ struct ThreadInfo { } impl ThreadInfo { - fn thread_now(rid: &StoreId) -> TimePoint { - Self::with(|ti| ti.now(rid)) + /// The current `TimePoint`, including all user-set timelines, + /// plus the default `log_time` (if enabled). + fn thread_now(rid: &StoreId, log_time: bool) -> TimePoint { + Self::with(|ti| ti.now(rid, log_time)) } fn set_thread_time(rid: &StoreId, timeline: TimelineName, cell: TimeCell) { @@ -2377,9 +2560,15 @@ impl ThreadInfo { }) } - fn now(&self, rid: &StoreId) -> TimePoint { + /// The current `TimePoint`, including all user-set timelines, + /// plus the default `log_time` (if enabled). + fn now(&self, rid: &StoreId, log_time: bool) -> TimePoint { let mut timepoint = self.timepoints.get(rid).cloned().unwrap_or_default(); - timepoint.insert_cell(TimelineName::log_time(), TimeCell::timestamp_now()); + + if log_time { + timepoint.insert_cell(TimelineName::log_time(), TimeCell::timestamp_now()); + } + timepoint } @@ -2404,10 +2593,28 @@ impl ThreadInfo { } impl RecordingStream { - /// Returns the current time of the recording on the current thread. + /// Returns the current time of the recording on the current calling thread. + /// + /// This is the [`TimePoint`] that would be injected into data logged right now from this + /// thread: it contains every user timeline set via [`Self::set_time`] and friends, plus the + /// automatic `log_time` timeline if it is enabled (see [`Self::set_log_time_enabled`]). + /// + /// Note that the automatic `log_tick` timeline is _not_ included here — it is only assigned + /// at the moment data is actually logged. + /// + /// Returns an empty [`TimePoint`] if the recording is disabled. + /// + /// See also: + /// - [`Self::set_time`] + /// - [`Self::disable_timeline`] + /// - [`Self::reset_time`] pub fn now(&self) -> TimePoint { - let f = - move |inner: &RecordingStreamInner| ThreadInfo::thread_now(&inner.store_info.store_id); + let f = move |inner: &RecordingStreamInner| { + ThreadInfo::thread_now( + &inner.store_info.store_id, + inner.default_timelines.log_time(), + ) + }; if let Some(res) = self.with(f) { res } else { @@ -2635,6 +2842,58 @@ impl RecordingStream { re_log::warn_once!("Recording disabled - call to reset_time() ignored"); } } + + /// Whether the `log_tick` timeline is automatically injected into logged data. + /// + /// Defaults to `false` (opt-in), overridable via the `RERUN_LOG_TICK` env-var. + /// See also [`Self::set_log_tick_enabled`]. + pub fn log_tick_enabled(&self) -> bool { + self.with(|inner| inner.default_timelines.log_tick()) + .unwrap_or(false) + } + + /// Whether the `log_time` timeline is automatically injected into logged data. + /// + /// Defaults to `true` (opt-out), overridable via the `RERUN_LOG_TIME` env-var. + /// See also [`Self::set_log_time_enabled`]. + pub fn log_time_enabled(&self) -> bool { + self.with(|inner| inner.default_timelines.log_time()) + .unwrap_or(false) + } + + /// Enable or disable automatic injection of the `log_tick` timeline into logged data. + /// + /// `log_tick` is a per-recording counter that increments on every logging call. + /// It is disabled by default; this lets you turn it on (or off) at runtime, overriding + /// the `RERUN_LOG_TICK` env-var. + /// + /// See also [`Self::set_log_time_enabled`]. + pub fn set_log_tick_enabled(&self, enabled: bool) { + let f = move |inner: &RecordingStreamInner| { + inner.default_timelines.set_log_tick(enabled); + }; + + if self.with(f).is_none() { + re_log::warn_once!("Recording disabled - call to set_log_tick_enabled() ignored"); + } + } + + /// Enable or disable automatic injection of the `log_time` timeline into logged data. + /// + /// `log_time` is the wall-clock time at which data was logged. + /// It is enabled by default; this lets you turn it off (or on) at runtime, overriding + /// the `RERUN_LOG_TIME` env-var. + /// + /// See also [`Self::set_log_tick_enabled`]. + pub fn set_log_time_enabled(&self, enabled: bool) { + let f = move |inner: &RecordingStreamInner| { + inner.default_timelines.set_log_time(enabled); + }; + + if self.with(f).is_none() { + re_log::warn_once!("Recording disabled - call to set_log_time_enabled() ignored"); + } + } } // --- @@ -2670,6 +2929,73 @@ mod tests { assert_send_sync::(); } + #[test] + fn default_timeline_injection_toggles() { + use re_log_types::example_components::{MyPoint, MyPoints}; + use re_sdk_types::Loggable; + + // Logs a single row (with a user timeline) and returns the set of timeline names + // that ended up on the resulting data chunk. + #[expect(clippy::fn_params_excessive_bools)] + fn injected_timelines( + log_tick: bool, + log_time: bool, + ) -> std::collections::BTreeSet { + let (rec, storage) = RecordingStreamBuilder::new("rerun_example_default_timelines") + .enabled(true) + .batcher_config(ChunkBatcherConfig::NEVER) + .memory() + .unwrap(); + + rec.set_log_tick_enabled(log_tick); + rec.set_log_time_enabled(log_time); + + // A user timeline, so there is always at least one index. + rec.set_time_sequence("frame", 1); + + let row = PendingRow { + row_id: RowId::new(), + timepoint: TimePoint::default(), + components: std::iter::once(( + MyPoints::descriptor_points().component, + SerializedComponentBatch::new( + ::to_arrow([MyPoint::new(1.0, 2.0)]).unwrap(), + MyPoints::descriptor_points(), + ), + )) + .collect(), + }; + rec.record_row("points".into(), row, true); + rec.flush_blocking().ok(); + + let mut timelines = std::collections::BTreeSet::new(); + for msg in storage.take() { + if let LogMsg::ArrowMsg(_, msg) = msg { + let chunk = Chunk::from_arrow_msg(&msg).unwrap(); + if chunk.entity_path() == &EntityPath::from("points") { + timelines.extend(chunk.timelines().keys().map(|t| t.to_string())); + } + } + } + timelines + } + + let set = |names: &[&str]| { + names + .iter() + .map(|s| (*s).to_owned()) + .collect::>() + }; + + assert_eq!(injected_timelines(false, true), set(&["frame", "log_time"])); + assert_eq!( + injected_timelines(true, true), + set(&["frame", "log_tick", "log_time"]) + ); + assert_eq!(injected_timelines(false, false), set(&["frame"])); + assert_eq!(injected_timelines(true, false), set(&["frame", "log_tick"])); + } + #[test] fn never_flush() { let rec = RecordingStreamBuilder::new("rerun_example_never_flush") @@ -2752,7 +3078,7 @@ mod tests { fn always_flush() { let rec = RecordingStreamBuilder::new("rerun_example_always_flush") .enabled(true) - .batcher_config(ChunkBatcherConfig::ALWAYS) + .batcher_config(ChunkBatcherConfig::ALWAYS_TEST_ONLY) .buffered() .unwrap(); @@ -2898,7 +3224,7 @@ mod tests { fn disabled() { let (rec, storage) = RecordingStreamBuilder::new("rerun_example_disabled") .enabled(false) - .batcher_config(ChunkBatcherConfig::ALWAYS) + .batcher_config(ChunkBatcherConfig::ALWAYS_TEST_ONLY) .memory() .unwrap(); @@ -3226,7 +3552,7 @@ mod tests { // Changing the sink should have no effect since an explicit config is in place. rec.set_sink(Box::new(BatcherConfigTestSink { - config: ChunkBatcherConfig::ALWAYS, + config: ChunkBatcherConfig::ALWAYS_TEST_ONLY, })); // Don't want to stall the test for CONFIG_CHANGE_TIMEOUT here. let new_config_recv_result = rx.recv_timeout(std::time::Duration::from_millis(100)); diff --git a/crates/top/re_sdk/src/spawn.rs b/crates/top/re_sdk/src/spawn.rs index 590ac0737d87..fe04f4fc8c3e 100644 --- a/crates/top/re_sdk/src/spawn.rs +++ b/crates/top/re_sdk/src/spawn.rs @@ -66,6 +66,9 @@ pub struct SpawnOptions { /// Detach Rerun Viewer process from the application process. pub detach_process: bool, + + /// Run the spawned viewer in headless mode (no OS window). + pub headless: bool, } // NOTE: No need for .exe extension on windows. @@ -85,6 +88,7 @@ impl Default for SpawnOptions { new: false, hide_welcome_screen: false, detach_process: true, + headless: false, } } } @@ -176,6 +180,20 @@ impl std::fmt::Debug for SpawnError { } } +/// Result of [`spawn`]. +#[derive(Debug, Clone, Copy)] +pub struct SpawnInfo { + /// The port the spawned (or reused) Rerun Viewer is listening on. + pub port: u16, + + /// PID of the newly spawned viewer process, or `None` if an existing + /// viewer was reused (so this call did not actually spawn anything). + /// + /// Useful when the caller wants to forward signals to the child or kill + /// it on shutdown. + pub child_pid: Option, +} + /// Spawns a new Rerun Viewer process ready to listen for connections. /// /// If there is already a process listening on this port (Rerun or not), this function returns `Ok` @@ -185,7 +203,7 @@ impl std::fmt::Debug for SpawnError { /// /// This only starts a Viewer process: if you'd like to connect to it and start sending data, refer /// to [`crate::RecordingStream::connect_grpc`] or use [`crate::RecordingStream::spawn`] directly. -pub fn spawn(opts: &SpawnOptions) -> Result { +pub fn spawn(opts: &SpawnOptions) -> Result { use std::net::TcpStream; #[cfg(target_family = "unix")] use std::os::unix::process::CommandExt as _; @@ -246,7 +264,10 @@ pub fn spawn(opts: &SpawnOptions) -> Result { "A process is already listening at this address. Assuming it's a Rerun Viewer. \ Use `new: true` in SpawnOptions or `--port auto` on the CLI to force a new viewer." ); - return Ok(port); + return Ok(SpawnInfo { + port, + child_pid: None, + }); } // When --new is requested and the default port is already taken, find a free one. @@ -345,27 +366,44 @@ pub fn spawn(opts: &SpawnOptions) -> Result { rerun_bin.arg("--hide-welcome-screen"); } + if opts.headless { + rerun_bin.arg("--headless"); + } + rerun_bin.args(opts.extra_args.clone()); rerun_bin.envs(opts.extra_env.clone()); - if opts.detach_process { - // SAFETY: This code is only run in the child fork, we are not modifying any memory - // that is shared with the parent process. - #[cfg(target_family = "unix")] + #[cfg(target_family = "unix")] + { + // A headless viewer must stay attached so it shuts down together with the + // spawning process; only a windowed viewer is detached, and only when asked. + let should_detach = opts.detach_process && !opts.headless; + + // SAFETY: This code only runs in the forked child before exec; we only call + // async-signal-safe libc functions and don't touch memory shared with the parent. #[expect(unsafe_code)] unsafe { - rerun_bin.pre_exec(|| { - // On unix systems, we want to make sure that the child process becomes its - // own session leader, so that it doesn't die if the parent process crashes - // or is killed. - libc::setsid(); + rerun_bin.pre_exec(move || { + if should_detach { + // Make the child its own session leader so a detached viewer doesn't die + // if the parent process crashes or is killed. + libc::setsid(); + } else { + // Put the attached child in its own process group (it does not become a + // session leader). The `rerun` launcher forks the actual viewer process and + // exits, and that viewer inherits this group — so the owner can terminate the + // whole group on close, instead of only the launcher (which would orphan the + // viewer and leak the port it holds). + libc::setpgid(0, 0); + } Ok(()) }) }; } - rerun_bin.spawn().map_err(map_err)?; + let child = rerun_bin.spawn().map_err(map_err)?; + let child_pid = child.id(); if opts.wait_for_bind { // Give the newly spawned Rerun Viewer some time to bind. @@ -376,15 +414,20 @@ pub fn spawn(opts: &SpawnOptions) -> Result { let bind_addr = std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), port); let mut bound = false; - for i in 0..5 { + for i in 0..30 { re_log::debug!("connection attempt {}", i + 1); if TcpStream::connect_timeout(&bind_addr, Duration::from_secs(1)).is_ok() { bound = true; break; } - std::thread::sleep(Duration::from_millis(100)); + std::thread::sleep(Duration::from_millis(200)); } + if !bound { + re_log::warn!( + "Spawned Rerun Viewer did not bind to port {port} in time. Connections to it may fail." + ); + } re_log::debug_assert!( bound, "Spawned Rerun Viewer did not bind to port {port} in time" @@ -393,6 +436,10 @@ pub fn spawn(opts: &SpawnOptions) -> Result { // Simply forget about the child process, we want it to outlive the parent process if needed. _ = rerun_bin; + _ = child; - Ok(port) + Ok(SpawnInfo { + port, + child_pid: Some(child_pid), + }) } diff --git a/crates/top/re_sdk/src/web_viewer.rs b/crates/top/re_sdk/src/web_viewer.rs index 53f11f3e2da2..d97f7336bbfb 100644 --- a/crates/top/re_sdk/src/web_viewer.rs +++ b/crates/top/re_sdk/src/web_viewer.rs @@ -67,6 +67,7 @@ impl WebViewerSink { .expect("failed to spawn thread for message proxy server"); let webviewer_server = WebViewerServer::new(bind_ip, web_port)?; + let http_web_viewer_bound_url = webviewer_server.bound_url(); let http_web_viewer_url = webviewer_server.server_url(); let viewer_url = @@ -76,7 +77,9 @@ impl WebViewerSink { format!("{http_web_viewer_url}?url=rerun%2Bhttp://{grpc_server_addr}/proxy") }; - re_log::info!("Hosting a web-viewer at {viewer_url}"); + re_log::info!( + "Hosting a web-viewer at {http_web_viewer_bound_url} - connect at {viewer_url}" + ); if open_browser { webbrowser::open(&viewer_url).ok(); } @@ -123,7 +126,7 @@ impl Drop for WebViewerSink { // before the browser has a chance to connect. // Let's give it a little more time: re_log::info!("Sleeping a short while to give the browser time to connect…"); - std::thread::sleep(std::time::Duration::from_millis(1000)); + std::thread::sleep(std::time::Duration::from_secs(1)); } self.server_shutdown_signal.stop(); @@ -201,6 +204,7 @@ impl WebViewerConfig { } = self; let web_server = WebViewerServer::new(&bind_ip, web_port)?; + let http_web_viewer_bound_url = web_server.bound_url(); let http_web_viewer_url = web_server.server_url(); let mut viewer_url = http_web_viewer_url; @@ -231,7 +235,9 @@ impl WebViewerConfig { append_argument(format!("video_decoder={video_decoder}")); } - re_log::info!("Hosting a web-viewer at {viewer_url}"); + re_log::info!( + "Hosting a web-viewer at {http_web_viewer_bound_url} - connect at {viewer_url}" + ); if open_browser { webbrowser::open(&viewer_url).ok(); } diff --git a/crates/top/re_sdk/tests/lenses/operations.rs b/crates/top/re_sdk/tests/lenses/operations.rs index a87360f4655a..dfd0c40665a4 100644 --- a/crates/top/re_sdk/tests/lenses/operations.rs +++ b/crates/top/re_sdk/tests/lenses/operations.rs @@ -4,8 +4,9 @@ use std::sync::Arc; use arrow::array::{AsArray as _, Int32Builder, ListArray, ListBuilder}; use arrow::datatypes::{DataType, Field}; +use itertools::Itertools as _; use re_chunk::{ArrowArray as _, Chunk, ChunkId, TimeColumn, TimelineName}; -use re_sdk::lenses::{Lens, Lenses, OutputMode, Selector, op}; +use re_sdk::lenses::{CastTo, Lens, Lenses, OutputMode, Selector}; use re_sdk_types::ComponentDescriptor; use re_sdk_types::archetypes::Scalars; @@ -119,7 +120,7 @@ fn nullability_chunk() -> Chunk { Chunk::from_auto_row_ids( ChunkId::new(), "nullability".into(), - std::iter::once((TimelineName::new("tick"), time_column)).collect(), + std::iter::once((TimelineName::from("tick"), time_column)).collect(), components.collect(), ) .unwrap() @@ -130,15 +131,15 @@ fn test_destructure_cast() { let original_chunk = nullability_chunk(); println!("{original_chunk}"); - let destructure = Lens::for_input_column("structs") - .output_columns_at("nullability/a", |out| { - out.component( - Scalars::descriptor_scalars(), - Selector::parse(".a")?.pipe(op::cast(DataType::Float64)), - ) - }) - .unwrap() - .build(); + let destructure = Lens::derive("structs") + .output_entity("nullability/a") + .to_component_with_cast( + Scalars::descriptor_scalars(), + Selector::parse(".a").unwrap(), + CastTo::Auto, + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::DropUnmatched).add_lens_with_filter( re_log_types::EntityPathFilter::parse_forgiving("nullability"), @@ -146,8 +147,8 @@ fn test_destructure_cast() { ); let res: Vec = lenses - .apply(&original_chunk) - .collect::>() + .apply(&original_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); assert_eq!(res.len(), 1); @@ -161,12 +162,14 @@ fn test_destructure() { let original_chunk = nullability_chunk(); println!("{original_chunk}"); - let destructure = Lens::for_input_column("structs") - .output_columns_at("nullability/b", |out| { - out.component(Scalars::descriptor_scalars(), Selector::parse(".b")?) - }) - .unwrap() - .build(); + let destructure = Lens::derive("structs") + .output_entity("nullability/b") + .to_component( + Scalars::descriptor_scalars(), + Selector::parse(".b").unwrap(), + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::DropUnmatched).add_lens_with_filter( re_log_types::EntityPathFilter::parse_forgiving("nullability"), @@ -174,8 +177,8 @@ fn test_destructure() { ); let res: Vec = lenses - .apply(&original_chunk) - .collect::>() + .apply(&original_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); assert_eq!(res.len(), 1); @@ -218,7 +221,7 @@ fn test_time_column_extraction() { let original_chunk = Chunk::from_auto_row_ids( ChunkId::new(), "timestamped".into(), - std::iter::once((TimelineName::new("tick"), time_column)).collect(), + std::iter::once((TimelineName::from("tick"), time_column)).collect(), components.collect(), ) .unwrap(); @@ -226,16 +229,18 @@ fn test_time_column_extraction() { println!("{original_chunk}"); // Create a lens that extracts the timestamp as a time column and keeps the original timestamp as a component - let time_lens = Lens::for_input_column("my_timestamp") - .output_columns(|out| { - out.time("my_timeline", TimeType::Sequence, Selector::parse(".")?)? - .component( - ComponentDescriptor::partial("extracted_time"), - Selector::parse(".")?, - ) - }) - .unwrap() - .build(); + let time_lens = Lens::derive("my_timestamp") + .to_timeline( + "my_timeline", + TimeType::Sequence, + Selector::parse(".").unwrap(), + ) + .to_component( + ComponentDescriptor::partial("extracted_time"), + Selector::parse(".").unwrap(), + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::DropUnmatched).add_lens_with_filter( re_log_types::EntityPathFilter::parse_forgiving("timestamped"), @@ -243,8 +248,8 @@ fn test_time_column_extraction() { ); let res: Vec = lenses - .apply(&original_chunk) - .collect::>() + .apply(&original_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); assert_eq!(res.len(), 1); @@ -252,17 +257,17 @@ fn test_time_column_extraction() { println!("{chunk}"); // Verify the chunk has both the original timeline and the new custom timeline - assert!(chunk.timelines().contains_key(&TimelineName::new("tick"))); + assert!(chunk.timelines().contains_key(&TimelineName::from("tick"))); assert!( chunk .timelines() - .contains_key(&TimelineName::new("my_timeline")) + .contains_key(&TimelineName::from("my_timeline")) ); // Verify the custom timeline has the correct values let my_timeline = chunk .timelines() - .get(&TimelineName::new("my_timeline")) + .get(&TimelineName::from("my_timeline")) .unwrap(); assert_eq!(my_timeline.times_raw().len(), 5); assert_eq!(my_timeline.times_raw()[0], 100); @@ -357,27 +362,25 @@ fn test_scatter_columns() { println!("{original_chunk}"); // Create a scatter lens that explodes the nested lists - let scatter_lens = Lens::for_input_column("nested_data") - .scatter() - .output_columns_at("scatter_test/exploded", |out| { - out.component( - ComponentDescriptor::partial("exploded_strings"), - Selector::parse(".value")?, - )? - .time( - "my_timestamp", - TimeType::Sequence, - Selector::parse(".timestamp")?, - ) - }) - .unwrap() - .build(); + let scatter_lens = Lens::scatter("nested_data") + .output_entity("scatter_test/exploded") + .to_component( + ComponentDescriptor::partial("exploded_strings"), + Selector::parse(".value").unwrap(), + ) + .to_timeline( + "my_timestamp", + TimeType::Sequence, + Selector::parse(".timestamp").unwrap(), + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::DropUnmatched).add_lens(scatter_lens); let res: Vec = lenses - .apply(&original_chunk) - .collect::>() + .apply(&original_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); assert_eq!(res.len(), 1); @@ -396,7 +399,7 @@ fn test_scatter_columns() { // Verify tick timeline is replicated correctly // Original tick: [1, 2, 3] // Scattered tick: [1, 1, 1, 2, 3] (row 0 scatters into 3 rows) - let tick_timeline = chunk.timelines().get(&TimelineName::new("tick")).unwrap(); + let tick_timeline = chunk.timelines().get(&TimelineName::from("tick")).unwrap(); assert_eq!(tick_timeline.times_raw().len(), 5); assert_eq!(tick_timeline.times_raw()[0], 1); assert_eq!(tick_timeline.times_raw()[1], 1); @@ -409,7 +412,7 @@ fn test_scatter_columns() { // After scattering: [1, 2, 3, 4, 5] let event_timeline = chunk .timelines() - .get(&TimelineName::new("my_timestamp")) + .get(&TimelineName::from("my_timestamp")) .unwrap(); assert_eq!(event_timeline.times_raw().len(), 5); assert_eq!(event_timeline.times_raw()[0], 1); @@ -443,27 +446,25 @@ fn test_scatter_columns_static() { println!("{original_chunk}"); // Create a scatter lens that explodes the nested lists - let scatter_lens = Lens::for_input_column("nested_data") - .scatter() - .output_columns_at("scatter_test/exploded", |out| { - out.component( - ComponentDescriptor::partial("exploded_strings"), - Selector::parse(".value")?, - )? - .time( - "my_timestamp", - TimeType::Sequence, - Selector::parse(".timestamp")?, - ) - }) - .unwrap() - .build(); + let scatter_lens = Lens::scatter("nested_data") + .output_entity("scatter_test/exploded") + .to_component( + ComponentDescriptor::partial("exploded_strings"), + Selector::parse(".value").unwrap(), + ) + .to_timeline( + "my_timestamp", + TimeType::Sequence, + Selector::parse(".timestamp").unwrap(), + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::DropUnmatched).add_lens(scatter_lens); let res: Vec = lenses - .apply(&original_chunk) - .collect::>() + .apply(&original_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); assert_eq!(res.len(), 1); @@ -488,7 +489,7 @@ fn test_scatter_columns_static() { // After scattering: [1, 2, 3, 4, 5] let event_timeline = chunk .timelines() - .get(&TimelineName::new("my_timestamp")) + .get(&TimelineName::from("my_timestamp")) .unwrap(); assert_eq!(event_timeline.times_raw().len(), 5); assert_eq!(event_timeline.times_raw()[0], 1); @@ -536,7 +537,7 @@ fn test_output_overwrites_same_named_component() { ChunkId::new(), "collision".into(), std::iter::once(( - TimelineName::new("tick"), + TimelineName::from("tick"), TimeColumn::new_sequence("tick", 0..2), )) .collect(), @@ -544,16 +545,19 @@ fn test_output_overwrites_same_named_component() { ) .unwrap(); - let lens = Lens::for_input_column("input") - .output_columns(|out| { - out.component(ComponentDescriptor::partial("value"), Selector::parse(".")?) - }) - .unwrap() - .build(); + let lens = Lens::derive("input") + .to_component( + ComponentDescriptor::partial("value"), + Selector::parse(".").unwrap(), + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::DropUnmatched).add_lens(lens); - let results: Vec<_> = lenses.apply(&original_chunk).collect(); + let results: Vec<_> = lenses + .apply(&original_chunk, &re_lenses::default_runtime()) + .collect(); assert_eq!(results.len(), 1); let chunk = results.into_iter().next().unwrap().unwrap(); diff --git a/crates/top/re_sdk/tests/lenses/output_mode.rs b/crates/top/re_sdk/tests/lenses/output_mode.rs index 0e13ac9cb573..1a109a4c1511 100644 --- a/crates/top/re_sdk/tests/lenses/output_mode.rs +++ b/crates/top/re_sdk/tests/lenses/output_mode.rs @@ -1,13 +1,14 @@ #![expect(clippy::unwrap_used)] use arrow::array::{ListBuilder, StringBuilder}; +use itertools::Itertools as _; use re_chunk::{Chunk, ChunkId, TimeColumn, TimelineName}; use re_log_types::EntityPathFilter; use re_sdk::lenses::{Lens, Lenses, OutputMode, Selector}; use re_sdk_types::ComponentDescriptor; /// Helper to create a simple chunk with string data for testing -fn create_test_chunk(entity_path: &str, component_name: &str) -> Chunk { +fn create_test_chunk(entity_path: &str, component_name: &'static str) -> Chunk { let mut builder = ListBuilder::new(StringBuilder::new()); builder.values().append_value("test"); builder.append(true); @@ -22,7 +23,7 @@ fn create_test_chunk(entity_path: &str, component_name: &str) -> Chunk { Chunk::from_auto_row_ids( ChunkId::new(), entity_path.into(), - std::iter::once((TimelineName::new("tick"), time_column)).collect(), + std::iter::once((TimelineName::from("tick"), time_column)).collect(), components.collect(), ) .unwrap() @@ -35,23 +36,22 @@ fn test_output_mode_forward_all() { let unmatched_chunk = create_test_chunk("other/entity", "other_component"); // Create a lens that only matches the first chunk - let lens = Lens::for_input_column("test_component") - .output_columns_at("matched/output", |out| { - out.component( - ComponentDescriptor::partial("transformed"), - Selector::parse(".")?, - ) - }) - .unwrap() - .build(); + let lens = Lens::derive("test_component") + .output_entity("matched/output") + .to_component( + ComponentDescriptor::partial("transformed"), + Selector::parse(".").unwrap(), + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::ForwardAll) .add_lens_with_filter(EntityPathFilter::parse_forgiving("matched/**"), lens); // Apply to matching chunk let matching_results: Vec<_> = lenses - .apply(&matching_chunk) - .collect::>() + .apply(&matching_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); // Should get the original chunk first, then the transformed chunk @@ -64,8 +64,8 @@ fn test_output_mode_forward_all() { // Apply to unmatched chunk let unmatched_results: Vec<_> = lenses - .apply(&unmatched_chunk) - .collect::>() + .apply(&unmatched_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); // Should get only the original chunk (no lens matched) @@ -83,23 +83,22 @@ fn test_output_mode_forward_unmatched() { let unmatched_chunk = create_test_chunk("other/entity", "other_component"); // Create a lens that only matches the first chunk - let lens = Lens::for_input_column("test_component") - .output_columns_at("matched/output", |out| { - out.component( - ComponentDescriptor::partial("transformed"), - Selector::parse(".")?, - ) - }) - .unwrap() - .build(); + let lens = Lens::derive("test_component") + .output_entity("matched/output") + .to_component( + ComponentDescriptor::partial("transformed"), + Selector::parse(".").unwrap(), + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::ForwardUnmatched) .add_lens_with_filter(EntityPathFilter::parse_forgiving("matched/**"), lens); // Apply to matching chunk (all components are matched, so no untouched remainder) let matching_results: Vec<_> = lenses - .apply(&matching_chunk) - .collect::>() + .apply(&matching_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); // Should get only the transformed chunk (no empty untouched remainder) @@ -108,8 +107,8 @@ fn test_output_mode_forward_unmatched() { // Apply to unmatched chunk let unmatched_results: Vec<_> = lenses - .apply(&unmatched_chunk) - .collect::>() + .apply(&unmatched_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); // Should get the original chunk forwarded @@ -127,23 +126,22 @@ fn test_output_mode_drop_unmatched() { let unmatched_chunk = create_test_chunk("other/entity", "other_component"); // Create a lens that only matches the first chunk - let lens = Lens::for_input_column("test_component") - .output_columns_at("matched/output", |out| { - out.component( - ComponentDescriptor::partial("transformed"), - Selector::parse(".")?, - ) - }) - .unwrap() - .build(); + let lens = Lens::derive("test_component") + .output_entity("matched/output") + .to_component( + ComponentDescriptor::partial("transformed"), + Selector::parse(".").unwrap(), + ) + .build() + .unwrap(); let lenses = Lenses::new(OutputMode::DropUnmatched) .add_lens_with_filter(EntityPathFilter::parse_forgiving("matched/**"), lens); // Apply to matching chunk let matching_results: Vec<_> = lenses - .apply(&matching_chunk) - .collect::>() + .apply(&matching_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); // Should get only the transformed chunk @@ -152,8 +150,8 @@ fn test_output_mode_drop_unmatched() { // Apply to unmatched chunk let unmatched_results: Vec<_> = lenses - .apply(&unmatched_chunk) - .collect::>() + .apply(&unmatched_chunk, &re_lenses::default_runtime()) + .try_collect() .unwrap(); // Should get nothing (unmatched data is dropped) diff --git a/crates/top/rerun-cli/README.md b/crates/top/rerun-cli/README.md index 1c80c3a912ba..eb2f320cbbae 100644 --- a/crates/top/rerun-cli/README.md +++ b/crates/top/rerun-cli/README.md @@ -1,6 +1,6 @@

- banner + Banner with Rerun logo

diff --git a/crates/top/rerun/Cargo.toml b/crates/top/rerun/Cargo.toml index ab29a0d2cff7..da8f6c283694 100644 --- a/crates/top/rerun/Cargo.toml +++ b/crates/top/rerun/Cargo.toml @@ -51,9 +51,9 @@ clap = ["dep:clap"] ## Support for using Rerun's importers directly from the SDK. ## -## See our `log_file` example and +## See our `log_file` example and ## for more information. -importers = ["dep:re_mcap", "re_sdk?/importers"] +importers = ["dep:re_mcap", "dep:mcap", "re_sdk?/importers"] ## Access to Rerun's dataframe API and related types. dataframe = ["dep:re_dataframe"] @@ -88,9 +88,11 @@ nasm = ["re_video/nasm"] ## Support spawning a native viewer and allow to extend the viewer. ## This adds a lot of extra dependencies, so only enable this feature if you need it! -native_viewer = ["dep:re_viewer", "dep:re_crash_handler"] +## +## Also enables the `rerun viewer-mcp` command. +native_viewer = ["dep:re_viewer", "dep:re_crash_handler", "dep:re_viewer_mcp"] -## Enable the in-memory Rerun Server, useful for testing. +## Enable the in-memory Rerun data server. oss_server = ["dep:re_server"] ## Enables integration with `re_perf_telemetry` (OpenTelemetry, Jaeger). @@ -165,7 +167,6 @@ ahash.workspace = true anyhow.workspace = true arrow.workspace = true camino.workspace = true -cfg-if.workspace = true crossbeam.workspace = true document-features.workspace = true indexmap.workspace = true @@ -174,6 +175,7 @@ itertools.workspace = true parking_lot.workspace = true similar-asserts.workspace = true tokio = { workspace = true, features = ["rt-multi-thread"] } +walkdir.workspace = true # Optional dependencies: re_analytics = { workspace = true, optional = true } @@ -184,10 +186,12 @@ re_data_source = { workspace = true, optional = true } re_dataframe = { workspace = true, optional = true } re_grpc_server = { workspace = true, optional = true } re_mcap = { workspace = true, optional = true } +mcap = { workspace = true, optional = true } re_sdk = { workspace = true, optional = true } re_server = { workspace = true, optional = true } re_sdk_types = { workspace = true, optional = true } re_viewer = { workspace = true, optional = true } +re_viewer_mcp = { workspace = true, optional = true } re_web_viewer_server = { workspace = true, optional = true } env_filter = { workspace = true, optional = true } diff --git a/crates/top/rerun/README.md b/crates/top/rerun/README.md index dab7f4e6dd3d..f238f3427bc0 100644 --- a/crates/top/rerun/README.md +++ b/crates/top/rerun/README.md @@ -1,6 +1,6 @@

- banner + Banner with Rerun logo

diff --git a/crates/top/rerun/src/commands/auth.rs b/crates/top/rerun/src/commands/auth.rs index be00598a1e06..8b92d25f688b 100644 --- a/crates/top/rerun/src/commands/auth.rs +++ b/crates/top/rerun/src/commands/auth.rs @@ -5,7 +5,7 @@ pub enum AuthCommands { /// Log into Rerun. /// /// This command opens a page in your default browser, allowing you - /// to log in to the Rerun Data Platform. + /// to log in to Rerun Hub. /// /// Once you've logged in, your credentials are stored on your machine. /// @@ -21,12 +21,12 @@ pub enum AuthCommands { /// Retrieve the stored access token. /// /// The access token is part of the credentials produced by `rerun auth login`, - /// and is used to authorize requests to the Rerun Data Platform. + /// and is used to authorize requests to Rerun Hub. Token(TokenCommand), /// Generate a fresh access token. /// - /// You can use this token to authorize requests to the Rerun Data Platform. + /// You can use this token to authorize requests to Rerun Hub. /// /// It's closer to an API key than an access token, as it can be revoked before /// it expires. diff --git a/crates/top/rerun/src/commands/download.rs b/crates/top/rerun/src/commands/download.rs index 67aa00255fc6..aaf3edc94057 100644 --- a/crates/top/rerun/src/commands/download.rs +++ b/crates/top/rerun/src/commands/download.rs @@ -41,7 +41,6 @@ impl DownloadCommand { re_log_types::FileSource::Cli, url, &FromUriOptions { - follow: false, accept_extensionless_http: true, }, ); @@ -70,6 +69,7 @@ impl DownloadCommand { let streaming_options = re_redap_client::StreamingOptions { force_full_download: true, + download: re_redap_client::SegmentDownload::default(), on_progress: Some(Arc::new(move |bytes_downloaded, total_bytes| { downloaded_for_progress.store(bytes_downloaded, Ordering::Relaxed); match total_bytes { @@ -87,7 +87,7 @@ impl DownloadCommand { ); } None => { - eprint!("\r {}", re_format::format_bytes(bytes_downloaded as _),); + eprint!("\r {}", re_format::format_bytes(bytes_downloaded as _)); } } })), diff --git a/crates/top/rerun/src/commands/entrypoint.rs b/crates/top/rerun/src/commands/entrypoint.rs index a2f68ed94a77..724afb35b320 100644 --- a/crates/top/rerun/src/commands/entrypoint.rs +++ b/crates/top/rerun/src/commands/entrypoint.rs @@ -257,14 +257,6 @@ When persisted, the state will be stored at the following locations: #[clap(long)] pub expect_data_soon: bool, - /// Tail .rrd files, waiting for new data to be appended after reaching EOF. - /// - /// Without this flag, .rrd files are read once and the viewer stops loading when EOF is reached. - /// With this flag, the viewer will keep watching for new data, which is useful for live streaming - /// from a writer process. - #[clap(long)] - pub follow: bool, - /// WebSocket server URL for publishing viewer interaction events /// (clicks, keyboard teleop commands). /// @@ -294,7 +286,7 @@ When persisted, the state will be stored at the following locations: - A path to a Rerun .rrd recording - A path to a Rerun .rbl blueprint - An HTTP(S) URL to an .rrd or .rbl file to load -- A path to an image or mesh, or any other file that Rerun can load (see https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview?speculative-link) +- A path to an image or mesh, or any other file that Rerun can load (see https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview) If no arguments are given, a server will be hosted which a Rerun SDK can connect to.")] pub url_or_paths: Vec, @@ -325,6 +317,14 @@ If no arguments are given, a server will be hosted which a Rerun SDK can connect #[clap(long)] pub detach_process: bool, + /// Run the viewer in headless mode (no OS window). + /// + /// The viewer is driven by an offscreen `egui_kittest` harness, while the + /// gRPC server keeps running so SDK clients can still log data and request + /// screenshots via `save_screenshot`. + #[clap(long)] + headless: bool, + /// Set the screen resolution (in logical points), e.g. "1920x1080". /// Useful together with `--screenshot-to`. #[clap(long)] @@ -471,10 +471,8 @@ impl Args { !arg.is_positional() && !arg.is_hide_set() && arg.get_long() != Some("help") }); - let full_name = full_name - .into_iter() - .chain(std::iter::once(name.to_owned())) - .collect_vec(); + let full_name = + std::iter::chain(full_name, std::iter::once(name.to_owned())).collect_vec(); if !any_positional_args && !any_floating_args && !any_subcommands { return; @@ -541,7 +539,7 @@ impl Args { // > - A path to a Rerun .rrd recording // > - A path to a Rerun .rbl blueprint // > - An HTTP(S) URL to an .rrd or .rbl file to load - // > - A path to an image or mesh, or any other file that Rerun can load (see https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview?speculative-link) + // > - A path to an image or mesh, or any other file that Rerun can load (see https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview) // > // > If no arguments are given, a server will be hosted which a Rerun SDK can connect to. // """ @@ -615,7 +613,7 @@ enum Command { /// Download recordings and save them as .rrd files. /// - /// Supports downloading from Rerun Cloud as well as any other supported URI. + /// Supports downloading from Rerun Hub as well as any other supported URI. Download(DownloadCommand), /// Generates the Rerun CLI manual (markdown). @@ -628,6 +626,30 @@ enum Command { #[command(subcommand)] Mcap(McapCommands), + /// Run an MCP server that controls a running Rerun Viewer. + /// + /// See the [mcp docs](https://rerun.io/docs/reference/viewer/mcp) for more info about using + /// `rerun viewer-mcp`. + /// + /// Use the following to commands to register the mcp with your agent: + /// - `claude mcp add rerun -- rerun viewer-mcp` + /// - `codex mcp add rerun -- rerun viewer-mcp` + /// + /// Or add a mcp.json with the following content: + /// ```json + /// { + /// "mcpServers": { + /// "rerun": { + /// "command": "rerun", + /// "args": ["viewer-mcp"], + /// } + /// } + /// } + /// ``` + #[cfg(feature = "native_viewer")] + #[command(name = "viewer-mcp")] + ViewerMcp, + /// Reset the memory of the Rerun Viewer. /// /// Only run this if you're having trouble with the Viewer, @@ -739,7 +761,9 @@ where --- title: ⌨️ CLI manual order: 1150 - ---\ + --- + + \ ", ); println!("{web_header}\n\n{man}"); @@ -749,6 +773,9 @@ where #[cfg(feature = "importers")] Command::Mcap(mcap) => mcap.run(), + #[cfg(feature = "native_viewer")] + Command::ViewerMcp => tokio_runtime.block_on(re_viewer_mcp::serve()), + #[cfg(feature = "native_viewer")] Command::Reset => re_viewer::reset_viewer_persistence(), @@ -883,7 +910,6 @@ pub fn run_impl( &UrlParamProcessingConfig::convert_everything_to_data_sources(), &connection_registry, None, - args.follow, )?; save_or_test_receive( args.save, @@ -894,14 +920,13 @@ pub fn run_impl( server_options, ) } else if args.serve_grpc { - cfg_if::cfg_if! { - if #[cfg(feature = "server")] { + cfg_select! { + feature = "server" => { let receivers = ReceiversFromUrlParams::new( url_or_paths, &UrlParamProcessingConfig::convert_everything_to_data_sources(), &connection_registry, None, - args.follow, )?; serve_grpc( receivers, @@ -909,23 +934,20 @@ pub fn run_impl( server_addr, server_options, ) - } else { - Err(anyhow::anyhow!( - "rerun-cli must be compiled with the 'server' feature enabled" - )) } + _ => Err(anyhow::anyhow!( + "rerun-cli must be compiled with the 'server' feature enabled" + )), } } else if args.serve_web { - cfg_if::cfg_if! { - if #[cfg(not(feature = "server"))] { - Err(anyhow::anyhow!( - "Can't host server - rerun was not compiled with the 'server' feature" - )) - } else if #[cfg(not(feature = "web_viewer"))] { - Err(anyhow::anyhow!( - "Can't host web-viewer - rerun was not compiled with the 'web_viewer' feature" - )) - } else { + cfg_select! { + not(feature = "server") => Err(anyhow::anyhow!( + "Can't host server - rerun was not compiled with the 'server' feature" + )), + not(feature = "web_viewer") => Err(anyhow::anyhow!( + "Can't host web-viewer - rerun was not compiled with the 'web_viewer' feature" + )), + _ => { // We always host the web-viewer in case the users wants it, // but we only open a browser automatically with the `--web-viewer` flag. let open_browser = args.web_viewer; @@ -935,7 +957,6 @@ pub fn run_impl( &UrlParamProcessingConfig::grpc_server_and_web_viewer(), &connection_registry, None, - args.follow, )?; #[cfg(all(feature = "server", feature = "web_viewer"))] serve_web( @@ -956,37 +977,32 @@ pub fn run_impl( &UrlParamProcessingConfig::convert_everything_to_data_sources(), &connection_registry, None, - args.follow, )?; connect_to_existing_server(receivers, server_addr) } else { - cfg_if::cfg_if! { - if #[cfg(feature = "native_viewer")] { - start_native_viewer( - &args, - url_or_paths, - _main_thread_token, - _build_info, - _call_source, - tokio_runtime_handle, - profiler, - connection_registry, - #[cfg(feature = "server")] - server_addr, - #[cfg(feature = "server")] - server_options, - ) - } else { - Err(anyhow::anyhow!( - "Can't start viewer - rerun was compiled without the 'native_viewer' feature" - )) - } + cfg_select! { + feature = "native_viewer" => start_native_viewer( + &args, + url_or_paths, + _main_thread_token, + _build_info, + _call_source, + tokio_runtime_handle, + profiler, + connection_registry, + #[cfg(feature = "server")] + server_addr, + #[cfg(feature = "server")] + server_options, + ), + _ => Err(anyhow::anyhow!( + "Can't start viewer - rerun was compiled without the 'native_viewer' feature" + )), } } } #[cfg(feature = "native_viewer")] -#[expect(clippy::too_many_arguments)] #[allow(clippy::allow_attributes, unused_variables)] pub fn start_native_viewer( args: &Args, @@ -1000,16 +1016,23 @@ pub fn start_native_viewer( #[cfg(feature = "server")] server_addr: std::net::SocketAddr, #[cfg(feature = "server")] server_options: re_sdk::ServerOptions, ) -> anyhow::Result<()> { - use re_viewer::external::re_viewer_context; + use re_viewer::external::{eframe, re_viewer_context}; use crate::external::re_ui::{UICommand, UICommandSender as _}; let startup_options = native_startup_options_from_args(args)?; let connect = args.connect.is_some(); - let follow = args.follow; let renderer = args.renderer.as_deref(); - let memory_limit = args.memory_limit.clone(); + let memory_limit = args + .memory_limit + .as_ref() + .map(|memory_limit| { + re_log::debug!("Parsing --memory-limit (for Viewer)"); + re_memory::MemoryLimit::parse(memory_limit) + }) + .transpose() + .map_err(|err| anyhow::format_err!("Bad --memory-limit: {err}"))?; let (command_tx, command_rx) = re_viewer_context::command_channel(); @@ -1023,86 +1046,132 @@ pub fn start_native_viewer( // so we catch any warnings produced during startup. let text_log_rx = re_viewer::register_text_log_receiver(); - re_viewer::run_native_app( - _main_thread_token, - Box::new(move |cc| { - { - let tx = command_tx.clone(); - let egui_ctx = cc.egui_ctx.clone(); - tokio::spawn(async move { - // We catch ctrl-c commands so we can properly quit. - // Without this, recent state changes might not be persisted. - match tokio::signal::ctrl_c().await { - Ok(()) => { - re_log::info!("Caught Ctrl-C, quitting Rerun Viewer…"); - tx.send_ui(UICommand::Quit); - egui_ctx.request_repaint(); - } - Err(err) => { - re_log::error!("Failed to listen for ctrl-c signal: {err}"); - } + #[allow(clippy::allow_attributes, unused_mut)] + let ReceiversFromUrlParams { + mut log_receivers, + urls_to_pass_on_to_viewer, + } = ReceiversFromUrlParams::new( + url_or_paths, + &UrlParamProcessingConfig::native_viewer(), + &connection_registry, + Some(auth_error_handler), + )?; + + let create_app = move |cc: &eframe::CreationContext<'_>| -> re_viewer::App { + { + let tx = command_tx.clone(); + let egui_ctx = cc.egui_ctx.clone(); + tokio::spawn(async move { + // We catch ctrl-c commands so we can properly quit. + // Without this, recent state changes might not be persisted. + match tokio::signal::ctrl_c().await { + Ok(()) => { + re_log::info!("Caught Ctrl-C, quitting Rerun Viewer…"); + tx.send_ui(UICommand::Quit); + egui_ctx.request_repaint(); } - }); - } - let mut app = re_viewer::App::with_commands( - _main_thread_token, - _build_info, - call_source.app_env(), - startup_options, - cc, - Some(connection_registry.clone()), - re_viewer::AsyncRuntimeHandle::new_native(tokio_runtime_handle), - text_log_rx, - (command_tx, command_rx), - ); + Err(err) => { + re_log::error!("Failed to listen for ctrl-c signal: {err}"); + } + } + }); + } + let mut app = re_viewer::App::with_commands( + _main_thread_token, + _build_info, + call_source.app_env(), + startup_options, + cc, + Some(connection_registry.clone()), + re_viewer::AsyncRuntimeHandle::new_native(tokio_runtime_handle), + text_log_rx, + (command_tx, command_rx), + ); - if let Some(memory_limit) = memory_limit { - re_log::debug!("Parsing --memory-limit (for Viewer)"); - let memory_limit = re_memory::MemoryLimit::parse(&memory_limit) - .map_err(|err| anyhow::format_err!("Bad --memory-limit: {err}"))?; - app.app_options_mut().memory_limit = memory_limit; - } + if let Some(memory_limit) = memory_limit { + app.app_options_mut().memory_limit = memory_limit; + } - #[allow(clippy::allow_attributes, unused_mut)] - let ReceiversFromUrlParams { - mut log_receivers, - urls_to_pass_on_to_viewer, - } = ReceiversFromUrlParams::new( - url_or_paths, - &UrlParamProcessingConfig::native_viewer(), - &connection_registry, - Some(auth_error_handler), - follow, - )?; + // If we're **not** connecting to an existing server, we spawn a new one and add it to the list of receivers. + #[cfg(feature = "server")] + if !connect { + // The internal catalog is served (loopback-only) on the proxy server's port below, and + // also reached in-process by the viewer. + #[cfg(not(target_arch = "wasm32"))] + let internal_catalog = re_viewer::internal_catalog::build(server_addr); + #[cfg(not(target_arch = "wasm32"))] + connection_registry.set_internal(( + internal_catalog.origin.clone(), + internal_catalog.connection.clone(), + )); + + #[cfg_attr(target_arch = "wasm32", expect(unused_mut))] + let mut extra_services = re_grpc_server::LoopbackServices::default(); + + #[cfg(not(target_arch = "wasm32"))] + extra_services.add_service(internal_catalog.grpc_service()); + + let (log_receiver, grpc_server_handle) = re_grpc_server::spawn_with_recv_and_services( + server_addr, + server_options, + re_grpc_server::shutdown::never(), + extra_services, + ); - // If we're **not** connecting to an existing server, we spawn a new one and add it to the list of receivers. - #[cfg(feature = "server")] - if !connect { - let log_receiver = re_grpc_server::spawn_with_recv( - server_addr, - server_options, - re_grpc_server::shutdown::never(), - ); + log_receivers.push(log_receiver); - log_receivers.push(log_receiver); + struct ProxyHandleWrapper { + handle: re_grpc_server::MessageProxyHandle, } - app.set_profiler(profiler); - for rx in log_receivers { - app.add_log_receiver(rx); - } - for url in urls_to_pass_on_to_viewer { - app.open_url_or_file(&url); - } - if let Ok(url) = std::env::var("EXAMPLES_MANIFEST_URL") { - app.set_examples_manifest_url(url); + impl re_viewer::ExternalMemoryUser for ProxyHandleWrapper { + fn capture(&mut self) -> Option { + self.handle + .capture_memory() + .map(|tree| re_byte_size::NamedMemUsageTree { + name: "GRPC Server".to_owned(), + value: tree, + }) + } } - Ok(Box::new(app)) - }), - renderer, - ) - .map_err(|err| err.into()) + app.add_external_memory_user(Box::new(ProxyHandleWrapper { + handle: grpc_server_handle, + })); + } + + app.set_profiler(profiler); + for rx in log_receivers { + app.add_log_receiver(rx); + } + for url in urls_to_pass_on_to_viewer { + app.open_url_or_file(&url); + } + if let Ok(url) = std::env::var("EXAMPLES_MANIFEST_URL") { + app.set_examples_manifest_url(url); + } + + app + }; + + if args.headless { + let window_size = args + .window_size + .as_deref() + .map(parse_size) + .transpose()? + .map(|[w, h]| re_viewer::external::egui::Vec2::new(w, h)); + + re_viewer::run_headless_app(Box::new(create_app), renderer, window_size) + .map_err(|err| err.into()) + } else { + re_viewer::run_native_app( + _main_thread_token, + Box::new(move |cc| Ok(Box::new(create_app(cc)))), + renderer, + ) + .map_err(|err| err.into()) + } } #[cfg(feature = "native_viewer")] @@ -1210,7 +1279,8 @@ fn serve_web( // Spawn a server which the Web Viewer can connect to. // All `rxs` are consumed by the server. - re_grpc_server::spawn_from_rx_set( + // We don't render a dev panel here so we don't need to keep the handle. + let _ = re_grpc_server::spawn_from_rx_set( server_addr, server_options, re_grpc_server::shutdown::never(), @@ -1262,7 +1332,8 @@ fn serve_grpc( let (signal, shutdown) = re_grpc_server::shutdown::shutdown(); // Spawn a server which the Web Viewer can connect to. - re_grpc_server::spawn_from_rx_set( + // No dev panel in this mode, so we drop the handle. + let _ = re_grpc_server::spawn_from_rx_set( server_addr, server_options, shutdown, @@ -1294,7 +1365,7 @@ fn save_or_test_receive( #[cfg(feature = "server")] { - let log_rx = re_grpc_server::spawn_with_recv( + let (log_rx, _handle) = re_grpc_server::spawn_with_recv( server_addr, server_options, re_grpc_server::shutdown::never(), @@ -1643,7 +1714,6 @@ impl ReceiversFromUrlParams { config: &UrlParamProcessingConfig, connection_registry: &re_redap_client::ConnectionRegistryHandle, auth_error_handler: Option, - follow: bool, ) -> anyhow::Result { let mut data_sources = Vec::new(); let mut urls_to_pass_on_to_viewer = Vec::new(); @@ -1653,7 +1723,6 @@ impl ReceiversFromUrlParams { re_log_types::FileSource::Cli, &url, &re_data_source::FromUriOptions { - follow, accept_extensionless_http: true, }, ) { @@ -1745,7 +1814,6 @@ fn record_cli_command_analytics(args: &Args) { detach_process, // Not logged - follow: _, threads: _, url_or_paths: _, version: _, @@ -1760,6 +1828,7 @@ fn record_cli_command_analytics(args: &Args) { cors_allow_origin: _, port: _, new: _, + headless: _, } = args; let (command, subcommand) = match command { @@ -1795,6 +1864,9 @@ fn record_cli_command_analytics(args: &Args) { return; } + #[cfg(feature = "native_viewer")] + Some(Command::ViewerMcp) => ("viewer-mcp", None), + Some(Command::Download(_)) => ("download", None), #[cfg(feature = "native_viewer")] @@ -1932,6 +2004,9 @@ where #[cfg(feature = "importers")] Command::Mcap(mcap) => mcap.run(), + #[cfg(feature = "native_viewer")] + Command::ViewerMcp => tokio_runtime.block_on(re_viewer_mcp::serve()), + #[cfg(feature = "native_viewer")] Command::Reset => re_viewer::reset_viewer_persistence(), @@ -2011,7 +2086,6 @@ fn run_impl_with_wrapper( &UrlParamProcessingConfig::convert_everything_to_data_sources(), &connection_registry, None, - args.follow, )?; save_or_test_receive( args.save, @@ -2022,14 +2096,13 @@ fn run_impl_with_wrapper( server_options, ) } else if args.serve_grpc { - cfg_if::cfg_if! { - if #[cfg(feature = "server")] { + cfg_select! { + feature = "server" => { let receivers = ReceiversFromUrlParams::new( url_or_paths, &UrlParamProcessingConfig::convert_everything_to_data_sources(), &connection_registry, None, - args.follow, )?; serve_grpc( receivers, @@ -2037,30 +2110,26 @@ fn run_impl_with_wrapper( server_addr, server_options, ) - } else { - Err(anyhow::anyhow!( - "rerun-cli must be compiled with the 'server' feature enabled" - )) } + _ => Err(anyhow::anyhow!( + "rerun-cli must be compiled with the 'server' feature enabled" + )), } } else if args.serve_web { - cfg_if::cfg_if! { - if #[cfg(not(feature = "server"))] { - Err(anyhow::anyhow!( - "Can't host server - rerun was not compiled with the 'server' feature" - )) - } else if #[cfg(not(feature = "web_viewer"))] { - Err(anyhow::anyhow!( - "Can't host web-viewer - rerun was not compiled with the 'web_viewer' feature" - )) - } else { + cfg_select! { + not(feature = "server") => Err(anyhow::anyhow!( + "Can't host server - rerun was not compiled with the 'server' feature" + )), + not(feature = "web_viewer") => Err(anyhow::anyhow!( + "Can't host web-viewer - rerun was not compiled with the 'web_viewer' feature" + )), + _ => { let open_browser = args.web_viewer; let receivers = ReceiversFromUrlParams::new( url_or_paths, &UrlParamProcessingConfig::grpc_server_and_web_viewer(), &connection_registry, None, - args.follow, )?; #[cfg(all(feature = "server", feature = "web_viewer"))] serve_web( @@ -2080,38 +2149,38 @@ fn run_impl_with_wrapper( &UrlParamProcessingConfig::convert_everything_to_data_sources(), &connection_registry, None, - args.follow, )?; connect_to_existing_server(receivers, server_addr) } else { - cfg_if::cfg_if! { - if #[cfg(feature = "native_viewer")] { - start_native_viewer_with_wrapper( - &args, - url_or_paths, - _main_thread_token, - _build_info, - _call_source, - tokio_runtime_handle, - profiler, - connection_registry, - #[cfg(feature = "server")] - server_addr, - #[cfg(feature = "server")] - server_options, - app_wrapper, - startup_patch, - ) - } else { - Err(anyhow::anyhow!( - "Can't start viewer - rerun was compiled without the 'native_viewer' feature" - )) - } + cfg_select! { + feature = "native_viewer" => start_native_viewer_with_wrapper( + &args, + url_or_paths, + _main_thread_token, + _build_info, + _call_source, + tokio_runtime_handle, + profiler, + connection_registry, + #[cfg(feature = "server")] + server_addr, + #[cfg(feature = "server")] + server_options, + app_wrapper, + startup_patch, + ), + _ => Err(anyhow::anyhow!( + "Can't start viewer - rerun was compiled without the 'native_viewer' feature" + )), } } } /// Like `start_native_viewer` but wraps the App via `app_wrapper` if provided. +/// +/// Kept in sync with `start_native_viewer`; the only differences are the +/// `startup_patch` applied to [`re_viewer::StartupOptions`] and the +/// `app_wrapper` applied to the finished [`re_viewer::App`]. #[cfg(feature = "native_viewer")] #[expect(clippy::too_many_arguments)] #[allow(clippy::allow_attributes, unused_variables)] @@ -2129,21 +2198,28 @@ fn start_native_viewer_with_wrapper( app_wrapper: Option, startup_patch: Option, ) -> anyhow::Result<()> { - use re_viewer::external::re_viewer_context; + use re_viewer::external::{eframe, re_viewer_context}; use crate::external::re_ui::{UICommand, UICommandSender as _}; let mut startup_options = native_startup_options_from_args(args)?; - if let Some(patch) = startup_patch { - if patch.on_event.is_some() { - startup_options.on_event = patch.on_event; - } + if let Some(patch) = startup_patch + && patch.on_event.is_some() + { + startup_options.on_event = patch.on_event; } let connect = args.connect.is_some(); - let follow = args.follow; let renderer = args.renderer.as_deref(); - let memory_limit = args.memory_limit.clone(); + let memory_limit = args + .memory_limit + .as_ref() + .map(|memory_limit| { + re_log::debug!("Parsing --memory-limit (for Viewer)"); + re_memory::MemoryLimit::parse(memory_limit) + }) + .transpose() + .map_err(|err| anyhow::format_err!("Bad --memory-limit: {err}"))?; let (command_tx, command_rx) = re_viewer_context::command_channel(); @@ -2157,89 +2233,143 @@ fn start_native_viewer_with_wrapper( // so we catch any warnings produced during startup. let text_log_rx = re_viewer::register_text_log_receiver(); - re_viewer::run_native_app( - _main_thread_token, - Box::new(move |cc| { - { - let tx = command_tx.clone(); - let egui_ctx = cc.egui_ctx.clone(); - tokio::spawn(async move { - // We catch ctrl-c commands so we can properly quit. - // Without this, recent state changes might not be persisted. - match tokio::signal::ctrl_c().await { - Ok(()) => { - re_log::info!("Caught Ctrl-C, quitting Rerun Viewer…"); - tx.send_ui(UICommand::Quit); - egui_ctx.request_repaint(); - } - Err(err) => { - re_log::error!("Failed to listen for ctrl-c signal: {err}"); - } + #[allow(clippy::allow_attributes, unused_mut)] + let ReceiversFromUrlParams { + mut log_receivers, + urls_to_pass_on_to_viewer, + } = ReceiversFromUrlParams::new( + url_or_paths, + &UrlParamProcessingConfig::native_viewer(), + &connection_registry, + Some(auth_error_handler), + )?; + + let create_app = move |cc: &eframe::CreationContext<'_>| -> re_viewer::App { + { + let tx = command_tx.clone(); + let egui_ctx = cc.egui_ctx.clone(); + tokio::spawn(async move { + // We catch ctrl-c commands so we can properly quit. + // Without this, recent state changes might not be persisted. + match tokio::signal::ctrl_c().await { + Ok(()) => { + re_log::info!("Caught Ctrl-C, quitting Rerun Viewer…"); + tx.send_ui(UICommand::Quit); + egui_ctx.request_repaint(); } - }); - } - let mut app = re_viewer::App::with_commands( - _main_thread_token, - _build_info, - call_source.app_env(), - startup_options, - cc, - Some(connection_registry.clone()), - re_viewer::AsyncRuntimeHandle::new_native(tokio_runtime_handle), - text_log_rx, - (command_tx, command_rx), - ); + Err(err) => { + re_log::error!("Failed to listen for ctrl-c signal: {err}"); + } + } + }); + } + let mut app = re_viewer::App::with_commands( + _main_thread_token, + _build_info, + call_source.app_env(), + startup_options, + cc, + Some(connection_registry.clone()), + re_viewer::AsyncRuntimeHandle::new_native(tokio_runtime_handle), + text_log_rx, + (command_tx, command_rx), + ); - if let Some(memory_limit) = memory_limit { - re_log::debug!("Parsing --memory-limit (for Viewer)"); - let memory_limit = re_memory::MemoryLimit::parse(&memory_limit) - .map_err(|err| anyhow::format_err!("Bad --memory-limit: {err}"))?; - app.app_options_mut().memory_limit = memory_limit; - } + if let Some(memory_limit) = memory_limit { + app.app_options_mut().memory_limit = memory_limit; + } - #[allow(clippy::allow_attributes, unused_mut)] - let ReceiversFromUrlParams { - mut log_receivers, - urls_to_pass_on_to_viewer, - } = ReceiversFromUrlParams::new( - url_or_paths, - &UrlParamProcessingConfig::native_viewer(), - &connection_registry, - Some(auth_error_handler), - follow, - )?; + // If we're **not** connecting to an existing server, we spawn a new one and add it to the list of receivers. + #[cfg(feature = "server")] + if !connect { + // The internal catalog is served (loopback-only) on the proxy server's port below, and + // also reached in-process by the viewer. + #[cfg(not(target_arch = "wasm32"))] + let internal_catalog = re_viewer::internal_catalog::build(server_addr); + #[cfg(not(target_arch = "wasm32"))] + connection_registry.set_internal(( + internal_catalog.origin.clone(), + internal_catalog.connection.clone(), + )); + + #[cfg_attr(target_arch = "wasm32", expect(unused_mut))] + let mut extra_services = re_grpc_server::LoopbackServices::default(); + + #[cfg(not(target_arch = "wasm32"))] + extra_services.add_service(internal_catalog.grpc_service()); + + let (log_receiver, grpc_server_handle) = re_grpc_server::spawn_with_recv_and_services( + server_addr, + server_options, + re_grpc_server::shutdown::never(), + extra_services, + ); - // If we're **not** connecting to an existing server, we spawn a new one and add it to the list of receivers. - #[cfg(feature = "server")] - if !connect { - let log_receiver = re_grpc_server::spawn_with_recv( - server_addr, - server_options, - re_grpc_server::shutdown::never(), - ); + log_receivers.push(log_receiver); - log_receivers.push(log_receiver); + struct ProxyHandleWrapper { + handle: re_grpc_server::MessageProxyHandle, } - app.set_profiler(profiler); - for rx in log_receivers { - app.add_log_receiver(rx); - } - for url in urls_to_pass_on_to_viewer { - app.open_url_or_file(&url); - } - if let Ok(url) = std::env::var("EXAMPLES_MANIFEST_URL") { - app.set_examples_manifest_url(url); + impl re_viewer::ExternalMemoryUser for ProxyHandleWrapper { + fn capture(&mut self) -> Option { + self.handle + .capture_memory() + .map(|tree| re_byte_size::NamedMemUsageTree { + name: "GRPC Server".to_owned(), + value: tree, + }) + } } - // Apply the DimOS wrapper if provided, otherwise return stock App. - if let Some(wrapper) = app_wrapper { - wrapper(app) - } else { - Ok(Box::new(app)) - } - }), - renderer, - ) - .map_err(|err| err.into()) + app.add_external_memory_user(Box::new(ProxyHandleWrapper { + handle: grpc_server_handle, + })); + } + + app.set_profiler(profiler); + for rx in log_receivers { + app.add_log_receiver(rx); + } + for url in urls_to_pass_on_to_viewer { + app.open_url_or_file(&url); + } + if let Ok(url) = std::env::var("EXAMPLES_MANIFEST_URL") { + app.set_examples_manifest_url(url); + } + + app + }; + + if args.headless { + if app_wrapper.is_some() { + re_log::warn!( + "Ignoring the dimos-viewer app wrapper: it is not supported in --headless mode." + ); + } + + let window_size = args + .window_size + .as_deref() + .map(parse_size) + .transpose()? + .map(|[w, h]| re_viewer::external::egui::Vec2::new(w, h)); + + re_viewer::run_headless_app(Box::new(create_app), renderer, window_size) + .map_err(|err| err.into()) + } else { + re_viewer::run_native_app( + _main_thread_token, + Box::new(move |cc| { + let app = create_app(cc); + // Apply the DimOS wrapper if provided, otherwise return the stock App. + match app_wrapper { + Some(wrapper) => wrapper(app), + None => Ok(Box::new(app)), + } + }), + renderer, + ) + .map_err(|err| err.into()) + } } diff --git a/crates/top/rerun/src/commands/mcap/info.rs b/crates/top/rerun/src/commands/mcap/info.rs new file mode 100644 index 000000000000..92defff7017a --- /dev/null +++ b/crates/top/rerun/src/commands/mcap/info.rs @@ -0,0 +1,311 @@ +//! `rerun mcap info` — inspect timeline structure of an MCAP file. +//! +//! Each MCAP chunk holds messages from many topics interleaved together. When loaded +//! into Rerun, an MCAP chunk is split per topic into one rerun chunk per (topic, mcap chunk), +//! and each rerun chunk gets its timelines reordered via [`re_chunk::Chunk::from_auto_row_ids`] +//! (stable lex sort across all timelines). A rerun chunk's secondary timelines stay sorted +//! only if every timeline agrees on the row order; otherwise the chunk ends up with some +//! `TimeColumn::is_sorted() == false`. +//! +//! This command groups messages by topic and runs that same check both per chunk and +//! across the entire topic. +//! +//! By default only the timelines available at the raw MCAP level +//! (`message_log_time`, `message_publish_time`) are inspected. With `--full`, the +//! `re_mcap` decoder pipeline runs so that timelines extracted from message bodies +//! (e.g. `ros2_timestamp` from a ROS 2 message `Header.stamp`, `timestamp` from custom +//! decoders) are inspected too. + +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::path::PathBuf; + +use anyhow::Context as _; +use parking_lot::Mutex; + +use re_log_types::{TimeType, TimelineName}; +use re_mcap::decoders::{DecoderRegistry, TopicFilter}; +use re_mcap::read_summary; + +#[derive(Debug, Clone, clap::Parser)] +pub struct InfoCommand { + /// Path to the .mcap file to inspect. + path: PathBuf, + + /// Run the full `re_mcap` decoder pipeline. + /// + /// Surfaces timelines added by per-message decoders (e.g. `ros2_timestamp` from + /// a ROS 2 `Header.stamp`). Without this flag only the raw MCAP-level timelines + /// `message_log_time` / `message_publish_time` are inspected. + #[clap(long)] + full: bool, +} + +impl InfoCommand { + pub fn run(&self) -> anyhow::Result<()> { + let Self { path, full } = self; + + let bytes = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?; + + let summary = read_summary(std::io::Cursor::new(&bytes[..]))? + .context("MCAP file has no summary section")?; + + let by_topic = if *full { + collect_by_topic_full(&bytes, &summary)? + } else { + collect_by_topic_raw(&bytes, &summary)? + }; + + let timeline_names = timeline_names(&by_topic); + + println!("File: {}", path.display()); + println!("Channels: {}", summary.channels.len()); + println!("MCAP chunks: {}", summary.chunk_indexes.len()); + println!( + "Mode: {}", + if *full { + "full (decoder pipeline)" + } else { + "raw" + } + ); + println!( + "Timelines: {}", + timeline_names + .iter() + .map(TimelineName::to_string) + .collect::>() + .join(", ") + ); + println!(); + println!("Per-topic line format:"); + println!(" id= topic=<…> chunks= [issues…]"); + println!(); + println!("Possible issues:"); + println!(" - N chunks with row-order conflicts: timelines within a chunk disagree on row"); + println!(" order, so no row permutation keeps every TimeColumn sorted simultaneously."); + println!(" - whole-topic row-order conflict: the same conflict when all messages on the"); + println!(" topic are concatenated together."); + println!( + " - N unordered chunks on : chunks (in mcap arrival order) whose min time" + ); + println!( + " falls below the running max on this timeline, i.e. chunks not sorted by time." + ); + println!( + " Independent of row-order conflicts: a chunk can be internally consistent and" + ); + println!(" still arrive out of order relative to its predecessors."); + println!(); + + let channel_id_by_topic: BTreeMap<&str, u16> = summary + .channels + .iter() + .map(|(id, ch)| (ch.topic.as_str(), *id)) + .collect(); + + for (topic, chunks) in &by_topic { + let num_chunks = chunks.len(); + let num_conflicting_chunks = chunks.iter().filter(|t| !t.timelines_agree()).count(); + + let mut whole_topic = TimeColumns::default(); + for tc in chunks { + whole_topic.append(tc); + } + let whole_topic_conflict = !whole_topic.timelines_agree(); + + let unordered = unordered_chunk_counts(chunks, &timeline_names); + + let mut issues: Vec = Vec::new(); + if num_conflicting_chunks > 0 { + issues.push(format!( + "{num_conflicting_chunks} chunks with row-order conflicts" + )); + } + if whole_topic_conflict { + issues.push("whole-topic row-order conflict".to_owned()); + } + for (tl, n) in &unordered { + if *n > 0 { + issues.push(format!("{n} unordered chunks on {tl}")); + } + } + + let status = if issues.is_empty() { "ok" } else { "PROBLEM" }; + let issues_str = issues.join(", "); + + let channel_id = channel_id_by_topic + .get(topic.as_str()) + .map_or_else(|| "?".to_owned(), u16::to_string); + + println!( + "{status:<7} id={channel_id:<3} topic={topic:<48} \ + chunks={num_chunks:<4} {issues_str}" + ); + } + + Ok(()) + } +} + +/// Times for a set of messages on one topic, keyed by timeline name. +/// +/// Row `i` across all column vectors refers to the same message. +#[derive(Default)] +struct TimeColumns { + columns: BTreeMap>, +} + +impl TimeColumns { + fn push_pairs(&mut self, pairs: impl IntoIterator) { + for (name, v) in pairs { + self.columns.entry(name).or_default().push(v); + } + } + + fn append(&mut self, other: &Self) { + for (k, vs) in &other.columns { + self.columns.entry(*k).or_default().extend_from_slice(vs); + } + } + + fn len(&self) -> usize { + self.columns.values().next().map_or(0, Vec::len) + } + + /// Stable lex sort permutation across all timelines (matching + /// [`re_chunk::Chunk::from_auto_row_ids`]). + fn sorted_permutation(&self) -> Vec { + let count = self.len(); + let cols: Vec<&Vec> = self.columns.values().collect(); + let mut perm: Vec = (0..count).collect(); + perm.sort_by(|&a, &b| { + for col in &cols { + let ord = col[a].cmp(&col[b]); + if ord != Ordering::Equal { + return ord; + } + } + Ordering::Equal + }); + perm + } + + /// Do all timelines agree on a single row order? + /// + /// Equivalent to: after lex-sorting rows by all timelines, is every individual + /// timeline non-decreasing? If false, no row permutation can keep all + /// [`re_chunk::TimeColumn`]s sorted simultaneously: they conflict. + fn timelines_agree(&self) -> bool { + if self.len() < 2 { + return true; + } + let perm = self.sorted_permutation(); + self.columns + .values() + .all(|col| perm.windows(2).all(|w| col[w[0]] <= col[w[1]])) + } +} + +type ByTopic = BTreeMap>; + +/// Raw mode: walk MCAP messages directly; only `message_log_time`/`message_publish_time` +/// are available. Grouped by (topic, mcap chunk). +fn collect_by_topic_raw(bytes: &[u8], summary: &mcap::Summary) -> anyhow::Result { + let mut by_topic_chunk: BTreeMap> = BTreeMap::new(); + for (mcap_idx, chunk) in summary.chunk_indexes.iter().enumerate() { + for msg in summary.stream_chunk(bytes, chunk)? { + let msg = msg?; + by_topic_chunk + .entry(msg.channel.topic.clone()) + .or_default() + .entry(mcap_idx) + .or_default() + .push_pairs([ + ( + TimelineName::from("message_log_time"), + msg.log_time.cast_signed(), + ), + ( + TimelineName::from("message_publish_time"), + msg.publish_time.cast_signed(), + ), + ]); + } + } + Ok(by_topic_chunk + .into_iter() + .map(|(topic, chunks)| (topic, chunks.into_values().collect())) + .collect()) +} + +/// Full mode: run the decoder pipeline and inspect every rerun chunk it emits. +/// Picks up extra timelines added by decoders (e.g. `ros2_timestamp`). +fn collect_by_topic_full(bytes: &[u8], summary: &mcap::Summary) -> anyhow::Result { + let plan = + DecoderRegistry::all_with_raw_fallback().plan(bytes, summary, &TopicFilter::default())?; + + let chunks: Mutex> = Mutex::new(Vec::new()); + plan.run(bytes, summary, TimeType::TimestampNs, &|chunk| { + chunks.lock().push(chunk); + })?; + let chunks = chunks.into_inner(); + + let mut by_topic: ByTopic = BTreeMap::new(); + for chunk in chunks { + if chunk.timelines().is_empty() { + // Static chunk — no timelines to analyze. + continue; + } + let topic = chunk.entity_path().to_string(); + let mut times = TimeColumns::default(); + for (name, time_col) in chunk.timelines() { + let column = times.columns.entry(*name).or_default(); + column.extend_from_slice(time_col.times_raw()); + } + by_topic.entry(topic).or_default().push(times); + } + Ok(by_topic) +} + +/// Per timeline, count chunks (in mcap arrival order) whose min time falls below the +/// running max of all preceding chunks, i.e. chunks that are not in monotone time order +/// on that timeline. +fn unordered_chunk_counts( + chunks: &[TimeColumns], + timelines: &[TimelineName], +) -> Vec<(TimelineName, usize)> { + timelines + .iter() + .map(|tl| { + let mut prev_max: Option = None; + let mut unordered = 0usize; + for tc in chunks { + let Some(col) = tc.columns.get(tl) else { + continue; + }; + let Some(&min) = col.iter().min() else { + continue; + }; + let max = *col.iter().max().expect("col non-empty"); + if let Some(p) = prev_max + && min < p + { + unordered += 1; + } + prev_max = Some(prev_max.map_or(max, |p| p.max(max))); + } + (*tl, unordered) + }) + .collect() +} + +fn timeline_names(by_topic: &ByTopic) -> Vec { + let mut names: std::collections::BTreeSet = std::collections::BTreeSet::new(); + for chunks in by_topic.values() { + for tc in chunks { + names.extend(tc.columns.keys().copied()); + } + } + names.into_iter().collect() +} diff --git a/crates/top/rerun/src/commands/mcap/mod.rs b/crates/top/rerun/src/commands/mcap/mod.rs index fe78d750c68d..2ce1e8d63899 100644 --- a/crates/top/rerun/src/commands/mcap/mod.rs +++ b/crates/top/rerun/src/commands/mcap/mod.rs @@ -1,3 +1,5 @@ +mod info; + use std::collections::BTreeSet; use std::fs::File; use std::io::BufWriter; @@ -5,11 +7,13 @@ use std::io::BufWriter; use clap::Subcommand; use clap::builder::TypedValueParser as _; use re_log_encoding::Encoder; -use re_log_types::{LogMsg, RecordingId, TimeType}; +use re_log_types::{Duration, LogMsg, RecordingId, TimeType, Timestamp}; use re_mcap::{DecoderIdentifier, SelectedDecoders, TopicFilter}; use re_sdk::external::re_importer::{McapImporter, supported_mcap_decoder_identifiers}; use re_sdk::{ApplicationId, ImportedData, Importer, ImporterSettings}; +use info::InfoCommand; + fn possible_timeline_types() -> impl clap::builder::TypedValueParser { clap::builder::PossibleValuesParser::new(["timestamp", "duration"]).map(|value: String| { match value.as_str() { @@ -94,6 +98,30 @@ pub struct ConvertCommand { /// (or no includes are set) AND matches no exclude. #[clap(short = 'n', long = "exclude-topic-regex")] exclude_topic_regex: Vec, + + /// Inclusive lower bound on the raw MCAP `log_time`. + /// + /// Accepts Unix timestamps with a unit suffix (`ns`, `ms`, `s`, …), or an RFC 3339 timestamp. + /// Bare integers are interpreted as nanoseconds. + /// + /// If set, only data within this time range gets converted. + #[clap(long = "start-time", value_name = "TIME", value_parser = parse_time)] + start_time: Option, + + /// Exclusive upper bound on the raw MCAP `log_time`. + /// + /// Accepts Unix timestamps with a unit suffix (`ns`, `ms`, `s`, …), or an RFC 3339 timestamp. + /// Bare integers are interpreted as nanoseconds. + /// + /// If set, only data within this time range gets converted. + #[clap(long = "end-time", value_name = "TIME", value_parser = parse_time)] + end_time: Option, + + /// Recover a missing or invalid MCAP summary in memory. + /// + /// This allows conversion of MCAP files that lack a footer (e.g. corrupted recordings). + #[clap(long = "recover")] + recover: bool, } fn compile_topic_filter(include: &[String], exclude: &[String]) -> anyhow::Result { @@ -114,6 +142,46 @@ fn compile_topic_filter(include: &[String], exclude: &[String]) -> anyhow::Resul .map_err(|err| anyhow::anyhow!("Invalid topic regex in include/exclude filters: {err}")) } +fn parse_time(value: &str) -> Result { + if let Ok(nanos) = value.parse::() { + return Ok(nanos); + } + + // `Duration` only provides unit-aware parsing here; the result remains an absolute offset from + // the Unix epoch, not an offset relative to the start of the MCAP file. + if let Ok(duration) = value.parse::() { + return u64::try_from(duration.as_nanos()) + .map_err(|_err| "Time cannot be negative".to_owned()); + } + + if let Ok(timestamp) = value.parse::() { + return u64::try_from(timestamp.nanos_since_epoch()) + .map_err(|_err| "Time cannot be before the Unix epoch".to_owned()); + } + + Err(format!( + "invalid time {value:?}; expected nanoseconds, a Unix timestamp with a unit suffix, or an RFC 3339 timestamp" + )) +} + +fn compile_time_range( + start_time: Option, + end_time: Option, +) -> anyhow::Result> { + if start_time.is_none() && end_time.is_none() { + return Ok(None); + } + + let start = start_time.unwrap_or(0); + let end = end_time.unwrap_or(u64::MAX); + anyhow::ensure!( + start < end, + "start-time ({start}) must be less than end-time ({end}); the range is half-open [start, end)" + ); + + Ok(Some((start, end))) +} + impl ConvertCommand { fn run(&self) -> anyhow::Result<()> { let Self { @@ -127,9 +195,13 @@ impl ConvertCommand { timeline_type, include_topic_regex, exclude_topic_regex, + start_time, + end_time, + recover, } = self; let topic_filter = compile_topic_filter(include_topic_regex, exclude_topic_regex)?; + let time_range = compile_time_range(*start_time, *end_time)?; let start_time = std::time::Instant::now(); @@ -157,7 +229,9 @@ impl ConvertCommand { let importer: &dyn Importer = &McapImporter::new(&selected_decoders) .with_raw_fallback(!*disable_raw_fallback) - .with_topic_filter(topic_filter); + .with_topic_filter(topic_filter) + .with_time_range(time_range) + .with_recover(*recover); // TODO(#10862): This currently loads the entire file into memory. let (tx, rx) = crossbeam::channel::bounded::(1024); @@ -193,12 +267,16 @@ impl ConvertCommand { pub enum McapCommands { /// Convert an .mcap file to an .rrd Convert(ConvertCommand), + + /// Print timeline / sortedness diagnostics for an .mcap file + Info(InfoCommand), } impl McapCommands { pub fn run(&self) -> anyhow::Result<()> { match self { Self::Convert(cmd) => cmd.run(), + Self::Info(cmd) => cmd.run(), } } } diff --git a/crates/top/rerun/src/commands/mod.rs b/crates/top/rerun/src/commands/mod.rs index de14da278940..97cf26710f0e 100644 --- a/crates/top/rerun/src/commands/mod.rs +++ b/crates/top/rerun/src/commands/mod.rs @@ -45,5 +45,5 @@ pub use self::entrypoint::{ pub use self::mcap::McapCommands; pub use self::rrd::RrdCommands; pub use self::stdio::{ - read_raw_rrd_streams_from_file_or_stdin, read_rrd_streams_from_file_or_stdin, + InputSource, read_raw_rrd_streams_from_file_or_stdin, read_rrd_streams_from_file_or_stdin, }; diff --git a/crates/top/rerun/src/commands/rrd/compare.rs b/crates/top/rerun/src/commands/rrd/compare.rs index 9c2445db9eb4..24a288033ead 100644 --- a/crates/top/rerun/src/commands/rrd/compare.rs +++ b/crates/top/rerun/src/commands/rrd/compare.rs @@ -29,6 +29,14 @@ pub struct CompareCommand { /// If specified, the comparison will ignore chunks without components. #[clap(long, default_value_t = false)] ignore_chunks_without_components: bool, + + /// Timelines to ignore entirely during comparison (their presence, absence, and values). + /// + /// Useful when comparing recordings produced with different default-timeline settings, + /// e.g. `--ignore-timeline log_tick` (which is opt-in). + /// Can be specified multiple times. + #[clap(long = "ignore-timeline", value_name = "TIMELINE")] + ignore_timelines: Vec, } impl CompareCommand { @@ -43,8 +51,14 @@ impl CompareCommand { unordered, full_dump, ignore_chunks_without_components, + ignore_timelines, } = self; + let ignore_timelines: Vec = ignore_timelines + .iter() + .map(re_chunk::TimelineName::try_new) + .collect::>()?; + re_log::debug!("Comparing {path_to_rrd1:?} to {path_to_rrd2:?}…"); let path_to_rrd1 = PathBuf::from(path_to_rrd1); @@ -92,10 +106,14 @@ impl CompareCommand { let mut unmatched_chunks1 = Vec::new(); for chunk1 in &chunks1 { - if let Some(pos) = chunks2_remaining - .iter() - .position(|chunk2| re_chunk::Chunk::ensure_similar(chunk1, chunk2).is_ok()) - { + if let Some(pos) = chunks2_remaining.iter().position(|chunk2| { + re_chunk::Chunk::ensure_similar_ignoring_timelines( + chunk1, + chunk2, + &ignore_timelines, + ) + .is_ok() + }) { chunks2_remaining.swap_remove(pos); } else { unmatched_chunks1.push(chunk1.clone()); @@ -140,7 +158,12 @@ impl CompareCommand { ); for (chunk1, chunk2) in izip!(chunks1, chunks2) { - re_chunk::Chunk::ensure_similar(&chunk1, &chunk2).with_context(|| { + re_chunk::Chunk::ensure_similar_ignoring_timelines( + &chunk1, + &chunk2, + &ignore_timelines, + ) + .with_context(|| { format!( "Chunks diff:\n{}", similar_asserts::SimpleDiff::from_str( diff --git a/crates/top/rerun/src/commands/rrd/merge_optimize.rs b/crates/top/rerun/src/commands/rrd/merge_optimize.rs index 534135c2a98e..e8de087dc36a 100644 --- a/crates/top/rerun/src/commands/rrd/merge_optimize.rs +++ b/crates/top/rerun/src/commands/rrd/merge_optimize.rs @@ -3,7 +3,7 @@ use std::io::{IsTerminal as _, Write as _}; use anyhow::Context as _; use itertools::Either; use re_byte_size::SizeBytes as _; -use re_chunk_store::{ChunkStoreConfig, CompactionOptions, IsStartOfGop}; +use re_chunk_store::{ChunkStoreConfig, CompactionOptions, IsStartOfGop, OptimizationProfile}; use re_entity_db::EntityDb; use re_log_types::StoreId; use re_sdk::StoreKind; @@ -59,24 +59,75 @@ impl MergeCommand { // --- +/// Parse a human-readable size string (e.g. `2MiB`, `512KiB`, `1GB`) into a byte count. +/// +/// Accepts both binary (`KiB`/`MiB`/`GiB`/`TiB`) and decimal (`kB`/`MB`/`GB`/`TB`) units, +/// as well as a plain `B` suffix (e.g. `1024B`). +fn parse_size(s: &str) -> Result { + let bytes = re_format::parse_bytes(s).ok_or_else(|| { + format!( + "invalid size {s:?}; expected a value with a unit suffix, e.g. `2MiB`, `1GB`, `1024B`" + ) + })?; + u64::try_from(bytes).map_err(|err| format!("size {s:?} must be non-negative: {err}")) +} + +// --- + +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +pub enum ProfileArg { + /// Small chunks tuned for the live Viewer workflow. + Live, + + /// Larger chunks tuned for object-store-backed query and streaming. + ObjectStore, +} + +impl ProfileArg { + fn to_profile(self) -> OptimizationProfile { + match self { + Self::Live => OptimizationProfile::LIVE, + Self::ObjectStore => OptimizationProfile::OBJECT_STORE, + } + } +} + #[derive(Debug, Clone, clap::Parser)] pub struct OptimizeCommand { /// Paths to read from. Reads from standard input if none are specified. path_to_input_rrds: Vec, - /// Path to write to. Writes to standard output if unspecified. + /// Path to write the optimized recording to. + /// + /// In single-file mode (the default), this is the output file path. If unspecified, + /// the recording is written to standard output. + /// + /// In directory mirror mode (when any input is a directory), this must be set and + /// is treated as the output directory root: the input folder structure is mirrored + /// underneath it, with each `.rrd`/`.rbl` file optimized independently. #[arg(short = 'o', long = "output", value_name = "dst.(rrd|rbl)")] path_to_output_rrd: Option, - /// What is the threshold, in bytes, after which a Chunk cannot be compacted any further? + /// Optimization profile to start from. /// - /// Overrides `RERUN_CHUNK_MAX_BYTES` if set. - #[arg(long = "max-bytes")] - max_bytes: Option, + /// Per-knob flags and `RERUN_CHUNK_MAX_*` env vars override the profile's + /// values. `RERUN_STORE_ENABLE_CHANGELOG` is ignored by this command — + /// `rerun rrd optimize` is always headless. + #[arg(long = "profile", value_enum, default_value_t = ProfileArg::ObjectStore)] + profile: ProfileArg, + + /// Threshold after which a Chunk cannot be compacted any further. + /// + /// Accepts a size string with a unit suffix, e.g. `2MiB`, `512KiB`, `1GB`, `1024B`. + /// Both binary (`KiB`/`MiB`/`GiB`/`TiB`) and decimal (`kB`/`MB`/`GB`/`TB`) units are accepted. + /// + /// Overrides the profile's value and `RERUN_CHUNK_MAX_BYTES` if set. + #[arg(long = "max-size", value_parser = parse_size)] + max_size: Option, /// What is the threshold, in rows, after which a Chunk cannot be compacted any further? /// - /// Overrides `RERUN_CHUNK_MAX_ROWS` if set. + /// Overrides the profile's value and `RERUN_CHUNK_MAX_ROWS` if set. #[arg(long = "max-rows")] max_rows: Option, @@ -84,11 +135,12 @@ pub struct OptimizeCommand { /// /// This specifically applies to _non_ time-sorted chunks. /// - /// Overrides `RERUN_CHUNK_MAX_ROWS_IF_UNSORTED` if set. + /// Overrides the profile's value and `RERUN_CHUNK_MAX_ROWS_IF_UNSORTED` if set. #[arg(long = "max-rows-if-unsorted")] max_rows_if_unsorted: Option, /// Configures the number of extra compaction passes to run on the data. + /// Overrides the profile's value. Default per profile: 50. /// /// Compaction in Rerun is an iterative, convergent process: every single pass will improve the /// quality of the compaction (with diminishing returns), until it eventually converges into a @@ -103,8 +155,8 @@ pub struct OptimizeCommand { /// /// If/When the data reaches a stable optimum, the computation will stop immediately, regardless of /// how many passes are left. - #[arg(long = "num-pass", default_value_t = 50)] - num_extra_passes: u32, + #[arg(long = "num-pass")] + num_extra_passes: Option, /// If set, will try to proceed even in the face of IO and/or decoding errors in the input data. #[clap(long = "continue-on-error", default_value_t = false)] @@ -118,10 +170,19 @@ pub struct OptimizeCommand { /// /// Note: GoP rebatching never splits a GoP across chunks, so streams with /// long keyframe intervals (e.g. 10+ seconds between I-frames) can produce - /// chunks much larger than `--max-bytes`. + /// chunks much larger than `--max-size`. #[clap(long = "no-rebatch-videos", default_value_t = false)] no_rebatch_videos: bool, + /// Drop any user-supplied `VideoStream:is_keyframe` labels and re-derive + /// them from the encoded samples. + /// + /// By default, `rrd optimize` validates user-supplied keyframe labels against + /// the encoded samples and errors out if they disagree. Pass this flag to + /// ignore the existing labels and unconditionally re-derive them. + #[clap(long = "fix-keyframe", default_value_t = false)] + fix_keyframe: bool, + /// If set, split chunks so no two archetype groups sharing a chunk differ in /// byte size by more than this factor. Values should be `>= 1`; at `1.0`, /// every archetype is forced into its own chunk. @@ -131,7 +192,7 @@ pub struct OptimizeCommand { /// thin data without dragging along the thick payload. Components belonging to /// the same archetype are always kept together. /// - /// A good starting value is 10.0. If unset, no thick/thin split is performed. + /// A good starting value is 10.0. If unset, the profile's value is used. #[arg(long = "split-size-ratio")] split_size_ratio: Option, } @@ -141,12 +202,14 @@ impl OptimizeCommand { let Self { path_to_input_rrds, path_to_output_rrd, - max_bytes, + profile, + max_size, max_rows, max_rows_if_unsorted, num_extra_passes, continue_on_error, no_rebatch_videos, + fix_keyframe, split_size_ratio, } = self; @@ -157,13 +220,15 @@ impl OptimizeCommand { ); } - let mut store_config = ChunkStoreConfig::from_env().unwrap_or_default(); - // NOTE: We're doing headless processing, there's no point in running subscribers, it will just - // (massively) slow us down. - store_config.enable_changelog = false; + let profile = profile.to_profile(); - if let Some(max_bytes) = max_bytes { - store_config.chunk_max_bytes = *max_bytes; + // Seed from profile, then env, then CLI flags. Force enable_changelog=false + // last (optimize is headless; we never want subscribers). + let mut store_config = profile.to_chunk_store_config(); + store_config = store_config.apply_env()?; + + if let Some(max_size) = max_size { + store_config.chunk_max_bytes = *max_size; } if let Some(max_rows) = max_rows { store_config.chunk_max_rows = *max_rows; @@ -172,21 +237,55 @@ impl OptimizeCommand { store_config.chunk_max_rows_if_unsorted = *max_rows_if_unsorted; } + store_config.enable_changelog = false; + + let num_extra_passes = num_extra_passes.unwrap_or(profile.num_extra_passes); + + let gop_batching = !*no_rebatch_videos && profile.gop_batching; + + if let Some(ratio) = *split_size_ratio { + anyhow::ensure!( + ratio.is_finite() && ratio >= 1.0, + "--split-size-ratio must be finite and >= 1.0, got {ratio}" + ); + } + + let split_size_ratio = split_size_ratio.or(profile.split_size_ratio); + let is_start_of_gop: IsStartOfGop = std::sync::Arc::new(|data, codec| { re_video::is_start_of_gop(data, codec.into()).map_err(|err| anyhow::anyhow!(err)) }); let compaction_options = CompactionOptions { config: store_config.clone(), - num_extra_passes: Some(*num_extra_passes as usize), - is_start_of_gop: if *no_rebatch_videos { - None - } else { - Some(is_start_of_gop) - }, - split_size_ratio: *split_size_ratio, + num_extra_passes: Some(num_extra_passes as usize), + is_start_of_gop: gop_batching.then_some(is_start_of_gop), + split_size_ratio, + fix_keyframe: *fix_keyframe, }; + // Directory mirror mode: if any input is a directory, recursively expand it + // to its `*.rrd`/`*.rbl` files and optimize each one independently, mirroring + // the input folder structure under the output path. + let any_input_is_dir = path_to_input_rrds + .iter() + .any(|p| std::path::Path::new(p).is_dir()); + + if any_input_is_dir { + let output_root = path_to_output_rrd.as_ref().ok_or_else(|| { + anyhow::anyhow!( + "directory inputs require an output path (`-o `); cannot mirror to stdout" + ) + })?; + return optimize_dir_mirror( + *continue_on_error, + &store_config, + &compaction_options, + path_to_input_rrds, + output_root, + ); + } + merge_and_compact( *continue_on_error, &store_config, @@ -197,6 +296,103 @@ impl OptimizeCommand { } } +/// Walk every input (file or directory), pair each `*.rrd`/`*.rbl` with an output +/// path under `output_root` that mirrors the input folder structure, and optimize +/// each pair independently. +fn optimize_dir_mirror( + continue_on_error: bool, + store_config: &ChunkStoreConfig, + compaction_options: &CompactionOptions, + inputs: &[String], + output_root: &str, +) -> anyhow::Result<()> { + let output_root = std::path::PathBuf::from(output_root); + if output_root.exists() && !output_root.is_dir() { + anyhow::bail!( + "output path {output_root:?} must be a directory when any input is a directory" + ); + } + + let mut pairs: Vec<(std::path::PathBuf, std::path::PathBuf)> = Vec::new(); + + for input in inputs { + let input_path = std::path::Path::new(input); + if input_path.is_dir() { + for entry in walkdir::WalkDir::new(input_path).follow_links(false) { + let entry = entry.with_context(|| format!("walking {input_path:?}"))?; + if !entry.file_type().is_file() { + continue; + } + if !is_rrd_like(entry.path()) { + continue; + } + let relative = entry + .path() + .strip_prefix(input_path) + .with_context(|| format!("strip_prefix({input_path:?}, {:?})", entry.path()))?; + pairs.push((entry.path().to_path_buf(), output_root.join(relative))); + } + } else if input_path.is_file() { + let file_name = input_path + .file_name() + .ok_or_else(|| anyhow::anyhow!("input path has no file name: {input_path:?}"))?; + pairs.push((input_path.to_path_buf(), output_root.join(file_name))); + } else { + anyhow::bail!("input path does not exist or is not a file/directory: {input_path:?}"); + } + } + + if pairs.is_empty() { + anyhow::bail!( + "no `.rrd`/`.rbl` files found under any of: {inputs:?}\n\ + (directory mirror mode skips other extensions)" + ); + } + + re_log::info!( + num_files = pairs.len(), + output_root = %output_root.display(), + "optimizing files in directory mirror mode", + ); + + let total = pairs.len(); + let done = std::sync::atomic::AtomicUsize::new(0); + + use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator as _}; + pairs + .par_iter() + .try_for_each(|(src, dst)| -> anyhow::Result<()> { + if let Some(parent) = dst.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("creating output dir {parent:?}"))?; + } + let idx = done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; + re_log::info!( + "[{idx}/{total}] optimizing {} -> {}", + src.display(), + dst.display(), + ); + let src_str = src.to_string_lossy().into_owned(); + let dst_str = dst.to_string_lossy().into_owned(); + merge_and_compact( + continue_on_error, + store_config, + Some(compaction_options), + std::slice::from_ref(&src_str), + Some(&dst_str), + ) + })?; + + Ok(()) +} + +fn is_rrd_like(path: &std::path::Path) -> bool { + matches!( + path.extension().and_then(|s| s.to_str()), + Some("rrd" | "rbl"), + ) +} + // --- /// Stub for the old `rerun rrd compact` name. Accepts any arguments and errors out with a @@ -235,9 +431,7 @@ fn merge_and_compact( let now = std::time::Instant::now(); re_log::info!( - max_rows = %re_format::format_uint(store_config.chunk_max_rows), - max_rows_if_unsorted = %re_format::format_uint(store_config.chunk_max_rows_if_unsorted), - max_bytes = %re_format::format_bytes(store_config.chunk_max_bytes as _), + config = %store_config, srcs = ?path_to_input_rrds, "merge/compaction started" ); @@ -326,7 +520,7 @@ fn merge_and_compact( ); } - log_chunk_size_stats(&entity_dbs, "post-compaction"); + log_chunk_size_stats(&entity_dbs, store_config, "post-compaction"); let mut rrd_out = if let Some(path) = path_to_output_rrd { Either::Left(std::io::BufWriter::new( @@ -366,7 +560,7 @@ fn merge_and_compact( encoding_options, // NOTE: We want to make sure all blueprints come first, so that the viewer can immediately // set up the viewport correctly. - messages_rbl.chain(messages_rrd), + std::iter::chain(messages_rbl, messages_rrd), &mut rrd_out, ) .context("couldn't encode messages")?; @@ -400,19 +594,58 @@ fn merge_and_compact( Ok(()) } -fn log_chunk_size_stats(entity_dbs: &std::collections::HashMap, label: &str) { +fn log_chunk_size_stats( + entity_dbs: &std::collections::HashMap, + store_config: &ChunkStoreConfig, + label: &str, +) { + let max_rows_limit = store_config.chunk_max_rows as usize; + let max_rows_if_unsorted_limit = store_config.chunk_max_rows_if_unsorted as usize; + let mut min_bytes = u64::MAX; let mut max_bytes = 0u64; let mut total_bytes = 0u64; + let mut min_rows = usize::MAX; + let mut max_rows_seen = 0usize; + let mut total_rows = 0u64; let mut num_chunks = 0u64; + let mut num_unordered = 0u64; + + // Capped-chunk stats: chunks that hit a row-count limit during compaction. + // The "rest" are chunks that converged below the limits, and are the most + // interesting input for tuning chunk-size targets. + let mut num_unordered_at_limit = 0u64; + let mut num_sorted_at_max_rows = 0u64; + let mut rest_num_chunks = 0u64; + let mut rest_total_bytes = 0u64; + let mut rest_total_rows = 0u64; for db in entity_dbs.values() { for chunk in db.storage_engine().store().iter_physical_chunks() { let size = chunk.heap_size_bytes(); + let rows = chunk.num_rows(); + let all_timelines_sorted = chunk.all_timelines_sorted(); + min_bytes = min_bytes.min(size); max_bytes = max_bytes.max(size); total_bytes += size; + min_rows = min_rows.min(rows); + max_rows_seen = max_rows_seen.max(rows); + total_rows += rows as u64; + if !all_timelines_sorted { + num_unordered += 1; + } num_chunks += 1; + + if !all_timelines_sorted && rows == max_rows_if_unsorted_limit { + num_unordered_at_limit += 1; + } else if all_timelines_sorted && rows == max_rows_limit { + num_sorted_at_max_rows += 1; + } else { + rest_num_chunks += 1; + rest_total_bytes += size; + rest_total_rows += rows as u64; + } } } @@ -421,6 +654,15 @@ fn log_chunk_size_stats(entity_dbs: &std::collections::HashMap output.rrd` /// - /// * `rerun rrd optimize --max-rows 4096 --max-bytes=1048576 /my/recordings/*.rrd > output.rrd` + /// * Directory mirror mode — optimize every `.rrd`/`.rbl` under a tree, preserving structure: + /// `rerun rrd optimize --max-size 2MiB /my/recordings -o /my/recordings-compacted` Optimize(OptimizeCommand), /// Deprecated: renamed to `optimize`. diff --git a/crates/top/rerun/src/commands/rrd/print.rs b/crates/top/rerun/src/commands/rrd/print.rs index 2a8e0fe079c8..f93245e31a0a 100644 --- a/crates/top/rerun/src/commands/rrd/print.rs +++ b/crates/top/rerun/src/commands/rrd/print.rs @@ -251,7 +251,7 @@ fn print_msg(options: &Options, msg: LogMsg) -> anyhow::Result<()> { .map(|(descr, _)| descr.to_string()) .collect_vec() .join(" "); - println!("data columns: [{column_descriptors}]",); + println!("data columns: [{column_descriptors}]"); } _ => { println!("\n{}\n", options.format_record_batch(&migrared_chunk)); diff --git a/crates/top/rerun/src/commands/rrd/split.rs b/crates/top/rerun/src/commands/rrd/split.rs index 0c2205211659..7dcecae0895d 100644 --- a/crates/top/rerun/src/commands/rrd/split.rs +++ b/crates/top/rerun/src/commands/rrd/split.rs @@ -446,7 +446,10 @@ impl SplitCommand { } } - let Some(cutoff_timeline) = known_timelines.remove(&timeline.as_str().into()) else { + let Some(cutoff_timeline) = TimelineName::try_new(timeline) + .ok() + .and_then(|name| known_timelines.remove(&name)) + else { anyhow::bail!( "timeline '{timeline}' does not exist in the input recording, available timelines are {}", known_timelines.keys().map(|name| name.as_str()).join(", ") @@ -493,14 +496,15 @@ impl SplitCommand { let time_span = max_time.saturating_sub(min_time) / *num_parts as i64; let mut cur_time = min_time; - (0..*num_parts as u64) - .map(|_| { + std::iter::chain( + (0..*num_parts as u64).map(|_| { let t = cur_time; cur_time += time_span; TimeInt::new_temporal(t) - }) - .chain(std::iter::once(TimeInt::new_temporal(max_time))) - .collect() + }), + std::iter::once(TimeInt::new_temporal(max_time)), + ) + .collect() } else if !times.is_empty() { let times = times .iter() @@ -676,21 +680,21 @@ impl SplitCommand { // Special cases: transforms and/or pinholes with multiplexed coordinate frames let entity_has_multiplexed_transforms_on_timeline = store.entity_has_component_on_timeline( - cutoff_timeline.name(), + Some(cutoff_timeline.name()), entity, transform_parent_frame_identifier, ) || store.entity_has_component_on_timeline( - cutoff_timeline.name(), + Some(cutoff_timeline.name()), entity, transform_child_frame_identifier, ); let entity_has_multiplexed_pinholes_on_timeline = store.entity_has_component_on_timeline( - cutoff_timeline.name(), + Some(cutoff_timeline.name()), entity, pinhole_parent_frame_identifier, ) || store.entity_has_component_on_timeline( - cutoff_timeline.name(), + Some(cutoff_timeline.name()), entity, pinhole_child_frame_identifier, ); @@ -1022,7 +1026,7 @@ fn extract_chunks_for_single_split( } } - chunks_bootstrap.chain(chunks) + std::iter::chain(chunks_bootstrap, chunks) } // --- diff --git a/crates/top/rerun/src/commands/rrd/stats.rs b/crates/top/rerun/src/commands/rrd/stats.rs index 1dbae6618412..5c38d20c9221 100644 --- a/crates/top/rerun/src/commands/rrd/stats.rs +++ b/crates/top/rerun/src/commands/rrd/stats.rs @@ -1,6 +1,10 @@ +use std::collections::BTreeMap; + use ahash::{HashMap, HashMapExt as _}; use itertools::Itertools as _; +use re_chunk::Chunk; use re_log_encoding::ToApplication as _; +use re_log_types::{EntityPath, TimelineName}; use re_protos::log_msg::v1alpha1::log_msg::Msg; use re_quota_channel::send_crossbeam; @@ -37,6 +41,9 @@ impl StatsCommand { let mut num_chunks_per_entity: HashMap = HashMap::new(); let mut num_chunks_per_index: HashMap = HashMap::new(); let mut num_chunks_per_component: HashMap = HashMap::new(); + // Per entity, per timeline: `true` iff every chunk seen so far has this timeline sorted. + let mut timeline_is_sorted: BTreeMap> = + BTreeMap::new(); let mut num_rows = Vec::with_capacity(num_chunks as _); let mut num_static = 0u64; let mut num_indexes = Vec::with_capacity(num_chunks as _); @@ -46,7 +53,7 @@ impl StatsCommand { let mut ipc_schema_size_bytes_uncompressed = Vec::with_capacity(num_chunks as _); let mut ipc_data_size_bytes_uncompressed = Vec::with_capacity(num_chunks as _); - let (rx_raw, _) = read_raw_rrd_streams_from_file_or_stdin(path_to_input_rrds); + let (rx_raw, rx_footers) = read_raw_rrd_streams_from_file_or_stdin(path_to_input_rrds); // Each message is accompanied by the original compressed payload size (in bytes). // For uncompressed messages, this equals the payload size. @@ -122,6 +129,16 @@ impl StatsCommand { num_static += (stats.num_indexes == 0) as u64; num_indexes.push(stats.num_indexes); num_components.push(stats.num_components); + for (entity_path, timeline_name, sorted) in + stats.timeline_sortedness + { + let entry = timeline_is_sorted + .entry(entity_path) + .or_default() + .entry(timeline_name) + .or_insert(true); + *entry &= sorted; + } } ipc_size_bytes_compressed @@ -324,10 +341,136 @@ impl StatsCommand { println!("------------------------------"); print_ipc_size_bytes_stats(ipc_data_size_bytes_uncompressed); + if !*no_decode { + println!(); + println!("Unsorted timelines"); + println!("------------------"); + let entities_with_unsorted: Vec<&EntityPath> = timeline_is_sorted + .iter() + .filter(|(_, timelines)| timelines.values().any(|sorted| !*sorted)) + .map(|(entity, _)| entity) + .collect(); + + if entities_with_unsorted.is_empty() { + println!("(none — every timeline on every chunk is sorted)"); + } else { + println!( + "{} entity(ies) had at least one chunk with an unsorted timeline. \ + For each such entity, all of its timelines are listed below:", + re_format::format_uint(entities_with_unsorted.len()) + ); + for entity in entities_with_unsorted { + println!(" {entity}"); + for (timeline, sorted) in &timeline_is_sorted[entity] { + let status = if *sorted { "sorted" } else { "UNSORTED" }; + println!(" {timeline}: {status}"); + } + } + } + } + + // The footer is parsed straight from the raw bytes, so these stats are available even with + // `--no-decode`. + println!(); + println!("Footers"); + println!("-------"); + match rx_footers.recv() { + Ok((_size_bytes, footers)) => print_footer_stats(footers, *continue_on_error)?, + Err(_) => println!("(none — the input stream produced no footer metadata)"), + } + Ok(()) } } +/// Prints statistics about the RRD footer(s), i.e. the `RrdManifest`s carried by the trailing +/// `::End` message(s) of the stream. +/// +/// Each manifest catalogs every chunk in a single recording without requiring any of that chunk +/// data to be decoded, so all of these stats are derived purely from the footer. +fn print_footer_stats( + footers: Vec<( + crate::commands::InputSource, + anyhow::Result, + )>, + continue_on_error: bool, +) -> anyhow::Result<()> { + if footers.is_empty() { + println!("(none — no RRD footer was found)"); + return Ok(()); + } + + let num_manifests = footers.iter().filter(|(_, res)| res.is_ok()).count(); + println!( + "num_manifests = {} (one per recording)", + re_format::format_uint(num_manifests) + ); + + for (source, res) in footers { + let manifest = match res { + Ok(manifest) => manifest, + Err(err) => { + re_log::error_once!( + "failed to parse footer from {source}: {}", + re_error::format(err) + ); + if !continue_on_error { + anyhow::bail!( + "one or more corrupt RRD footers in the input stream (check logs)" + ) + } + continue; + } + }; + + let num_chunks = manifest.data.num_rows() as u64; + let num_static_chunks = manifest.col_chunk_is_static()?.filter(|s| *s).count() as u64; + let num_entity_paths = manifest.col_chunk_entity_path()?.unique().count(); + let byte_size_total: u64 = manifest.col_chunk_byte_size()?.sum(); + let byte_size_uncompressed_total: u64 = manifest.col_chunk_byte_size_uncompressed()?.sum(); + + let sha256 = manifest + .sorbet_schema_sha256 + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); + + println!(); + println!("Footer manifest for {:?}", manifest.store_id); + println!( + " num_chunks_indexed = {}", + re_format::format_uint(num_chunks) + ); + println!( + " num_static_chunks = {}", + re_format::format_uint(num_static_chunks) + ); + println!( + " num_entity_paths = {}", + re_format::format_uint(num_entity_paths) + ); + println!( + " manifest_num_columns = {}", + re_format::format_uint(manifest.data.num_columns()) + ); + println!( + " sorbet_schema_num_fields = {}", + re_format::format_uint(manifest.sorbet_schema.fields.len()) + ); + println!(" sorbet_schema_sha256 = {sha256}"); + println!( + " chunk_byte_size_total (native) = {}", + re_format::format_bytes(byte_size_total as f64) + ); + println!( + " chunk_byte_size_uncompressed_total = {}", + re_format::format_bytes(byte_size_uncompressed_total as f64) + ); + } + + Ok(()) +} + #[derive(Clone, Debug)] struct ChunkStats { app: Option, @@ -354,6 +497,9 @@ struct ChunkStatsApplication { num_rows: u64, num_indexes: u64, num_components: u64, + + /// Per-timeline sortedness for this chunk, scoped to its entity path. + timeline_sortedness: Vec<(EntityPath, TimelineName, bool)>, } fn compute_stats(app: bool, compressed_size: u64, msg: &Msg) -> anyhow::Result> { @@ -452,6 +598,23 @@ fn compute_stats(app: bool, compressed_size: u64, msg: &Msg) -> anyhow::Result chunk + .timelines() + .iter() + .map(|(name, tc)| (chunk.entity_path().clone(), *name, tc.is_sorted())) + .collect(), + Err(err) => { + re_log::warn_once!( + "Failed to promote ArrowMsg into a Chunk for sorted-timeline check: {err}" + ); + Vec::new() + } + }; + Some(ChunkStatsApplication { entity_path, @@ -461,6 +624,8 @@ fn compute_stats(app: bool, compressed_size: u64, msg: &Msg) -> anyhow::Result Self { Self::new( CErrorCode::InvalidStringArgument, - &format!("Argument {parameter_name:?} is not valid UTF-8: {utf8_error}",), + &format!("Argument {parameter_name:?} is not valid UTF-8: {utf8_error}"), ) } diff --git a/crates/top/rerun_c/src/lib.rs b/crates/top/rerun_c/src/lib.rs index af529a349f5d..57a8e2acef20 100644 --- a/crates/top/rerun_c/src/lib.rs +++ b/crates/top/rerun_c/src/lib.rs @@ -19,14 +19,15 @@ use arrow::array::{ArrayRef as ArrowArrayRef, ListArray as ArrowListArray}; use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema}; use arrow_utils::arrow_array_from_c_ffi; use component_type_registry::COMPONENT_TYPES; +use itertools::Itertools as _; use re_arrow_util::ArrowArrayDowncastRef as _; use re_sdk::external::nohash_hasher::IntMap; use re_sdk::external::re_log_types::TimelineName; use re_sdk::log::{Chunk, ChunkId, PendingRow, TimeColumn}; use re_sdk::time::TimeType; use re_sdk::{ - ComponentDescriptor, EntityPath, RecordingStream, RecordingStreamBuilder, StoreKind, TimeCell, - TimePoint, Timeline, + ArchetypeName, ComponentDescriptor, ComponentIdentifier, ComponentType, EntityPath, + RecordingStream, RecordingStreamBuilder, StoreKind, TimeCell, TimePoint, Timeline, }; use recording_streams::{RECORDING_STREAMS, recording_stream}; @@ -307,7 +308,8 @@ impl TryFrom for Timeline { type Error = CError; fn try_from(timeline: CTimeline) -> Result { - let name = timeline.name.as_nonempty_str("timeline.name")?; + let name = TimelineName::try_new(timeline.name.as_nonempty_str("timeline.name")?) + .map_err(|err| CError::new(CErrorCode::InvalidStringArgument, &err.to_string()))?; let typ = match timeline.typ { CTimeType::Sequence => TimeType::Sequence, CTimeType::Duration => TimeType::DurationNs, @@ -492,9 +494,10 @@ fn rr_register_component_type_impl( component_type_descr.as_optional_str("component_type.descriptor.component_type")?; let component_descr = ComponentDescriptor { - archetype: archetype_name.map(Into::into), - component: component.into(), - component_type: component_type_descr.map(Into::into), + archetype: archetype_name.and_then(|s| ArchetypeName::try_new(s).ok()), + component: ComponentIdentifier::try_new(component) + .map_err(|err| CError::new(CErrorCode::InvalidStringArgument, &err.to_string()))?, + component_type: component_type_descr.and_then(|s| ComponentType::try_new(s).ok()), }; let field = arrow::datatypes::Field::try_from(&component_type.schema).map_err(|err| { @@ -646,8 +649,8 @@ pub extern "C" fn rr_recording_stream_free(id: CRecordingStream) { drop(stream); } } else { - // Yes, at least as of writing we can still log things in this state! - re_log::debug!( + // ⚠️ Don't use `re_log` here since it goes through `tracing` which _also_ may have shut down thread locals at this point, causing a panic when accessing them. + eprintln!( "rr_recording_stream_free called on a thread that is shutting down and can no longer access thread locals. We can't handle this and have to ignore this call." ); } @@ -824,7 +827,7 @@ fn rr_recording_stream_serve_grpc_impl( let cors_allowed_origins: Vec = cors_allow_origins .iter() .map(|s| Ok(s.as_nonempty_str("cors_allow_origin")?.to_owned())) - .collect::, CError>>()?; + .try_collect()?; let server_options = re_sdk::ServerOptions { playback_behavior: re_sdk::PlaybackBehavior::from_newest_first(newest_first), @@ -963,7 +966,8 @@ fn rr_recording_stream_set_time_impl( time_type: CTimeType, value: i64, ) -> Result<(), CError> { - let timeline = timeline_name.as_nonempty_str("timeline_name")?; + let timeline = TimelineName::try_new(timeline_name.as_nonempty_str("timeline_name")?) + .map_err(|err| CError::new(CErrorCode::InvalidStringArgument, &err.to_string()))?; let stream = recording_stream(stream)?; let time_type = match time_type { CTimeType::Sequence => TimeType::Sequence, @@ -993,7 +997,8 @@ fn rr_recording_stream_disable_timeline_impl( stream: CRecordingStream, timeline_name: CStringView, ) -> Result<(), CError> { - let timeline = timeline_name.as_nonempty_str("timeline_name")?; + let timeline = TimelineName::try_new(timeline_name.as_nonempty_str("timeline_name")?) + .map_err(|err| CError::new(CErrorCode::InvalidStringArgument, &err.to_string()))?; recording_stream(stream)?.disable_timeline(timeline); Ok(()) } @@ -1018,6 +1023,28 @@ pub extern "C" fn rr_recording_stream_reset_time(stream: CRecordingStream) { } } +#[expect(unsafe_code)] +#[unsafe(no_mangle)] +pub extern "C" fn rr_recording_stream_set_log_tick_enabled( + stream: CRecordingStream, + enabled: bool, +) { + if let Some(stream) = RECORDING_STREAMS.lock().get(stream) { + stream.set_log_tick_enabled(enabled); + } +} + +#[expect(unsafe_code)] +#[unsafe(no_mangle)] +pub extern "C" fn rr_recording_stream_set_log_time_enabled( + stream: CRecordingStream, + enabled: bool, +) { + if let Some(stream) = RECORDING_STREAMS.lock().get(stream) { + stream.set_log_time_enabled(enabled); + } +} + #[expect(unsafe_code)] #[expect(clippy::result_large_err)] #[expect(clippy::needless_pass_by_value)] // Conceptually we're consuming the data_row, as we take ownership of data it points to. @@ -1217,7 +1244,7 @@ fn rr_recording_stream_send_columns_impl( ), )) }) - .collect::>()?; + .try_collect()?; let components: IntMap = { let component_type_registry = COMPONENT_TYPES.read(); @@ -1246,7 +1273,7 @@ fn rr_recording_stream_send_columns_impl( Ok((component_type.descriptor.clone(), component_values.clone())) }) - .collect::>()? + .try_collect()? }; let chunk = Chunk::from_auto_row_ids( diff --git a/crates/top/rerun_c/src/video.rs b/crates/top/rerun_c/src/video.rs index 936ea3e3f1b3..45e6b61c3bca 100644 --- a/crates/top/rerun_c/src/video.rs +++ b/crates/top/rerun_c/src/video.rs @@ -43,7 +43,6 @@ pub extern "C" fn rr_video_asset_read_frame_timestamps_nanos( video_bytes, media_type_str, "AssetVideo", - re_sdk::external::re_tuid::Tuid::new(), ) { Ok(video) => video, Err(err) => { @@ -70,9 +69,9 @@ pub extern "C" fn rr_video_asset_read_frame_timestamps_nanos( return std::ptr::null_mut(); }; - for (segment, timestamp_nanos) in video_timestamps_iter.zip(timestamps_nanos.iter_mut()) { + let ptr = timestamps_nanos.as_mut_ptr(); + for (segment, timestamp_nanos) in std::iter::zip(video_timestamps_iter, timestamps_nanos) { *timestamp_nanos = segment; } - - timestamps_nanos.as_mut_ptr() + ptr } diff --git a/crates/utils/re_analytics/src/cli.rs b/crates/utils/re_analytics/src/cli.rs index 8173e55553a3..833d8d8eeb7a 100644 --- a/crates/utils/re_analytics/src/cli.rs +++ b/crates/utils/re_analytics/src/cli.rs @@ -23,7 +23,7 @@ pub fn clear() -> Result<(), CliError> { let config = Config::load_or_default()?; fn delete_dir(dir: &Path) -> Result<(), CliError> { - eprint!("Are you sure you want to delete directory {dir:?}? [y/N]: ",); + eprint!("Are you sure you want to delete directory {dir:?}? [y/N]: "); let mut input = String::new(); std::io::stdin().read_line(&mut input)?; diff --git a/crates/utils/re_analytics/src/native/pipeline.rs b/crates/utils/re_analytics/src/native/pipeline.rs index 6df0633ec013..bb209bd2c2b2 100644 --- a/crates/utils/re_analytics/src/native/pipeline.rs +++ b/crates/utils/re_analytics/src/native/pipeline.rs @@ -245,7 +245,7 @@ fn flush_pending_events( Ok(()) } -#[expect(clippy::needless_return, clippy::too_many_arguments)] +#[expect(clippy::needless_return)] fn realtime_pipeline( config: &Config, sink: &PostHogSink, diff --git a/crates/utils/re_analytics/src/posthog.rs b/crates/utils/re_analytics/src/posthog.rs index 0a77d025e288..bc9ad91bd145 100644 --- a/crates/utils/re_analytics/src/posthog.rs +++ b/crates/utils/re_analytics/src/posthog.rs @@ -40,8 +40,7 @@ impl<'a> PostHogEvent<'a> { timestamp: event.time_utc, event: event.name.as_ref(), distinct_id: analytics_id, - properties: properties - .chain([("session_id", session_id.into())]) + properties: std::iter::chain(properties, [("session_id", session_id.into())]) .collect(), }), crate::EventKind::Identify => Self::Identify(PostHogIdentifyEvent { diff --git a/crates/utils/re_arrow_util/src/arrays.rs b/crates/utils/re_arrow_util/src/arrays.rs index 15e1831e46ab..0e93829a269d 100644 --- a/crates/utils/re_arrow_util/src/arrays.rs +++ b/crates/utils/re_arrow_util/src/arrays.rs @@ -183,12 +183,12 @@ pub fn pad_list_array_back(list_array: &ListArray, target_len: usize) -> ListArr let fields = list_array_fields(list_array); let offsets = { - OffsetBuffer::from_lengths( + OffsetBuffer::from_lengths(std::iter::chain( list_array .iter() - .map(|array| array.map_or(0, |array| array.len())) - .chain(repeat_n(0, missing_len)), - ) + .map(|array| array.map_or(0, |array| array.len())), + repeat_n(0, missing_len), + )) }; let values = list_array.values().clone(); @@ -196,12 +196,13 @@ pub fn pad_list_array_back(list_array: &ListArray, target_len: usize) -> ListArr let nulls = { if let Some(nulls) = list_array.nulls() { #[expect(clippy::from_iter_instead_of_collect)] - NullBuffer::from_iter(nulls.iter().chain(repeat_n(false, missing_len))) + NullBuffer::from_iter(std::iter::chain(nulls.iter(), repeat_n(false, missing_len))) } else { #[expect(clippy::from_iter_instead_of_collect)] - NullBuffer::from_iter( - repeat_n(true, list_array.len()).chain(repeat_n(false, missing_len)), - ) + NullBuffer::from_iter(std::iter::chain( + repeat_n(true, list_array.len()), + repeat_n(false, missing_len), + )) } }; @@ -220,13 +221,12 @@ pub fn pad_list_array_front(list_array: &ListArray, target_len: usize) -> ListAr let fields = list_array_fields(list_array); let offsets = { - OffsetBuffer::from_lengths( - repeat_n(0, missing_len).chain( - list_array - .iter() - .map(|array| array.map_or(0, |array| array.len())), - ), - ) + OffsetBuffer::from_lengths(std::iter::chain( + repeat_n(0, missing_len), + list_array + .iter() + .map(|array| array.map_or(0, |array| array.len())), + )) }; let values = list_array.values().clone(); @@ -234,12 +234,13 @@ pub fn pad_list_array_front(list_array: &ListArray, target_len: usize) -> ListAr let nulls = { if let Some(nulls) = list_array.nulls() { #[expect(clippy::from_iter_instead_of_collect)] - NullBuffer::from_iter(repeat_n(false, missing_len).chain(nulls.iter())) + NullBuffer::from_iter(std::iter::chain(repeat_n(false, missing_len), nulls.iter())) } else { #[expect(clippy::from_iter_instead_of_collect)] - NullBuffer::from_iter( - repeat_n(false, missing_len).chain(repeat_n(true, list_array.len())), - ) + NullBuffer::from_iter(std::iter::chain( + repeat_n(false, missing_len), + repeat_n(true, list_array.len()), + )) } }; @@ -269,6 +270,7 @@ pub fn new_list_array_of_empties(child_datatype: &DataType, len: usize) -> ListA /// /// Returns an error if the arrays don't share the exact same datatype. pub fn concat_arrays(arrays: &[&dyn Array]) -> arrow::error::Result { + re_tracing::profile_function!(); #[expect(clippy::disallowed_methods)] // that's the whole point let mut array = arrow::compute::concat(arrays)?; array.shrink_to_fit(); // VERY IMPORTANT! https://github.com/rerun-io/rerun/issues/7222 diff --git a/crates/utils/re_arrow_util/src/batches.rs b/crates/utils/re_arrow_util/src/batches.rs index acdefaa133ae..a01ba16db48c 100644 --- a/crates/utils/re_arrow_util/src/batches.rs +++ b/crates/utils/re_arrow_util/src/batches.rs @@ -32,6 +32,8 @@ pub fn concat_polymorphic_batches(batches: &[RecordBatch]) -> arrow::error::Resu return Ok(RecordBatch::new_empty(Arc::new(Schema::empty()))); } + re_tracing::profile_function!(); + let schema_merged = { let mut schema_builder = SchemaBuilder::new(); for batch in batches { @@ -211,14 +213,12 @@ impl RecordBatchExt for RecordBatch { let (schema_ref, columns, row_count) = self.into_parts(); let Schema { fields, metadata } = Arc::unwrap_or_clone(schema_ref); - let (fields, columns): (Vec<_>, Vec<_>) = fields - .iter() - .map(Arc::clone) - .zip(columns) - .sorted_by(|(left_field, _), (right_field, _)| { - cmp_fn(left_field.as_ref(), right_field.as_ref()) - }) - .unzip(); + let (fields, columns): (Vec<_>, Vec<_>) = + std::iter::zip(fields.iter().map(Arc::clone), columns) + .sorted_by(|(left_field, _), (right_field, _)| { + cmp_fn(left_field.as_ref(), right_field.as_ref()) + }) + .unzip(); Self::try_new_with_options( Arc::new(Schema::new_with_metadata(fields, metadata)), @@ -234,12 +234,10 @@ impl RecordBatchExt for RecordBatch { let (schema_ref, columns, row_count) = self.into_parts(); let Schema { fields, metadata } = Arc::unwrap_or_clone(schema_ref); - let (new_fields, new_columns): (Vec<_>, Vec<_>) = fields - .iter() - .map(Arc::clone) - .zip(columns) - .filter(|(field, _)| predicate(field)) - .unzip(); + let (new_fields, new_columns): (Vec<_>, Vec<_>) = + std::iter::zip(fields.iter().map(Arc::clone), columns) + .filter(|(field, _)| predicate(field)) + .unzip(); Self::try_new_with_options( Arc::new(Schema::new_with_metadata(new_fields, metadata)), diff --git a/crates/utils/re_arrow_util/src/format.rs b/crates/utils/re_arrow_util/src/format.rs index 3e23047dd645..e76e53ce8c48 100644 --- a/crates/utils/re_arrow_util/src/format.rs +++ b/crates/utils/re_arrow_util/src/format.rs @@ -134,7 +134,7 @@ impl std::fmt::Display for DisplayMetadata { (false, true) => trim_name(value), (false, false) => value, }; - format!("{prefix}{key}: {value}",) + format!("{prefix}{key}: {value}") }) .collect_vec() .join("\n"), @@ -356,7 +356,7 @@ fn format_dataframe_without_metadata( table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); } - let formatters = itertools::izip!(fields.iter(), columns.iter()) + let formatters = itertools::izip!(fields, columns) .map(|(field, array)| custom_array_formatter(field, &**array, redact_non_deterministic)) .collect_vec(); @@ -494,11 +494,13 @@ fn format_cell(string: String, max_cell_content_width: usize) -> Cell { let chars: Vec<_> = string.chars().collect(); if chars.len() > max_cell_content_width { Cell::new( - chars - .into_iter() - .take(max_cell_content_width.saturating_sub(1)) - .chain(['…']) - .collect::(), + std::iter::chain( + chars + .into_iter() + .take(max_cell_content_width.saturating_sub(1)), + ['…'], + ) + .collect::(), ) } else { Cell::new(string) diff --git a/crates/utils/re_arrow_util/src/test_extensions.rs b/crates/utils/re_arrow_util/src/test_extensions.rs index a4c5d31470a0..2e1e3e66c207 100644 --- a/crates/utils/re_arrow_util/src/test_extensions.rs +++ b/crates/utils/re_arrow_util/src/test_extensions.rs @@ -140,15 +140,15 @@ impl RecordBatchTestExt for arrow::array::RecordBatch { } fn sort_rows_by(&self, columns: &[&str]) -> Result { - let sort_exprs = columns + let sort_exprs: Vec<_> = columns .iter() - .map(|column| { + .map(|column| -> Result<_, DataFusionError> { Ok(PhysicalSortExpr::new( col(column, self.schema_ref())?, SortOptions::default(), )) }) - .collect::, DataFusionError>>()?; + .try_collect()?; let Some(ordering) = LexOrdering::new(sort_exprs) else { return Ok(self.clone()); @@ -158,17 +158,17 @@ impl RecordBatchTestExt for arrow::array::RecordBatch { } fn auto_sort_rows(&self) -> Result { - let sort_exprs = self + let sort_exprs: Vec<_> = self .schema() .fields() .iter() - .map(|column| { + .map(|column| -> Result<_, DataFusionError> { Ok(PhysicalSortExpr::new( col(column.name(), self.schema_ref())?, SortOptions::default(), )) }) - .collect::, DataFusionError>>()?; + .try_collect()?; let Some(ordering) = LexOrdering::new(sort_exprs) else { return Ok(self.clone()); @@ -409,7 +409,7 @@ impl SchemaTestExt for arrow::datatypes::Schema { } }); - metadata.into_iter().chain(fields).join("\n") + std::iter::chain(metadata, fields).join("\n") } } diff --git a/crates/utils/re_auth/Cargo.toml b/crates/utils/re_auth/Cargo.toml index 7c519ac4f0f4..54a3547771e8 100644 --- a/crates/utils/re_auth/Cargo.toml +++ b/crates/utils/re_auth/Cargo.toml @@ -28,6 +28,7 @@ oauth = ["dep:directories", "dep:ehttp", "dep:getrandom", "dep:ring", "dep:tiny_ [package.metadata.cargo-shear] ignored = [ + "getrandom", # used by `oauth/api.rs`, only when the `oauth` feature is enabled "getrandom02", # transitive dependency ] diff --git a/crates/utils/re_auth/src/lib.rs b/crates/utils/re_auth/src/lib.rs index 60f6eaa7523b..d18857c63265 100644 --- a/crates/utils/re_auth/src/lib.rs +++ b/crates/utils/re_auth/src/lib.rs @@ -20,7 +20,7 @@ mod claims; mod service; mod token; -/// Rerun Cloud permissions +/// Rerun Hub permissions #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum Permission { /// User can read data. diff --git a/crates/utils/re_auth/src/oauth/api.rs b/crates/utils/re_auth/src/oauth/api.rs index fb4077e8d894..a63c09500cbc 100644 --- a/crates/utils/re_auth/src/oauth/api.rs +++ b/crates/utils/re_auth/src/oauth/api.rs @@ -429,7 +429,7 @@ impl IntoRequest for AuthenticateWithDeviceCode<'_> { fn into_request(self) -> Result { ehttp::Request::post_json( - format_args!("{base}/user_management/authenticate", base = *WORKOS_API,), + format_args!("{base}/user_management/authenticate", base = *WORKOS_API), &self, ) .map_err(Error::Serialize) diff --git a/crates/utils/re_auth/src/provider.rs b/crates/utils/re_auth/src/provider.rs index f6cf68272094..001ee8e5b541 100644 --- a/crates/utils/re_auth/src/provider.rs +++ b/crates/utils/re_auth/src/provider.rs @@ -97,7 +97,7 @@ impl Default for VerificationOptions { fn default() -> Self { Self { // 5 minutes to prevent clock skew - leeway: Some(Duration::from_secs(5 * 60)), + leeway: Some(Duration::from_mins(5)), } } } @@ -164,7 +164,7 @@ impl RedapProvider { } /// Allow users from the given organization to authenticate via - /// their Rerun Cloud credentials. + /// their Rerun Hub credentials. #[cfg(feature = "oauth")] pub async fn with_rerun_cloud_provider(self, org_id: impl Into) -> Result { use crate::oauth::api; diff --git a/crates/utils/re_auth/src/token.rs b/crates/utils/re_auth/src/token.rs index 83a7ea696acf..14c6afcdd909 100644 --- a/crates/utils/re_auth/src/token.rs +++ b/crates/utils/re_auth/src/token.rs @@ -162,7 +162,7 @@ fn extract_allowed_hosts_from_jwt(jwt: &Jwt) -> Result, JwtDecodeErr /// Check if a token's `allowed_hosts` claim permits the given host. /// -/// Works for both Rerun Cloud tokens (RS256, from `WorkOS`) and Redap +/// Works for both Rerun Hub tokens (RS256, from `WorkOS`) and Redap /// machine tokens (HS256, from `generate-token`). /// /// Returns `true` if: diff --git a/crates/utils/re_auth/tests/tokens.rs b/crates/utils/re_auth/tests/tokens.rs index 8ed2636e94ab..750735dddd35 100644 --- a/crates/utils/re_auth/tests/tokens.rs +++ b/crates/utils/re_auth/tests/tokens.rs @@ -19,7 +19,7 @@ fn generate_read_only_token_with_duration() { let token = provider .token( - Duration::from_secs(2 * 60 * 60), + Duration::from_hours(2), "re_auth_test", "test@rerun.io", re_auth::Permission::ReadWrite, diff --git a/crates/utils/re_backoff/Cargo.toml b/crates/utils/re_backoff/Cargo.toml index 0a785032a4ee..fa7fa586a0f0 100644 --- a/crates/utils/re_backoff/Cargo.toml +++ b/crates/utils/re_backoff/Cargo.toml @@ -34,6 +34,8 @@ tokio = { workspace = true, features = ["time"] } [target.'cfg(target_arch = "wasm32")'.dependencies] # Needed to enable `rand` on wasm: getrandom = { workspace = true, features = ["wasm_js"] } +# Used to bridge the `!Send` JS timer future into a `Send` one (oneshot channel). +futures.workspace = true js-sys.workspace = true wasm-bindgen-futures.workspace = true web-sys = { workspace = true, features = ["Window"] } diff --git a/crates/utils/re_backoff/src/lib.rs b/crates/utils/re_backoff/src/lib.rs index 659d9adeae58..25ffa171fc53 100644 --- a/crates/utils/re_backoff/src/lib.rs +++ b/crates/utils/re_backoff/src/lib.rs @@ -1,5 +1,9 @@ //! This module provides a simple exponential back-off generator with jitter (exponent 2, custom base). //! +//! Jitter uses the "full jitter" strategy: each backoff sleeps for a random duration in +//! `[0, base)` (with the default jitter factor). This de-synchronizes concurrent clients retrying +//! the same endpoint, avoiding a thundering herd. +//! //! ### Example //! //! ``` @@ -11,7 +15,8 @@ //! //! let b = generator.gen_next(); //! assert_eq!(b.base(), Duration::from_secs(1)); -//! assert!(b.jittered() >= Duration::from_secs(1) && b.jittered() <= Duration::from_secs_f64(1.0 + 0.5)); +//! // Full jitter: the actual sleep is somewhere in `[0, base)`. +//! assert!(b.jittered() <= b.base()); //! // sleep with: //! // b.sleep().await; //! @@ -19,7 +24,7 @@ //! for expected in expected_backoffs { //! let b = generator.gen_next(); //! assert_eq!(b.base(), Duration::from_secs(expected)); -//! assert!(b.jittered() >= Duration::from_secs(expected) && b.jittered() <= Duration::from_secs(expected + expected / 2)); +//! assert!(b.jittered() <= b.base()); //! } //! ``` @@ -39,10 +44,43 @@ async fn sleep(duration: Duration) { tokio::time::sleep(duration).await; } +/// Run a (possibly `!Send`) wasm future to completion on the local executor, exposing the wait as a +/// `Send` future. +/// +/// `spawn_local` confines the `!Send` future to the single-threaded wasm executor; the future this +/// returns is just the oneshot `Receiver`, which *is* `Send` and never captures `f`. This lets +/// JS-backed futures be awaited from `Send`-bounded contexts (e.g. a backoff sleep threaded through +/// a DataFusion stream in `re_datafusion`). +/// +/// This must be a plain `fn` returning `impl Future + Send` (not an `async fn`): an `async fn` +/// would keep `f` in its own generator state and so be `!Send`. Same technique as +/// `re_datafusion::wasm_compat::make_future_send`, duplicated here to avoid a new crate just for it. +#[cfg(target_arch = "wasm32")] +fn run_local(f: F) -> impl std::future::Future + Send +where + F: std::future::Future + 'static, +{ + use futures::FutureExt as _; + + let (tx, rx) = futures::channel::oneshot::channel::<()>(); + + wasm_bindgen_futures::spawn_local(async move { + f.await; + // The receiver is gone if the caller stopped waiting; nothing to do then. + tx.send(()).ok(); + }); + + // If the spawned task is dropped before it signals, `rx` resolves to `Err`; either way we're + // done waiting. + rx.map(|_result| ()) +} + #[cfg(target_arch = "wasm32")] async fn sleep(duration: Duration) { - // Hack to get async sleep on wasm - async fn sleep_ms(millis: i32) { + let millis = duration.as_millis() as i32; + + // The `setTimeout` + `JsFuture` dance is `!Send`; `run_local` bridges it to a `Send` future. + run_local(async move { let mut cb = |resolve: js_sys::Function, _reject: js_sys::Function| { web_sys::window() .expect("Failed to get window") @@ -53,9 +91,8 @@ async fn sleep(duration: Duration) { wasm_bindgen_futures::JsFuture::from(p) .await .expect("Failed to await sleep promise"); - } - - sleep_ms(duration.as_millis() as i32).await; + }) + .await; } impl Backoff { @@ -95,7 +132,9 @@ pub struct BackoffGenerator { } impl BackoffGenerator { - pub const DEFAULT_JITTER_FACTOR: f64 = 0.5; + /// Default jitter factor: `1.0` means "full jitter", i.e. the sleep is uniformly random in + /// `[0, base)`. See [`Self::new_with_custom_jitter`] for the precise meaning. + pub const DEFAULT_JITTER_FACTOR: f64 = 1.0; /// Create a new `BackoffGenerator` with the given base and max durations. /// A random jitter will be added to the backoff duration with a @@ -104,9 +143,12 @@ impl BackoffGenerator { Self::new_with_custom_jitter(base, max, Self::DEFAULT_JITTER_FACTOR) } - /// Create a new `BackoffGenerator` with the given base and max durations. - /// A random jitter will be added to the backoff duration with a - /// custom `jitter_factor`. + /// Create a new `BackoffGenerator` with the given base and max durations and a custom + /// `jitter_factor` in `[0, 1]`. + /// + /// The jittered sleep is uniformly random in `[(1.0 - jitter_factor) * base, base)`: + /// * `1.0` → `[0, base)` (full jitter, the default). + /// * `0.0` → `[base, base]` (no jitter). pub fn new_with_custom_jitter( base: Duration, max: Duration, @@ -127,9 +169,12 @@ impl BackoffGenerator { } fn jitter(&self, duration: Duration) -> Duration { - // between 0 and self.jitter_factor - let jitter = rand::random::() * self.jitter_factor; - let jittered_secs = duration.as_secs_f64() * (1.0 + jitter); + // Full jitter: pick a random duration in `[(1.0 - jitter_factor) * base, base)`. + // With the default `jitter_factor = 1.0` this is `[0, base)`, which de-synchronizes + // concurrent clients retrying the same endpoint (avoids a thundering herd). + let rand = rand::random::(); // [0, 1) + let factor = (1.0 - self.jitter_factor) + self.jitter_factor * rand; // [1 - jitter_factor, 1) + let jittered_secs = duration.as_secs_f64() * factor; Duration::try_from_secs_f64(jittered_secs).unwrap_or(duration) } @@ -170,3 +215,45 @@ impl BackoffGenerator { self.iteration = 0; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn full_jitter_stays_within_zero_and_base() { + let mut generator = + BackoffGenerator::new(Duration::from_secs(1), Duration::from_secs(8)).unwrap(); + + // Exponential bases, clamped to `max`. + let expected_bases = [1, 2, 4, 8, 8, 8]; + for expected in expected_bases { + // Sample a few times to exercise the randomness. + for _ in 0..100 { + let mut g = BackoffGenerator::new(generator.base, generator.max).unwrap(); + g.iteration = generator.iteration; + let b = g.gen_next(); + assert_eq!(b.base(), Duration::from_secs(expected)); + // Full jitter: `[0, base)`. + assert!(b.jittered() <= b.base()); + } + generator.gen_next(); + } + } + + #[test] + fn zero_jitter_factor_yields_exactly_base() { + let mut generator = BackoffGenerator::new_with_custom_jitter( + Duration::from_millis(100), + Duration::from_secs(1), + 0.0, + ) + .unwrap(); + + for _ in 0..100 { + let b = generator.gen_next(); + assert_eq!(b.jittered(), b.base()); + generator.reset(); + } + } +} diff --git a/crates/utils/re_byte_size/Cargo.toml b/crates/utils/re_byte_size/Cargo.toml index 8b4d4ce1bb7e..5885424b64ba 100644 --- a/crates/utils/re_byte_size/Cargo.toml +++ b/crates/utils/re_byte_size/Cargo.toml @@ -21,19 +21,24 @@ all-features = true [features] ecolor = ["dep:ecolor"] +egui = ["dep:egui", "ecolor"] glam = ["dep:glam"] +macaw = ["dep:macaw"] [dependencies] +re_byte_size_derive = { workspace = true } + arrow.workspace = true half.workspace = true parking_lot.workspace = true smallvec.workspace = true vec1.workspace = true # Small enough to always depend on it. -# Optional dependencies: ecolor = { workspace = true, optional = true } +egui = { workspace = true, optional = true } glam = { workspace = true, optional = true } +macaw = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/utils/re_byte_size/src/egui_sizes.rs b/crates/utils/re_byte_size/src/egui_sizes.rs new file mode 100644 index 000000000000..3605a5ceb5f2 --- /dev/null +++ b/crates/utils/re_byte_size/src/egui_sizes.rs @@ -0,0 +1,17 @@ +use egui::emath; + +use crate::SizeBytes; + +impl SizeBytes for emath::History { + fn heap_size_bytes(&self) -> u64 { + let s = std::mem::size_of::<(f64, T)>() as u64 * self.len() as u64; + if T::IS_POD { + s + } else { + s + self + .iter() + .map(|t: (f64, T)| t.heap_size_bytes()) + .sum::() + } + } +} diff --git a/crates/utils/re_byte_size/src/lib.rs b/crates/utils/re_byte_size/src/lib.rs index 58cd7e6f6737..839448913e83 100644 --- a/crates/utils/re_byte_size/src/lib.rs +++ b/crates/utils/re_byte_size/src/lib.rs @@ -2,6 +2,8 @@ mod arrow_sizes; mod bookkeeping_btreemap; +#[cfg(feature = "egui")] +mod egui_sizes; mod mem_usage_tree; mod parking_lot_sizes; mod primitive_sizes; @@ -16,13 +18,17 @@ pub use self::mem_usage_tree::{ MemUsageNode, MemUsageTree, MemUsageTreeCapture, NamedMemUsageTree, }; +/// Derive macro for the `SizeBytes` trait. +pub use re_byte_size_derive::SizeBytes; + // --- /// Approximations of stack and heap size for both internal and external types. /// -/// Motly used for statistics and triggering events such as garbage collection. -// TODO(#8630): Derive macro for this trait. +/// Mostly used for statistics and triggering events such as garbage collection. pub trait SizeBytes { + const IS_POD: bool = false; + /// Returns the total size of `self` in bytes, accounting for both stack and heap space. #[inline] fn total_size_bytes(&self) -> u64 { @@ -45,12 +51,4 @@ pub trait SizeBytes { /// If we however are the sole owner of the memory (e.g. a `Vec`), then we return /// the heap size of all children plus the capacity of the buffer. fn heap_size_bytes(&self) -> u64; - - /// Is `Self` just plain old data? - /// - /// If `true`, this will make most blanket implementations of `SizeBytes` much faster (e.g. `Vec`). - #[inline] - fn is_pod() -> bool { - false - } } diff --git a/crates/utils/re_byte_size/src/mem_usage_tree.rs b/crates/utils/re_byte_size/src/mem_usage_tree.rs index 932357fbe08c..f97049a91f19 100644 --- a/crates/utils/re_byte_size/src/mem_usage_tree.rs +++ b/crates/utils/re_byte_size/src/mem_usage_tree.rs @@ -2,6 +2,7 @@ /// A snapshot of memory usage of a value, /// produced by [`MemUsageTreeCapture::capture_mem_usage_tree`]. +#[derive(Clone)] pub enum MemUsageTree { /// A leaf node with a known size in bytes. Bytes(u64), @@ -33,6 +34,7 @@ impl MemUsageTree { } /// A named child in a [`MemUsageNode`]. +#[derive(Clone)] pub struct NamedMemUsageTree { /// Name of this child node. pub name: String, @@ -55,7 +57,7 @@ impl NamedMemUsageTree { } /// A node in a [`MemUsageTree`] with children. -#[derive(Default)] +#[derive(Default, Clone)] pub struct MemUsageNode { /// Children of this node. children: Vec, diff --git a/crates/utils/re_byte_size/src/primitive_sizes.rs b/crates/utils/re_byte_size/src/primitive_sizes.rs index 050d5f9b8ec2..fdb4d54453e8 100644 --- a/crates/utils/re_byte_size/src/primitive_sizes.rs +++ b/crates/utils/re_byte_size/src/primitive_sizes.rs @@ -8,15 +8,12 @@ use crate::SizeBytes; macro_rules! impl_size_bytes_pod { ($ty:ty) => { impl SizeBytes for $ty { + const IS_POD: bool = true; + #[inline] fn heap_size_bytes(&self) -> u64 { 0 } - - #[inline] - fn is_pod() -> bool { - true - } } }; ($ty:ty, $($rest:ty),+) => { @@ -25,12 +22,63 @@ macro_rules! impl_size_bytes_pod { } impl_size_bytes_pod!( - u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, bool, f32, f64 + u8, + u16, + u32, + u64, + u128, + usize, + i8, + i16, + i32, + i64, + i128, + bool, + f32, + f64, + std::num::NonZeroU8, + std::num::NonZeroU16, + std::num::NonZeroU32, + std::num::NonZeroU64, + std::num::NonZeroU128, + std::num::NonZeroUsize, + std::num::NonZeroI8, + std::num::NonZeroI16, + std::num::NonZeroI32, + std::num::NonZeroI64, + std::num::NonZeroI128, + std::num::NonZeroIsize, + std::sync::atomic::AtomicU8, + std::sync::atomic::AtomicU16, + std::sync::atomic::AtomicU32, + std::sync::atomic::AtomicU64, + std::sync::atomic::AtomicUsize, + std::sync::atomic::AtomicI8, + std::sync::atomic::AtomicI16, + std::sync::atomic::AtomicI32, + std::sync::atomic::AtomicI64, + std::sync::atomic::AtomicIsize, + std::sync::atomic::AtomicBool, + &'static str, + &'static [u8], + std::time::Duration ); impl_size_bytes_pod!(half::f16); #[cfg(feature = "ecolor")] impl_size_bytes_pod!(ecolor::Color32); +#[cfg(feature = "egui")] +impl_size_bytes_pod!(egui::Id, egui::Pos2, egui::Rect, egui::Vec2); + #[cfg(feature = "glam")] -impl_size_bytes_pod!(glam::Vec3, glam::DAffine3); +impl_size_bytes_pod!( + glam::Mat3, + glam::Quat, + glam::Vec2, + glam::Vec3, + glam::DAffine3 +); + +#[cfg(feature = "macaw")] +impl_size_bytes_pod!(macaw::BoundingBox, macaw::IsoTransform); diff --git a/crates/utils/re_byte_size/src/smallvec_sizes.rs b/crates/utils/re_byte_size/src/smallvec_sizes.rs index 9ce231e77ddf..51867ef64607 100644 --- a/crates/utils/re_byte_size/src/smallvec_sizes.rs +++ b/crates/utils/re_byte_size/src/smallvec_sizes.rs @@ -10,14 +10,14 @@ impl SizeBytes for SmallVec<[T; N]> { // The `SmallVec` is still smaller than the threshold so no heap data has been // allocated yet, beyond the heap data each element might have. - if T::is_pod() { + if T::IS_POD { 0 // early-out } else { self.iter().map(SizeBytes::heap_size_bytes).sum::() } } else { // NOTE: It's all on the heap at this point. - if T::is_pod() { + if T::IS_POD { (self.capacity() * std::mem::size_of::()) as _ } else { (self.capacity() * std::mem::size_of::()) as u64 diff --git a/crates/utils/re_byte_size/src/std_sizes.rs b/crates/utils/re_byte_size/src/std_sizes.rs index 5bdc09f4c5a5..16255d0b3040 100644 --- a/crates/utils/re_byte_size/src/std_sizes.rs +++ b/crates/utils/re_byte_size/src/std_sizes.rs @@ -2,8 +2,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use std::mem::size_of; -use std::ops::RangeInclusive; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use crate::SizeBytes; @@ -72,13 +71,13 @@ impl SizeBytes for BTreeMap { // so there's no tuple padding like in HashMap. let base_size = btree_heap_size(self.len(), size_of::() + size_of::()); - let heap_in_keys = if K::is_pod() { + let heap_in_keys = if K::IS_POD { 0 } else { self.keys().map(SizeBytes::heap_size_bytes).sum::() }; - let heap_in_values = if V::is_pod() { + let heap_in_values = if V::IS_POD { 0 } else { self.values().map(SizeBytes::heap_size_bytes).sum::() @@ -94,7 +93,7 @@ impl SizeBytes for BTreeSet { // NOTE: It's all on the heap at this point. let base_size = btree_heap_size(self.len(), size_of::()); - let heap_in_keys = if K::is_pod() { + let heap_in_keys = if K::IS_POD { 0 } else { self.iter().map(SizeBytes::heap_size_bytes).sum::() @@ -139,13 +138,13 @@ impl SizeBytes for HashMap { // For example, (u32, u8) takes 8 bytes, not 5. let entry_size = (num_slots * size_of::<(K, V)>()) as u64; - let heap_in_keys = if K::is_pod() { + let heap_in_keys = if K::IS_POD { 0 } else { self.keys().map(SizeBytes::heap_size_bytes).sum::() }; - let heap_in_values = if V::is_pod() { + let heap_in_values = if V::IS_POD { 0 } else { self.values().map(SizeBytes::heap_size_bytes).sum::() @@ -166,7 +165,7 @@ impl SizeBytes for HashSet { let entry_size = (num_slots * size_of::()) as u64; - let heap_in_keys = if K::is_pod() { + let heap_in_keys = if K::IS_POD { 0 } else { self.iter().map(SizeBytes::heap_size_bytes).sum::() @@ -184,7 +183,7 @@ impl SizeBytes for HashSet { impl SizeBytes for [T; N] { #[inline] fn heap_size_bytes(&self) -> u64 { - if T::is_pod() { + if T::IS_POD { 0 // it's a const-sized array } else { self.iter().map(SizeBytes::heap_size_bytes).sum::() @@ -196,7 +195,7 @@ impl SizeBytes for Vec { #[inline] fn heap_size_bytes(&self) -> u64 { // NOTE: It's all on the heap at this point. - if T::is_pod() { + if T::IS_POD { (self.capacity() * size_of::()) as _ } else { (self.capacity() * size_of::()) as u64 @@ -205,6 +204,18 @@ impl SizeBytes for Vec { } } +impl SizeBytes for Box<[T]> { + fn heap_size_bytes(&self) -> u64 { + let slice_size = (self.len() * size_of::()) as u64; + + if T::IS_POD { + slice_size + } else { + slice_size + self.iter().map(SizeBytes::heap_size_bytes).sum::() + } + } +} + impl SizeBytes for std::borrow::Cow<'_, [T]> { #[inline] fn heap_size_bytes(&self) -> u64 { @@ -219,7 +230,7 @@ impl SizeBytes for VecDeque { #[inline] fn heap_size_bytes(&self) -> u64 { // NOTE: It's all on the heap at this point. - if T::is_pod() { + if T::IS_POD { (self.capacity() * size_of::()) as _ } else { (self.capacity() * size_of::()) as u64 @@ -264,16 +275,33 @@ impl SizeBytes for Arc { } } -impl SizeBytes for Box { +impl SizeBytes for Weak { + const IS_POD: bool = true; + + #[inline] + fn heap_size_bytes(&self) -> u64 { + // Not owned, so don't count the size. + 0 + } +} + +impl SizeBytes for Box { #[inline] fn heap_size_bytes(&self) -> u64 { T::total_size_bytes(&**self) } } -impl SizeBytes for RangeInclusive { +impl SizeBytes for std::ops::RangeInclusive { #[inline] fn heap_size_bytes(&self) -> u64 { self.start().heap_size_bytes() + self.end().heap_size_bytes() } } + +impl SizeBytes for core::range::RangeInclusive { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.start.heap_size_bytes() + self.last.heap_size_bytes() + } +} diff --git a/crates/utils/re_byte_size/src/tuple_sizes.rs b/crates/utils/re_byte_size/src/tuple_sizes.rs index db96d21a0c79..fdf5624bf86b 100644 --- a/crates/utils/re_byte_size/src/tuple_sizes.rs +++ b/crates/utils/re_byte_size/src/tuple_sizes.rs @@ -1,15 +1,12 @@ use crate::SizeBytes; impl SizeBytes for () { + const IS_POD: bool = true; + #[inline] fn heap_size_bytes(&self) -> u64 { 0 } - - #[inline] - fn is_pod() -> bool { - true - } } impl SizeBytes for (T, U) @@ -17,16 +14,13 @@ where T: SizeBytes, U: SizeBytes, { + const IS_POD: bool = T::IS_POD && U::IS_POD; + #[inline] fn heap_size_bytes(&self) -> u64 { let (a, b) = self; a.heap_size_bytes() + b.heap_size_bytes() } - - #[inline] - fn is_pod() -> bool { - T::is_pod() && U::is_pod() - } } impl SizeBytes for (T, U, V) @@ -35,16 +29,13 @@ where U: SizeBytes, V: SizeBytes, { + const IS_POD: bool = T::IS_POD && U::IS_POD && V::IS_POD; + #[inline] fn heap_size_bytes(&self) -> u64 { let (a, b, c) = self; a.heap_size_bytes() + b.heap_size_bytes() + c.heap_size_bytes() } - - #[inline] - fn is_pod() -> bool { - T::is_pod() && U::is_pod() && V::is_pod() - } } impl SizeBytes for (T, U, V, W) @@ -54,14 +45,11 @@ where V: SizeBytes, W: SizeBytes, { + const IS_POD: bool = T::IS_POD && U::IS_POD && V::IS_POD && W::IS_POD; + #[inline] fn heap_size_bytes(&self) -> u64 { let (a, b, c, d) = self; a.heap_size_bytes() + b.heap_size_bytes() + c.heap_size_bytes() + d.heap_size_bytes() } - - #[inline] - fn is_pod() -> bool { - T::is_pod() && U::is_pod() && V::is_pod() && W::is_pod() - } } diff --git a/crates/utils/re_byte_size/tests/derive.rs b/crates/utils/re_byte_size/tests/derive.rs new file mode 100644 index 000000000000..53ab8c02233e --- /dev/null +++ b/crates/utils/re_byte_size/tests/derive.rs @@ -0,0 +1,147 @@ +#![expect(clippy::assertions_on_constants)] // We use these for the test. + +// `re_byte_size` re-exports our derive macro behind its (default) `derive` feature, so a single +// import brings both the trait and the macro into scope — exactly how downstream crates use it. +use re_byte_size::SizeBytes; + +#[derive(SizeBytes)] +struct Pod { + x: u32, + y: f32, +} + +#[derive(SizeBytes)] +struct Named { + a: Vec, + b: String, +} + +#[derive(SizeBytes)] +struct Tuple(Vec, u32); + +#[derive(SizeBytes)] +struct Unit; + +#[derive(SizeBytes)] +struct WithIgnored { + keep: Vec, + + #[size_bytes(ignore)] + skip: Vec, +} + +#[derive(SizeBytes)] +struct Outer { + inner: Pod, + name: String, +} + +#[derive(SizeBytes)] +struct Generic { + items: Vec, +} + +#[derive(SizeBytes)] +enum MyEnum { + Unit, + Tuple(Vec, u32), + Named { data: String }, + Ignoring(#[size_bytes(ignore)] Vec, u32), +} + +// `crate_root` names the `re_byte_size` crate directly, sidestepping the `Cargo.toml` lookup. +#[derive(SizeBytes)] +#[size_bytes(crate_root = re_byte_size)] +struct WithCrateRoot { + a: Vec, +} + +#[test] +fn pod_struct_has_no_heap() { + assert!(Pod::IS_POD); + assert_eq!(Pod { x: 1, y: 2.0 }.heap_size_bytes(), 0); +} + +#[test] +fn unit_struct_has_no_heap() { + assert!(Unit::IS_POD); + assert_eq!(Unit.heap_size_bytes(), 0); +} + +#[test] +fn named_struct_sums_its_fields() { + let value = Named { + a: vec![1, 2, 3], + b: "hello".to_owned(), + }; + assert!(!Named::IS_POD); + assert_eq!( + value.heap_size_bytes(), + value.a.heap_size_bytes() + value.b.heap_size_bytes() + ); +} + +#[test] +fn tuple_struct_sums_its_fields() { + let value = Tuple(vec![1, 2, 3], 7); + assert!(!Tuple::IS_POD); + assert_eq!(value.heap_size_bytes(), value.0.heap_size_bytes()); +} + +#[test] +fn ignored_field_is_left_out() { + let value = WithIgnored { + keep: vec![1, 2, 3], + skip: vec![4, 5, 6, 7], + }; + assert_eq!(value.heap_size_bytes(), value.keep.heap_size_bytes()); +} + +#[test] +fn pod_ness_propagates_through_nesting() { + // `Pod` is POD, so `Outer` is POD exactly when `name` would be — it isn't. + assert!(!Outer::IS_POD); + let value = Outer { + inner: Pod { x: 1, y: 2.0 }, + name: "hello".to_owned(), + }; + assert_eq!(value.heap_size_bytes(), value.name.heap_size_bytes()); +} + +#[test] +fn generic_struct_sums_its_fields() { + let value = Generic { + items: vec![1u32, 2, 3], + }; + assert!(!Generic::::IS_POD); + assert_eq!(value.heap_size_bytes(), value.items.heap_size_bytes()); +} + +#[test] +fn enum_sizes_the_active_variant() { + assert!(!MyEnum::IS_POD); + + assert_eq!(MyEnum::Unit.heap_size_bytes(), 0); + + let data = vec![1u8, 2, 3]; + assert_eq!( + MyEnum::Tuple(data.clone(), 7).heap_size_bytes(), + data.heap_size_bytes() + ); + + let text = "hello".to_owned(); + assert_eq!( + MyEnum::Named { data: text.clone() }.heap_size_bytes(), + text.heap_size_bytes() + ); + + // The first field is ignored, the second is POD, so the variant has no heap. + assert_eq!(MyEnum::Ignoring(vec![1, 2, 3], 9).heap_size_bytes(), 0); +} + +#[test] +fn crate_root_override_sizes_its_fields() { + let value = WithCrateRoot { a: vec![1, 2, 3] }; + assert!(!WithCrateRoot::IS_POD); + assert_eq!(value.heap_size_bytes(), value.a.heap_size_bytes()); +} diff --git a/crates/utils/re_byte_size_derive/Cargo.toml b/crates/utils/re_byte_size_derive/Cargo.toml new file mode 100644 index 000000000000..707a8f749f58 --- /dev/null +++ b/crates/utils/re_byte_size_derive/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "re_byte_size_derive" +authors.workspace = true +description = "Derive macro for the `SizeBytes` trait from `re_byte_size`." +edition.workspace = true +homepage.workspace = true +include.workspace = true +license.workspace = true +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + + +[package.metadata.docs.rs] +all-features = true + + +[lib] +proc-macro = true + + +[dependencies] +proc-macro-crate.workspace = true +proc-macro2 = { workspace = true, features = ["proc-macro"] } +quote.workspace = true +syn.workspace = true diff --git a/crates/utils/re_byte_size_derive/README.md b/crates/utils/re_byte_size_derive/README.md new file mode 100644 index 000000000000..6b6ace3ea173 --- /dev/null +++ b/crates/utils/re_byte_size_derive/README.md @@ -0,0 +1,10 @@ +# re_byte_size_derive + +Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. + +[![Latest version](https://img.shields.io/crates/v/re_byte_size_derive.svg)](https://crates.io/crates/re_byte_size_derive) +[![Documentation](https://docs.rs/re_byte_size_derive/badge.svg)](https://docs.rs/re_byte_size_derive) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +Derive macro for the `SizeBytes` trait from [`re_byte_size`](https://crates.io/crates/re_byte_size). diff --git a/crates/utils/re_byte_size_derive/src/lib.rs b/crates/utils/re_byte_size_derive/src/lib.rs new file mode 100644 index 000000000000..dc9f70dcaac9 --- /dev/null +++ b/crates/utils/re_byte_size_derive/src/lib.rs @@ -0,0 +1,336 @@ +//! Derive macro for the `SizeBytes` trait from `re_byte_size`. +//! +//! ```ignore +//! use ::re_byte_size::SizeBytes; +//! use re_byte_size_derive::SizeBytes; +//! +//! #[derive(SizeBytes)] +//! struct Foo { +//! name: String, +//! values: Vec, +//! +//! #[size_bytes(ignore)] +//! cache: Vec, +//! } +//! ``` + +use std::cell::OnceCell; + +use proc_macro::TokenStream; +use proc_macro_crate::{FoundCrate, crate_name}; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::{ + Data, DataEnum, DeriveInput, Field, Fields, GenericParam, Generics, Index, Member, Type, + parse_macro_input, +}; + +thread_local! { + /// The resolved path to the `SizeBytes` trait, as text, for the crate currently being compiled. + static SIZE_BYTES_PATH: OnceCell> = const { OnceCell::new() }; +} + +/// The path the generated code uses to name the `SizeBytes` trait. +/// +/// Internal crates depend on `re_byte_size` directly, so `::re_byte_size::SizeBytes` works. +/// External users only depend on `rerun`, which re-exports the trait as `rerun::SizeBytes`. +/// Errors at `span` when the consuming crate depends on neither. +fn size_bytes_path(span: Span) -> syn::Result { + let path = SIZE_BYTES_PATH.with(|cell| cell.get_or_init(resolve_size_bytes_path).clone()); + match path { + Some(path) => Ok(path + .parse() + .expect("resolved trait path should be valid tokens")), + None => Err(syn::Error::new( + span, + "`SizeBytes` trait not found in default locations, either manually set location with `#[size_bytes(crate_root = some::path)]`, or depend on `rerun` or `re_byte_size`", + )), + } +} + +fn resolve_size_bytes_path() -> Option { + let found = crate_name("re_byte_size") + .ok() + .or_else(|| crate_name("rerun").ok())?; + let krate = match found { + FoundCrate::Itself => "crate".to_owned(), + FoundCrate::Name(name) => format!("::{name}"), + }; + Some(format!("{krate}::SizeBytes")) +} + +/// Derives `SizeBytes` for a struct or enum. +/// +/// The generated `heap_size_bytes` sums the heap size of every field. The associated `const IS_POD` +/// is `true` only when every field type is itself POD, in which case `heap_size_bytes` +/// short-circuits to `0`. +/// +/// Annotate a field with `#[size_bytes(ignore)]` to leave it out of both the sum and the `IS_POD` +/// computation. +/// +/// Annotate the type with `#[size_bytes(profile)]` to insert a `re_tracing::profile_function!()` +/// at the top of the generated `heap_size_bytes`. The consuming crate must depend on `re_tracing`. +/// +/// By default the macro finds the `re_byte_size` crate by reading the consuming crate's `Cargo.toml`. +/// Annotate the type with `#[size_bytes(crate_root = some::path::to::re_byte_size)]` to name it +/// directly instead, skipping that lookup. +#[proc_macro_derive(SizeBytes, attributes(size_bytes))] +pub fn derive_size_bytes(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +fn expand(input: &DeriveInput) -> syn::Result { + let name = &input.ident; + let options = parse_type_options(&input.attrs)?; + + // An explicit `crate_root` sidesteps the (somewhat costly) `Cargo.toml` lookup. + let trait_path = match &options.crate_root { + Some(crate_root) => quote! { #crate_root::SizeBytes }, + None => size_bytes_path(name.span())?, + }; + + let generics = add_trait_bounds(input.generics.clone(), &trait_path); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let (is_pod, heap_size_body) = match &input.data { + Data::Struct(data) => struct_body(&data.fields, &trait_path)?, + Data::Enum(data) => enum_body(data, &trait_path)?, + Data::Union(_) => { + return Err(syn::Error::new( + input.ident.span(), + "`SizeBytes` cannot be derived for unions", + )); + } + }; + + let profile_stmt = if options.profile { + quote! { ::re_tracing::profile_function!(); } + } else { + quote!() + }; + + Ok(quote! { + #[automatically_derived] + impl #impl_generics #trait_path for #name #ty_generics #where_clause { + const IS_POD: bool = #is_pod; + + fn heap_size_bytes(&self) -> u64 { + #profile_stmt + #heap_size_body + } + } + }) +} + +/// Type-level `#[size_bytes(...)]` options. +#[derive(Default)] +struct TypeOptions { + /// Insert a `re_tracing::profile_function!()` at the top of `heap_size_bytes`. + profile: bool, + + /// Path to the `re_byte_size` crate, overriding the automatic lookup. + crate_root: Option, +} + +fn parse_type_options(attrs: &[syn::Attribute]) -> syn::Result { + let mut options = TypeOptions::default(); + for attr in attrs { + if !attr.path().is_ident("size_bytes") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("profile") { + options.profile = true; + Ok(()) + } else if meta.path.is_ident("crate_root") { + options.crate_root = Some(meta.value()?.parse()?); + Ok(()) + } else { + Err(meta.error("unknown `size_bytes` option, expected `profile` or `crate_root`")) + } + })?; + } + Ok(options) +} + +/// Builds the `IS_POD` expression and `heap_size_bytes` body for a struct. +fn struct_body( + fields: &Fields, + trait_path: &TokenStream2, +) -> syn::Result<(TokenStream2, TokenStream2)> { + let mut members = Vec::new(); + let mut types = Vec::new(); + let mut ignored = Vec::new(); + for (index, field) in fields.iter().enumerate() { + let member = member_for(index, field); + if is_ignored(field)? { + ignored.push(member); + continue; + } + members.push(member); + types.push(&field.ty); + } + + let is_pod = all_pod_expr(&types, trait_path); + + let terms: Vec = std::iter::zip(&members, &types) + .map(|(member, ty)| { + quote! { + (if <#ty as #trait_path>::IS_POD { + 0 + } else { + #trait_path::heap_size_bytes(&self.#member) + }) + } + }) + .collect(); + let sum = sum_expr(&terms); + + let body = quote! { + // Read the ignored fields so they don't trip the `dead_code` lint. + #( let _ = &self.#ignored; )* + #sum + }; + + Ok((is_pod, body)) +} + +/// Builds the `IS_POD` expression and `heap_size_bytes` body for an enum. +fn enum_body( + data: &DataEnum, + trait_path: &TokenStream2, +) -> syn::Result<(TokenStream2, TokenStream2)> { + let mut all_types: Vec<&Type> = Vec::new(); + let mut arms: Vec = Vec::new(); + + for variant in &data.variants { + let variant_ident = &variant.ident; + let mut bindings: Vec = Vec::new(); + let mut binding_types: Vec<&Type> = Vec::new(); + + let pattern = match &variant.fields { + Fields::Named(named) => { + let mut patterns = Vec::new(); + for field in &named.named { + let ident = field.ident.clone().expect("named field has an identifier"); + if is_ignored(field)? { + // Bind to an underscore name so the field is read (no `dead_code`) but + // still counts as unused (no `unused_variables`). + let binding = format_ident!("_{}", ident); + patterns.push(quote! { #ident: #binding }); + continue; + } + all_types.push(&field.ty); + binding_types.push(&field.ty); + patterns.push(quote! { #ident }); + bindings.push(ident); + } + quote! { Self::#variant_ident { #(#patterns),* } } + } + Fields::Unnamed(unnamed) => { + let mut patterns = Vec::new(); + for (index, field) in unnamed.unnamed.iter().enumerate() { + if is_ignored(field)? { + let binding = format_ident!("_field_{}", index); + patterns.push(quote! { #binding }); + continue; + } + all_types.push(&field.ty); + binding_types.push(&field.ty); + let binding = format_ident!("field_{}", index); + patterns.push(quote! { #binding }); + bindings.push(binding); + } + quote! { Self::#variant_ident( #(#patterns),* ) } + } + Fields::Unit => quote! { Self::#variant_ident }, + }; + + let terms: Vec = std::iter::zip(&bindings, &binding_types) + .map(|(binding, ty)| { + quote! { + (if <#ty as #trait_path>::IS_POD { + 0 + } else { + #trait_path::heap_size_bytes(#binding) + }) + } + }) + .collect(); + let sum = sum_expr(&terms); + arms.push(quote! { #pattern => #sum, }); + } + + let is_pod = all_pod_expr(&all_types, trait_path); + + let body = if data.variants.is_empty() { + quote! { 0 } + } else { + quote! { + match self { + #(#arms)* + } + } + }; + + Ok((is_pod, body)) +} + +/// The accessor used to reach a field through `self`, e.g. `name` or `0`. +fn member_for(index: usize, field: &Field) -> Member { + match &field.ident { + Some(ident) => Member::Named(ident.clone()), + None => Member::Unnamed(Index::from(index)), + } +} + +/// A `bool` expression that is `true` only when every given type is POD. +fn all_pod_expr(types: &[&Type], trait_path: &TokenStream2) -> TokenStream2 { + match types.split_first() { + None => quote! { true }, + Some((first, rest)) => quote! { + <#first as #trait_path>::IS_POD + #( && <#rest as #trait_path>::IS_POD )* + }, + } +} + +/// Joins the heap-size terms with `+`, or `0` when there are none. +fn sum_expr(terms: &[TokenStream2]) -> TokenStream2 { + match terms.split_first() { + None => quote! { 0 }, + Some((first, rest)) => quote! { #first #( + #rest )* }, + } +} + +/// Bounds every generic type parameter with `SizeBytes`. +fn add_trait_bounds(mut generics: Generics, trait_path: &TokenStream2) -> Generics { + for param in &mut generics.params { + if let GenericParam::Type(type_param) = param { + type_param.bounds.push(syn::parse_quote!(#trait_path)); + } + } + generics +} + +/// Whether a field carries `#[size_bytes(ignore)]`. +fn is_ignored(field: &Field) -> syn::Result { + let mut ignored = false; + for attr in &field.attrs { + if !attr.path().is_ident("size_bytes") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("ignore") { + ignored = true; + Ok(()) + } else { + Err(meta.error("unknown `size_bytes` option, expected `ignore`")) + } + })?; + } + Ok(ignored) +} diff --git a/crates/utils/re_case/src/lib.rs b/crates/utils/re_case/src/lib.rs index cf3c5895db66..3655ec9c6e5d 100644 --- a/crates/utils/re_case/src/lib.rs +++ b/crates/utils/re_case/src/lib.rs @@ -21,6 +21,7 @@ pub fn to_snake_case(s: &str) -> String { if let Some(last) = parts.last_mut() { *last = last .replace("UVec", "uvec") + .replace("IVec", "ivec") .replace("DVec", "dvec") .replace("UInt", "uint"); *last = rerun_snake.convert(last.as_str()); @@ -56,6 +57,14 @@ fn test_to_snake_case() { to_snake_case("rerun.datatypes.uvec2d"), "rerun.datatypes.uvec2d" ); + assert_eq!( + to_snake_case("rerun.datatypes.IVec3D"), + "rerun.datatypes.ivec3d" + ); + assert_eq!( + to_snake_case("rerun.datatypes.ivec3d"), + "rerun.datatypes.ivec3d" + ); assert_eq!( to_snake_case("rerun.datatypes.UInt32"), @@ -111,6 +120,7 @@ pub fn to_pascal_case(s: &str) -> String { if let Some(last) = parts.last_mut() { *last = last .replace("uvec", "UVec") + .replace("ivec", "IVec") .replace("dvec", "DVec") .replace("uint", "UInt") .replace("2d", "2D") // NOLINT @@ -140,6 +150,14 @@ fn test_to_pascal_case() { to_pascal_case("rerun.datatypes.UVec2D"), "rerun.datatypes.UVec2D" ); + assert_eq!( + to_pascal_case("rerun.datatypes.ivec3d"), + "rerun.datatypes.IVec3D" + ); + assert_eq!( + to_pascal_case("rerun.datatypes.IVec3D"), + "rerun.datatypes.IVec3D" + ); assert_eq!( to_pascal_case("rerun.datatypes.uint32"), @@ -192,8 +210,10 @@ pub fn to_human_case(s: &str) -> String { *last = rerun_human.convert(last.as_str()); *last = last .replace("Uvec", "UVec") + .replace("Ivec", "IVec") .replace("Uint", "UInt") .replace("U vec", "UVec") + .replace("I vec", "IVec") .replace("U int", "UInt") .replace("Int 32", "Int32") .replace("mat 3x 3", "mat3x3") @@ -224,6 +244,14 @@ fn test_to_human_case() { to_human_case("rerun.datatypes.UVec2D"), "rerun.datatypes.UVec 2D" ); + assert_eq!( + to_human_case("rerun.datatypes.ivec3d"), + "rerun.datatypes.IVec 3D" + ); + assert_eq!( + to_human_case("rerun.datatypes.IVec3D"), + "rerun.datatypes.IVec 3D" + ); assert_eq!( to_human_case("rerun.datatypes.uint32"), diff --git a/crates/utils/re_crash_handler/src/lib.rs b/crates/utils/re_crash_handler/src/lib.rs index ef3027501cfb..2cab40f8cedf 100644 --- a/crates/utils/re_crash_handler/src/lib.rs +++ b/crates/utils/re_crash_handler/src/lib.rs @@ -216,9 +216,7 @@ pub fn callstack_from(start_patterns: &[&str]) -> String { // Trim it a bit: let mut stack = stack.as_str(); - let start_patterns = start_patterns - .iter() - .chain(std::iter::once(&"callstack_from")); + let start_patterns = std::iter::chain(start_patterns, std::iter::once(&"callstack_from")); // Trim the top (closest to the panic handler) to cut out some noise: for start_pattern in start_patterns { diff --git a/crates/utils/re_format/src/lib.rs b/crates/utils/re_format/src/lib.rs index 4035b626f6d9..ba744d57dfd7 100644 --- a/crates/utils/re_format/src/lib.rs +++ b/crates/utils/re_format/src/lib.rs @@ -576,6 +576,40 @@ pub fn format_bytes(number_of_bytes: f64) -> String { } } +/// Pretty format a bitrate (bits per second) using decimal SI notation (base 10). +/// +/// Note: bitrate is conventionally given in decimal (base 1000) units, not binary (base 1024) units. +/// +/// ``` +/// # use re_format::format_bits_per_second; +/// assert_eq!(format_bits_per_second(123.0), "123 bit/s"); +/// assert_eq!(format_bits_per_second(12_345.0), "12.3 kbit/s"); +/// assert_eq!(format_bits_per_second(1_234_567.0), "1.2 Mbit/s"); +/// assert_eq!(format_bits_per_second(1_234_567_890.0), "1.2 Gbit/s"); +/// ``` +pub fn format_bits_per_second(bits_per_second: f64) -> String { + if bits_per_second < 0.0 { + return format!("{MINUS}{}", format_bits_per_second(-bits_per_second)); + } + + // Bitrate is conventionally given in decimal (base 1000) units. + let (value, unit) = if bits_per_second < 1e3 { + (bits_per_second, "bit/s") + } else if bits_per_second < 1e6 { + (bits_per_second / 1e3, "kbit/s") + } else if bits_per_second < 1e9 { + (bits_per_second / 1e6, "Mbit/s") + } else { + (bits_per_second / 1e9, "Gbit/s") + }; + + if unit == "bit/s" { + format!("{value:.0} {unit}") + } else { + format!("{value:.1} {unit}") + } +} + #[test] fn test_format_bytes() { let test_cases = [ diff --git a/crates/utils/re_grpc_headers/Cargo.toml b/crates/utils/re_grpc_headers/Cargo.toml new file mode 100644 index 000000000000..cbf73771770d --- /dev/null +++ b/crates/utils/re_grpc_headers/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "re_grpc_headers" +authors.workspace = true +description = "Rerun gRPC header conventions: well-known header names, the `RerunVersionInterceptor`, and the tower `Layer` machinery that propagates them across requests and responses." +edition.workspace = true +homepage.workspace = true +include.workspace = true +license.workspace = true +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + + +[dependencies] +http.workspace = true +pin-project-lite.workspace = true +tonic.workspace = true +tower.workspace = true diff --git a/crates/utils/re_grpc_headers/README.md b/crates/utils/re_grpc_headers/README.md new file mode 100644 index 000000000000..bc8630757f34 --- /dev/null +++ b/crates/utils/re_grpc_headers/README.md @@ -0,0 +1,7 @@ +# re_grpc_headers + +Rerun gRPC header conventions. + +Contains the well-known `x-rerun-*` header names, the `RerunVersionInterceptor` that stamps every outbound request with the client (or server) identity and version, the matching tower `Layer` helpers that wire it into a stack, and a small fork of `tower-http::propagate_header` used to propagate multiple Rerun headers between requests and responses. + +Everything here is plain `tonic`/`tower`/`http` plumbing — no rerun-internal types — so it can sit on the `crates/utils` tier and be consumed by any crate that needs the same gRPC header behavior. diff --git a/crates/utils/re_grpc_headers/src/lib.rs b/crates/utils/re_grpc_headers/src/lib.rs new file mode 100644 index 000000000000..89b8cf054099 --- /dev/null +++ b/crates/utils/re_grpc_headers/src/lib.rs @@ -0,0 +1,343 @@ +//! Rerun gRPC header conventions: the `x-rerun-*` header consts, the +//! [`RerunVersionInterceptor`] that stamps every outbound request with the +//! client (or server) identity and version, the matching tower `Layer` +//! helpers that wire it into a stack, and a small fork of +//! `tower-http::propagate_header` used to propagate multiple Rerun headers +//! between requests and responses. + +/// The HTTP header key to pass an entry ID to the `RerunCloudService` APIs. +pub const RERUN_HTTP_HEADER_ENTRY_ID: &str = "x-rerun-entry-id"; + +/// The HTTP header key to pass an entry name to the `RerunCloudService` APIs. +/// +/// This will automatically be resolved to an entry ID, as long as a dataset with the associated +/// name can be found in the database. +/// +/// This is serialized as base64-encoded data (hence `-bin`), since entry names can be any UTF8 strings, +/// while HTTP2 headers only support ASCII. +pub const RERUN_HTTP_HEADER_ENTRY_NAME: &str = "x-rerun-entry-name-bin"; + +/// The HTTP header key that all our official gRPC clients use to specify their identity and version. +/// +/// All our official gRPC servers make sure to always return a copy of this header to the client as-is, in +/// addition to propagating it into our gRPC metrics and traces. +pub const RERUN_HTTP_HEADER_CLIENT_VERSION: &str = "x-rerun-client-version"; + +/// The HTTP header key that all our official gRPC servers use to specify their identity and version. +/// +/// All our official gRPC servers always set this header in all their responses, in addition to +/// propagating it into our gRPC metrics and traces. +pub const RERUN_HTTP_HEADER_SERVER_VERSION: &str = "x-rerun-server-version"; + +/// HTTP authorization header key, used to transport authorization tokens +pub const HTTP_HEADER_AUTHORIZATION: &str = "authorization"; + +// --- + +pub type RerunHeadersLayer = tower::layer::util::Stack< + PropagateHeadersLayer, + tower::layer::util::Stack< + tonic::service::InterceptorLayer, + tower::layer::util::Identity, + >, +>; + +/// Instantiates a compound [`tower::Layer`] that handles all things related to Rerun headers. +pub fn new_rerun_headers_layer( + name: Option, + version: Option, + is_client: bool, +) -> RerunHeadersLayer { + tower::ServiceBuilder::new() + .layer(tonic::service::interceptor::InterceptorLayer::new({ + RerunVersionInterceptor::new(is_client, name, version) + })) + .layer(new_rerun_headers_propagation_layer()) + .into_inner() +} + +/// Build the standard SDK-side Rerun headers layer. +/// +/// This is the `(name, version, is_client)` triple every Rerun gRPC client should use +/// unless it has a specific reason not to (e.g. the `redap_cli` binary, which advertises +/// its own `CARGO_PKG_VERSION`). It is the single source of truth for client-side header +/// configuration, so any path that opens a sibling channel (the main redap RPC stack, the +/// per-connection analytics OTLP exports, etc.) presents the same +/// `x-rerun-client-version` value to the server. +/// +/// On wasm, the identity is hard-coded to `"rerun-web"` so the cloud server can +/// distinguish browser traffic. On native, identity is left to fall through the standard +/// `RerunVersionInterceptor` chain (`OTEL_SERVICE_NAME` → exe stem → `re_protos`'s +/// `CARGO_PKG_NAME`) and the version respects `RERUN_CLIENT_VERSION_OVERRIDE` for tests. +#[cfg(target_arch = "wasm32")] +pub fn new_rerun_client_headers_layer() -> RerunHeadersLayer { + new_rerun_headers_layer( + Some("rerun-web".to_owned()), + None, + /* is_client */ true, + ) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn new_rerun_client_headers_layer() -> RerunHeadersLayer { + new_rerun_headers_layer( + None, + std::env::var("RERUN_CLIENT_VERSION_OVERRIDE").ok(), + /* is_client */ true, + ) +} + +/// Creates a new [`tower::Layer`] middleware that always makes sure to propagate Rerun headers +/// back and forth across requests and responses. +pub fn new_rerun_headers_propagation_layer() -> PropagateHeadersLayer { + PropagateHeadersLayer::new( + [ + http::HeaderName::from_static(RERUN_HTTP_HEADER_ENTRY_ID), + http::HeaderName::from_static(RERUN_HTTP_HEADER_CLIENT_VERSION), + http::HeaderName::from_static(RERUN_HTTP_HEADER_SERVER_VERSION), + ] + .into_iter() + .collect(), + ) +} + +/// Implements a `[tonic::service::Interceptor]` that records the identity and version of the client and/or server +/// in well-known headers. +/// +/// See also [`RERUN_HTTP_HEADER_CLIENT_VERSION`] & [`RERUN_HTTP_HEADER_SERVER_VERSION`]. +#[derive(Clone)] +pub struct RerunVersionInterceptor { + is_client: bool, + name: String, + version: String, +} + +impl RerunVersionInterceptor { + pub fn new_client(name: Option, version: Option) -> Self { + Self::new(true, name, version) + } + + pub fn new_server(name: Option, version: Option) -> Self { + Self::new(false, name, version) + } + + pub fn new(is_client: bool, name: Option, version: Option) -> Self { + let mut name = name + .or_else(|| std::env::var("OTEL_SERVICE_NAME").ok()) + .or_else(|| { + let path = std::env::current_exe().ok()?; + path.file_stem() + .map(|stem| stem.to_string_lossy().to_string()) + }) + .unwrap_or_else(|| env!("CARGO_PKG_NAME").to_owned()); + + if !name.is_ascii() { + // Cannot have non ASCII data in HTTP headers. + name = "".to_owned(); + } + + let version = version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_owned()); + + Self { + is_client, + name, + version, + } + } +} + +impl tonic::service::Interceptor for RerunVersionInterceptor { + fn call(&mut self, mut req: tonic::Request<()>) -> tonic::Result> { + let Self { + is_client, + name, + version, + } = self; + + let version = format!("{name}/{version}"); + + req.metadata_mut().insert( + if *is_client { + RERUN_HTTP_HEADER_CLIENT_VERSION + } else { + RERUN_HTTP_HEADER_SERVER_VERSION + }, + version + .parse() + .expect("cannot fail, checked in constructor"), + ); + + Ok(req) + } +} + +// --- + +// NOTE: This is a fork of . +// +// It exists to prevent never-ending chains of generics when propagating multiple headers, e.g.: +// ``` +// pub type RedapClientStack = +// re_perf_telemetry::external::tower_http::propagate_header::PropagateHeader< +// re_perf_telemetry::external::tower_http::propagate_header::PropagateHeader< +// re_perf_telemetry::external::tower_http::propagate_header::PropagateHeader< +// re_perf_telemetry::external::tower_http::propagate_header::PropagateHeader< +// re_perf_telemetry::external::tower_http::trace::Trace< +// tonic::service::interceptor::InterceptedService< +// tonic::service::interceptor::InterceptedService< +// tonic::transport::Channel, +// re_auth::client::AuthDecorator, +// >, +// re_perf_telemetry::TracingInjectorInterceptor, +// >, +// re_perf_telemetry::external::tower_http::classify::SharedClassifier< +// re_perf_telemetry::external::tower_http::classify::GrpcErrorsAsFailures, +// >, +// re_perf_telemetry::GrpcMakeSpan, +// >, +// >, +// >, +// >, +// >; +// ``` +// which instead becomes this: +// ``` +// pub type RedapClientStack = +// PropagateHeaders< +// re_perf_telemetry::external::tower_http::trace::Trace< +// tonic::service::interceptor::InterceptedService< +// tonic::service::interceptor::InterceptedService< +// tonic::transport::Channel, +// re_auth::client::AuthDecorator, +// >, +// re_perf_telemetry::TracingInjectorInterceptor, +// >, +// re_perf_telemetry::external::tower_http::classify::SharedClassifier< +// re_perf_telemetry::external::tower_http::classify::GrpcErrorsAsFailures, +// >, +// re_perf_telemetry::GrpcMakeSpan, +// >, +// >; +// ``` + +use std::collections::HashSet; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll, ready}; + +use http::header::HeaderName; +use http::{HeaderValue, Request, Response}; +use pin_project_lite::pin_project; +use tower::Service; +use tower::layer::Layer; + +/// Layer that applies [`PropagateHeaders`] which propagates multiple headers at once from requests to responses. +/// +/// If the headers are present on the request they'll be applied to the response as well. This could +/// for example be used to propagate headers such as `x-rerun-entry-id`, `x-rerun-client-version`, etc. +#[derive(Clone, Debug)] +pub struct PropagateHeadersLayer { + headers: HashSet, +} + +impl PropagateHeadersLayer { + /// Create a new [`PropagateHeadersLayer`]. + pub fn new(headers: HashSet) -> Self { + Self { headers } + } +} + +impl Layer for PropagateHeadersLayer { + type Service = PropagateHeaders; + + fn layer(&self, inner: S) -> Self::Service { + PropagateHeaders { + inner, + headers: self.headers.clone(), + } + } +} + +/// Middleware that propagates multiple headers at once from requests to responses. +/// +/// If the headers are present on the request they'll be applied to the response as well. This could +/// for example be used to propagate headers such as `x-rerun-entry-id`, `x-rerun-client-version`, etc. +#[derive(Clone, Debug)] +pub struct PropagateHeaders { + inner: S, + headers: HashSet, +} + +impl PropagateHeaders { + /// Create a new [`PropagateHeaders`] that propagates the given header. + pub fn new(inner: S, headers: HashSet) -> Self { + Self { inner, headers } + } + + /// Returns a new [`Layer`] that wraps services with a `PropagateHeaders` middleware. + /// + /// [`Layer`]: tower::layer::Layer + pub fn layer(headers: HashSet) -> PropagateHeadersLayer { + PropagateHeadersLayer::new(headers) + } +} + +impl Service> for PropagateHeaders +where + S: Service, Response = Response>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = ResponseFuture; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let headers_and_values = self + .headers + .iter() + .filter_map(|name| { + req.headers() + .get(name) + .cloned() + .map(|value| (name.clone(), value)) + }) + .collect(); + + ResponseFuture { + future: self.inner.call(req), + headers_and_values, + } + } +} + +pin_project! { + /// Response future for [`PropagateHeaders`]. + #[derive(Debug)] + pub struct ResponseFuture { + #[pin] + future: F, + headers_and_values: Vec<(HeaderName, HeaderValue)>, + } +} + +impl Future for ResponseFuture +where + F: Future, E>>, +{ + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + let mut res = ready!(this.future.poll(cx)?); + + for (header, value) in std::mem::take(this.headers_and_values) { + res.headers_mut().insert(header, value); + } + + Poll::Ready(Ok(res)) + } +} diff --git a/crates/utils/re_log/Cargo.toml b/crates/utils/re_log/Cargo.toml index 23b64d7d5d23..3f0f4bd7a723 100644 --- a/crates/utils/re_log/Cargo.toml +++ b/crates/utils/re_log/Cargo.toml @@ -19,7 +19,7 @@ workspace = true all-features = true [package.metadata.cargo-shear] -ignored = ["js-sys", "env_filter"] +ignored = [] [features] @@ -27,7 +27,7 @@ default = [] ## Feature to set up logging in binaries, ## i.e. from `main` or in a web-app. -setup = ["dep:env_logger", "dep:js-sys", "dep:wasm-bindgen"] +setup = ["dep:tracing-log", "dep:tracing-web"] [dependencies] @@ -36,15 +36,10 @@ crossbeam.workspace = true log-once.workspace = true parking_lot.workspace = true -# make sure dependencies that user tracing gets forwarded to `log`: -tracing = { workspace = true, features = ["log"] } - -# Native dependencies: -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -env_filter.workspace = true -env_logger = { workspace = true, optional = true, features = ["auto-color", "humantime"] } +tracing.workspace = true +tracing-subscriber.workspace = true +tracing-log = { workspace = true, optional = true } # web dependencies: [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { workspace = true, optional = true } -wasm-bindgen = { workspace = true, optional = true } +tracing-web = { workspace = true, optional = true } diff --git a/crates/utils/re_log/src/channel_logger.rs b/crates/utils/re_log/src/channel_logger.rs index 20b5ab6d6ad1..4b26781cfee9 100644 --- a/crates/utils/re_log/src/channel_logger.rs +++ b/crates/utils/re_log/src/channel_logger.rs @@ -1,62 +1,95 @@ //! Capture log messages and send them to some receiver over a channel. +use std::sync::LazyLock; + pub use crossbeam::channel::{Receiver, Sender}; +use crate::event_visitor::FieldValue; + +/// A tracing layer that pipes log messages to registered channels. +#[derive(Default)] +pub struct ChannelLayer { + channels: parking_lot::RwLock>, +} + #[derive(Clone, Debug)] pub struct LogMsg { /// The verbosity level. - pub level: log::Level, + pub level: tracing::Level, /// The module, starting with the crate name. pub target: String, /// The contents of the log message. - pub msg: String, + pub message: String, + + /// Custom key-value fields captured from structured logging, + /// e.g. `re_log::info!(?name, id = user_id(), "A person logged out")`. + pub fields: Vec<(&'static str, FieldValue)>, } -/// Pipe log messages to a channel. -pub struct ChannelLogger { - filter: log::LevelFilter, - tx: parking_lot::Mutex>, +struct Channel { + filter: tracing_subscriber::filter::LevelFilter, + tx: Sender, } -impl ChannelLogger { - pub fn new(filter: log::LevelFilter) -> (Self, Receiver) { - // can't block on web, so we cannot apply backpressure - #[cfg_attr(not(target_arch = "wasm32"), expect(clippy::disallowed_methods))] - let (tx, rx) = crossbeam::channel::unbounded(); - ( - Self { - filter, - tx: tx.into(), - }, - rx, - ) - } +pub fn channel_logger() -> &'static ChannelLayer { + static CHANNEL_LAYER: LazyLock = LazyLock::new(ChannelLayer::default); + &CHANNEL_LAYER } -impl log::Log for ChannelLogger { - fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - crate::is_log_enabled(self.filter, metadata) - } +/// Register a new receiver for log messages. +pub fn add_log_msg_receiver(filter: tracing_subscriber::filter::LevelFilter) -> Receiver { + // can't block on web, so we cannot apply backpressure + #[cfg_attr(not(target_arch = "wasm32"), expect(clippy::disallowed_methods))] + let (tx, rx) = crossbeam::channel::unbounded(); + channel_logger() + .channels + .write() + .push(Channel { filter, tx }); + rx +} + +impl tracing_subscriber::Layer for &'static ChannelLayer +where + S: tracing::Subscriber, +{ + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let metadata = event.metadata(); - fn log(&self, record: &log::Record<'_>) { - if !self.enabled(record.metadata()) { + let mut channels = self.channels.write(); + if !channels + .iter() + .any(|channel| crate::is_log_enabled(channel.filter, metadata)) + { return; } - // Ok with a naked `send` here, because we use an unbounded channel, - // so this can never block. - #[cfg_attr(not(target_arch = "wasm32"), expect(clippy::disallowed_methods))] - self.tx - .lock() - .send(LogMsg { - level: record.level(), - target: record.target().to_owned(), - msg: record.args().to_string(), - }) - .ok(); - } + let mut visitor = crate::event_visitor::EventVisitor::default(); + event.record(&mut visitor); + let (message, fields) = visitor.into_message_and_fields(); - fn flush(&self) {} + channels.retain(|channel| { + if crate::is_log_enabled(channel.filter, metadata) { + // Ok with a naked `send` here, because we use an unbounded channel, + // so this can never block. + #[cfg_attr(not(target_arch = "wasm32"), expect(clippy::disallowed_methods))] + channel + .tx + .send(LogMsg { + level: *metadata.level(), + target: metadata.target().to_owned(), + message: message.clone(), + fields: fields.clone(), + }) + .is_ok() + } else { + true + } + }); + } } diff --git a/crates/utils/re_log/src/event_visitor.rs b/crates/utils/re_log/src/event_visitor.rs new file mode 100644 index 000000000000..59e4d18b0cc9 --- /dev/null +++ b/crates/utils/re_log/src/event_visitor.rs @@ -0,0 +1,115 @@ +//! Helpers for extracting a plain-text message from structured [`tracing`] events. + +/// A value captured by structured logging. +#[derive(Clone, Debug)] +pub enum FieldValue { + Bool(bool), + I64(i64), + U64(u64), + String(String), + + /// [`std::fmt::Debug`]-formatting of some value + Debug(String), + + /// [`std::fmt::Display`]-formatting of an [`std::error::Error`] + Error(String), +} + +impl std::fmt::Display for FieldValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Bool(value) => write!(f, "{value}"), + Self::I64(value) => write!(f, "{value}"), + Self::U64(value) => write!(f, "{value}"), + Self::String(value) | Self::Debug(value) | Self::Error(value) => write!(f, "{value}"), + } + } +} + +/// A visitor that formats a [`tracing::Event`] like the old `log::Record` message. +#[derive(Default)] +pub struct EventVisitor { + message: Option, + fields: Vec<(&'static str, FieldValue)>, +} + +impl EventVisitor { + /// Returns the message and the structured key-value fields separately. + pub fn into_message_and_fields(self) -> (String, Vec<(&'static str, FieldValue)>) { + let Self { message, fields } = self; + let message = message.unwrap_or_default(); + (message, fields) + } + + /// Returns the formatted message followed by any structured fields on a single line. + #[cfg(not(target_arch = "wasm32"))] // Only used by the non-wasm panic-on-warn path. + pub fn format_as_string(self) -> String { + let (message, fields) = self.into_message_and_fields(); + let fields = fields + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>(); + match (message.is_empty(), fields.is_empty()) { + (_, true) => message, + (true, false) => fields.join(" "), + (false, false) => format!("{message} {}", fields.join(" ")), + } + } +} + +impl tracing::field::Visit for EventVisitor { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + self.message = Some(format!("{value:?}")); + } else { + self.fields + .push((field.name(), FieldValue::Debug(format!("{value:?}")))); + } + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "message" { + self.message = Some(value.to_owned()); + } else { + self.fields + .push((field.name(), FieldValue::String(value.to_owned()))); + } + } + + fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { + if field.name() == "message" { + self.message = Some(value.to_string()); + } else { + self.fields.push((field.name(), FieldValue::I64(value))); + } + } + + fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { + if field.name() == "message" { + self.message = Some(value.to_string()); + } else { + self.fields.push((field.name(), FieldValue::U64(value))); + } + } + + fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { + if field.name() == "message" { + self.message = Some(value.to_string()); + } else { + self.fields.push((field.name(), FieldValue::Bool(value))); + } + } + + fn record_error( + &mut self, + field: &tracing::field::Field, + value: &(dyn std::error::Error + 'static), + ) { + if field.name() == "message" { + self.message = Some(value.to_string()); + } else { + self.fields + .push((field.name(), FieldValue::Error(value.to_string()))); + } + } +} diff --git a/crates/utils/re_log/src/lib.rs b/crates/utils/re_log/src/lib.rs index ba0430ed3683..dc59d433d1ae 100644 --- a/crates/utils/re_log/src/lib.rs +++ b/crates/utils/re_log/src/lib.rs @@ -15,27 +15,26 @@ //! In the viewer these logs, if >= info, become notifications. See //! `re_ui::notifications` for more information. +#[cfg(feature = "setup")] mod channel_logger; mod debug_assert; -mod result_extensions; - #[cfg(feature = "setup")] -mod multi_logger; - +mod event_visitor; +mod result_extensions; #[cfg(feature = "setup")] mod setup; +#[cfg(feature = "setup")] +pub use channel_logger::{LogMsg, Receiver, Sender, add_log_msg_receiver}; +#[cfg(feature = "setup")] +pub use event_visitor::FieldValue; -#[cfg(all(feature = "setup", target_arch = "wasm32"))] -mod web_logger; - -pub use channel_logger::*; -pub use log::{Level, LevelFilter}; +pub use tracing::Level; +#[cfg(feature = "setup")] +pub use tracing_subscriber::filter::LevelFilter; // The `re_log::info_once!(…)` etc are nice helpers, but the `log-once` crate is a bit lacking. // In the future we should implement our own macros to de-duplicate based on the callsite, // similar to how the log console in a browser will automatically suppress duplicates. -pub use log_once::{debug_once, error_once, info_once, log_once, trace_once, warn_once}; -#[cfg(feature = "setup")] -pub use multi_logger::{MultiLoggerNotSetupError, add_boxed_logger, add_logger}; +pub use log_once::{debug_once, error_once, info_once, trace_once, warn_once}; pub use result_extensions::ResultExt; #[cfg(all(feature = "setup", not(target_arch = "wasm32")))] pub use setup::PanicOnWarnScope; @@ -44,6 +43,20 @@ pub use setup::{setup_logging, setup_logging_with_filter}; // The tracing macros support more syntax features than the log, that's why we use them: pub use tracing::{debug, error, info, trace, warn}; +/// Log once at the given [`Level`]. +#[macro_export] +macro_rules! log_once { + ($level:expr, $($arg:tt)+) => { + match $level { + $crate::Level::ERROR => $crate::error_once!($($arg)+), + $crate::Level::WARN => $crate::warn_once!($($arg)+), + $crate::Level::INFO => $crate::info_once!($($arg)+), + $crate::Level::DEBUG => $crate::debug_once!($($arg)+), + $crate::Level::TRACE => $crate::trace_once!($($arg)+), + } + }; +} + /// Log a warning in debug builds, or a debug message in release builds. /// /// This is useful for logging messages that should be visible during development @@ -236,35 +249,68 @@ fn add_builtin_log_filter(base_log_filter: &str) -> String { } /// Should we log this message given the filter? -fn is_log_enabled(filter: log::LevelFilter, metadata: &log::Metadata<'_>) -> bool { +#[cfg(feature = "setup")] +fn is_log_enabled( + filter: tracing_subscriber::filter::LevelFilter, + metadata: &tracing::Metadata<'_>, +) -> bool { if CRATES_AT_ERROR_LEVEL .iter() .any(|crate_name| metadata.target().starts_with(crate_name)) { - return metadata.level() <= log::LevelFilter::Error; - } - - if CRATES_AT_WARN_LEVEL + *metadata.level() <= tracing_subscriber::filter::LevelFilter::ERROR + } else if CRATES_AT_WARN_LEVEL .iter() .any(|crate_name| metadata.target().starts_with(crate_name)) { - return metadata.level() <= log::LevelFilter::Warn; - } - - if CRATES_AT_INFO_LEVEL + *metadata.level() <= tracing_subscriber::filter::LevelFilter::WARN + } else if CRATES_AT_INFO_LEVEL .iter() .any(|crate_name| metadata.target().starts_with(crate_name)) { - return metadata.level() <= log::LevelFilter::Info; + *metadata.level() <= tracing_subscriber::filter::LevelFilter::INFO + } else { + *metadata.level() <= filter } +} - metadata.level() <= filter +/// Check if an environment variable is set to a truthy value. +/// +/// Returns `true` if the environment variable is set to "1/true/yes/on" (case-insensitive). +/// Returns `false` if the environment variable is set to "0/false/no/off" (case-insensitive). +/// Otherwise returns `None`. +/// +/// # Example +/// +/// ```ignore +/// if env_var_flag("TELEMETRY_ENABLED") == Some(true) { +/// // enable telemetry +/// } +/// ``` +pub fn env_var_flag(var_name: &str) -> Option { + match std::env::var(var_name) + .ok()? + .trim() + .to_ascii_lowercase() + .as_str() + { + "" => None, + "0" | "false" | "no" | "off" => Some(false), + "1" | "true" | "yes" | "on" => Some(true), + value => { + crate::warn_once!( + "Ignoring unrecognized value {value:?} for environment variable {var_name:?} \ + (expected one of: 1/true/yes/on, 0/false/no/off); falling back to the default." + ); + None + } + } } /// Check if an environment variable is set to a truthy value. /// -/// Returns `true` if the environment variable is set to "1", "true", or "yes" (case-insensitive). -/// Returns `false` otherwise (including when the variable is not set). +/// Returns `true` if the environment variable is set to "1/true/yes/on" (case-insensitive). +/// Otherwise returns `false`. /// /// # Example /// @@ -274,12 +320,21 @@ fn is_log_enabled(filter: log::LevelFilter, metadata: &log::Metadata<'_>) -> boo /// } /// ``` pub fn env_var_is_truthy(var_name: &str) -> bool { - std::env::var(var_name) - .map(|v| { - let v = v.to_lowercase(); - v == "1" || v == "true" || v == "yes" - }) - .unwrap_or(false) + env_var_flag(var_name).unwrap_or(false) +} + +/// Is `RERUN_VERY_STRICT` set to a truthy value? +/// +/// In very strict mode, Rerun may panic anywhere, at any time, for any reason whenever it +/// detects something it does not like — e.g. out-of-order chunks, unsorted timelines, +/// or other invariant violations. Very strict mode is meant for development, testing and +/// CI, never for production: enable it to catch silent corruption early. +/// +/// The result is cached on the first call, so subsequent calls are very cheap and +/// changing the environment variable at runtime has no effect. +pub fn is_rerun_very_strict() -> bool { + static VERY_STRICT: std::sync::OnceLock = std::sync::OnceLock::new(); + *VERY_STRICT.get_or_init(|| env_var_is_truthy("RERUN_VERY_STRICT")) } /// Shorten a path to a Rust source file. diff --git a/crates/utils/re_log/src/multi_logger.rs b/crates/utils/re_log/src/multi_logger.rs deleted file mode 100644 index 625e943f02fc..000000000000 --- a/crates/utils/re_log/src/multi_logger.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Have multiple loggers implementing [`log::Log`] at once. - -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::SeqCst; - -static MULTI_LOGGER: MultiLogger = MultiLogger::new(); - -static HAS_MULTI_LOGGER: AtomicBool = AtomicBool::new(false); - -/// Produced when trying to install additional loggers when [`crate::setup_logging`] has not been called. -/// -/// This can happen for example when users of the `rerun` crate use the `spawn` method, -/// and they aren't using `re_log`. -#[derive(Clone, Copy, Debug)] -pub struct MultiLoggerNotSetupError {} - -/// Install the multi-logger as the default logger. -pub fn init() -> Result<(), log::SetLoggerError> { - HAS_MULTI_LOGGER.store(true, SeqCst); - log::set_logger(&MULTI_LOGGER) -} - -/// Install an additional global logger. -pub fn add_boxed_logger(logger: Box) -> Result<(), MultiLoggerNotSetupError> { - add_logger(Box::leak(logger)) -} - -/// Install an additional global logger. -pub fn add_logger(logger: &'static dyn log::Log) -> Result<(), MultiLoggerNotSetupError> { - if HAS_MULTI_LOGGER.load(SeqCst) { - MULTI_LOGGER.loggers.write().push(logger); - Ok(()) - } else { - Err(MultiLoggerNotSetupError {}) - } -} - -/// Forward log messages to multiple [`log::log`] receivers. -struct MultiLogger { - loggers: parking_lot::RwLock>, -} - -impl MultiLogger { - pub const fn new() -> Self { - Self { - loggers: parking_lot::RwLock::new(vec![]), - } - } -} - -impl log::Log for MultiLogger { - fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - self.loggers - .read() - .iter() - .any(|logger| logger.enabled(metadata)) - } - - fn log(&self, record: &log::Record<'_>) { - for logger in self.loggers.read().iter() { - logger.log(record); - } - } - - fn flush(&self) { - for logger in self.loggers.read().iter() { - logger.flush(); - } - } -} diff --git a/crates/utils/re_log/src/setup.rs b/crates/utils/re_log/src/setup.rs index 3d52709855cc..1b1a7316d5ed 100644 --- a/crates/utils/re_log/src/setup.rs +++ b/crates/utils/re_log/src/setup.rs @@ -1,6 +1,7 @@ //! Function to setup logging in binaries and web apps. use std::sync::atomic::AtomicIsize; +use tracing_subscriber::prelude::*; // This can be useful to enable to figure out what is causing a log message. #[cfg(not(target_arch = "wasm32"))] @@ -19,14 +20,8 @@ pub fn setup_logging() { /// Automatically does the right thing depending on target environment (native vs. web). /// Directs [`log`] calls to stderr on native. pub fn setup_logging_with_filter(log_filter: &str) { - use std::str::FromStr as _; - - let primary_log_filter = log_filter.split(',').next().unwrap_or("info"); - let max_level = - log::LevelFilter::from_str(primary_log_filter).unwrap_or(log::LevelFilter::Info); - #[cfg(not(target_arch = "wasm32"))] - fn setup(max_level: log::LevelFilter, log_filter: &str) { + fn create_tracing_subscriber(log_filter: &str) -> impl tracing::Subscriber { if cfg!(debug_assertions) && std::env::var("RUST_BACKTRACE").is_err() { // In debug build, default `RUST_BACKTRACE` to `1` if it is not set. // This ensures sure we produce backtraces if our examples (etc) panics. @@ -44,48 +39,68 @@ pub fn setup_logging_with_filter(log_filter: &str) { } } - crate::multi_logger::init().expect("Failed to set logger"); - let mut stderr_logger = env_logger::Builder::new(); + let fmt_layer = tracing_subscriber::fmt::layer() + .with_writer(std::io::stderr) + .with_file(LOG_FILE_LINE) + .with_line_number(LOG_FILE_LINE); + let env_filter = tracing_subscriber::EnvFilter::new(log_filter); + let panic_on_warn = PanicOnWarn { + always_enabled: env_var_bool("RERUN_PANIC_ON_WARN") == Some(true), + }; - log::set_max_level(max_level); + tracing_subscriber::registry() + .with(env_filter) + .with(fmt_layer) + .with(panic_on_warn) + .with(crate::channel_logger::channel_logger()) + } - if LOG_FILE_LINE { - stderr_logger.format(|buf, record| { - use std::io::Write as _; - writeln!( - buf, - "{} {}:{} {}", - record.level(), - record.file().unwrap_or_default(), - record.line().unwrap_or_default(), - record.args() - ) - }); - } + #[cfg(target_arch = "wasm32")] + fn create_tracing_subscriber(_log_filter: &str) -> impl tracing::Subscriber { + let fmt_layer = tracing_subscriber::Layer::with_filter( + tracing_subscriber::fmt::layer() + .with_ansi(false) + .without_time() + .with_writer(tracing_web::MakeWebConsoleWriter::new()), + // Cap output to DEBUG since browsers don't have a trace level in the web console. + tracing_subscriber::filter::LevelFilter::DEBUG, + ); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(crate::channel_logger::channel_logger()) + } - stderr_logger.parse_filters(log_filter); - crate::add_boxed_logger(Box::new(stderr_logger.build())).expect("Failed to install logger"); - crate::add_boxed_logger(Box::new(PanicOnWarn { - always_enabled: env_var_bool("RERUN_PANIC_ON_WARN") == Some(true), - })) - .expect("Failed to install panic-on-warn logger"); + use std::sync::Once; + static START: Once = Once::new(); + START.call_once(|| { + use std::str::FromStr as _; if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") { crate::warn!("Rerun does not officially support Intel Macs (x86/x64)"); } - } - #[cfg(target_arch = "wasm32")] - fn setup(max_level: log::LevelFilter, _log_filter: &str) { - crate::multi_logger::init().expect("Failed to set logger"); + let primary_log_filter = log_filter.split(',').next().unwrap_or("info"); + let max_level = + log::LevelFilter::from_str(primary_log_filter).unwrap_or(log::LevelFilter::Info); log::set_max_level(max_level); - crate::add_boxed_logger(Box::new(crate::web_logger::WebLogger::new(max_level))) - .expect("Failed to install logger"); - } - use std::sync::Once; - static START: Once = Once::new(); - START.call_once(|| setup(max_level, log_filter)); + // Forward `log` calls to `tracing`, so that if a dependency uses `log` instead of `tracing`, + // the log messages will still be captured by our `tracing` setup. + if let Err(err) = tracing_log::LogTracer::init() { + eprintln!("Failed to set log to tracing forwarding: {err}"); + } + + let subscriber = create_tracing_subscriber(log_filter); + if tracing::subscriber::set_global_default(subscriber).is_err() { + eprintln!( + "Failed to set global tracing subscriber. This can cause problems with log messages not being captured." + ); + crate::debug_panic!( + "Failed to set global tracing subscriber. This can cause problems with log messages not being captured." + ); + } + }); } // ---------------------------------------------------------------------------- @@ -148,30 +163,32 @@ struct PanicOnWarn { } #[cfg(not(target_arch = "wasm32"))] -impl log::Log for PanicOnWarn { - fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - match metadata.level() { - log::Level::Error | log::Level::Warn => { - self.always_enabled - || PANIC_ON_WARN_SCOPE_DEPTH - .with(|enabled| enabled.load(std::sync::atomic::Ordering::Relaxed) > 0) - } - log::Level::Info | log::Level::Debug | log::Level::Trace => false, - } - } - - fn log(&self, record: &log::Record<'_>) { - // `enabled` isn't called automatically by the `log!` macros, so we have to call it here. - // (it is only used by `log_enabled!`) - if self.enabled(record.metadata()) { - let level = match record.level() { - log::Level::Error => "error", - log::Level::Warn => "warning", - log::Level::Info | log::Level::Debug | log::Level::Trace => return, - }; - panic!("{level} logged with RERUN_PANIC_ON_WARN: {}", record.args()); +impl tracing_subscriber::Layer for PanicOnWarn +where + S: tracing::Subscriber, +{ + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let metadata = event.metadata(); + let level = match *metadata.level() { + tracing::Level::ERROR => "error", + tracing::Level::WARN => "warning", + tracing::Level::INFO | tracing::Level::DEBUG | tracing::Level::TRACE => return, + }; + + let enabled = self.always_enabled + || PANIC_ON_WARN_SCOPE_DEPTH + .with(|enabled| enabled.load(std::sync::atomic::Ordering::Relaxed) > 0); + if enabled { + let mut visitor = crate::event_visitor::EventVisitor::default(); + event.record(&mut visitor); + panic!( + "{level} logged with RERUN_PANIC_ON_WARN: {}", + visitor.format_as_string() + ); } } - - fn flush(&self) {} } diff --git a/crates/utils/re_log/src/web_logger.rs b/crates/utils/re_log/src/web_logger.rs deleted file mode 100644 index 5c6cf353cdcb..000000000000 --- a/crates/utils/re_log/src/web_logger.rs +++ /dev/null @@ -1,73 +0,0 @@ -/// Implements [`log::Log`] to log messages to `console.log`, `console.warn`, etc. -pub struct WebLogger { - filter: log::LevelFilter, -} - -impl WebLogger { - pub fn new(filter: log::LevelFilter) -> Self { - Self { filter } - } -} - -impl log::Log for WebLogger { - fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { - crate::is_log_enabled(self.filter, metadata) - } - - fn log(&self, record: &log::Record<'_>) { - if !self.enabled(record.metadata()) { - return; - } - - let msg = if let (Some(file), Some(line)) = (record.file(), record.line()) { - let file = crate::shorten_file_path(file); - format!("[{}] {file}:{line}: {}", record.target(), record.args()) - } else { - format!("[{}] {}", record.target(), record.args()) - }; - - match record.level() { - log::Level::Trace => console::trace(&msg), - log::Level::Debug => console::debug(&msg), - log::Level::Info => console::info(&msg), - log::Level::Warn => console::warn(&msg), - - // Using console.error causes crashes for unknown reason - // https://github.com/emilk/egui/pull/2961 - // log::Level::Error => console::error(&msg), - log::Level::Error => console::warn(&format!("ERROR: {msg}")), - } - } - - fn flush(&self) {} -} - -/// js-bindings for console.log, console.warn, etc -mod console { - use wasm_bindgen::prelude::*; - - #[wasm_bindgen] - extern "C" { - /// `console.trace` - #[wasm_bindgen(js_namespace = console)] - pub fn trace(s: &str); - - /// `console.debug` - #[wasm_bindgen(js_namespace = console)] - pub fn debug(s: &str); - - /// `console.info` - #[wasm_bindgen(js_namespace = console)] - pub fn info(s: &str); - - /// `console.warn` - #[wasm_bindgen(js_namespace = console)] - pub fn warn(s: &str); - - // Using console.error causes crashes for unknown reason - // https://github.com/emilk/egui/pull/2961 - // /// `console.error` - // #[wasm_bindgen(js_namespace = console)] - // pub fn error(s: &str); - } -} diff --git a/crates/utils/re_memory/src/accounting_allocator.rs b/crates/utils/re_memory/src/accounting_allocator.rs index ab45dd0d27e6..e6a3888babe1 100644 --- a/crates/utils/re_memory/src/accounting_allocator.rs +++ b/crates/utils/re_memory/src/accounting_allocator.rs @@ -242,10 +242,11 @@ pub fn tracking_stats() -> Option { let mut top_medium_callstacks = tracker_stats(&MEDIUM_ALLOCATION_TRACKER.lock()); is_thread_in_allocation_tracker.set(false); - let mut top_callstacks: Vec<_> = top_big_callstacks - .drain(..) - .chain(top_medium_callstacks.drain(..)) - .collect(); + let mut top_callstacks: Vec<_> = std::iter::chain( + top_big_callstacks.drain(..), + top_medium_callstacks.drain(..), + ) + .collect(); #[expect(clippy::cast_possible_wrap)] top_callstacks.sort_by_key(|c| -(c.estimated().size as i64)); @@ -314,11 +315,13 @@ unsafe impl std::alloc::GlobalAlloc } unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) { + // Note deallocation first, otherwise there'd be a race where another allocation could allocate + // at this pointer before we note down the dealloc. + note_dealloc(ptr, layout.size()); + // SAFETY: // We just do book-keeping and then let another allocator do all the actual work. unsafe { self.allocator.dealloc(ptr, layout) }; - - note_dealloc(ptr, layout.size()); } unsafe fn realloc( diff --git a/crates/utils/re_memory/src/lib.rs b/crates/utils/re_memory/src/lib.rs index cc4428794a45..779986be56dd 100644 --- a/crates/utils/re_memory/src/lib.rs +++ b/crates/utils/re_memory/src/lib.rs @@ -64,6 +64,16 @@ pub use self::memory_use::MemoryUse; pub use self::peak_memory_stats::PeakMemoryStats; pub use self::ram_warner::*; +#[cfg(not(target_arch = "wasm32"))] +pub fn default_memory_limit() -> MemoryLimit { + MemoryLimit::from_fraction_of_total(0.75) +} + +#[cfg(target_arch = "wasm32")] +pub fn default_memory_limit() -> MemoryLimit { + MemoryLimit::from_bytes(2_500_000_000) +} + /// Number of allocation and their total size. #[derive(Copy, Clone, Default, PartialEq, Eq, Hash)] pub struct CountAndSize { diff --git a/crates/utils/re_memory/src/memory_limit.rs b/crates/utils/re_memory/src/memory_limit.rs index 8c9e9a45bba7..41df7e0b7d98 100644 --- a/crates/utils/re_memory/src/memory_limit.rs +++ b/crates/utils/re_memory/src/memory_limit.rs @@ -36,6 +36,20 @@ impl MemoryLimit { /// No limit. pub const UNLIMITED: Self = Self { max_bytes: None }; + /// The default memory limit for native; 75% of reported + /// system memory. + #[cfg(not(target_arch = "wasm32"))] + pub fn default_for_current_platform() -> Self { + Self::from_fraction_of_total(0.75) + } + + /// The default memory for web, where we try to be extra careful + /// to not oom. + #[cfg(target_arch = "wasm32")] + pub fn default_for_current_platform() -> Self { + Self::from_bytes(2_500_000_000) + } + /// Set the limit to some number of bytes. pub fn from_bytes(max_bytes: u64) -> Self { Self { @@ -49,7 +63,7 @@ impl MemoryLimit { if let Some(total_memory) = total_memory { let max_bytes = (fraction as f64 * total_memory as f64).round(); - re_log::debug!( + re_log::debug_once!( "Setting memory limit to {}, which is {}% of total available memory ({}).", re_format::format_bytes(max_bytes), 100.0 * fraction, @@ -60,7 +74,9 @@ impl MemoryLimit { max_bytes: Some(max_bytes as _), } } else { - re_log::info!("Couldn't determine total available memory. Setting no memory limit."); + re_log::info_once!( + "Couldn't determine total available memory. Setting no memory limit." + ); Self { max_bytes: None } } } diff --git a/crates/utils/re_mutex/Cargo.toml b/crates/utils/re_mutex/Cargo.toml index d6fccaae09f7..f7c10debb569 100644 --- a/crates/utils/re_mutex/Cargo.toml +++ b/crates/utils/re_mutex/Cargo.toml @@ -19,7 +19,7 @@ workspace = true all-features = true [dependencies] +re_byte_size.workspace = true re_log.workspace = true parking_lot.workspace = true -cfg-if.workspace = true diff --git a/crates/utils/re_mutex/src/lib.rs b/crates/utils/re_mutex/src/lib.rs index f79bb3132d99..1a3f5e0d0840 100644 --- a/crates/utils/re_mutex/src/lib.rs +++ b/crates/utils/re_mutex/src/lib.rs @@ -19,6 +19,14 @@ pub struct Mutex { lock: parking_lot::Mutex, } +impl re_byte_size::SizeBytes for Mutex { + const IS_POD: bool = T::IS_POD; + + fn heap_size_bytes(&self) -> u64 { + self.lock().heap_size_bytes() + } +} + /// The lock you get from [`Mutex`]. pub use parking_lot::MutexGuard; @@ -38,8 +46,8 @@ impl Mutex { #[inline(always)] #[cfg_attr(debug_assertions, track_caller)] pub fn lock(&self) -> MutexGuard<'_, T> { - cfg_if::cfg_if! { - if #[cfg(debug_assertions)] { + cfg_select! { + debug_assertions => { let loc = Location::caller(); let guard = self .lock @@ -59,9 +67,8 @@ impl Mutex { *self.last_lock_location.lock() = Some(Location::caller()); guard - } else { - self.lock.lock() } + _ => self.lock.lock(), } } @@ -78,10 +85,46 @@ impl Mutex { // ---------------------------------------------------------------------------- /// The lock you get from [`RwLock::read`]. -pub use parking_lot::MappedRwLockReadGuard as RwLockReadGuard; +pub use parking_lot::RwLockReadGuard; /// The lock you get from [`RwLock::write`]. -pub use parking_lot::MappedRwLockWriteGuard as RwLockWriteGuard; +pub use parking_lot::RwLockWriteGuard; + +/// The lock you get from [`RwLock::read_upgradable`]. +pub struct RwLockUpgradableReadGuard<'a, T: ?Sized>(parking_lot::RwLockUpgradableReadGuard<'a, T>); + +impl<'a, T: ?Sized> RwLockUpgradableReadGuard<'a, T> { + /// Atomically upgrades this upgradable read lock into an exclusive write lock. + /// + /// Will log a warning in debug builds if the lock can't be upgraded within 10 seconds. + #[inline(always)] + #[cfg_attr(debug_assertions, track_caller)] + pub fn upgrade(self) -> RwLockWriteGuard<'a, T> { + if cfg!(debug_assertions) { + let loc = Location::caller(); + parking_lot::RwLockUpgradableReadGuard::try_upgrade_for(self.0, DEADLOCK_DURATION) + .unwrap_or_else(|guard| { + re_log::warn_once!( + "[DEBUG] Failed to upgrade RWLock after {}s. Deadlock?\n Latest upgrade location: {loc}", + DEADLOCK_DURATION.as_secs(), + ); + + parking_lot::RwLockUpgradableReadGuard::upgrade(guard) + }) + } else { + parking_lot::RwLockUpgradableReadGuard::upgrade(self.0) + } + } +} + +impl std::ops::Deref for RwLockUpgradableReadGuard<'_, T> { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.0 + } +} /// Provides interior mutability. /// @@ -106,7 +149,7 @@ impl RwLock { #[inline(always)] #[cfg_attr(debug_assertions, track_caller)] pub fn read(&self) -> RwLockReadGuard<'_, T> { - let guard = if cfg!(debug_assertions) { + if cfg!(debug_assertions) { let loc = Location::caller(); self.0.try_read_for(DEADLOCK_DURATION).unwrap_or_else(|| { re_log::warn_once!( @@ -118,8 +161,38 @@ impl RwLock { }) } else { self.0.read() + } + } + + /// Try to acquire upgradable read-access to the lock. + /// + /// Will log a warning in debug builds if the lock can't be acquired within 10 seconds. + #[inline(always)] + #[cfg_attr(debug_assertions, track_caller)] + pub fn read_upgradable(&self) -> RwLockUpgradableReadGuard<'_, T> { + let guard = if cfg!(debug_assertions) { + let loc = Location::caller(); + self.0.try_upgradable_read_for(DEADLOCK_DURATION).unwrap_or_else(|| { + re_log::warn_once!( + "[DEBUG] Failed to acquire RWLock upgradable read after {}s. Deadlock?\n Latest upgradable read location: {loc}", + DEADLOCK_DURATION.as_secs(), + ); + + self.0.upgradable_read() + }) + } else { + self.0.upgradable_read() }; - parking_lot::RwLockReadGuard::map(guard, |v| v) + RwLockUpgradableReadGuard(guard) + } + + /// Try to acquire upgradable read-access to the lock. + /// + /// Alias for [`Self::read_upgradable`]. + #[inline(always)] + #[cfg_attr(debug_assertions, track_caller)] + pub fn upgradable_read(&self) -> RwLockUpgradableReadGuard<'_, T> { + self.read_upgradable() } /// Try to acquire write-access to the lock. @@ -128,7 +201,7 @@ impl RwLock { #[inline(always)] #[cfg_attr(debug_assertions, track_caller)] pub fn write(&self) -> RwLockWriteGuard<'_, T> { - let guard = if cfg!(debug_assertions) { + if cfg!(debug_assertions) { let loc = Location::caller(); self.0.try_write_for(DEADLOCK_DURATION).unwrap_or_else(|| { re_log::warn_once!( @@ -140,8 +213,7 @@ impl RwLock { }) } else { self.0.write() - }; - parking_lot::RwLockWriteGuard::map(guard, |v| v) + } } /// Returns a mutable reference to the underlying data. @@ -277,4 +349,17 @@ mod tests_rwlock { // Thread #0 now grabs a write lock, which is legal let _t0w0 = lock.write(); } + + #[test] + fn rwlock_upgradable_read() { + let lock = RwLock::new(1); + let guard = lock.read_upgradable(); + assert_eq!(*guard, 1); + + let mut guard = guard.upgrade(); + *guard = 2; + drop(guard); + + assert_eq!(*lock.read(), 2); + } } diff --git a/crates/utils/re_perf_telemetry/Cargo.toml b/crates/utils/re_perf_telemetry/Cargo.toml index e0d49fff0bb4..6de0510cd059 100644 --- a/crates/utils/re_perf_telemetry/Cargo.toml +++ b/crates/utils/re_perf_telemetry/Cargo.toml @@ -20,9 +20,6 @@ default = [] ## If set, `TELEMETRY_ENABLED` will be `true` by default. enabled = [] -## If set, `OTEL_SDK_ENABLED` will be `true` by default. -otel_enabled = [] - ## If set, `TRACY_ENABLED` will be `true` by default. tracy_enabled = [] @@ -42,12 +39,23 @@ tracy_enabled = [] ## E.g. an async function that yields 50 times will be counted as 51 (the first call + 50 yields). tracy = ["dep:tracing-tracy"] -## PyO3 integration for cross-boundary tracing -pyo3 = ["dep:pyo3"] +## Enable [`Telemetry::init_with_session_id_reader`]. +## +## SDK bindings (today: `rerun_py`) register a host-language-specific reader at +## init time so the per-RPC tracestate enricher can resolve the active session +## id without this crate ever needing to know about Python/JS/etc. +## +## Default-off — end customers of `re_perf_telemetry` never see the extra +## public API. +session_id_reader = [] [dependencies] +# Rerun +re_auth = { workspace = true, features = ["oauth"] } +re_grpc_headers.workspace = true + # External ahash.workspace = true anyhow.workspace = true @@ -57,20 +65,20 @@ clap = { workspace = true, features = ["derive", "env"] } http.workspace = true memory-stats = { workspace = true, features = ["always_use_statm"] } opentelemetry = { workspace = true, features = ["metrics"] } -opentelemetry-appender-tracing = { workspace = true, features = [ - "experimental_use_tracing_span_context", -] } +opentelemetry-appender-tracing.workspace = true opentelemetry-http.workspace = true -opentelemetry-otlp = { workspace = true, features = ["grpc-tonic"] } +opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "gzip-tonic"] } +rand.workspace = true opentelemetry_sdk = { workspace = true, features = [ "rt-tokio", "experimental_metrics_custom_reader", + "experimental_metrics_periodicreader_with_async_runtime", "spec_unstable_metrics_views", ] } parking_lot.workspace = true serde.workspace = true serde_json.workspace = true -tonic.workspace = true +tonic = { workspace = true, features = ["transport", "tls-native-roots"] } tower-http = { workspace = true, features = ["propagate-header", "trace"] } tower.workspace = true tracing.workspace = true @@ -82,10 +90,10 @@ tokio.workspace = true # External (optional) tracing-tracy = { workspace = true, optional = true } -pyo3 = { workspace = true, optional = true } [dev-dependencies] opentelemetry_sdk = { workspace = true, features = ["testing"] } +re_test_mocks.workspace = true [lints] workspace = true diff --git a/crates/utils/re_perf_telemetry/README.md b/crates/utils/re_perf_telemetry/README.md index 21e91503c384..abded81b2168 100644 --- a/crates/utils/re_perf_telemetry/README.md +++ b/crates/utils/re_perf_telemetry/README.md @@ -10,9 +10,8 @@ Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. In and out of process telemetry and profiling utilities for Rerun & Redap. Performance telemetry is always disabled by default. It is gated both by a feature flag (`perf_telemetry`) and runtime configuration in the form of environment variables: -* `TELEMETRY_ENABLED`: is performance telemetry enabled at all (default: `false`)? +* `TELEMETRY_ENABLED`: is performance telemetry enabled at all (default: `false`)? When on, the OpenTelemetry SDK is initialized; individual exporters (logs/traces/metrics) only fire when their endpoint env var (or the umbrella `OTEL_EXPORTER_OTLP_ENDPOINT`) is set. * `TRACY_ENABLED`: is the tracy integration enabled (default: `false`)? works even if `TELEMETRY_ENABLED=false`, to reduce noise in measurements. -* `OTEL_SDK_ENABLED`: is the OpenTelemetry enabled (default: `false`)? does nothing if `TELEMETRY_ENABLED=false`. Note that despite the name, this crate also hands all log output to the telemetry backend. @@ -27,7 +26,7 @@ What you can or cannot do with that depends on which project you're working on ( ### Redap -If you have source access to the Rerun Data Platform check the Readme there. +If you have source access to Rerun Hub check the Readme there. ### Rerun SDK @@ -63,8 +62,8 @@ print(df.count()) # install the tracing extra so the OpenTelemetry Python packages are available: $ pixi run uv pip install 'rerun-sdk[tracing]' - # Run your script with both telemetry and the OpenTelemetry integration enabled: - $ TELEMETRY_ENABLED=true OTEL_SDK_ENABLED=true + # Run your script with telemetry enabled and traces pointed at the local Jaeger: + $ TELEMETRY_ENABLED=true OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4317 # Go to the Jaeger UI (http://localhost:16686/search) to look at the results ``` diff --git a/crates/utils/re_perf_telemetry/src/args.rs b/crates/utils/re_perf_telemetry/src/args.rs index 981044aeea1f..9b5de1988982 100644 --- a/crates/utils/re_perf_telemetry/src/args.rs +++ b/crates/utils/re_perf_telemetry/src/args.rs @@ -100,25 +100,6 @@ pub struct TelemetryArgs { )] pub tracy_enabled: bool, - /// Enable `OpenTelemetry`? - /// - /// This will initialize all the different `OpenTelemetry` subscribers, so that the data gets - /// uploaded to OTLP-compatible external services. - /// - /// The base telemetry in and of itself will keep working even if this is disabled. E.g. logs - /// will be forwarded to standard IO regardless. - /// - /// This has no effect if `TELEMETRY_ENABLED` is false. - #[cfg_attr( - feature = "otel_enabled", - clap(long, env = "OTEL_SDK_ENABLED", default_value_t = true) - )] - #[cfg_attr( - not(feature = "otel_enabled"), - clap(long, env = "OTEL_SDK_ENABLED", default_value_t = false) - )] - pub otel_enabled: bool, - /// The service name used for all things telemetry. /// /// This is mandatory, but we leave it as optional to give users a chance to set it at initialization @@ -169,14 +150,11 @@ pub struct TelemetryArgs { /// The gRPC OTLP endpoint to send the logs to. /// - /// It's fine for the target endpoint to be down. + /// When unset (or empty), no log exporter is created. As a fallback, the umbrella + /// `OTEL_EXPORTER_OTLP_ENDPOINT` env var is consulted at telemetry init. /// /// Part of the `OpenTelemetry` spec. - #[clap( - long, - env = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", - default_value = "http://localhost:4317" - )] + #[clap(long, env = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", default_value = "")] pub log_endpoint: String, /// Same as `RUST_LOG`, but for traces. @@ -187,14 +165,13 @@ pub struct TelemetryArgs { /// The gRPC OTLP endpoint to send the traces to. /// - /// It's fine for the target endpoint to be down. + /// When unset (or empty), no trace exporter is created — spans still flow through + /// the in-process tracing pipeline (so propagators / `current_trace_id()` keep + /// working) but nothing is shipped to a collector. As a fallback, the umbrella + /// `OTEL_EXPORTER_OTLP_ENDPOINT` env var is consulted at telemetry init. /// /// Part of the `OpenTelemetry` spec. - #[clap( - long, - env = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", - default_value = "http://localhost:4317" - )] + #[clap(long, env = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", default_value = "")] pub trace_endpoint: String, /// How are spans sampled? @@ -235,19 +212,6 @@ pub struct TelemetryArgs { #[clap(long, env = "OTEL_METRIC_EXPORT_INTERVAL", default_value = "10000")] pub metric_interval: String, - /// Additional key-value pairs to include in the `tracestate` for trace context propagation. - /// - /// Expects a comma-separated string of key=value pairs, e.g. `bench_id=my_bench,env=prod`. - /// These will be added to the W3C tracestate header for distributed tracing. - /// - /// This is useful for passing application-specific context that should propagate - /// across service boundaries. - /// - /// Keys must conform to the W3C tracestate spec: lowercase letters, digits, - /// underscores, dashes, asterisks, and forward slashes only. - #[clap(long, env = "OTEL_PROPAGATORS_TRACESTATE", default_value = "")] - pub tracestate: String, - /// Listening address for dedicated HTTP /metrics endpoint for scraping. /// /// Setting this has no immediate effect. The actual listener has to be @@ -258,7 +222,7 @@ pub struct TelemetryArgs { /// Format: ":9091", "0.0.0.0:9091", or "127.0.0.1:9091" /// Empty value means the listener is disabled. /// - /// This has no effect if `TELEMETRY_ENABLED` or `OTEL_SDK_ENABLED` is false. + /// This has no effect if `TELEMETRY_ENABLED` is false. #[clap(long, env = "METRICS_LISTEN_ADDRESS", default_value = "")] pub metrics_listen_address: String, } diff --git a/crates/utils/re_perf_telemetry/src/grpc.rs b/crates/utils/re_perf_telemetry/src/grpc.rs index d0ec51a56f51..2eefa4825238 100644 --- a/crates/utils/re_perf_telemetry/src/grpc.rs +++ b/crates/utils/re_perf_telemetry/src/grpc.rs @@ -6,6 +6,11 @@ const RERUN_HTTP_HEADER_ENTRY_ID: &str = "x-rerun-entry-id"; const RERUN_HTTP_HEADER_CLIENT_VERSION: &str = "x-rerun-client-version"; const RERUN_HTTP_HEADER_SERVER_VERSION: &str = "x-rerun-server-version"; +// Server-injected trace id, returned to the client in response headers. +// Mirrors `re_protos::trace_id_layer::RERUN_HTTP_HEADER_REQUEST_TRACE_ID` +// (kept as a string here to avoid the dependency). +const RERUN_HTTP_HEADER_REQUEST_TRACE_ID: &str = "x-request-trace-id"; + // --- Telemetry middlewares --- /// Implements [`tower_http::trace::MakeSpan`] where the trace name is the gRPC method name. @@ -17,8 +22,8 @@ const RERUN_HTTP_HEADER_SERVER_VERSION: &str = "x-rerun-server-version"; pub struct GrpcMakeSpan { gauge: opentelemetry::metrics::Gauge, // unfortunately we can't have different implementation of `MakeSpan` as that creates a ripple effect - // through the entire hierarchy of types of the RedapClient and its usage, hence to disable the span - // creation, we create noop spans instead if telemetry is disabled at runtime + // through the entire hierarchy of types of the redap client stack and its usage, hence to disable + // the span creation, we create noop spans instead if telemetry is disabled at runtime create_noop_spans: bool, } @@ -57,6 +62,24 @@ impl tower_http::trace::MakeSpan for GrpcMakeSpan { prop.extract(&opentelemetry_http::HeaderExtractor(request.headers())) }); + // Pull the rerun session id out of the inbound `tracestate` (if any) so we can + // record it directly as a span field at construction time, instead of relying on + // a separate `tracing_subscriber::Layer` that records into a pre-declared field. + // + // Recording the value here means the field is populated before the span is exported, + // and we sidestep the "silent no-op when the field wasn't pre-declared on the span" + // pitfall of `Span::record`. + let rerun_session_id = { + use opentelemetry::trace::TraceContextExt as _; + parent_ctx + .span() + .span_context() + .trace_state() + .get(crate::RERUN_SESSION_TRACESTATE_KEY) + .and_then(crate::RerunTracingSessionId::parse) + .map(String::from) + }; + // This replaces the current tracing context with the extracted one, and it ensures that // any spans created within this scope will be children of the extracted context. // Note on the guard: guard must stay alive until after span creation so that tracing::span!() @@ -128,22 +151,20 @@ impl tower_http::trace::MakeSpan for GrpcMakeSpan { rpc.service = %rpc_service, rpc.method = %rpc_method, - // Record benchmark_id as a top level span field. - // - // At this stage we may not know yet the actual value (depending on whether - // we're generating a new trace or continuing an existing one). However, - // we need to pre-declare the field if we want to record a value for it later. - // - // The field will be filled in by a separate [`tracing_subscriber::Layer`] (see - // [`BenchmarkIdLayer`]). - // - // This will only be filled if we have a benchmark_id in the tracestate. - // That's OK, it won't be printed if empty. - benchmark_id = tracing::field::Empty, + // The rerun session id, recorded as a top-level span field so it is queryable + // in Tempo as `{ .rerun_session_id = "…" }`. Extracted from the inbound + // `tracestate` header above. Empty when no `tracing_session()` is active on + // the client. + rerun_session_id = rerun_session_id.as_deref(), // The gRPC status code (e.g. "Ok", "AlreadyExists", "DeadlineExceeded"). // Filled in later by `GrpcOnResponse` or `GrpcOnEos`, depending on the endpoint type (unary vs streaming). grpc_status = tracing::field::Empty, + + // The trace id reported back by the server in the `x-request-trace-id` response header. + // Filled in client-side by `ClientOnResponse` so we can correlate client spans with the + // server-side trace. + server_trace_id = tracing::field::Empty, ); let size = SpanMetadata::insert_opt( @@ -228,10 +249,29 @@ impl Default for SpanMetadata { } } +/// Number of in-flight gRPC requests/streams, labeled by `endpoint`. +/// +/// An entry lives in `SPAN_METADATA` for exactly the lifetime of a request (inserted in +/// [`GrpcMakeSpan`]'s `make_span`, removed at end-of-stream, on immediate error, or on span close), +/// so counting `+1` on a genuine insert and `-1` on a real removal makes this gauge exactly the +/// number of live entries per endpoint — balanced by construction, no matter which termination path +/// a request takes. It is a non-monotonic sum, so it exports as a gauge. +fn requests_in_flight() -> &'static opentelemetry::metrics::UpDownCounter { + static INSTANCE: std::sync::OnceLock> = + std::sync::OnceLock::new(); + INSTANCE.get_or_init(|| { + opentelemetry::global::meter("grpc") + .i64_up_down_counter("grpc_requests_in_flight") + .with_description("Number of in-flight gRPC requests/streams, by endpoint") + .build() + }) +} + impl SpanMetadata { /// Returns the new size of the map. #[expect(clippy::needless_pass_by_value)] fn insert(span_id: tracing::span::Id, metadata: Self, expect_conflict: bool) -> usize { + let endpoint = metadata.endpoint.clone(); let (is_overwrite, new_len) = { let mut state = SPAN_METADATA.get_or_init(Default::default).write(); let is_overwrite = state.insert(span_id.clone(), metadata).is_some(); @@ -243,6 +283,12 @@ impl SpanMetadata { tracing::warn!(id=?span_id, "overwritten span metadata -- this should never happen"); } + // Only a genuine new entry (not an in-place update of an existing request's metadata) adds + // an in-flight request; the matching -1 happens in `remove`/`remove_silent`. + if !is_overwrite { + requests_in_flight().add(1, &[opentelemetry::KeyValue::new("endpoint", endpoint)]); + } + new_len } @@ -280,7 +326,15 @@ impl SpanMetadata { .get() .and_then(|spans| spans.write().remove(span_id)); - if md.is_none() { + if let Some(md) = &md { + requests_in_flight().add( + -1, + &[opentelemetry::KeyValue::new( + "endpoint", + md.endpoint.clone(), + )], + ); + } else { tracing::warn!(id=?span_id, "missing span metadata -- this should never happen"); } @@ -298,7 +352,17 @@ impl SpanMetadata { /// already been removed by [`GrpcOnEos`] or [`GrpcOnResponse`]. fn remove_silent(span_id: &tracing::span::Id) -> Option { let spans = SPAN_METADATA.get()?; - spans.write().remove(span_id) + let md = spans.write().remove(span_id); + if let Some(md) = &md { + requests_in_flight().add( + -1, + &[opentelemetry::KeyValue::new( + "endpoint", + md.endpoint.clone(), + )], + ); + } + md } } @@ -750,10 +814,45 @@ pub fn new_server_telemetry_layer(options: TelemetryLayerOptions) -> ServerTelem .on_eos(GrpcOnEos::new()) } +/// Implements a [`tower_http::trace::OnResponse`] middleware for the gRPC client. +/// +/// Records the server-reported trace id (from the `x-request-trace-id` response header) +/// onto the client span, so client-side traces can be correlated with the server-side trace. +#[derive(Debug, Clone, Default)] +pub struct ClientOnResponse {} + +impl ClientOnResponse { + pub fn new() -> Self { + Self {} + } +} + +impl tower_http::trace::OnResponse for ClientOnResponse { + fn on_response( + self, + response: &http::Response, + _latency: std::time::Duration, + span: &tracing::Span, + ) { + if let Some(trace_id) = response + .headers() + .get(RERUN_HTTP_HEADER_REQUEST_TRACE_ID) + .and_then(|v| v.to_str().ok()) + { + span.record("server_trace_id", trace_id); + } + } +} + pub type ClientTelemetryLayer = tower::layer::util::Stack< tonic::service::interceptor::InterceptorLayer, tower::layer::util::Stack< - tower_http::trace::TraceLayer, + tower_http::trace::TraceLayer< + tower_http::trace::GrpcMakeClassifier, + GrpcMakeSpan, + tower_http::trace::DefaultOnRequest, + ClientOnResponse, + >, tower::layer::util::Identity, >, >; @@ -770,7 +869,8 @@ pub fn new_client_telemetry_layer() -> ClientTelemetryLayer { // Note: we're actually disabling all DEBUG level logs for `tower` in re_log, so if you want to enable it // you'll need to adjust that as well. See crates/utils/re_log/src/lib.rs .on_failure(DefaultOnFailure::new().level(tracing::Level::DEBUG)) - .make_span_with(GrpcMakeSpan::new()); + .make_span_with(GrpcMakeSpan::new()) + .on_response(ClientOnResponse::new()); tower::ServiceBuilder::new() .layer(trace_layer) @@ -828,54 +928,12 @@ impl tonic::service::Interceptor for TracingInjectorInterceptor { // --- -use opentelemetry::trace::TraceContextExt as _; use tower_http::trace::DefaultOnFailure; +use tracing::Subscriber; use tracing::span::Id; -use tracing::{Span, Subscriber}; -use tracing_opentelemetry::OpenTelemetrySpanExt as _; use tracing_subscriber::Layer; use tracing_subscriber::layer::Context; -/// A `tracing_subscriber::Layer` that injects the opentelemetry `benchmark_id` as a -/// top level field on every span that pre-declares it. -/// -/// The `benchmark_id` is extracted from the W3C `tracestate` header. -#[derive(Default)] -pub struct BenchmarkIdLayer { - _private: (), -} - -// Just a marker to avoid injecting multiple times per span. -struct BenchmarkIdInjected; - -impl Layer for BenchmarkIdLayer -where - S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, -{ - fn on_enter(&self, id: &Id, ctx: Context<'_, S>) { - if let Some(span_ref) = ctx.span(id) { - if span_ref.extensions().get::().is_some() { - return; - } - - let current_span = Span::current(); - let otel_cx = current_span.context(); - let otel_span = otel_cx.span(); - let span_cx = otel_span.span_context(); - - if span_cx.is_valid() { - let trace_state = span_cx.trace_state(); - if let Some(benchmark_id) = trace_state.get("benchmark_id") { - current_span.record("benchmark_id", benchmark_id.to_owned()); - } - span_ref.extensions_mut().insert(BenchmarkIdInjected); - } - } - } -} - -// --- - /// A [`tracing_subscriber::Layer`] that cleans up `SpanMetadata` entries when spans close. /// /// In the normal flow, metadata is removed by [`GrpcOnEos`] `on_eos` (streaming responses) diff --git a/crates/utils/re_perf_telemetry/src/lib.rs b/crates/utils/re_perf_telemetry/src/lib.rs index 9dd93bab1af8..0152d232aca2 100644 --- a/crates/utils/re_perf_telemetry/src/lib.rs +++ b/crates/utils/re_perf_telemetry/src/lib.rs @@ -4,7 +4,7 @@ //! including all log output. //! //! This sort of telemetry is always disabled on our OSS binaries, and is only used for -//! * The Rerun Cloud infrastructure +//! * The Rerun Hub infrastructure //! * Profiling by Rerun developer //! //! Logging strategy @@ -49,12 +49,11 @@ mod grpc; mod memory_telemetry; mod metrics_server; mod prometheus; -#[cfg(feature = "pyo3")] -mod python_bridge; mod shared_reader; mod telemetry; mod trace_id_format; mod tracestate; +mod tracing_session; mod utils; use std::collections::HashMap; @@ -63,19 +62,23 @@ use opentelemetry_sdk::propagation::TraceContextPropagator; pub use self::args::{LogFormat, TelemetryArgs}; pub use self::grpc::{ - BenchmarkIdLayer, ClientTelemetryLayer, GrpcMakeSpan, GrpcOnEos, GrpcOnFirstBodyChunk, + ClientOnResponse, ClientTelemetryLayer, GrpcMakeSpan, GrpcOnEos, GrpcOnFirstBodyChunk, GrpcOnRequest, GrpcOnResponse, GrpcOnResponseOptions, ServerTelemetryLayer, SpanMetadataCleanupLayer, TelemetryLayerOptions, TracingInjectorInterceptor, new_client_telemetry_layer, new_server_telemetry_layer, }; -pub use self::telemetry::{Telemetry, TelemetryDropBehavior}; +pub use self::telemetry::{Telemetry, TelemetryDropBehavior, is_telemetry_active}; pub use self::utils::to_short_str; -#[cfg(feature = "pyo3")] -pub use self::python_bridge::{ - TRACE_CONTEXT_VAR_NAME, extract_trace_context_from_contextvar, get_trace_context_var, +pub use self::tracing_session::{ + RERUN_SESSION_TRACESTATE_KEY, RerunTracingSessionId, current_rerun_session_id, + dec_active_tracing_session_count, inc_active_tracing_session_count, + with_current_tracing_session, with_tracing_session, }; +#[cfg(feature = "session_id_reader")] +pub use self::tracing_session::SessionIdReader; + pub mod external { #[cfg(feature = "tracy")] pub use tracing_tracy; @@ -139,7 +142,7 @@ impl TraceHeaders { pub const TRACEPARENT_KEY: &'static str = "traceparent"; pub const TRACESTATE_KEY: &'static str = "tracestate"; - pub(crate) fn empty() -> Self { + pub fn empty() -> Self { Self { traceparent: String::new(), tracestate: None, diff --git a/crates/utils/re_perf_telemetry/src/prometheus.rs b/crates/utils/re_perf_telemetry/src/prometheus.rs index 2e0f9510805a..8f063a79be13 100644 --- a/crates/utils/re_perf_telemetry/src/prometheus.rs +++ b/crates/utils/re_perf_telemetry/src/prometheus.rs @@ -74,7 +74,7 @@ pub fn convert_to_prometheus( // Process each scope's metrics for scope in resource_metrics.scope_metrics() { for metric in scope.metrics() { - let metric_name = sanitize_metric_name(metric.name()); + let metric_name = sanitize_name(metric.name()); // Handle different metric types using the enum pattern use opentelemetry_sdk::metrics::data::{AggregatedMetrics, MetricData}; @@ -716,7 +716,7 @@ fn register_exponential_histogram_u64( // Helper functions -fn sanitize_metric_name(name: &str) -> String { +fn sanitize_name(name: &str) -> String { name.chars() .map(|c| { if c.is_ascii_alphanumeric() || c == '_' { @@ -731,7 +731,12 @@ fn sanitize_metric_name(name: &str) -> String { fn create_dynamic_labels(attributes: &[KeyValue]) -> DynamicLabels { let mut labels: Vec<(String, String)> = attributes .iter() - .map(|kv| (kv.key.as_str().to_owned(), kv.value.as_str().into_owned())) + .map(|kv| { + ( + sanitize_name(kv.key.as_str()), + kv.value.as_str().into_owned(), + ) + }) .collect(); labels.sort_by(|a, b| a.0.cmp(&b.0)); // Ensure consistent ordering DynamicLabels(labels) @@ -864,4 +869,19 @@ mod tests { "expected bucket ≈ √2, output: {output}" ); } + + #[test] + fn escape_dot_in_counter_label() { + let attrs = vec![KeyValue::new("otel.metric.overflow", "true")]; + let labels = create_dynamic_labels(&attrs); + let family = Family::::default(); + family.get_or_create(&labels).inc(); + + let mut registry = Registry::default(); + registry.register("test_counter", "help", family); + let mut buf = String::new(); + encode(&mut buf, ®istry).unwrap(); + + assert!(buf.contains(r#"test_counter_total{otel_metric_overflow="true"} 1"#)); + } } diff --git a/crates/utils/re_perf_telemetry/src/python_bridge.rs b/crates/utils/re_perf_telemetry/src/python_bridge.rs deleted file mode 100644 index cd08667dd5d8..000000000000 --- a/crates/utils/re_perf_telemetry/src/python_bridge.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Python↔Rust trace context bridge via a shared [`ContextVar`]. -//! -//! See `rerun_py/src/catalog/trace_context.rs` for the full bridge documentation -//! and the Rust-side entry points that call into these helpers. -//! -//! [`ContextVar`]: https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar - -use crate::TraceHeaders; - -/// The name of the Python `ContextVar` used for trace context propagation. -pub const TRACE_CONTEXT_VAR_NAME: &str = "TRACE_CONTEXT"; - -/// Get the trace context `ContextVar` object. -/// -/// This returns the same Python `ContextVar` instance every time, ensuring that -/// values set on it can be read back later. It is up to the caller to ensure trace context -/// is reset and cleared as needed. -pub fn get_trace_context_var(py: pyo3::Python<'_>) -> pyo3::PyResult> { - use pyo3::prelude::*; - - static CONTEXT_VAR: parking_lot::Mutex>> = - parking_lot::Mutex::new(None); - - let mut guard = CONTEXT_VAR.lock(); - - if let Some(var) = guard.as_ref() { - return Ok(var.bind(py).clone()); - } - - // Create the trace context ContextVar - let module = py.import("contextvars")?; - let contextvar_class = module.getattr("ContextVar")?; - let trace_ctx_var = contextvar_class.call1((TRACE_CONTEXT_VAR_NAME,))?; - let trace_ctx_unbound = trace_ctx_var.clone().unbind(); - - *guard = Some(trace_ctx_unbound); - - Ok(trace_ctx_var) -} - -/// Extract trace context from the Python `ContextVar` for cross-boundary propagation. -/// -/// Returns empty [`TraceHeaders`] if the `ContextVar` is unset or extraction fails. -pub fn extract_trace_context_from_contextvar(py: pyo3::Python<'_>) -> TraceHeaders { - use pyo3::prelude::*; - use pyo3::types::PyDict; - - fn try_extract(py: pyo3::Python<'_>) -> PyResult { - let context_var = get_trace_context_var(py)?; - - match context_var.call_method0("get") { - Ok(trace_data) => { - if let Ok(dict) = trace_data.downcast::() { - let traceparent = dict - .get_item(TraceHeaders::TRACEPARENT_KEY)? - .and_then(|v| v.extract::().ok()) - .unwrap_or_default(); - - let tracestate = dict - .get_item(TraceHeaders::TRACESTATE_KEY)? - .and_then(|v| v.extract::().ok()); - - let headers = TraceHeaders { - traceparent, - tracestate, - }; - - tracing::debug!("Trace headers: {:?}", headers); - Ok(headers) - } else { - Ok(TraceHeaders::empty()) - } - } - Err(_) => Ok(TraceHeaders::empty()), - } - } - - try_extract(py).unwrap_or_else(|err| { - tracing::debug!("Failed to extract trace context: {err}"); - TraceHeaders::empty() - }) -} diff --git a/crates/utils/re_perf_telemetry/src/telemetry.rs b/crates/utils/re_perf_telemetry/src/telemetry.rs index efcaf00bb24d..2cab251475a9 100644 --- a/crates/utils/re_perf_telemetry/src/telemetry.rs +++ b/crates/utils/re_perf_telemetry/src/telemetry.rs @@ -11,7 +11,299 @@ use tracing_subscriber::{EnvFilter, Layer as _}; use crate::shared_reader::SharedManualReader; use crate::trace_id_format::TraceIdFormat; -use crate::{BenchmarkIdLayer, LogFormat, SpanMetadataCleanupLayer, TelemetryArgs}; +use crate::{LogFormat, SpanMetadataCleanupLayer, TelemetryArgs}; + +const OTLP_EXPORTER_ENV_VAR: &str = "OTEL_EXPORTER_OTLP_ENDPOINT"; + +/// Resolved trace destinations for `Telemetry::init`. Each field is +/// `Some(url)` iff the corresponding exporter should be built. The two +/// fields are independent — both, either, or neither can be active. +/// +/// When both are set, every root span is fanned out through *both* +/// exporters. Dual-publishing (Hub + a local collector like Jaeger/Tempo) +/// is the reason this struct exists; if you want a single destination, +/// set only one env var. +#[derive(Debug, Clone, PartialEq, Eq)] +struct ResolvedTraceEndpoints { + /// Rerun-authed exporter routing through the Hub frontend. Set when + /// the SDK-side `RERUN_TELEMETRY_ENDPOINT` env var is non-empty. + /// + /// The value is the `http(s)://` transport URL the exporter dials — + /// the input rewritten to its underlying transport (`rerun://` and + /// `rerun+https://` → `https://`, `rerun+http://` → `http://`) or + /// passed through verbatim for plain `http(s)://` schemes. Any other + /// scheme is a config error returned as `Err` by `resolve`. + /// + /// Never mirrored into `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` — keeping + /// values set via the SDK-side knob out of the standard env var is + /// the entire point of having a dedicated knob. + rerun_authed: Option, + + /// Plain OTLP gRPC exporter driven by the standard + /// `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` (or its + /// `OTEL_EXPORTER_OTLP_ENDPOINT` umbrella fallback). The URL is + /// passed through verbatim — we never inspect it for `rerun://` + /// schemes; that's the SDK-side knob's job. + /// + /// `Telemetry::init` mirrors this URL back into + /// `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` so the `OTel` SDK's exporter + /// builder reads it from there. + standard: Option, +} + +impl ResolvedTraceEndpoints { + /// Resolve which exporters (if any) to build from the two trace-endpoint + /// inputs. + /// + /// * `rerun_telemetry_endpoint`: raw value of the SDK-side + /// `RERUN_TELEMETRY_ENDPOINT` env var (empty when unset). + /// * `standard_otel_endpoint`: value of + /// `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` already merged with the + /// `OTEL_EXPORTER_OTLP_ENDPOINT` umbrella fallback. + /// + /// The two inputs are independent — both, either, or neither may + /// produce a destination. The standard endpoint is *never* parsed for + /// `rerun://` schemes; server-side configurations point at plain + /// Alloy / Jaeger / Tempo collectors through this knob. + /// + /// Accepted schemes for `RERUN_TELEMETRY_ENDPOINT`: `rerun`, + /// `rerun+http`, `rerun+https`, `http`, `https`. Anything else is a + /// config error returned as `Err` even when `standard_otel_endpoint` + /// is valid — a typo in the dedicated knob surfaces at init time + /// instead of silently dropping the Hub destination. + fn resolve( + rerun_telemetry_endpoint: &str, + standard_otel_endpoint: &str, + ) -> anyhow::Result { + let rerun_authed = if rerun_telemetry_endpoint.is_empty() { + None + } else { + let reject = || { + anyhow::anyhow!( + "RERUN_TELEMETRY_ENDPOINT={rerun_telemetry_endpoint:?} is not a supported endpoint URL — \ + accepted schemes are rerun://, rerun+http://, rerun+https://, http://, https://" + ) + }; + let (scheme, rest) = rerun_telemetry_endpoint + .split_once("://") + .ok_or_else(reject)?; + let transport_scheme = match scheme { + "rerun" | "rerun+https" | "https" => "https", + "rerun+http" | "http" => "http", + _ => return Err(reject()), + }; + Some(format!("{transport_scheme}://{rest}")) + }; + + let standard = + (!standard_otel_endpoint.is_empty()).then(|| standard_otel_endpoint.to_owned()); + + Ok(Self { + rerun_authed, + standard, + }) + } + + fn any(&self) -> bool { + self.rerun_authed.is_some() || self.standard.is_some() + } + + /// Short tag used in the `Telemetry initialized` log line and the + /// init-failure stderr fallback. Keep the strings stable — operators + /// grep these out of logs. + fn trace_mode(&self) -> &'static str { + match (self.rerun_authed.is_some(), self.standard.is_some()) { + (true, true) => "rerun-authed+otlp", + (true, false) => "rerun-authed", + (false, true) => "otlp", + (false, false) => "off", + } + } + + /// Human-readable destination(s) for the same log lines. Renders the + /// dual-publish case as `" + "` so both URLs are + /// visible in one grep. + fn summary(&self) -> String { + match (&self.rerun_authed, &self.standard) { + (Some(rerun), Some(std)) => format!("{rerun} + {std}"), + (Some(url), None) | (None, Some(url)) => url.clone(), + (None, None) => "off".to_owned(), + } + } +} + +/// `SpanExporter` decorator that refreshes the Rerun SDK auth token just-in-time +/// before each export, delegating the actual gRPC send to the inner OTLP +/// exporter. +/// +/// `SpanExporter::export` is async, and per its contract is never called +/// concurrently for the same instance. Before delegating, this wrapper awaits +/// `provider.get_token()` and writes the result into the shared `token_cache` +/// that the inner exporter's synchronous tonic interceptor reads from. The +/// credentials provider has its own internal cache and short-circuits on a +/// still-valid JWT, so the steady-state cost is a single async lock read; +/// real network refresh only fires near token expiry. +/// +/// On refresh failure the cache is left untouched — a stale but still-valid +/// JWT continues to be used, and the inner exporter's own error handling +/// applies if the server rejects. A single `warn!` fires on the *rising edge* +/// of a failure run, re-arming on the next success, so sustained outages +/// don't spam the log. +#[derive(Debug)] +struct AuthRefreshingSpanExporter { + inner: opentelemetry_otlp::SpanExporter, + provider: Arc

, + token_cache: Arc>, + refresh_failing: std::sync::atomic::AtomicBool, +} + +impl

opentelemetry_sdk::trace::SpanExporter for AuthRefreshingSpanExporter

+where + P: re_auth::credentials::CredentialsProvider + Send + Sync + std::fmt::Debug + 'static, +{ + async fn export( + &self, + batch: Vec, + ) -> opentelemetry_sdk::error::OTelSdkResult { + use std::sync::atomic::Ordering; + + match self.provider.get_token().await { + Ok(Some(jwt)) => { + *self.token_cache.write() = jwt.to_string(); + self.refresh_failing.store(false, Ordering::Relaxed); + } + Ok(None) => { + self.token_cache.write().clear(); + self.refresh_failing.store(false, Ordering::Relaxed); + } + Err(err) => { + // Leave the cached token in place — if it's still inside its + // validity window, the server will accept it. + if !self.refresh_failing.swap(true, Ordering::Relaxed) { + tracing::warn!( + "Hub auth token refresh failed, continuing with cached token: {err}" + ); + } + } + } + + self.inner.export(batch).await + } + + fn shutdown_with_timeout( + &self, + timeout: std::time::Duration, + ) -> opentelemetry_sdk::error::OTelSdkResult { + self.inner.shutdown_with_timeout(timeout) + } + + fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult { + self.inner.force_flush() + } + + fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) { + self.inner.set_resource(resource); + } +} + +/// Build an OTLP `SpanExporter` that pushes through a tonic Channel whose +/// outbound requests carry a Rerun SDK Bearer token in the `authorization` +/// metadata. The token comes from +/// [`re_auth::credentials::CliCredentialsProvider`] — same global credentials +/// store the rest of the SDK uses (populated by `rerun auth login`) — and is +/// refreshed just before each export by [`AuthRefreshingSpanExporter`]. +/// +/// The actual TCP/TLS handshake is deferred to first use via +/// `Endpoint::connect_lazy()` so init stays sync. +fn build_rerun_authed_span_exporter( + transport_url: &str, +) -> anyhow::Result> { + use re_auth::credentials::CliCredentialsProvider; + + build_rerun_authed_span_exporter_with_provider( + transport_url, + Arc::new(CliCredentialsProvider::new()), + ) +} + +/// Inner constructor parameterized on the [`re_auth::credentials::CredentialsProvider`]. +/// The public [`build_rerun_authed_span_exporter`] wires up `CliCredentialsProvider`; +/// tests inject [`re_auth::credentials::StaticCredentialsProvider`] with a known JWT. +fn build_rerun_authed_span_exporter_with_provider

( + transport_url: &str, + provider: Arc

, +) -> anyhow::Result> +where + P: re_auth::credentials::CredentialsProvider + Send + Sync + std::fmt::Debug + 'static, +{ + let token_cache: Arc> = + Arc::new(parking_lot::RwLock::new(String::new())); + + // Build the tonic Channel by hand so we can attach our auth interceptor + // and so the TLS config matches `re_redap_client` (rustls + system roots + // via `tonic/tls-native-roots`). + let mut endpoint: tonic::transport::Endpoint = transport_url.parse()?; + if transport_url.starts_with("https://") { + endpoint = endpoint.tls_config( + tonic::transport::ClientTlsConfig::new() + .with_enabled_roots() + .assume_http2(true), + )?; + } + let channel = endpoint.connect_lazy(); + + // Single combined interceptor that both injects the Bearer token AND + // delegates to `RerunVersionInterceptor` to set `x-rerun-client-version`. + // Each call to `TonicExporterBuilder::with_interceptor` only accepts one + // interceptor, so we compose them here. The standard SDK setup uses + // `new_rerun_client_headers_layer()` but that's a tower::Layer and we'd + // need to pass a layered service via `with_channel`, which the OTLP + // builder doesn't allow. + let token_for_interceptor: Arc> = Arc::clone(&token_cache); + let mut version_interceptor = re_grpc_headers::RerunVersionInterceptor::new_client(None, None); + // Rising-edge gate so a malformed cached token warns once per failure run, + // not on every export. Mirrors `refresh_failing` on the wrapping struct. + // Arc because the interceptor closure has to be `Clone` for `with_interceptor`. + let parse_failing: Arc = + Arc::new(std::sync::atomic::AtomicBool::new(false)); + let interceptor = move |mut req: tonic::Request<()>| -> tonic::Result> { + use std::sync::atomic::Ordering; + let token = token_for_interceptor.read().clone(); + if !token.is_empty() { + match format!("Bearer {token}").parse() { + Ok(value) => { + req.metadata_mut().insert("authorization", value); + parse_failing.store(false, Ordering::Relaxed); + } + Err(err) => { + if !parse_failing.swap(true, Ordering::Relaxed) { + tracing::warn!( + "Cached Hub auth token failed to parse as an HTTP header value; aborting send: {err}", + ); + } + return Err(tonic::Status::internal( + "cached Hub auth token is not a valid HTTP header value", + )); + } + } + } + tonic::service::Interceptor::call(&mut version_interceptor, req) + }; + + let inner = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_channel(channel) + .with_interceptor(interceptor) + .with_compression(opentelemetry_otlp::Compression::Gzip) + .build()?; + + Ok(AuthRefreshingSpanExporter { + inner, + provider, + token_cache, + refresh_failing: std::sync::atomic::AtomicBool::new(false), + }) +} // --- @@ -50,6 +342,44 @@ pub enum TelemetryDropBehavior { Shutdown, } +/// Set to `true` by [`Telemetry::init`] once it has successfully wired up the +/// `tracing` subscriber, OTLP exporters, and global propagator. Read by +/// [`is_telemetry_active`] (and through it, by [`crate::with_tracing_session`] +/// and the Python `tracing_session()` bridge) to detect the case where a +/// caller is trying to use telemetry features before initializing the stack. +/// +/// Stays `true` for the rest of the process lifetime; not cleared on +/// `Telemetry` drop (matches Python's `_is_telemetry_active` semantics). +static TELEMETRY_ACTIVE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +/// Returns `true` once [`Telemetry::init`] has run with telemetry enabled +/// (i.e. the `tracing` subscriber, OTLP exporters, and global propagator are +/// installed). +/// +/// Used by [`crate::with_tracing_session`] to no-op with a warning when a +/// caller attempts session scoping before initializing telemetry. Process- +/// wide single source of truth for this question — the Python +/// `_is_telemetry_active()` binding also reads it via this function. +pub fn is_telemetry_active() -> bool { + TELEMETRY_ACTIVE.load(std::sync::atomic::Ordering::Acquire) +} + +/// Test-only: flip the [`TELEMETRY_ACTIVE`] flag without running the full +/// [`Telemetry::init`] pipeline. Lets in-crate tests exercise APIs that +/// gate on `is_telemetry_active` (notably `with_tracing_session`) without +/// having to stand up the `OTel` stack. +/// +/// **Concurrency:** mutates a process-global atomic. Tests that call this +/// (or assert on `TELEMETRY_ACTIVE` / `ACTIVE_TRACING_SESSION_COUNT`) are +/// race-free only when each test runs in its own process. Use `cargo +/// nextest` (the project's standard, per `rerun/CLAUDE.md`) — it spawns +/// a subprocess per test. Plain `cargo test` runs tests as threads inside +/// one process and will be flaky against these tests. +#[cfg(test)] +pub(crate) fn set_telemetry_active_for_test(active: bool) { + TELEMETRY_ACTIVE.store(active, std::sync::atomic::Ordering::Release); +} + impl Telemetry { pub fn flush(&self) { let Self { @@ -124,12 +454,35 @@ impl Drop for Telemetry { } impl Telemetry { + /// Same as [`Self::init`], plus registers `reader` as the host-language + /// callback that [`crate::current_rerun_session_id`] consults on its slow + /// path (and that [`crate::with_current_tracing_session`] invokes once at + /// the boundary). + /// + /// Intended for SDK bindings (today: `rerun_py`) that hold the active + /// session id in a host-language-specific store this crate has no way to + /// reach. First-call-wins: the registration happens once, atomically, + /// before `init` returns, and any subsequent registration attempt is a + /// silent no-op. + /// + /// Gated behind the `session_id_reader` feature so end customers of + /// `re_perf_telemetry` never see the extra public API. + #[cfg(feature = "session_id_reader")] + #[must_use = "dropping this will flush and shutdown all telemetry systems"] + pub fn init_with_session_id_reader( + args: TelemetryArgs, + drop_behavior: TelemetryDropBehavior, + reader: crate::SessionIdReader, + ) -> anyhow::Result { + crate::tracing_session::set_session_id_reader(reader); + Self::init(args, drop_behavior) + } + #[must_use = "dropping this will flush and shutdown all telemetry systems"] pub fn init(args: TelemetryArgs, drop_behavior: TelemetryDropBehavior) -> anyhow::Result { let TelemetryArgs { tracy_enabled, enabled, - otel_enabled, service_name, attributes, log_filter, @@ -142,357 +495,541 @@ impl Telemetry { trace_endpoint, trace_sampler, trace_sampler_args, - tracestate, metric_endpoint, metric_interval, metrics_listen_address: _, // TelemetryArgs only, used at the caller site } = args; - if !enabled { - if tracy_enabled { - #[cfg(feature = "tracy")] - { - tracing_subscriber::registry() - .with(self::tracy::tracy_layer()) - .try_init()?; - } - - #[cfg(not(feature = "tracy"))] - { - anyhow::bail!( - "`TRACY_ENABLED=true` but the 'tracy' feature flag is not toggled" - ); - } + // Resolve the umbrella `OTEL_EXPORTER_OTLP_ENDPOINT` as a fallback for any + // signal-specific endpoint that wasn't set. Mirrors the OTel SDK convention. + let umbrella_endpoint = std::env::var(OTLP_EXPORTER_ENV_VAR) + .ok() + .filter(|s| !s.is_empty()); + let resolve_endpoint = |signal: String| -> String { + if !signal.is_empty() { + signal + } else { + umbrella_endpoint.clone().unwrap_or_default() + } + }; + let log_endpoint = resolve_endpoint(log_endpoint); + let trace_endpoint = resolve_endpoint(trace_endpoint); + let metric_endpoint = resolve_endpoint(metric_endpoint); + + // Dedicated SDK-side trace endpoint, kept distinct from the standard + // `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` so its value doesn't leak into + // other OTel-aware libraries (e.g. Python's + // `opentelemetry-exporter-otlp-proto-grpc`) sharing the same process. + // Env-only; no clap arg, no CLI flag. + let rerun_telemetry_endpoint = + std::env::var("RERUN_TELEMETRY_ENDPOINT").unwrap_or_default(); + + // Decide which OTLP exporters the SDK should build. See + // [`ResolvedTraceEndpoints`] for the rules — the two endpoints are + // independent, so it's valid for both to be active at once + // (dual-publish to Hub and a local collector). When neither is set, + // spans still flow through the in-process pipeline but nothing + // leaves the process. Gated on `enabled` so a malformed + // `RERUN_TELEMETRY_ENDPOINT` doesn't break a `TELEMETRY_ENABLED=false` + // process (nor a `TRACY_ENABLED=true`-only one). + let trace_endpoints = if enabled { + ResolvedTraceEndpoints::resolve(&rerun_telemetry_endpoint, &trace_endpoint)? + } else { + ResolvedTraceEndpoints { + rerun_authed: None, + standard: None, } - - return Ok(Self { - logs: None, - metrics: None, - traces: None, - metrics_reader: None, - drop_behavior, - }); - } - - let Some(service_name) = service_name else { - anyhow::bail!( - "either `OTEL_SERVICE_NAME` or `TelemetryArgs::service_name` must be set in order to initialize telemetry" - ); }; - // For these things, all we need to do is make sure that the right OTEL env var is set. - // All the downstream libraries will do the right thing if they are. - // - // Safety: anything touching the env is unsafe, tis what it is. - #[expect(unsafe_code)] - unsafe { - std::env::set_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", log_endpoint); - std::env::set_var("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", metric_endpoint); - std::env::set_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", trace_endpoint); - std::env::set_var("OTEL_METRIC_EXPORT_INTERVAL", metric_interval); - std::env::set_var("OTEL_RESOURCE_ATTRIBUTES", attributes); - std::env::set_var("OTEL_SERVICE_NAME", &service_name); - std::env::set_var("OTEL_TRACES_SAMPLER", trace_sampler); - std::env::set_var("OTEL_TRACES_SAMPLER_ARG", trace_sampler_args); - } - - let create_filter = |base: &str, forced: &str| { - use crate::EnvFilterExt as _; - - EnvFilter::new(base) - .add_directive_if_absent(base, "aws_smithy_runtime", forced)? - .add_directive_if_absent(base, "datafusion", forced)? - .add_directive_if_absent(base, "datafusion_optimizer", forced)? - .add_directive_if_absent(base, "h2", forced)? - .add_directive_if_absent(base, "hyper", forced)? - .add_directive_if_absent(base, "hyper_util", forced)? - .add_directive_if_absent(base, "lance", forced)? - .add_directive_if_absent(base, "lance-arrow", forced)? - .add_directive_if_absent(base, "lance-core", forced)? - .add_directive_if_absent(base, "lance-datafusion", forced)? - .add_directive_if_absent(base, "lance-encoding", forced)? - .add_directive_if_absent(base, "lance-file", forced)? - .add_directive_if_absent(base, "lance-index", forced)? - .add_directive_if_absent(base, "lance-io", forced)? - .add_directive_if_absent(base, "lance-linalg", forced)? - .add_directive_if_absent(base, "lance-table", forced)? - .add_directive_if_absent(base, "lance", forced)? - .add_directive_if_absent(base, "opentelemetry-otlp", forced)? - .add_directive_if_absent(base, "opentelemetry", forced)? - .add_directive_if_absent(base, "opentelemetry_sdk", forced)? - .add_directive_if_absent(base, "rustls", forced)? - .add_directive_if_absent(base, "sqlparser", forced)? - .add_directive_if_absent(base, "tonic", forced)? - .add_directive_if_absent(base, "tonic_web", forced)? - .add_directive_if_absent(base, "tower", forced)? - .add_directive_if_absent(base, "tower_http", forced)? - .add_directive_if_absent(base, "tower_web", forced)? - .add_directive_if_absent(base, "typespec_client_core", forced)? - // - .add_directive_if_absent(base, "lance::index", "off")? - .add_directive_if_absent(base, "lance::io::exec", "off")? - .add_directive_if_absent(base, "lance::execution", "warn")? - .add_directive_if_absent(base, "lance::dataset::scanner", "off")? - .add_directive_if_absent(base, "lance_index", "off")? - .add_directive_if_absent(base, "lance::dataset::builder", "off")? - .add_directive_if_absent(base, "lance_encoding", "off") + // Pipeline summary fields. Computed once here so the success (`info!` once + // the subscriber is up) and failure (`eprintln!`, subscriber may not be up) + // paths can emit the same set of decision details. + let trace_mode: &'static str = trace_endpoints.trace_mode(); + let traces_summary = trace_endpoints.summary(); + let logs_summary: String = if log_otlp_enabled && !log_endpoint.is_empty() { + log_endpoint.clone() + } else { + "off".to_owned() }; + let metrics_summary: String = if metric_endpoint.is_empty() { + "off".to_owned() + } else { + metric_endpoint.clone() + }; + let service_name_summary: String = service_name.as_deref().unwrap_or("").to_owned(); + + let result: anyhow::Result = (move || -> anyhow::Result { + if !enabled { + if tracy_enabled { + #[cfg(feature = "tracy")] + { + tracing_subscriber::registry() + .with(self::tracy::tracy_layer()) + .try_init()?; + } + + #[cfg(not(feature = "tracy"))] + { + anyhow::bail!( + "`TRACY_ENABLED=true` but the 'tracy' feature flag is not toggled" + ); + } + } - // Logging strategy - // ================ - // - // * All our logs go through the structured `tracing` macros. - // - // * We always log from `tracing` directly into stdio: we never involve the OpenTelemetry - // logging API. Production is expected to read the logs from the pod's output. - // There is never any internal buffering going on, besides the buffering of stdio itself. - // - // * All logs that happen as part of the larger trace/span will automatically be uploaded - // with that trace/span. - // This makes our traces a very powerful debugging tool, in addition to a profiler. - // - // * If `OTEL_EXPORTER_OTLP_LOGS_ENABLED=true`, all logs will be forwarded to an OpenTelemetry - // collector in addition to standard IO. - - let layer_logs_and_traces_stdio = { - let layer = tracing_subscriber::fmt::layer() - .with_writer(std::io::stderr) - .with_file(true) - .with_line_number(true) - .with_target(false) - .with_thread_ids(true) - .with_thread_names(true) - .with_span_events(if log_closed_spans { - tracing_subscriber::fmt::format::FmtSpan::CLOSE - } else { - tracing_subscriber::fmt::format::FmtSpan::NONE + return Ok(Self { + logs: None, + metrics: None, + traces: None, + metrics_reader: None, + drop_behavior, }); + } - // Everything is generically typed, which is why this is such a nightmare to do. - macro_rules! handle_format { - ($format:ident, $is_json:expr) => {{ - let layer = layer - .$format() - .map_event_format(|f| TraceIdFormat::new(f, $is_json)); - if log_test_output { - layer.with_test_writer().boxed() - } else { - layer.boxed() - } - }}; + let Some(service_name) = service_name else { + anyhow::bail!( + "either `OTEL_SERVICE_NAME` or `TelemetryArgs::service_name` must be set in order to initialize telemetry" + ); + }; + + // For these things, all we need to do is make sure that the right OTEL env var is set. + // All the downstream libraries will do the right thing if they are. + // + // Endpoint env vars are only set when we actually have an endpoint to point at; + // overwriting them with empty strings would prevent the OTLP SDK builders from + // reading values that may have been set externally. + // + // Safety: anything touching the env is unsafe, tis what it is. + #[expect(unsafe_code)] + unsafe { + if !log_endpoint.is_empty() { + std::env::set_var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", &log_endpoint); + } + if !metric_endpoint.is_empty() { + std::env::set_var("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", &metric_endpoint); + } + // Mirror the `OTEL_*`-sourced trace endpoint back into its + // env var so the OTel SDK's exporter builder reads it from + // there — origin/main behavior. `RERUN_TELEMETRY_ENDPOINT` + // values live in `trace_endpoints.rerun_authed` and are + // never mirrored here regardless of their URL scheme; + // keeping them out of `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` + // is the entire point of having a dedicated knob. + if let Some(url) = &trace_endpoints.standard { + std::env::set_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", url); + } + std::env::set_var("OTEL_METRIC_EXPORT_INTERVAL", metric_interval); + std::env::set_var("OTEL_RESOURCE_ATTRIBUTES", attributes); + std::env::set_var("OTEL_SERVICE_NAME", &service_name); + std::env::set_var("OTEL_TRACES_SAMPLER", trace_sampler); + std::env::set_var("OTEL_TRACES_SAMPLER_ARG", trace_sampler_args); } - let layer = match log_format { - LogFormat::Pretty => handle_format!(pretty, false), - LogFormat::Compact => handle_format!(compact, false), - LogFormat::Json => handle_format!(json, true), + + let create_filter = |base: &str, forced: &str| { + use crate::EnvFilterExt as _; + + EnvFilter::new(base) + .add_directive_if_absent(base, "aws_smithy_runtime", forced)? + .add_directive_if_absent(base, "datafusion", forced)? + .add_directive_if_absent(base, "datafusion_optimizer", forced)? + .add_directive_if_absent(base, "h2", forced)? + .add_directive_if_absent(base, "hyper", forced)? + .add_directive_if_absent(base, "hyper_util", forced)? + .add_directive_if_absent(base, "lance", forced)? + .add_directive_if_absent(base, "lance-arrow", forced)? + .add_directive_if_absent(base, "lance-core", forced)? + .add_directive_if_absent(base, "lance-datafusion", forced)? + .add_directive_if_absent(base, "lance-encoding", forced)? + .add_directive_if_absent(base, "lance-file", forced)? + .add_directive_if_absent(base, "lance-index", forced)? + .add_directive_if_absent(base, "lance-io", forced)? + .add_directive_if_absent(base, "lance-linalg", forced)? + .add_directive_if_absent(base, "lance-table", forced)? + .add_directive_if_absent(base, "lance", forced)? + .add_directive_if_absent(base, "opentelemetry-otlp", forced)? + .add_directive_if_absent(base, "opentelemetry", forced)? + .add_directive_if_absent(base, "opentelemetry_sdk", forced)? + .add_directive_if_absent(base, "rustls", forced)? + .add_directive_if_absent(base, "sqlparser", forced)? + .add_directive_if_absent(base, "tonic", forced)? + .add_directive_if_absent(base, "tonic_web", forced)? + .add_directive_if_absent(base, "tower", forced)? + .add_directive_if_absent(base, "tower_http", forced)? + .add_directive_if_absent(base, "tower_web", forced)? + .add_directive_if_absent(base, "typespec_client_core", forced)? + // + .add_directive_if_absent(base, "lance::index", "off")? + .add_directive_if_absent(base, "lance::io::exec", "off")? + .add_directive_if_absent(base, "lance::execution", "warn")? + .add_directive_if_absent(base, "lance::dataset::scanner", "off")? + .add_directive_if_absent(base, "lance_index", "off")? + .add_directive_if_absent(base, "lance::dataset::builder", "off")? + .add_directive_if_absent(base, "lance_encoding", "off") }; - layer.with_filter(create_filter(&log_filter, "warn")?) - }; + // Logging strategy + // ================ + // + // * All our logs go through the structured `tracing` macros. + // + // * We always log from `tracing` directly into stdio: we never involve the OpenTelemetry + // logging API. Production is expected to read the logs from the pod's output. + // There is never any internal buffering going on, besides the buffering of stdio itself. + // + // * All logs that happen as part of the larger trace/span will automatically be uploaded + // with that trace/span. + // This makes our traces a very powerful debugging tool, in addition to a profiler. + // + // * If `OTEL_EXPORTER_OTLP_LOGS_ENABLED=true`, all logs will be forwarded to an OpenTelemetry + // collector in addition to standard IO. + + let layer_logs_and_traces_stdio = { + let layer = tracing_subscriber::fmt::layer() + .with_writer(std::io::stderr) + .with_file(true) + .with_line_number(true) + .with_target(false) + .with_thread_ids(true) + .with_thread_names(true) + .with_span_events(if log_closed_spans { + tracing_subscriber::fmt::format::FmtSpan::CLOSE + } else { + tracing_subscriber::fmt::format::FmtSpan::NONE + }); + + // Everything is generically typed, which is why this is such a nightmare to do. + macro_rules! handle_format { + ($format:ident, $is_json:expr) => {{ + let layer = layer + .$format() + .map_event_format(|f| TraceIdFormat::new(f, $is_json)); + if log_test_output { + layer.with_test_writer().boxed() + } else { + layer.boxed() + } + }}; + } + let layer = match log_format { + LogFormat::Pretty => handle_format!(pretty, false), + LogFormat::Compact => handle_format!(compact, false), + LogFormat::Json => handle_format!(json, true), + }; - let (logger_provider, layer_logs_otlp) = if otel_enabled && log_otlp_enabled { - use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; + layer.with_filter(create_filter(&log_filter, "warn")?) + }; - let exporter = opentelemetry_otlp::LogExporter::builder() - .with_tonic() // There's no good reason to use HTTP for logs (at the moment, that is) - .build()?; + let (logger_provider, layer_logs_otlp) = if log_otlp_enabled && !log_endpoint.is_empty() + { + use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; - let provider = SdkLoggerProvider::builder() - .with_batch_exporter(exporter) - .build(); + let exporter = opentelemetry_otlp::LogExporter::builder() + .with_tonic() // There's no good reason to use HTTP for logs (at the moment, that is) + .build()?; - let layer = OpenTelemetryTracingBridge::new(&provider).boxed(); + let provider = SdkLoggerProvider::builder() + .with_batch_exporter(exporter) + .build(); - ( - Some(provider), - Some(layer.with_filter(create_filter(&log_filter, "warn")?)), - ) - } else { - (None, None) - }; + let layer = OpenTelemetryTracingBridge::new(&provider).boxed(); - // Tracing strategy - // ================ - // - // * All our traces go through the structured `tracing` macros. We *never* use the - // OpenTelemetry macros. - // - // * The traces go through a first layer of filtering based on the value of `RUST_TRACE`, which - // functions similarly to a `RUST_LOG` filter. - // - // * The traces are then sent to the OpenTelemetry SDK, where they will go through a pass of - // sampling before being sent to the OTLP endpoint. - // The sampling mechanism is controlled by the official OTEL environment variables. - // - // * Spans that contains error logs will properly be marked as failed, and easily findable. - - let (tracer_provider, layer_traces_otlp) = if otel_enabled { - let exporter = opentelemetry_otlp::SpanExporter::builder() - .with_tonic() // There's no good reason to use HTTP for traces (at the moment, that is) - .with_compression(opentelemetry_otlp::Compression::Gzip) // use gzip compression to reduce bandwidth - .build()?; - - // we customize batch exporter config to ensure more optimal span exporting - let batch_config = BatchConfigBuilder::default() - // increase max queue size from default 2048 to ensure we don't drop spans during high throughput - .with_max_queue_size(8192) - // export more spans per batch to reduce number of requests (default is 512) - // together with queue size this help ensure more robust exporting under high throughput - .with_max_export_batch_size(2048) - .build(); - - let batch_processor = BatchSpanProcessor::builder(exporter) - .with_batch_config(batch_config) - .build(); - - let provider = SdkTracerProvider::builder() - .with_span_processor(batch_processor) - .build(); - - // This will be used by the `TracingInjectorInterceptor` to encode the trace information into the request headers. - // Additional `tracestate` can be added through the relevant env var and the custom enricher below. - let mut propagators: Vec< - Box, - > = vec![Box::new( - opentelemetry_sdk::propagation::TraceContextPropagator::new(), - )]; - - if !tracestate.is_empty() { - let enricher = crate::tracestate::TraceStateEnricher::new(&tracestate); - propagators.push(Box::new(enricher)); - } + ( + Some(provider), + Some(layer.with_filter(create_filter(&log_filter, "warn")?)), + ) + } else { + (None, None) + }; - opentelemetry::global::set_text_map_propagator( - opentelemetry::propagation::TextMapCompositePropagator::new(propagators), - ); + // Tracing strategy + // ================ + // + // * All our traces go through the structured `tracing` macros. We *never* use the + // OpenTelemetry macros. + // + // * The traces go through a first layer of filtering based on the value of `RUST_TRACE`, which + // functions similarly to a `RUST_LOG` filter. + // + // * The traces are then sent to the OpenTelemetry SDK, where they will go through a pass of + // sampling before being sent to the OTLP endpoint. + // The sampling mechanism is controlled by the official OTEL environment variables. + // + // * Spans that contains error logs will properly be marked as failed, and easily findable. + + // The `TracerProvider` is always built when telemetry is enabled, so propagators + // and `current_trace_id()` keep working. Up to two `BatchSpanProcessor`s are + // attached — one per active trace endpoint, see [`ResolvedTraceEndpoints`]. + // With neither endpoint set, spans flow through the in-process pipeline and + // are dropped at the end — no exporter chatter. + let (tracer_provider, layer_traces_otlp) = { + let mut builder = SdkTracerProvider::builder(); + if trace_endpoints.any() { + // Build a fresh batch config per processor — the OTel + // builder consumes it, and we may attach two processors + // when both endpoints are active. + let make_batch_config = || { + BatchConfigBuilder::default() + // increase max queue size from default 2048 to ensure we don't drop spans during high throughput + .with_max_queue_size(8192) + // export more spans per batch to reduce number of requests (default is 512) + // together with queue size this help ensure more robust exporting under high throughput + .with_max_export_batch_size(2048) + .build() + }; + + // Tag root spans with `rerun_session_id` whenever any + // exporter is active, so Tempo can find client-side + // traces by `{ .rerun_session_id = "rs_…" }`. When a + // vanilla OTLP destination is configured alongside Hub, + // it also receives the attribute on root spans — + // downstream tools that don't know about it ignore it. + builder = builder + .with_span_processor(crate::tracestate::RerunSessionRootSpanProcessor); + + if let Some(transport_url) = &trace_endpoints.rerun_authed { + // `RERUN_TELEMETRY_ENDPOINT` exporter, already + // normalized to its `http(s)://` transport form by + // `ResolvedTraceEndpoints::resolve`. Injects the + // SDK's auth token on every export — the dedicated + // knob always opts into the Rerun auth path + // regardless of scheme. + let exporter = build_rerun_authed_span_exporter(transport_url)?; + builder = builder.with_span_processor( + BatchSpanProcessor::builder(exporter) + .with_batch_config(make_batch_config()) + .build(), + ); + } - // This is to make sure that if some third-party system is logging raw OpenTelemetry - // spans (as opposed to `tracing` spans), we will catch them and forward them - // appropriately. - opentelemetry::global::set_tracer_provider(provider.clone()); + if trace_endpoints.standard.is_some() { + // Standard OTLP exporter — reads the endpoint from + // `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` (mirrored + // above), so no explicit URL passed here. + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() // There's no good reason to use HTTP for traces (at the moment, that is) + .with_compression(opentelemetry_otlp::Compression::Gzip) // use gzip compression to reduce bandwidth + .build()?; + builder = builder.with_span_processor( + BatchSpanProcessor::builder(exporter) + .with_batch_config(make_batch_config()) + .build(), + ); + } + } - let layer = tracing_opentelemetry::layer() - .with_tracer(provider.tracer(service_name.clone())) - .with_filter(create_filter(&trace_filter, "info")?) - .boxed(); + let provider = builder.build(); + + // Used by `TracingInjectorInterceptor` to encode the trace information into the + // outbound request headers. `TraceStateEnricher` runs after the W3C propagator + // and merges `rerun_session_id=` into `tracestate` whenever a tracing + // session is active (Rust `with_tracing_session` or Python `tracing_session()`). + // With no active scope it is a no-op. + let propagators: Vec< + Box, + > = vec![ + Box::new(opentelemetry_sdk::propagation::TraceContextPropagator::new()), + Box::new(crate::tracestate::TraceStateEnricher), + ]; + + opentelemetry::global::set_text_map_propagator( + opentelemetry::propagation::TextMapCompositePropagator::new(propagators), + ); - (Some(provider), Some(layer)) - } else { - (None, None) - }; + // This is to make sure that if some third-party system is logging raw OpenTelemetry + // spans (as opposed to `tracing` spans), we will catch them and forward them + // appropriately. + opentelemetry::global::set_tracer_provider(provider.clone()); - // Metric strategy - // =============== - // - // * Metrics can be pushed to an OTLP endpoint as defined by OTEL SDK variables. - // OTEL_METRIC_EXPORT_INTERVAL environment variable applies for push interval. - // This is enabled by setting OTEL_EXPORTER_OTLP_METRICS_ENDPOINT - // - // * Additionally a prometheus-style scraping endpoint can be enabled by calling - // start_metrics_listener() on the returned Telemetry instance. - // - // Both ways use the same data for actual metrics. - // - let (metric_provider, metrics_reader) = if otel_enabled { - let mut builder = SdkMeterProvider::builder(); - - // Use base-2 exponential histograms (OTel equivalent of Prometheus native - // histograms) instead of explicit bucket histograms. This avoids hardcoding - // bucket boundaries and lets the SDK auto-scale resolution. - builder = builder.with_view(|instrument: &opentelemetry_sdk::metrics::Instrument| { - if instrument.kind() == opentelemetry_sdk::metrics::InstrumentKind::Histogram { - opentelemetry_sdk::metrics::Stream::builder() - .with_aggregation(Aggregation::Base2ExponentialHistogram { - // Max buckets per positive/negative range. Negative buckets - // stay empty for duration/size metrics. Comparable to the - // ~10 explicit buckets we had before, but with auto-scaling - // boundaries. - max_size: 20, - // Starting resolution scale. The base of each bucket is - // 2^(2^(-scale)). At scale 20 (the maximum), buckets are - // extremely fine-grained; the SDK automatically downscales - // when observations exceed max_size buckets. - max_scale: 20, - record_min_max: true, - }) - .build() - .ok() - } else { - None - } - }); + let layer = tracing_opentelemetry::layer() + .with_tracer(provider.tracer(service_name.clone())) + .with_filter(create_filter(&trace_filter, "info")?) + .boxed(); - // OTLP exporter for push-based metrics - let otlp_exporter = opentelemetry_otlp::MetricExporter::builder() - .with_temporality(opentelemetry_sdk::metrics::Temporality::Cumulative) - .with_http() - .build()?; - builder = builder.with_periodic_exporter(otlp_exporter); + (Some(provider), Some(layer)) + }; - // Always add a ManualReader for potential metrics listener - // We use SharedManualReader to share the same reader instance between - // the MeterProvider (for registration) and the metrics server (for collection) - let shared_reader = - SharedManualReader::new(opentelemetry_sdk::metrics::Temporality::Cumulative); + // Metric strategy + // =============== + // + // * Metrics can be pushed to an OTLP endpoint as defined by OTEL SDK variables. + // OTEL_METRIC_EXPORT_INTERVAL environment variable applies for push interval. + // This is enabled by setting OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + // + // * Additionally a prometheus-style scraping endpoint can be enabled by calling + // start_metrics_listener() on the returned Telemetry instance. + // + // Both ways use the same data for actual metrics. + // + // The `MeterProvider` is always built so the `start_metrics_listener()` Prometheus + // path keeps working; the OTLP push exporter is only attached when an endpoint is + // configured (per-signal or via the umbrella). + let (metric_provider, metrics_reader) = { + let mut builder = SdkMeterProvider::builder(); + + // Use base-2 exponential histograms (OTel equivalent of Prometheus native + // histograms) instead of explicit bucket histograms. This avoids hardcoding + // bucket boundaries and lets the SDK auto-scale resolution. + builder = + builder.with_view(|instrument: &opentelemetry_sdk::metrics::Instrument| { + if instrument.kind() + == opentelemetry_sdk::metrics::InstrumentKind::Histogram + { + opentelemetry_sdk::metrics::Stream::builder() + .with_aggregation(Aggregation::Base2ExponentialHistogram { + // Max buckets per positive/negative range. Negative buckets + // stay empty for duration/size metrics. Comparable to the + // ~10 explicit buckets we had before, but with auto-scaling + // boundaries. + max_size: 20, + // Starting resolution scale. The base of each bucket is + // 2^(2^(-scale)). At scale 20 (the maximum), buckets are + // extremely fine-grained; the SDK automatically downscales + // when observations exceed max_size buckets. + max_scale: 20, + record_min_max: true, + }) + .build() + .ok() + } else { + None + } + }); + + // Drive the periodic export on the Tokio runtime rather than via + // `with_periodic_exporter`. That convenience method installs the + // thread-based `PeriodicReader`, which spawns a bare std thread and drives + // each export with `futures_executor::block_on`. The OTLP HTTP exporter uses + // a hyper client whose `tokio::time::timeout` panics ("there is no reactor + // running") when polled off a Tokio runtime, and with `panic = "abort"` that + // takes down the whole process. The async-runtime reader instead spawns its + // ticker via `tokio::spawn` (when the meter provider is built below), so + // exports run on the runtime's workers, which have a reactor. The interval + // still honors `OTEL_METRIC_EXPORT_INTERVAL` (read by the builder). + // + // This means the reader requires an ambient Tokio runtime at init time. + // Every caller initializes telemetry within one (services via + // `#[tokio::main]`, the Python SDK via `runtime.block_on`), but we guard + // explicitly: telemetry must never abort the host process. With no runtime + // we skip OTLP push metrics and fall back to the always-installed + // `SharedManualReader` (the Prometheus scrape path is unaffected). + if !metric_endpoint.is_empty() { + if tokio::runtime::Handle::try_current().is_ok() { + let otlp_exporter = opentelemetry_otlp::MetricExporter::builder() + .with_temporality(opentelemetry_sdk::metrics::Temporality::Cumulative) + .with_http() + .build()?; + + let reader = opentelemetry_sdk::metrics::periodic_reader_with_async_runtime::PeriodicReader::builder( + otlp_exporter, + opentelemetry_sdk::runtime::Tokio, + ) + .build(); + builder = builder.with_reader(reader); + } else { + tracing::warn!( + "OTLP metrics endpoint is set but telemetry was initialized outside a Tokio runtime; \ + skipping push-based metric export. Metrics are still available via the Prometheus \ + scrape listener if one is configured." + ); + } + } - let reader_for_telemetry = shared_reader.inner(); - builder = builder.with_reader(shared_reader); + // Always add a ManualReader for potential metrics listener + // We use SharedManualReader to share the same reader instance between + // the MeterProvider (for registration) and the metrics server (for collection) + let shared_reader = + SharedManualReader::new(opentelemetry_sdk::metrics::Temporality::Cumulative); - let provider = builder.build(); + let reader_for_telemetry = shared_reader.inner(); + builder = builder.with_reader(shared_reader); - // Set as global provider - this makes all metrics created via opentelemetry::global::meter() - // available to all registered readers: OTLP push and ManualReader - opentelemetry::global::set_meter_provider(provider.clone()); + let provider = builder.build(); - tracing::info!("metric provider created with manual reader support"); + // Set as global provider - this makes all metrics created via opentelemetry::global::meter() + // available to all registered readers: OTLP push and ManualReader + opentelemetry::global::set_meter_provider(provider.clone()); - (Some(provider), Some(reader_for_telemetry)) - } else { - (None, None) - }; + (Some(provider), Some(reader_for_telemetry)) + }; - if tracy_enabled { - #[cfg(feature = "tracy")] - { - tracing::warn!( - "using tracy in addition to standard telemetry stack, consider `TELEMETRY_ENABLED=false`" - ); + if tracy_enabled { + #[cfg(feature = "tracy")] + { + tracing_subscriber::registry() + .with(layer_logs_otlp) + .with(layer_logs_and_traces_stdio) + .with(layer_traces_otlp) + .with(SpanMetadataCleanupLayer::default()) + .with(self::tracy::tracy_layer()) + .try_init()?; + } + #[cfg(not(feature = "tracy"))] + { + anyhow::bail!( + "`TRACY_ENABLED=true` but the 'tracy' feature flag is not toggled" + ); + } + } else { tracing_subscriber::registry() .with(layer_logs_otlp) .with(layer_logs_and_traces_stdio) .with(layer_traces_otlp) - .with(BenchmarkIdLayer::default()) .with(SpanMetadataCleanupLayer::default()) - .with(self::tracy::tracy_layer()) .try_init()?; } - #[cfg(not(feature = "tracy"))] - { - anyhow::bail!("`TRACY_ENABLED=true` but the 'tracy' feature flag is not toggled"); - } - } else { - tracing_subscriber::registry() - .with(layer_logs_otlp) - .with(layer_logs_and_traces_stdio) - .with(layer_traces_otlp) - .with(BenchmarkIdLayer::default()) - .with(SpanMetadataCleanupLayer::default()) - .try_init()?; - } - - crate::memory_telemetry::install_memory_use_meters(); + crate::memory_telemetry::install_memory_use_meters(); - tracing::info!("Telemetry initialized"); + // Reached only on the enabled-true success path (subscriber + + // OTLP layers installed). Flips the process-wide flag that + // [`is_telemetry_active`] exposes; consumers like + // [`crate::with_tracing_session`] and the Python + // `tracing_session()` bridge gate on it. + TELEMETRY_ACTIVE.store(true, std::sync::atomic::Ordering::Release); - Ok(Self { - drop_behavior, - logs: logger_provider, - traces: tracer_provider, - metrics: metric_provider, - metrics_reader, - }) + Ok(Self { + drop_behavior, + logs: logger_provider, + traces: tracer_provider, + metrics: metric_provider, + metrics_reader, + }) + })(); + + match result { + Ok(self_) => { + // Emitted through the subscriber installed by `try_init` above + // (when `enabled` or `tracy_enabled`). Drops silently in the + // no-subscriber case — but that case has nothing else running + // either, so silence is appropriate. + tracing::info!( + enabled, + service = %service_name_summary, + trace_mode, + traces = %traces_summary, + logs = %logs_summary, + metrics = %metrics_summary, + tracy = tracy_enabled, + "Telemetry initialized" + ); + #[cfg(feature = "tracy")] + if tracy_enabled && enabled { + tracing::warn!( + "using tracy in addition to standard telemetry stack, consider `TELEMETRY_ENABLED=false`" + ); + } + Ok(self_) + } + Err(err) => { + // The subscriber is not guaranteed to be installed on the + // failure path (most error sites are pre-`try_init`), so fall + // back to stderr to ensure the diagnosis is visible. + eprintln!( + "Telemetry init failed (enabled={enabled} service={service_name_summary} trace_mode={trace_mode} traces={traces_summary} logs={logs_summary} metrics={metrics_summary} tracy={tracy_enabled}): {err:#}" + ); + Err(err) + } + } } /// Start a dedicated HTTP server for metrics collection at the given address. @@ -527,7 +1064,7 @@ impl Telemetry { let reader = self.metrics_reader.as_ref() .ok_or_else(|| anyhow::anyhow!( "Cannot start metrics listener: telemetry was not initialized with metrics support. \ - Ensure TELEMETRY_ENABLED=true and OTEL_SDK_ENABLED=true" + Ensure TELEMETRY_ENABLED=true" ))?; // Clone the Arc to pass to the server @@ -579,3 +1116,218 @@ mod tracy { tracing_tracy::TracyLayer::new(TracyConfig::default()) } } + +#[cfg(test)] +mod tests { + use super::ResolvedTraceEndpoints; + + /// Compact projection of `resolve`'s return for table-driven assertions. + /// `Endpoints` keeps both URLs so we can match the dual-publish cases + /// directly; `Err` collapses all malformed-input cases together — we + /// only assert "resolve rejects this", not the particular error type. + #[derive(Debug)] + enum Want { + Endpoints { + rerun_authed: Option<&'static str>, + standard: Option<&'static str>, + }, + Err, + } + + /// Shorthand constructors for the `Want::Endpoints` rows. + const fn rerun_only(url: &'static str) -> Want { + Want::Endpoints { + rerun_authed: Some(url), + standard: None, + } + } + const fn standard_only(url: &'static str) -> Want { + Want::Endpoints { + rerun_authed: None, + standard: Some(url), + } + } + const fn both(rerun_authed: &'static str, standard: &'static str) -> Want { + Want::Endpoints { + rerun_authed: Some(rerun_authed), + standard: Some(standard), + } + } + const NONE: Want = Want::Endpoints { + rerun_authed: None, + standard: None, + }; + + /// `ResolvedTraceEndpoints::resolve` behavior, table-driven. + /// + /// Each row: `(rerun_telemetry_endpoint, standard_otel_endpoint, expected)`. + #[test] + fn resolve_behavior() { + let cases: &[(&str, &str, Want)] = &[ + // -- No exporter --------------------------------------------- + ("", "", NONE), + // -- Only OTEL_*: standard, verbatim (never parsed for `rerun://`) - + ( + "", + "https://collector:4317", + standard_only("https://collector:4317"), + ), + ( + "", + "http://localhost:4317", + standard_only("http://localhost:4317"), + ), + ("", "grpc://collector", standard_only("grpc://collector")), + ( + "", + "rerun://api.example.com", + standard_only("rerun://api.example.com"), + ), + // -- Only RERUN_*, `rerun*` schemes: authed (normalized) ----- + ( + "rerun://api.example.com", + "", + rerun_only("https://api.example.com"), + ), + ( + "rerun+https://api.example.com:4317", + "", + rerun_only("https://api.example.com:4317"), + ), + ( + "rerun+http://localhost:4317", + "", + rerun_only("http://localhost:4317"), + ), + ( + "rerun://host/foo/bar?x=1", + "", + rerun_only("https://host/foo/bar?x=1"), + ), + // -- Only RERUN_*, plain `http(s)://`: still authed, URL unchanged --- + ( + "https://api.example.com:4317", + "", + rerun_only("https://api.example.com:4317"), + ), + ( + "http://localhost:4317", + "", + rerun_only("http://localhost:4317"), + ), + // -- Invalid RERUN_*: Err (no silent fallback to OTEL_*) ----- + ("ftp://collector", "", Want::Err), + ("grpc://collector", "", Want::Err), + ("garbage", "", Want::Err), + ("api.example.com", "", Want::Err), + ("RERUN://host", "", Want::Err), // Case-sensitive: `re_uri::Scheme` convention. + ("Rerun+Https://host", "", Want::Err), + ("HTTPS://host", "", Want::Err), + ("rerun:/host", "", Want::Err), + ("rerun", "", Want::Err), + ("ftp://bad", "https://otel:4317", Want::Err), // Malformed RERUN_* does NOT fall back to OTEL_*. + // -- Both set: dual-publish (both exporters active) ---------- + ( + "rerun://hub", + "https://collector", + both("https://hub", "https://collector"), + ), + ( + "http://hub", + "https://collector", + both("http://hub", "https://collector"), + ), + ( + "rerun+http://hub:4317", + "https://collector:4317", + both("http://hub:4317", "https://collector:4317"), + ), + ]; + + for (rerun, otel, want) in cases { + let got = ResolvedTraceEndpoints::resolve(rerun, otel); + let matches = match (&got, want) { + (Err(_), Want::Err) => true, + ( + Ok(endpoints), + Want::Endpoints { + rerun_authed, + standard, + }, + ) => { + endpoints.rerun_authed.as_deref() == *rerun_authed + && endpoints.standard.as_deref() == *standard + } + _ => false, + }; + assert!( + matches, + "resolve({rerun:?}, {otel:?})\n got: {got:?}\n expected: {want:?}", + ); + } + } + + /// Minimal structurally-valid JWT: `{"alg":"HS256","typ":"JWT"}` base64url + /// then `{}` then a stub signature. `re_auth::Jwt::try_from` only checks + /// that the header decodes, so this is enough. + const TEST_JWT: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.sig"; + + #[test] + fn build_rejects_invalid_transport_url() { + use re_auth::credentials::StaticCredentialsProvider; + + let jwt = re_auth::Jwt::try_from(TEST_JWT.to_owned()).unwrap(); + let provider = super::Arc::new(StaticCredentialsProvider::new(jwt)); + let result = + super::build_rerun_authed_span_exporter_with_provider("not a url at all", provider); + assert!(result.is_err(), "expected Err for malformed URL"); + } + + /// End-to-end: build the authed exporter with a known JWT, send a span + /// through it, and confirm the `MockOtlpCollector` receives an export + /// whose `authorization` metadata is `Bearer `. Exercises the + /// full wrapper → tonic interceptor → gRPC metadata pipeline. + #[tokio::test(flavor = "multi_thread")] + async fn authed_exporter_sends_bearer_metadata() { + use std::time::Duration; + + use opentelemetry::trace::{Tracer as _, TracerProvider as _}; + use opentelemetry_sdk::trace::{BatchSpanProcessor, SdkTracerProvider}; + use re_auth::credentials::StaticCredentialsProvider; + use re_test_mocks::otlp::MockOtlpCollector; + + let collector = MockOtlpCollector::spawn().await; + let jwt = re_auth::Jwt::try_from(TEST_JWT.to_owned()).unwrap(); + let provider = super::Arc::new(StaticCredentialsProvider::new(jwt)); + + let exporter = + super::build_rerun_authed_span_exporter_with_provider(&collector.endpoint(), provider) + .unwrap(); + + let tracer_provider = SdkTracerProvider::builder() + .with_span_processor(BatchSpanProcessor::builder(exporter).build()) + .build(); + let tracer = tracer_provider.tracer("test"); + + // Emit one span, then force a flush so we don't wait for the default + // 5-second scheduled-delay tick. + { + let span = tracer.start("authed_test_span"); + drop(span); + } + tracer_provider.force_flush().ok(); + + let received = collector + .wait_for(|_| true, Duration::from_secs(10)) + .await + .expect("collector should receive at least one span"); + + let auth = received + .metadata + .get("authorization") + .expect("authorization metadata missing") + .to_str() + .expect("authorization should be ASCII"); + assert_eq!(auth, format!("Bearer {TEST_JWT}")); + } +} diff --git a/crates/utils/re_perf_telemetry/src/trace_id_format.rs b/crates/utils/re_perf_telemetry/src/trace_id_format.rs index a4497058fff6..47dc79a82ee7 100644 --- a/crates/utils/re_perf_telemetry/src/trace_id_format.rs +++ b/crates/utils/re_perf_telemetry/src/trace_id_format.rs @@ -285,7 +285,7 @@ mod tests { ); } - /// Verifies the `OTel` mechanism used by the Data Platform async tasks to suppress + /// Verifies the `OTel` mechanism used by the catalog server async tasks to suppress /// span export while keeping `trace_id` in logs: setting an unsampled parent /// context on a child span causes the `parentbased_traceidratio` sampler to mark /// the child (and its subtree) as not-sampled, so spans are not exported, but the diff --git a/crates/utils/re_perf_telemetry/src/tracestate.rs b/crates/utils/re_perf_telemetry/src/tracestate.rs index b5e44aaf567c..d1ec6955f877 100644 --- a/crates/utils/re_perf_telemetry/src/tracestate.rs +++ b/crates/utils/re_perf_telemetry/src/tracestate.rs @@ -4,25 +4,25 @@ use opentelemetry::Context; use opentelemetry::propagation::{Extractor, Injector, TextMapPropagator}; use opentelemetry::trace::TraceContextExt as _; -/// A propagator that enriches `tracestate` with additional key-value pairs -#[derive(Debug, Clone)] -pub struct TraceStateEnricher { - additional_entries: Vec<(String, String)>, -} - -impl TraceStateEnricher { - pub fn new(tracestate_str: &str) -> Self { - Self { - additional_entries: parse_pairs(tracestate_str).into_iter().collect(), - } - } -} +/// Propagator that enriches the outbound `tracestate` header with the active +/// `rerun_session_id`, if any. +/// +/// The id source is resolved on every injection by +/// [`crate::current_rerun_session_id`] (the active Rust `with_tracing_session` +/// scope, or whatever the registered `SessionIdReader` returns — e.g. the +/// Python `tracing_session()` `ContextVar` when `rerun_py` is in use). When no +/// scope is active, this propagator is a no-op. +/// +/// Registered alongside `TraceContextPropagator` in the global propagator stack; +/// runs after it, so the existing tracestate (if any) is preserved and merged. +#[derive(Debug, Default, Clone)] +pub struct TraceStateEnricher; impl TextMapPropagator for TraceStateEnricher { fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) { - if self.additional_entries.is_empty() { + let Some(session_id) = crate::current_rerun_session_id() else { return; - } + }; let span = cx.span(); let span_context = span.span_context(); @@ -30,15 +30,10 @@ impl TextMapPropagator for TraceStateEnricher { return; } - // Start with existing `tracestate` from span context - let mut trace_state = span_context.trace_state().clone(); - - // Add our additional entries - for (key, value) in &self.additional_entries { - trace_state = trace_state - .insert(key.clone(), value.clone()) - .unwrap_or(trace_state); - } + let trace_state = span_context.trace_state().clone(); + let trace_state = trace_state + .insert(crate::RERUN_SESSION_TRACESTATE_KEY.to_owned(), session_id) + .unwrap_or(trace_state); let header = trace_state.header(); if !header.is_empty() { @@ -47,7 +42,7 @@ impl TextMapPropagator for TraceStateEnricher { } fn extract_with_context(&self, cx: &Context, _extractor: &dyn Extractor) -> Context { - // Don't modify extraction - let TraceContextPropagator handle it + // Don't modify extraction - let `TraceContextPropagator` handle it. cx.clone() } @@ -57,6 +52,69 @@ impl TextMapPropagator for TraceStateEnricher { } } +/// `SpanProcessor` that decorates **root spans** (those with no parent in the +/// `OTel` `Context`) with the active `rerun_session_id`, when one is set via +/// `tracing_session()`. +/// +/// Complement to [`TraceStateEnricher`]: +/// +/// - [`TraceStateEnricher`] writes the id into the outbound W3C `tracestate` +/// header, so the *server side* can extract it. +/// - [`RerunSessionRootSpanProcessor`] writes the id as a span attribute on +/// the local span at creation, so Tempo queries like +/// `{ .rerun_session_id = "rs_…" }` can find *client-side* spans. +/// +/// Both read from [`crate::current_rerun_session_id`] and use the same +/// [`crate::RERUN_SESSION_TRACESTATE_KEY`]. +/// +/// **Only registered on the Rerun-frontend OTLP path** (the `rerun://` / +/// `rerun+http(s)://` schemes — see `Telemetry::init`). Vanilla OTLP +/// destinations (Jaeger, generic collectors) get untagged spans. +/// +/// **Why only root spans:** child spans share their root's `trace_id`, so +/// once Tempo finds the trace by the attribute on the root, the entire tree +/// is reachable from the trace view. Tagging every span would be redundant +/// and bloat attribute storage. Matches how the server side tags its +/// `` span only (see `GrpcMakeSpan` in `grpc.rs`). +#[derive(Debug)] +pub(crate) struct RerunSessionRootSpanProcessor; + +impl opentelemetry_sdk::trace::SpanProcessor for RerunSessionRootSpanProcessor { + fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &opentelemetry::Context) { + use opentelemetry::trace::{Span as _, TraceContextExt as _}; + + // Root spans only: skip when the context already carries a valid + // parent span (the new span will be a child of that one). + if cx.span().span_context().is_valid() { + return; + } + + if let Some(id) = crate::current_rerun_session_id() { + span.set_attribute(opentelemetry::KeyValue::new( + crate::RERUN_SESSION_TRACESTATE_KEY, + id.to_string(), + )); + } + } + + fn on_end(&self, _span: opentelemetry_sdk::trace::SpanData) {} + + fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout( + &self, + _timeout: std::time::Duration, + ) -> opentelemetry_sdk::error::OTelSdkResult { + Ok(()) + } + + fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult { + Ok(()) + } +} + /// Parse `tracestate` pairs, keeping only valid pairs and ignoring malformed ones as /// per W3C spec guidance. We should never fail a request because of malformed tracestate. pub fn parse_pairs(input: &str) -> HashMap { @@ -88,8 +146,171 @@ pub fn parse_pairs(input: &str) -> HashMap { #[cfg(test)] mod tests { + use std::collections::HashMap; + + use opentelemetry::trace::{ + SpanContext, SpanId, TraceContextExt as _, TraceFlags, TraceId, TraceState, + }; + use super::*; + /// Test injector that records all `set` calls into a map. + #[derive(Default)] + struct MapInjector { + entries: HashMap, + } + + impl Injector for MapInjector { + fn set(&mut self, key: &str, value: String) { + self.entries.insert(key.to_owned(), value); + } + } + + fn ctx_with_state(state: TraceState) -> Context { + let span_cx = SpanContext::new( + TraceId::from_bytes([ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, + 0xcd, 0xef, + ]), + SpanId::from_bytes([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]), + TraceFlags::SAMPLED, + false, + state, + ); + Context::new().with_remote_span_context(span_cx) + } + + #[test] + fn enricher_no_session_is_noop() { + let cx = ctx_with_state(TraceState::default()); + let mut injector = MapInjector::default(); + + // No active tracing session in this test → `current_rerun_session_id` returns None. + TraceStateEnricher.inject_context(&cx, &mut injector); + + assert!( + injector.entries.is_empty(), + "expected no header writes, got {:?}", + injector.entries + ); + } + + #[test] + fn enricher_invalid_span_context_is_noop() { + // Default Context has no valid span context. + let cx = Context::new(); + let mut injector = MapInjector::default(); + + TraceStateEnricher.inject_context(&cx, &mut injector); + + assert!(injector.entries.is_empty()); + } + + /// Build a tracer wired up with `RerunSessionRootSpanProcessor` and an + /// in-memory exporter behind a `SimpleSpanProcessor`. The two processors + /// fire in order (registration order), so `on_start` has run before the + /// span is exported and its attributes are visible on `SpanData`. + fn build_tracer_with_processor() -> ( + opentelemetry_sdk::trace::Tracer, + opentelemetry_sdk::trace::InMemorySpanExporter, + opentelemetry_sdk::trace::SdkTracerProvider, + ) { + use opentelemetry::trace::TracerProvider as _; + let exporter = opentelemetry_sdk::trace::InMemorySpanExporter::default(); + let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_span_processor(RerunSessionRootSpanProcessor) + .with_span_processor(opentelemetry_sdk::trace::SimpleSpanProcessor::new( + exporter.clone(), + )) + .build(); + let tracer = provider.tracer("test"); + (tracer, exporter, provider) + } + + fn rerun_session_attr(span: &opentelemetry_sdk::trace::SpanData) -> Option { + span.attributes + .iter() + .find(|kv| kv.key.as_str() == crate::RERUN_SESSION_TRACESTATE_KEY) + .map(|kv| kv.value.to_string()) + } + + #[test] + fn processor_no_session_root_has_no_attribute() { + use opentelemetry::trace::Tracer as _; + let (tracer, exporter, provider) = build_tracer_with_processor(); + + // No `tracing_session()` scope active in this test build → the processor + // sees `current_rerun_session_id() == None` and skips the attribute. + let span = tracer.start("root"); + drop(span); + provider.force_flush().ok(); + + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + assert!( + rerun_session_attr(&spans[0]).is_none(), + "expected no rerun_session_id on root: got {:?}", + spans[0].attributes, + ); + } + + #[test] + fn processor_no_session_child_has_no_attribute() { + use opentelemetry::trace::Tracer as _; + let (tracer, exporter, provider) = build_tracer_with_processor(); + + // Attach a parent context with a valid (remote) span. The processor's + // first check (`cx.span().span_context().is_valid()`) is true here, so + // it returns early without touching attributes — independent of whether + // a session id is set. + let parent_cx = ctx_with_state(TraceState::default()); + let _attach = parent_cx.attach(); + let span = tracer.start("child"); + drop(span); + drop(_attach); + provider.force_flush().ok(); + + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + assert!( + rerun_session_attr(&spans[0]).is_none(), + "expected no rerun_session_id on child: got {:?}", + spans[0].attributes, + ); + } + + #[test] + fn processor_active_session_root_sets_attribute() { + use opentelemetry::trace::Tracer as _; + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let sid = crate::tracing_session::RerunTracingSessionId::parse("rs_cafebabe").unwrap(); + let expected = sid.to_string(); + let (tracer, exporter, provider) = build_tracer_with_processor(); + + rt.block_on(crate::tracing_session::scope_session_id_for_test( + Some(sid), + async { + let span = tracer.start("root"); + drop(span); + }, + )); + provider.force_flush().ok(); + + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + let attr = rerun_session_attr(&spans[0]).unwrap_or_else(|| { + panic!( + "expected rerun_session_id on root span: got {:?}", + spans[0].attributes, + ) + }); + assert_eq!(attr, expected); + } + #[test] fn test_parse_pairs_resilient() { // Valid pairs diff --git a/crates/utils/re_perf_telemetry/src/tracing_session.rs b/crates/utils/re_perf_telemetry/src/tracing_session.rs new file mode 100644 index 000000000000..13dcdae4eead --- /dev/null +++ b/crates/utils/re_perf_telemetry/src/tracing_session.rs @@ -0,0 +1,500 @@ +//! Customer-facing tracing-session correlation. +//! +//! See `rerun_py/rerun_sdk/rerun/_tracing_session.py` for the user-facing context +//! manager. The Rust side here owns the propagation pipeline: +//! +//! - The W3C `tracestate` key the session id rides under is +//! [`RERUN_SESSION_TRACESTATE_KEY`]. +//! - The `rs_<8-hex>` format is enforced by the [`RerunTracingSessionId`] newtype, whose +//! only constructor [`RerunTracingSessionId::parse`] returns `None` on malformed input. +//! Anything typed as `RerunTracingSessionId` past that boundary is by construction valid. +//! - The atomic gate ([`inc_active_tracing_session_count`] / +//! [`dec_active_tracing_session_count`]) lets the per-RPC injection path skip +//! invoking the [`SessionIdReader`] callback when nobody is opted in. The +//! Python GIL is the motivating cost — under `rerun_py` the reader reaches +//! into a Python `ContextVar` — but the gate is reader-agnostic. +//! - [`with_current_tracing_session`] calls the registered [`SessionIdReader`] +//! once at the host-language→Rust boundary and stashes the value in a +//! `tokio::task_local!` slot so every fan-out RPC inside the wrapped scope +//! shares one reader call. +//! - [`current_rerun_session_id`] is the lookup the propagator +//! (`TraceStateEnricher`) uses on every outbound gRPC injection. +//! - The `SessionIdReader` callback is supplied by the SDK binding via +//! [`crate::Telemetry::init_with_session_id_reader`] (gated on the +//! `session_id_reader` feature). Without it, this crate has no knowledge of +//! where the active id lives — by design, so the crate doesn't need to pull +//! in a host-language runtime (e.g. pyo3) just to read one string. + +/// The W3C `tracestate` key under which the rerun session id propagates. +/// +/// Server-side, `GrpcMakeSpan::make_span` reads this key and records the value as +/// the `rerun_session_id` span attribute, queryable in Tempo as +/// `{ .rerun_session_id = "…" }`. +pub const RERUN_SESSION_TRACESTATE_KEY: &str = "rerun_session_id"; + +/// A validated rerun session id. +/// +/// The only constructor is [`RerunTracingSessionId::parse`], which enforces the +/// `rs_<8-hex>` format (e.g. `rs_cafebabe`). Holding a value of this type is a +/// compile-time guarantee that the contained string is well-formed: malformed +/// user input never pollutes server-side span attributes or outbound +/// `tracestate` headers. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct RerunTracingSessionId(String); + +impl RerunTracingSessionId { + /// Generate a fresh random session id of the form `rs_<8 lowercase hex>`. + /// + /// Module-private: the only public way to start a session is + /// [`with_tracing_session`], which calls this internally. + fn fresh() -> Self { + let n: u32 = rand::random(); + Self(format!("rs_{n:08x}")) + } + + /// Parse a string into a [`RerunTracingSessionId`]. + /// + /// Accepts exactly `rs_` followed by 8 lowercase hex digits. Returns `None` + /// for any other input (wrong prefix, wrong length, uppercase, non-hex). + pub fn parse(s: &str) -> Option { + let rest = s.strip_prefix("rs_")?; + if rest.len() == 8 + && rest + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + { + Some(Self(s.to_owned())) + } else { + None + } + } + + /// Borrow the underlying string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for RerunTracingSessionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl From for String { + fn from(id: RerunTracingSessionId) -> Self { + id.0 + } +} + +/// Process-wide counter of active tracing-session scopes. +/// +/// Read on every outbound gRPC injection to short-circuit the [`SessionIdReader`] +/// call when nobody is opted in. Bumped from both entry points: Rust +/// [`with_tracing_session`] (via [`ActiveSessionGuard`]) and Python +/// `tracing_session().__enter__`/`__exit__`. +static ACTIVE_TRACING_SESSION_COUNT: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +/// Increment the active-session counter. Called by Python +/// `tracing_session().__enter__`; the Rust [`with_tracing_session`] path goes +/// through `ActiveSessionGuard` instead. +pub fn inc_active_tracing_session_count() { + ACTIVE_TRACING_SESSION_COUNT.fetch_add(1, std::sync::atomic::Ordering::Release); +} + +/// Decrement the active-session counter. Called by Python +/// `tracing_session().__exit__`; the Rust [`with_tracing_session`] path goes +/// through `ActiveSessionGuard`'s drop instead. +pub fn dec_active_tracing_session_count() { + ACTIVE_TRACING_SESSION_COUNT.fetch_sub(1, std::sync::atomic::Ordering::Release); +} + +/// RAII handle on [`ACTIVE_TRACING_SESSION_COUNT`]: increments on construction, +/// decrements on drop — including drop during panic unwind. Used by Rust +/// scopes ([`with_tracing_session`], `scope_session_id_for_test`) so a panic +/// inside the wrapped future doesn't leak the counter and leave the atomic +/// gate stuck "active" for the rest of the process. +/// +/// Python's `tracing_session().__exit__` runs on exception, so the Python +/// counterpart doesn't need this — the guard exists to match that behavior. +struct ActiveSessionGuard; + +impl ActiveSessionGuard { + fn new() -> Self { + inc_active_tracing_session_count(); + Self + } +} + +impl Drop for ActiveSessionGuard { + fn drop(&mut self) { + dec_active_tracing_session_count(); + } +} + +/// Callback signature for resolving the active session id from a +/// host-language store (e.g. a Python `ContextVar` in `rerun_py`). +/// +/// Registered once at telemetry init via +/// [`crate::Telemetry::init_with_session_id_reader`]. Read on the slow path of +/// [`current_rerun_session_id`] and on the read-once at +/// [`with_current_tracing_session`]. +#[cfg(feature = "session_id_reader")] +pub type SessionIdReader = fn() -> Option; + +#[cfg(feature = "session_id_reader")] +static SESSION_ID_READER: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// Install the host-language session-id reader. First call wins. Subsequent +/// calls are silently ignored — registration is owned by whichever crate +/// initializes `Telemetry`. +#[cfg(feature = "session_id_reader")] +pub(crate) fn set_session_id_reader(reader: SessionIdReader) { + // Result intentionally discarded: first-call-wins, subsequent attempts + // are a silent no-op (see doc comment). + SESSION_ID_READER.set(reader).ok(); +} + +/// Invoke the registered reader, if any. Returns `None` when no reader has +/// been installed or when the feature is off. +fn read_via_reader() -> Option { + #[cfg(feature = "session_id_reader")] + { + let reader = SESSION_ID_READER.get()?; + reader() + } + #[cfg(not(feature = "session_id_reader"))] + { + None + } +} + +// Per-tokio-task slot caching the rerun session id for the duration of a +// wrapped scope. +// +// Set once by `with_current_tracing_session` at the host-language→Rust boundary +// — typically a pyo3 catalog entry point in `rerun_py`, where calling the +// `SessionIdReader` means acquiring the GIL — and read on every outbound gRPC +// injection by `current_rerun_session_id` without re-invoking the reader. +// Propagates across `.await` within the same tokio task so DataFusion fan-out +// RPCs all share the value. +tokio::task_local! { + static CURRENT_TRACING_SESSION_ID: Option; +} + +/// Wrap `f` so the active rerun session id is resolved once at entry (via the +/// registered [`SessionIdReader`]) and stays accessible to every outbound gRPC +/// inside it without re-invoking the reader. +/// +/// Used at every pyo3 catalog entry point in `rerun_py` to amortize the GIL +/// cost across the catalog method's fan-out. +#[must_use] +pub fn with_current_tracing_session( + f: F, +) -> tokio::task::futures::TaskLocalFuture, F> +where + F: std::future::Future, +{ + let sid = read_current_tracing_session_id_at_boundary(); + CURRENT_TRACING_SESSION_ID.scope(sid, f) +} + +/// One-shot reader-callback invocation used by [`with_current_tracing_session`]. +/// Gates on the atomic counter so the host-language store is never touched +/// when no scope is active. +fn read_current_tracing_session_id_at_boundary() -> Option { + if ACTIVE_TRACING_SESSION_COUNT.load(std::sync::atomic::Ordering::Acquire) == 0 { + return None; + } + read_via_reader() +} + +/// Returns the active rerun session id, if any. +/// +/// Source resolution, in order: +/// +/// 1. Atomic gate: if no `tracing_session()` scope is active anywhere in the +/// process, return `None` immediately. One atomic load. +/// 2. tokio `task_local` set by [`with_current_tracing_session`] at the +/// host-language→Rust boundary: that value, possibly `None`, is +/// authoritative for the current task. +/// 3. Fallback: invoke the registered [`SessionIdReader`] callback (if any). +/// Only reached when the RPC fires outside any boundary helper (rare). +/// +/// Returns `None` when no scope is active, the value fails +/// [`RerunTracingSessionId::parse`], or no reader has been registered (e.g. +/// the binary was built without the `session_id_reader` feature). +pub fn current_rerun_session_id() -> Option { + if ACTIVE_TRACING_SESSION_COUNT.load(std::sync::atomic::Ordering::Acquire) == 0 { + return None; + } + + if let Ok(opt) = CURRENT_TRACING_SESSION_ID.try_with(|sid| sid.clone()) { + return opt; + } + + read_via_reader() +} + +/// Test-only: scope `sid` into the task-local that [`current_rerun_session_id`] +/// reads, *and* bump the process-wide active-session counter so the atomic gate +/// doesn't short-circuit to `None`. Used by sibling crate modules (e.g. the +/// `RerunSessionRootSpanProcessor` tests in `tracestate.rs`) that need to +/// simulate an active `tracing_session()` scope without a Python interpreter. +#[cfg(test)] +pub(crate) async fn scope_session_id_for_test( + sid: Option, + f: F, +) -> F::Output { + let _guard = ActiveSessionGuard::new(); + CURRENT_TRACING_SESSION_ID.scope(sid, f).await +} + +/// Tag every Rerun Hub request inside `f` with a fresh session id, so the +/// full set of requests can be correlated end-to-end for support. +/// +/// Two INFO log lines are emitted through the `tracing` stack — one on +/// entry, one on exit: +/// +/// ```text +/// INFO rerun tracing session started: rs_8f3a91e2 +/// … +/// INFO rerun tracing session finished rerun_session_id=rs_8f3a91e2 elapsed_s=12.345 +/// ``` +/// +/// The "started" log fires the moment the scope is entered, so the id +/// stays visible even if the workflow crashes or hangs before completing. +/// Send that id to Rerun support and they can query +/// `{ .rerun_session_id = "rs_…" }` in our trace store to surface every +/// related request. +/// +/// The "finished" log fires on normal return from `f` (whether it resolves +/// to `Ok` or `Err`) and includes the wall-clock duration. It is *skipped +/// if `f` panics* — the "started" log has already given the customer the +/// id, and a misleading "finished" log on a crash would just confuse. +/// +/// Counterpart to Python's `tracing_session()` context manager. When you +/// also opt into exporting client-side traces (by setting +/// `RERUN_TELEMETRY_ENDPOINT`), those exported spans are tagged +/// with the same id, so the client→server trace tree stays correlated. +/// +/// # Example +/// +/// ```ignore +/// use re_perf_telemetry::with_tracing_session; +/// +/// with_tracing_session(async { +/// let datasets = client.dataset_names().await?; +/// let ds = client.get_dataset("…").await?; +/// // … +/// }) +/// .await; +/// ``` +/// +/// Nested calls work as you'd expect: an inner scope shadows the outer +/// session id while open, and the outer id is restored when the inner +/// scope exits. +/// +/// # Getting the id programmatically +/// +/// Most callers don't need the id in code — the INFO log is the +/// customer-facing way to retrieve it. If you do need it (e.g., to embed +/// in a support ticket emitted by your own logger), call +/// [`current_rerun_session_id`] from inside `f`: +/// +/// ```ignore +/// use re_perf_telemetry::{current_rerun_session_id, with_tracing_session}; +/// +/// with_tracing_session(async { +/// let sid = current_rerun_session_id().expect("inside with_tracing_session"); +/// my_logger::warn!("about to run a long workflow under session {sid}"); +/// // … +/// }) +/// .await; +/// ``` +pub async fn with_tracing_session(f: F) -> F::Output { + // No-op + warn if the telemetry stack hasn't been initialized. Without + // it the propagator and span processor aren't installed, so outbound + // requests wouldn't actually be tagged — running the full setup would + // silently mislead the caller. Mirrors Python `tracing_session()`'s + // no-op-with-warning branch. + if !crate::is_telemetry_active() { + tracing::warn!( + "with_tracing_session is a no-op: the rerun telemetry stack is not active. \ + Call `Telemetry::init` first to enable session correlation." + ); + return f.await; + } + + let sid = RerunTracingSessionId::fresh(); + tracing::info!("rerun tracing session started: {sid}"); + // Participate in the process-wide active-scope counter (same as + // Python's `__enter__`/`__exit__`) so `current_rerun_session_id`'s + // atomic short-circuit correctly reflects that a session is active. + // RAII so an `f` panic still decrements — see [`ActiveSessionGuard`]. + let _guard = ActiveSessionGuard::new(); + let t0 = std::time::Instant::now(); + let out = CURRENT_TRACING_SESSION_ID.scope(Some(sid.clone()), f).await; + // Intentionally skipped on panic: if `f` panics the await unwinds and + // this line never runs. The "started" log has already surfaced the id, + // and a misleading "finished" log on a crash would just add noise. The + // counter, by contrast, is decremented unconditionally via `_guard`. + tracing::info!( + rerun_session_id = %sid, + elapsed_s = format!("{:.3}", t0.elapsed().as_secs_f64()), + "rerun tracing session finished", + ); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_malformed_ids() { + assert!(RerunTracingSessionId::parse("").is_none()); + assert!(RerunTracingSessionId::parse("rs_").is_none()); + assert!(RerunTracingSessionId::parse("rs_cafebab").is_none()); // 7 hex chars + assert!(RerunTracingSessionId::parse("rs_cafebabe1").is_none()); // 9 hex chars + assert!(RerunTracingSessionId::parse("rs_CAFEBABE").is_none()); // uppercase rejected + assert!(RerunTracingSessionId::parse("rs_cafebabz").is_none()); // non-hex + assert!(RerunTracingSessionId::parse("xx_cafebabe").is_none()); // wrong prefix + assert!(RerunTracingSessionId::parse("cafebabe").is_none()); // missing prefix + } + + #[test] + fn accepts_well_formed_id() { + assert_eq!( + RerunTracingSessionId::parse("rs_cafebabe") + .unwrap() + .as_str(), + "rs_cafebabe", + ); + assert!(RerunTracingSessionId::parse("rs_00000000").is_some()); + assert!(RerunTracingSessionId::parse("rs_ffffffff").is_some()); + assert!(RerunTracingSessionId::parse("rs_0123abcd").is_some()); + } + + /// `fresh()` must produce ids that round-trip through `parse`. + /// + /// Mirrors the Python `test_generated_id_is_valid` test on the + /// `_generate_session_id` / `_is_valid_session_id` pair. + #[test] + fn fresh_generates_valid_id() { + for _ in 0..16 { + let sid = RerunTracingSessionId::fresh(); + assert!( + RerunTracingSessionId::parse(&sid.to_string()).is_some(), + "fresh() produced unparsable id: {sid}" + ); + } + } + + /// Nested `with_tracing_session` scopes shadow the outer id while open and + /// restore it on exit. Mirrors the Python + /// `test_nested_sessions_shadow_and_restore` test — same semantics, here + /// implemented via `tokio::task_local::scope` instead of `ContextVar` + /// token reset. + #[test] + fn nested_sessions_shadow_and_restore() { + use parking_lot::Mutex; + use std::sync::Arc; + + // `with_tracing_session` no-ops unless the telemetry stack is up; flip + // the flag directly so this test exercises the active branch without + // standing up the full OTel pipeline. + crate::telemetry::set_telemetry_active_for_test(true); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let captures: Arc; 3]>> = + Arc::new(Mutex::new([None, None, None])); + let captures_outer = Arc::clone(&captures); + + rt.block_on(super::with_tracing_session(async move { + // 1: outer scope active + captures_outer.lock()[0] = current_rerun_session_id(); + + let captures_inner = Arc::clone(&captures_outer); + super::with_tracing_session(async move { + // 2: inner scope shadows outer + captures_inner.lock()[1] = current_rerun_session_id(); + }) + .await; + + // 3: outer restored after inner exits + captures_outer.lock()[2] = current_rerun_session_id(); + })); + + let captures = captures.lock(); + let outer = captures[0].clone().expect("outer scope should be active"); + let inner = captures[1].clone().expect("inner scope should be active"); + let after_inner = captures[2] + .clone() + .expect("outer should be restored after inner exit"); + drop(captures); + + assert_ne!(outer, inner, "nested scope should generate a distinct id"); + assert_eq!( + outer, after_inner, + "outer id should be restored after inner exits" + ); + + // 4: session fully cleared after outermost exits + assert!( + current_rerun_session_id().is_none(), + "session should be cleared after outermost exits" + ); + } + + /// Sanity: the active-session gate starts at zero and round-trips inc/dec. + #[test] + fn gate_inc_dec_round_trips() { + use std::sync::atomic::Ordering; + + // Fresh process: counter is zero. + assert_eq!(ACTIVE_TRACING_SESSION_COUNT.load(Ordering::Acquire), 0); + inc_active_tracing_session_count(); + assert_eq!(ACTIVE_TRACING_SESSION_COUNT.load(Ordering::Acquire), 1); + dec_active_tracing_session_count(); + assert_eq!(ACTIVE_TRACING_SESSION_COUNT.load(Ordering::Acquire), 0); + } + + /// A panic inside `with_tracing_session`'s body must not leak the + /// active-session counter. Without the RAII guard the atomic gate would + /// stay stuck "active" and every subsequent `current_rerun_session_id` + /// call in the process would skip the fast path forever. + #[test] + fn counter_balanced_on_panic_in_body() { + use std::panic::AssertUnwindSafe; + use std::sync::atomic::Ordering; + + crate::telemetry::set_telemetry_active_for_test(true); + + let baseline = ACTIVE_TRACING_SESSION_COUNT.load(Ordering::Acquire); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + #[expect(clippy::disallowed_methods, reason = "tests compile with panic=unwind")] + let result = std::panic::catch_unwind(AssertUnwindSafe(|| { + rt.block_on(super::with_tracing_session(async { + panic!("boom"); + })); + })); + assert!(result.is_err(), "panic should have propagated"); + + assert_eq!( + ACTIVE_TRACING_SESSION_COUNT.load(Ordering::Acquire), + baseline, + "active-session counter must return to baseline after panic", + ); + } +} diff --git a/crates/utils/re_quota_channel/src/async_mpsc_channel.rs b/crates/utils/re_quota_channel/src/async_mpsc_channel.rs index c7f0f40aa8c3..f90a3a7fbd87 100644 --- a/crates/utils/re_quota_channel/src/async_mpsc_channel.rs +++ b/crates/utils/re_quota_channel/src/async_mpsc_channel.rs @@ -60,6 +60,13 @@ impl Sender { pub fn blocking_send(&self, value: T) -> Result<(), mpsc::error::SendError> { tokio::runtime::Handle::current().block_on(self.send(value)) } + + /// Try to send a value without blocking. + /// + /// Returns `Err` if the channel is full or the receiver has been dropped. + pub fn try_send(&self, value: T) -> Result<(), mpsc::error::TrySendError> { + self.inner.try_send(value) + } } /// A receiver for an mpsc channel. diff --git a/crates/utils/re_quota_channel/src/sync/mod.rs b/crates/utils/re_quota_channel/src/sync/mod.rs index aedb84ce73a7..50b18c2e6631 100644 --- a/crates/utils/re_quota_channel/src/sync/mod.rs +++ b/crates/utils/re_quota_channel/src/sync/mod.rs @@ -423,6 +423,13 @@ impl Receiver { } } +impl re_byte_size::SizeBytes for Receiver { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.current_bytes() + } +} + // ---------------------------------------------------------------------------- /// Create a new byte-bounded channel. diff --git a/crates/utils/re_ros_msg/Cargo.toml b/crates/utils/re_ros_msg/Cargo.toml index 5655ed95ceda..a0a2430cb2fd 100644 --- a/crates/utils/re_ros_msg/Cargo.toml +++ b/crates/utils/re_ros_msg/Cargo.toml @@ -16,5 +16,6 @@ workspace = true [dependencies] anyhow.workspace = true -serde.workspace = true +itertools.workspace = true +re_cdr.workspace = true thiserror.workspace = true diff --git a/crates/utils/re_ros_msg/src/deserialize/mod.rs b/crates/utils/re_ros_msg/src/deserialize/mod.rs index d8c90f84dac3..fb87f5655f43 100644 --- a/crates/utils/re_ros_msg/src/deserialize/mod.rs +++ b/crates/utils/re_ros_msg/src/deserialize/mod.rs @@ -1,15 +1,14 @@ use std::collections::{BTreeMap, HashMap}; -use serde::de::{self, DeserializeSeed}; +use re_cdr::{CdrEndian, CdrReader, Error, Result}; -use crate::deserialize::primitive_array::PrimitiveArraySeed; -use crate::message_spec::{BuiltInType, ComplexType, MessageSpecification, Type}; +use crate::deserialize::primitive_array::PrimitiveArray; +use crate::message_spec::{ + ArraySize, BuiltInType, ComplexType, MessageSpecification, Type, message_package, +}; -pub mod primitive; pub mod primitive_array; -use primitive::{PrimitiveVisitor, StringVisitor}; - /// A single deserialized value of any type that can appear in a ROS message. #[derive(Clone, PartialEq)] pub enum Value { @@ -76,310 +75,182 @@ impl std::fmt::Debug for Value { /// How we resolve a [`ComplexType`] at runtime. pub trait TypeResolver { - fn resolve(&self, ty: &ComplexType) -> Option<&MessageSpecification>; + fn resolve( + &self, + scope: &MessageSpecification, + ty: &ComplexType, + ) -> Option<&MessageSpecification>; } -/// Efficient type resolver with separate maps for absolute and relative lookups. +/// Efficient type resolver for fully-qualified ROS message names. pub struct MapResolver<'a> { /// Maps "pkg/Type" -> [`MessageSpecification`] absolute: HashMap, - - /// Maps "Type" -> [`MessageSpecification`] - relative: HashMap, } impl<'a> MapResolver<'a> { pub fn new(specs: impl IntoIterator) -> Self { let mut absolute = HashMap::new(); - let mut relative = HashMap::new(); for (full_name, spec) in specs { - if let Some((_, name)) = full_name.rsplit_once('/') { - // This is an absolute type like "pkg/Type" - absolute.insert(full_name.clone(), spec); - relative.insert(name.to_owned(), spec); - } else { - // This is already a relative type like "Type" - relative.insert(full_name, spec); - } + absolute.insert(full_name, spec); } - Self { absolute, relative } + Self { absolute } } } impl TypeResolver for MapResolver<'_> { - fn resolve(&self, ty: &ComplexType) -> Option<&MessageSpecification> { + fn resolve( + &self, + scope: &MessageSpecification, + ty: &ComplexType, + ) -> Option<&MessageSpecification> { match ty { ComplexType::Absolute { package, name } => { let full_name = format!("{package}/{name}"); self.absolute.get(&full_name).copied() } - ComplexType::Relative { name } => self.relative.get(name).copied(), - } - } -} - -/// Whole message (struct) in field order. -pub struct MessageSeed<'a, R: TypeResolver> { - specification: &'a MessageSpecification, - type_resolver: &'a R, -} - -impl<'a, R: TypeResolver> MessageSeed<'a, R> { - pub fn new(spec: &'a MessageSpecification, type_resolver: &'a R) -> Self { - Self { - specification: spec, - type_resolver, - } - } -} - -impl<'de, R: TypeResolver> DeserializeSeed<'de> for MessageSeed<'_, R> { - type Value = Value; - - fn deserialize(self, de: D) -> Result - where - D: de::Deserializer<'de>, - { - de.deserialize_tuple( - self.specification.fields.len(), - MessageVisitor { - spec: self.specification, - type_resolver: self.type_resolver, - }, - ) - } -} - -struct MessageVisitor<'a, R: TypeResolver> { - spec: &'a MessageSpecification, - type_resolver: &'a R, -} - -impl<'de, R: TypeResolver> serde::de::Visitor<'de> for MessageVisitor<'_, R> { - type Value = Value; - - fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "cdr struct as fixed-length tuple") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut out = std::collections::BTreeMap::new(); - for field in &self.spec.fields { - let v = seq - .next_element_seed(SchemaSeed::new(&field.ty, self.type_resolver))? - .ok_or_else(|| serde::de::Error::custom("missing struct field"))?; - out.insert(field.name.clone(), v); + ComplexType::Relative { name } => { + let full_name = if let Some(package) = message_package(&scope.name) { + format!("{package}/{name}") + } else { + name.clone() + }; + self.absolute.get(&full_name).copied() + } } - Ok(Value::Message(out)) } } -/// One value, driven by a [`Type`] + resolver. -pub(super) struct SchemaSeed<'a, R: TypeResolver> { - ty: &'a Type, - resolver: &'a R, -} - -impl<'a, R: TypeResolver> SchemaSeed<'a, R> { - pub fn new(ty: &'a Type, resolver: &'a R) -> Self { - Self { ty, resolver } +/// Decode a CDR-encoded message into a [`Value`] by walking its [`MessageSpecification`]. +pub fn decode_message( + reader: &mut CdrReader<'_, BO>, + spec: &MessageSpecification, + resolver: &R, +) -> Result { + let mut fields = BTreeMap::new(); + for field in &spec.fields { + fields.insert( + field.name.clone(), + decode_value(reader, spec, &field.ty, resolver)?, + ); } + Ok(Value::Message(fields)) } -impl<'de, R: TypeResolver> DeserializeSeed<'de> for SchemaSeed<'_, R> { - type Value = Value; - - fn deserialize(self, de: D) -> Result - where - D: de::Deserializer<'de>, - { - use crate::message_spec::ArraySize::{Bounded, Fixed, Unbounded}; - use crate::message_spec::Type; - - match self.ty { - Type::BuiltIn(primitive_type) => deserialize_builtin_type(primitive_type, de), - Type::Array { ty, size } => match size { - Fixed(len) => { - // Check if this is a primitive array and use optimized path - if let Type::BuiltIn(prim_type) = ty.as_ref() { - PrimitiveArraySeed { - elem: prim_type, - fixed_len: Some(*len), - } - .deserialize(de) - .map(Value::PrimitiveArray) - } else { - SequenceSeed::new(ty, Some(*len), self.resolver) - .deserialize(de) - .map(Value::Array) - } - } - Bounded(_) | Unbounded => { - // Check if this is a primitive sequence and use optimized path - if let Type::BuiltIn(prim_type) = ty.as_ref() { - PrimitiveArraySeed { - elem: prim_type, - fixed_len: None, - } - .deserialize(de) - .map(Value::PrimitiveSeq) - } else { - // CDR: length-prefixed sequence; serde side is a seq. - SequenceSeed::new(ty, None, self.resolver) - .deserialize(de) - .map(Value::Sequence) - } - } - }, - Type::Complex(complex_ty) => { - let msg = self.resolver.resolve(complex_ty).ok_or_else(|| { - de::Error::custom(format!("unknown ComplexType: {complex_ty:?}")) - })?; - - // Some ROS2 schemas model enums as separate messages containing only constants. - // On the wire, fields of those types are encoded as a single primitive value. - if let Some(primitive_type) = msg - .underlying_type_if_enum_like() - .map_err(de::Error::custom)? - { - return deserialize_builtin_type(primitive_type, de); +fn decode_value( + reader: &mut CdrReader<'_, BO>, + scope: &MessageSpecification, + ty: &Type, + resolver: &R, +) -> Result { + match ty { + Type::BuiltIn(builtin) => decode_scalar(reader, builtin), + + Type::Array { ty, size } => { + let count = match size { + ArraySize::Fixed(len) => *len, + ArraySize::Bounded(_) | ArraySize::Unbounded => reader.read_sequence_length()?, + }; + let fixed = matches!(size, ArraySize::Fixed(_)); + let elem = ty.as_ref(); + + if let Type::BuiltIn(builtin) = elem { + let array = decode_primitive_array(reader, builtin, count)?; + Ok(if fixed { + Value::PrimitiveArray(array) + } else { + Value::PrimitiveSeq(array) + }) + } else { + let mut values = Vec::with_capacity(count); + for _ in 0..count { + values.push(decode_value(reader, scope, elem, resolver)?); } - - MessageSeed::new(msg, self.resolver).deserialize(de) + Ok(if fixed { + Value::Array(values) + } else { + Value::Sequence(values) + }) } } - } -} - -fn deserialize_builtin_type<'de, D>(primitive_type: &BuiltInType, de: D) -> Result -where - D: de::Deserializer<'de>, -{ - use crate::message_spec::BuiltInType::{ - Bool, Byte, Char, Float32, Float64, Int8, Int16, Int32, Int64, String, UInt8, UInt16, - UInt32, UInt64, WString, - }; - match primitive_type { - Bool => de - .deserialize_bool(PrimitiveVisitor::::new()) - .map(Value::Bool), - Byte | UInt8 => de - .deserialize_u8(PrimitiveVisitor::::new()) - .map(Value::U8), // ROS2: octet - Char | Int8 => de - .deserialize_i8(PrimitiveVisitor::::new()) - .map(Value::I8), // ROS2: char (int8) - Float32 => de - .deserialize_f32(PrimitiveVisitor::::new()) - .map(Value::F32), - Float64 => de - .deserialize_f64(PrimitiveVisitor::::new()) - .map(Value::F64), - Int16 => de - .deserialize_i16(PrimitiveVisitor::::new()) - .map(Value::I16), - Int32 => de - .deserialize_i32(PrimitiveVisitor::::new()) - .map(Value::I32), - Int64 => de - .deserialize_i64(PrimitiveVisitor::::new()) - .map(Value::I64), - UInt16 => de - .deserialize_u16(PrimitiveVisitor::::new()) - .map(Value::U16), - UInt32 => de - .deserialize_u32(PrimitiveVisitor::::new()) - .map(Value::U32), - UInt64 => de - .deserialize_u64(PrimitiveVisitor::::new()) - .map(Value::U64), - String(_bound) | WString(_bound) => de.deserialize_string(StringVisitor).map(Value::String), - } -} + Type::Complex(complex) => { + let msg = resolver + .resolve(scope, complex) + .ok_or_else(|| Error::Custom(format!("unknown ComplexType: {complex:?}")))?; -// Sequence/array of elements. -pub(super) struct SequenceSeed<'a, R: TypeResolver> { - elem: &'a Type, - fixed_len: Option, - resolver: &'a R, -} - -impl<'a, R: TypeResolver> SequenceSeed<'a, R> { - pub fn new(elem: &'a Type, fixed_len: Option, resolver: &'a R) -> Self { - Self { - elem, - fixed_len, - resolver, + // Some ROS2 schemas model enums as separate messages containing only constants. + // On the wire, fields of those types are encoded as a single primitive value. + match msg + .underlying_type_if_enum_like() + .map_err(|err| Error::Custom(err.to_string()))? + { + Some(builtin) => decode_scalar(reader, builtin), + None => decode_message(reader, msg, resolver), + } } } } -impl<'de, R: TypeResolver> DeserializeSeed<'de> for SequenceSeed<'_, R> { - type Value = Vec; - - fn deserialize(self, de: D) -> Result - where - D: de::Deserializer<'de>, - { - match self.fixed_len { - Some(len) => de.deserialize_tuple( - len, - SequenceVisitor { - elem: self.elem, - fixed_len: Some(len), - type_resolver: self.resolver, - }, - ), - None => de.deserialize_seq(SequenceVisitor { - elem: self.elem, - fixed_len: None, - type_resolver: self.resolver, - }), +fn decode_scalar(reader: &mut CdrReader<'_, BO>, ty: &BuiltInType) -> Result { + Ok(match ty { + BuiltInType::Bool => Value::Bool(reader.read_bool()?), + BuiltInType::Byte | BuiltInType::Char | BuiltInType::UInt8 => Value::U8(reader.read_u8()?), + BuiltInType::Int8 => Value::I8(reader.read_i8()?), + BuiltInType::Int16 => Value::I16(reader.read_i16()?), + BuiltInType::UInt16 => Value::U16(reader.read_u16()?), + BuiltInType::Int32 => Value::I32(reader.read_i32()?), + BuiltInType::UInt32 => Value::U32(reader.read_u32()?), + BuiltInType::Int64 => Value::I64(reader.read_i64()?), + BuiltInType::UInt64 => Value::U64(reader.read_u64()?), + BuiltInType::Float32 => Value::F32(reader.read_f32()?), + BuiltInType::Float64 => Value::F64(reader.read_f64()?), + BuiltInType::String(_) => Value::String(reader.read_string()?), + // `wstring` is UTF-16 on the wire, a different layout than `string`. Decoding it as UTF-8 + // would corrupt the rest of the message, so reject it. Channels with `wstring` are normally + // kept as raw data before reaching here. + BuiltInType::WString(_) => { + return Err(Error::Custom( + "ROS 2 `wstring` decoding is not supported".to_owned(), + )); } - } + }) } -struct SequenceVisitor<'a, R: TypeResolver> { - elem: &'a Type, - fixed_len: Option, - type_resolver: &'a R, -} - -impl<'de, R: TypeResolver> serde::de::Visitor<'de> for SequenceVisitor<'_, R> { - type Value = Vec; - - fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "cdr-encoded sequence/array") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let len = self.fixed_len.or_else(|| seq.size_hint()); - let mut out = Vec::with_capacity(len.unwrap_or(0)); - - if let Some(len) = len { - for _ in 0..len { - let v = seq - .next_element_seed(SchemaSeed::new(self.elem, self.type_resolver))? - .ok_or_else(|| serde::de::Error::custom("short sequence"))?; - out.push(v); - } - } else { - // Fallback for truly unbounded streams - while let Some(v) = - seq.next_element_seed(SchemaSeed::new(self.elem, self.type_resolver))? - { - out.push(v); - } +fn decode_primitive_array( + reader: &mut CdrReader<'_, BO>, + elem: &BuiltInType, + count: usize, +) -> Result { + Ok(match elem { + BuiltInType::Bool => PrimitiveArray::Bool( + (0..count) + .map(|_| reader.read_bool()) + .collect::>()?, + ), + BuiltInType::Byte | BuiltInType::Char | BuiltInType::UInt8 => { + PrimitiveArray::U8(reader.read_numeric_vec(count)?) } - Ok(out) - } + BuiltInType::Int8 => PrimitiveArray::I8(reader.read_numeric_vec(count)?), + BuiltInType::Int16 => PrimitiveArray::I16(reader.read_numeric_vec(count)?), + BuiltInType::UInt16 => PrimitiveArray::U16(reader.read_numeric_vec(count)?), + BuiltInType::Int32 => PrimitiveArray::I32(reader.read_numeric_vec(count)?), + BuiltInType::UInt32 => PrimitiveArray::U32(reader.read_numeric_vec(count)?), + BuiltInType::Int64 => PrimitiveArray::I64(reader.read_numeric_vec(count)?), + BuiltInType::UInt64 => PrimitiveArray::U64(reader.read_numeric_vec(count)?), + BuiltInType::Float32 => PrimitiveArray::F32(reader.read_numeric_vec(count)?), + BuiltInType::Float64 => PrimitiveArray::F64(reader.read_numeric_vec(count)?), + BuiltInType::String(_) => PrimitiveArray::String( + (0..count) + .map(|_| reader.read_string()) + .collect::>()?, + ), + BuiltInType::WString(_) => { + return Err(Error::Custom( + "ROS 2 `wstring` decoding is not supported".to_owned(), + )); + } + }) } diff --git a/crates/utils/re_ros_msg/src/deserialize/primitive.rs b/crates/utils/re_ros_msg/src/deserialize/primitive.rs deleted file mode 100644 index 29bf9a8be034..000000000000 --- a/crates/utils/re_ros_msg/src/deserialize/primitive.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::fmt; - -use serde::de::{self, Visitor}; - -pub(super) struct PrimitiveVisitor(std::marker::PhantomData); - -impl PrimitiveVisitor { - pub fn new() -> Self { - Self(std::marker::PhantomData) - } -} - -macro_rules! impl_primitive_visitor { - ($t:ty, $m:ident) => { - impl Visitor<'_> for PrimitiveVisitor<$t> { - type Value = $t; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, stringify!($t)) - } - - fn $m(self, v: $t) -> Result<$t, E> { - Ok(v) - } - } - }; -} - -impl_primitive_visitor!(i8, visit_i8); -impl_primitive_visitor!(u8, visit_u8); -impl_primitive_visitor!(i16, visit_i16); -impl_primitive_visitor!(u16, visit_u16); -impl_primitive_visitor!(i32, visit_i32); -impl_primitive_visitor!(u32, visit_u32); -impl_primitive_visitor!(i64, visit_i64); -impl_primitive_visitor!(u64, visit_u64); -impl_primitive_visitor!(f32, visit_f32); -impl_primitive_visitor!(f64, visit_f64); - -impl Visitor<'_> for PrimitiveVisitor { - type Value = bool; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "bool") - } - - fn visit_bool(self, v: bool) -> Result { - Ok(v) - } - - fn visit_u8(self, v: u8) -> Result - where - E: de::Error, - { - Ok(v != 0) - } -} - -pub(super) struct StringVisitor; - -impl Visitor<'_> for StringVisitor { - type Value = String; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "string") - } - - fn visit_string(self, v: String) -> Result { - Ok(v) - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - Ok(v.to_owned()) - } -} diff --git a/crates/utils/re_ros_msg/src/deserialize/primitive_array.rs b/crates/utils/re_ros_msg/src/deserialize/primitive_array.rs index c31811b4feab..f7f8dd221d1b 100644 --- a/crates/utils/re_ros_msg/src/deserialize/primitive_array.rs +++ b/crates/utils/re_ros_msg/src/deserialize/primitive_array.rs @@ -1,9 +1,5 @@ use std::fmt; -use serde::de::{self, DeserializeSeed, Visitor}; - -use crate::message_spec::BuiltInType; - #[derive(Clone, PartialEq)] pub enum PrimitiveArray { Bool(Vec), @@ -38,89 +34,3 @@ impl std::fmt::Debug for PrimitiveArray { } } } - -/// Specialized seed for primitive arrays (arrays/sequences of built-in types). -pub struct PrimitiveArraySeed<'a> { - pub elem: &'a BuiltInType, - pub fixed_len: Option, -} - -macro_rules! impl_primitive_array_visitor { - ($prim_type:ty, $array_variant:ident, $visit_method:ident) => { - struct $array_variant; - - impl<'de> Visitor<'de> for $array_variant { - type Value = Vec<$prim_type>; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "array of {}", stringify!($prim_type)) - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: de::SeqAccess<'de>, - { - let size_hint = seq.size_hint().unwrap_or(0); - let mut vec = Vec::with_capacity(size_hint); - - while let Some(elem) = seq.next_element()? { - vec.push(elem); - } - - Ok(vec) - } - } - }; -} - -impl_primitive_array_visitor!(bool, BoolArrayVisitor, visit_bool); -impl_primitive_array_visitor!(i8, I8ArrayVisitor, visit_i8); -impl_primitive_array_visitor!(u8, U8ArrayVisitor, visit_u8); -impl_primitive_array_visitor!(i16, I16ArrayVisitor, visit_i16); -impl_primitive_array_visitor!(u16, U16ArrayVisitor, visit_u16); -impl_primitive_array_visitor!(i32, I32ArrayVisitor, visit_i32); -impl_primitive_array_visitor!(u32, U32ArrayVisitor, visit_u32); -impl_primitive_array_visitor!(i64, I64ArrayVisitor, visit_i64); -impl_primitive_array_visitor!(u64, U64ArrayVisitor, visit_u64); -impl_primitive_array_visitor!(f32, F32ArrayVisitor, visit_f32); -impl_primitive_array_visitor!(f64, F64ArrayVisitor, visit_f64); -impl_primitive_array_visitor!(String, StringArrayVisitor, visit_string); - -impl<'de> DeserializeSeed<'de> for PrimitiveArraySeed<'_> { - type Value = PrimitiveArray; - - fn deserialize(self, de: D) -> Result - where - D: de::Deserializer<'de>, - { - use BuiltInType::{ - Bool, Byte, Char, Float32, Float64, Int8, Int16, Int32, Int64, String, UInt8, UInt16, - UInt32, UInt64, WString, - }; - - macro_rules! deserialize_array { - ($de:expr, $visitor:expr) => { - match self.fixed_len { - Some(n) => $de.deserialize_tuple(n, $visitor), - None => $de.deserialize_seq($visitor), - } - }; - } - - match self.elem { - Bool => deserialize_array!(de, BoolArrayVisitor).map(PrimitiveArray::Bool), - Byte | UInt8 => deserialize_array!(de, U8ArrayVisitor).map(PrimitiveArray::U8), - Char | Int8 => deserialize_array!(de, I8ArrayVisitor).map(PrimitiveArray::I8), - Float32 => deserialize_array!(de, F32ArrayVisitor).map(PrimitiveArray::F32), - Float64 => deserialize_array!(de, F64ArrayVisitor).map(PrimitiveArray::F64), - Int16 => deserialize_array!(de, I16ArrayVisitor).map(PrimitiveArray::I16), - Int32 => deserialize_array!(de, I32ArrayVisitor).map(PrimitiveArray::I32), - Int64 => deserialize_array!(de, I64ArrayVisitor).map(PrimitiveArray::I64), - UInt16 => deserialize_array!(de, U16ArrayVisitor).map(PrimitiveArray::U16), - UInt32 => deserialize_array!(de, U32ArrayVisitor).map(PrimitiveArray::U32), - UInt64 => deserialize_array!(de, U64ArrayVisitor).map(PrimitiveArray::U64), - String(_) => deserialize_array!(de, StringArrayVisitor).map(PrimitiveArray::String), - WString(_) => Err(de::Error::custom("wstring arrays not supported")), - } - } -} diff --git a/crates/utils/re_ros_msg/src/message_spec.rs b/crates/utils/re_ros_msg/src/message_spec.rs index b3aa3b22c01a..8cd8cf48890b 100644 --- a/crates/utils/re_ros_msg/src/message_spec.rs +++ b/crates/utils/re_ros_msg/src/message_spec.rs @@ -1,3 +1,4 @@ +use itertools::Itertools as _; use thiserror::Error; #[derive(Error, Debug)] @@ -43,6 +44,15 @@ pub struct MessageSpecification { pub constants: Vec, } +/// Returns the ROS package for a message name. +/// +/// Relative message references resolve within the same package as the containing message. +/// The package is the first path segment in names like `pkg/msg/Type` or `pkg/Type`. +/// See . +pub fn message_package(name: &str) -> Option<&str> { + name.split_once('/').map(|(package, _)| package) +} + impl MessageSpecification { pub(super) fn parse(name: &str, input: &str) -> Result { let mut fields = Vec::new(); @@ -80,12 +90,22 @@ impl MessageSpecification { } } - let spec = Self { + let mut spec = Self { name: name.to_owned(), fields, constants, }; + // rosidl pads field-less messages with a `structure_needs_at_least_one_member` + // byte, except constants-only enum-like ones. Mirror that so our wire layout matches. + if spec.fields.is_empty() && !matches!(spec.underlying_type_if_enum_like(), Ok(Some(_))) { + spec.fields.push(Field { + ty: Type::BuiltIn(BuiltInType::UInt8), + name: "structure_needs_at_least_one_member".to_owned(), + default: None, + }); + } + // Sanity check: if this is an enum-like message that only contains constants, // try if we can determine the underlying type or fail early here. spec.underlying_type_if_enum_like()?; @@ -95,14 +115,16 @@ impl MessageSpecification { /// Returns the primitive type of a constants-only enum-like specification. /// - /// A spec can be assumed enum-like when it has only constants definitions, no data fields, - /// and all constants share the same built-in type. - /// - /// For example, this has `int8` as its underlying type: + /// A spec is enum-like when it has no data fields and all constants share one built-in + /// type that is a single octet. For example, this has `int8` as its underlying type: /// ```text /// int8 FOO=0 /// int8 BAR=1 /// ``` + /// + /// Constants are never serialized, so on the wire a constants-only message is a single + /// padding octet. A wider constant type would read the wrong number of bytes, so we report it + /// as non-enum-like and treat it as a padded struct instead. pub fn underlying_type_if_enum_like(&self) -> Result, ParseError> { if !self.fields.is_empty() || self.constants.is_empty() { return Ok(None); @@ -120,14 +142,22 @@ impl MessageSpecification { for constant in &self.constants[1..] { if constant.ty != Type::BuiltIn(first_type.clone()) { - // Ambiguous typing can't be handled. - return Err(ParseError::Validate(format!( - "constants-only spec `{}` uses mixed constant types", - self.name - ))); + // Mixed constant types are not enum-like, so this is a padded struct. + return Ok(None); } } + if !matches!( + first_type, + BuiltInType::Bool + | BuiltInType::Byte + | BuiltInType::Char + | BuiltInType::Int8 + | BuiltInType::UInt8 + ) { + return Ok(None); + } + Ok(Some(first_type)) } } @@ -271,11 +301,23 @@ pub enum BuiltInType { WString(Option), // Optional max length for bounded wide strings. } +/// A ROS message field type parsed from a `.msg` definition. #[derive(Debug, Clone, PartialEq)] pub enum Type { + /// A primitive ROS field type, such as `int32`, `float64`, or `string`. BuiltIn(BuiltInType), - Complex(ComplexType), // Possibly qualified with package path, e.g. `pkg/Type - Array { ty: Box, size: ArraySize }, + + /// A message type reference, either relative (`Header`) or fully-qualified (`std_msgs/Header`). + Complex(ComplexType), + + /// A fixed-size, bounded, or unbounded array of another field type. + Array { + /// The element type stored by the array. + ty: Box, + + /// The declared array size constraint. + size: ArraySize, + }, } impl Type { @@ -340,7 +382,10 @@ impl Type { } } -/// A complex (non-primitive) type, possibly qualified with a package path. +/// A complex (non-primitive) message type reference. +/// +/// ROS resolves relative message references within the same package as the containing message. +/// See . /// /// Examples: /// ```text @@ -495,7 +540,7 @@ impl Literal { .map(|e| e.trim()) .filter(|e| !e.is_empty()) .map(|elem_str| Self::parse(elem_str, elem_ty)) - .collect::, ParseError>>()?; + .try_collect()?; Ok(Self::Array(elems)) } @@ -565,11 +610,9 @@ fn strip_comment(s: &str) -> &str { continue; } match c { - '\\' => { - // escape next character only inside quotes; outside it doesn't matter for '#' - if in_quote { - escaped = true; - } + // Escape the next character only inside quotes; outside it doesn't matter for '#'. + '\\' if in_quote => { + escaped = true; } '"' | '\'' => { if !in_quote { @@ -758,18 +801,42 @@ int8 BAR=1 ); } - /// Tests that constants-only enum-like specs reject mixed primitive constant types. + /// Mixed constant types are not enum-like, so they become padded structs. #[test] - fn constants_only_spec_rejects_mixed_enum_types() { - let result = MessageSpecification::parse( + fn constants_only_spec_with_mixed_types_is_a_padded_struct() { + let spec = MessageSpecification::parse( "test/DummyEnum", r#" int8 FOO=0 uint8 BAR=1 "#, - ); + ) + .unwrap(); + + assert_eq!(spec.underlying_type_if_enum_like().unwrap(), None); + assert_eq!(spec.fields.len(), 1); + assert_eq!(spec.fields[0].name, "structure_needs_at_least_one_member"); + assert_eq!(spec.fields[0].ty, Type::BuiltIn(BuiltInType::UInt8)); + } + + /// Tests that constants-only specs wider than one octet are padded structs, not enum-like. + #[test] + fn constants_only_spec_with_wide_type_is_a_padded_struct() { + // Constants are never serialized, so on the wire this is a single padding octet. + // Collapsing to `int32` would read/write four bytes and corrupt the message. + let spec = MessageSpecification::parse( + "test/WideEnum", + r#" +int32 FOO=0 +int32 BAR=1 +"#, + ) + .unwrap(); - assert!(result.is_err()); + assert_eq!(spec.underlying_type_if_enum_like().unwrap(), None); + assert_eq!(spec.fields.len(), 1); + assert_eq!(spec.fields[0].name, "structure_needs_at_least_one_member"); + assert_eq!(spec.fields[0].ty, Type::BuiltIn(BuiltInType::UInt8)); } #[test] diff --git a/crates/utils/re_rvl/src/lib.rs b/crates/utils/re_rvl/src/lib.rs index 6804cca40280..adaf15d4f5c5 100644 --- a/crates/utils/re_rvl/src/lib.rs +++ b/crates/utils/re_rvl/src/lib.rs @@ -11,6 +11,13 @@ use thiserror::Error; const CONFIG_HEADER_SIZE: usize = size_of::() + size_of::<[f32; 2]>(); const RESOLUTION_HEADER_SIZE: usize = size_of::<[u32; 2]>(); +/// Maximum number of pixels we're willing to decode (64 Mpx, 128 MiB as `u16`). +/// +/// RVL has no magic bytes, so garbage data (e.g. a mislabeled blob) can parse as a huge +/// resolution and make the decoder attempt an absurd allocation. Real payloads are camera +/// images, which are orders of magnitude smaller than this. +const MAX_DECODED_PIXELS: u64 = 1 << 26; + /// Metadata extracted from a ROS2 `compressedDepth` RVL payload. /// /// We haven't found any other documentation on this other than the implementation itself. @@ -38,6 +45,17 @@ impl RosRvlMetadata { self.num_pixels } + /// Whether the payload carries inverse-depth quantization parameters, + /// meaning it decodes to floating point depth in meters rather than raw `u16` values. + /// + /// ROS2's `compressed_depth_image_transport` sets the quantization parameters only for + /// `32FC1` images; for `16UC1` they are zero. + /// + #[inline] + pub fn has_quantization(&self) -> bool { + self.depth_quant_a != 0.0 + } + /// Parses RVL metadata from the start of a RVL payload. pub fn parse(data: &[u8]) -> Result { if data.len() <= CONFIG_HEADER_SIZE { @@ -62,7 +80,11 @@ impl RosRvlMetadata { let payload_offset = CONFIG_HEADER_SIZE + RESOLUTION_HEADER_SIZE; let num_pixels = (width as u64) .checked_mul(height as u64) - .ok_or(RvlDecodeError::ResolutionOverflow)? as usize; + .ok_or(RvlDecodeError::ResolutionOverflow)?; + if num_pixels > MAX_DECODED_PIXELS { + return Err(RvlDecodeError::ResolutionTooLarge { width, height }); + } + let num_pixels = num_pixels as usize; if data.len() < payload_offset { return Err(RvlDecodeError::PayloadLengthMismatch { width, height }); @@ -79,7 +101,7 @@ impl RosRvlMetadata { } } -#[derive(Debug, Error)] +#[derive(Debug, Error, Clone)] pub enum RvlDecodeError { #[error("compressed depth payload missing RVL header")] MissingHeader, @@ -93,6 +115,9 @@ pub enum RvlDecodeError { #[error("RVL image resolution would overflow")] ResolutionOverflow, + #[error("RVL payload reports an implausibly large resolution {width}x{height}")] + ResolutionTooLarge { width: u32, height: u32 }, + #[error("RVL payload shorter than expected for resolution {width}x{height}")] PayloadLengthMismatch { width: u32, height: u32 }, @@ -133,12 +158,8 @@ pub fn decode_rvl_with_quantization( let disparity = decode_rvl_without_quantization(data, metadata)?; let mut depth = Vec::with_capacity(disparity.len()); - // ROS2's compressed_depth_image_transport sets inverse depth quantization parameters only for 32FC1 images. - // For 16UC1, depth_quant_a/b are zero and zeros in the disparity map represent zero depth. - // https://github.com/ros-perception/image_transport_plugins/blob/8aa39fe13a812273066bbef9b3c330508bd21618/compressed_depth_image_transport/src/codec.cpp#L263 - let has_quantization = metadata.depth_quant_a != 0.0; - - if has_quantization { + // For 16UC1 (no quantization), zeros in the disparity map represent zero depth. + if metadata.has_quantization() { for value in disparity { if value == 0 { depth.push(f32::NAN); @@ -282,6 +303,51 @@ mod tests { assert_eq!(decoded, disparity); } + /// RVL has no magic bytes, so garbage data can parse as a huge resolution; + /// this must be rejected instead of making the decoder attempt an absurd allocation. + #[test] + fn rejects_implausibly_large_resolution() { + let disparity = [0u16; 4]; + let data = build_depth_message([2, 2], &disparity, (0.0, 0.0)); + let resolution_offset = CONFIG_HEADER_SIZE; + + // An absurd width must be rejected. + let mut absurd = data.clone(); + absurd[resolution_offset..resolution_offset + 4].copy_from_slice(&u32::MAX.to_le_bytes()); + assert!(matches!( + RosRvlMetadata::parse(&absurd), + Err(RvlDecodeError::ResolutionTooLarge { .. }) + )); + + // A large-but-sane single axis is fine — the limit is on the total pixel count. + // (The ROS codec stores rows/columns as uint32, so axes may exceed u16.) + let mut wide = data; + wide[resolution_offset..resolution_offset + 4].copy_from_slice(&100_000u32.to_le_bytes()); + wide[resolution_offset + 4..resolution_offset + 8].copy_from_slice(&1u32.to_le_bytes()); + let metadata = RosRvlMetadata::parse(&wide).unwrap(); + assert_eq!(metadata.width, 100_000); + assert_eq!(metadata.num_pixels(), 100_000); + } + + #[test] + fn detects_quantization() { + let disparity = [5u16, 0, 10]; + + let quantized = build_depth_message([3, 1], &disparity, (10.0, 1.0)); + assert!( + RosRvlMetadata::parse(&quantized) + .unwrap() + .has_quantization() + ); + + let unquantized = build_depth_message([3, 1], &disparity, (0.0, 0.0)); + assert!( + !RosRvlMetadata::parse(&unquantized) + .unwrap() + .has_quantization() + ); + } + #[test] fn decodes_rvl_f32_payload() { let disparity = [5u16, 0, 10]; diff --git a/crates/utils/re_span/src/lib.rs b/crates/utils/re_span/src/lib.rs index 5c39dcf97011..d3c20055b8af 100644 --- a/crates/utils/re_span/src/lib.rs +++ b/crates/utils/re_span/src/lib.rs @@ -20,6 +20,35 @@ pub struct Span { } impl Span { + /// Construct from `start` and `len`. + #[inline] + pub fn from_start_len(start: Idx, len: Idx) -> Self { + Self { start, len } + } + + /// Construct from `start` (inclusive) and `end` (exclusive). + /// + /// Expects `start <= end`, or you will get a panic in debug mode. + #[inline] + pub fn from_start_end(start: Idx, end: Idx) -> Self + where + Idx: PartialOrd, + { + #![expect( + clippy::disallowed_macros, + reason = "We don't want to depend on re_log for re_log::debug_assert" + )] + debug_assert!( + start <= end, + "DEBUG ASSERT: start must be less than or equal to end" + ); + + Self { + start, + len: end - start, + } + } + /// The next element, just outside the range. #[inline] pub fn end(&self) -> Idx { @@ -59,6 +88,18 @@ impl Span { } } +impl Span { + /// Cast to native pointer width; useful for indexing on native platforms. + #[inline] + pub fn range_usize(self) -> Range { + let Self { start, len } = self; + Range { + start: start as usize, + end: start as usize + len as usize, + } + } +} + impl From> for Range { #[inline] fn from(value: Span) -> Self { diff --git a/crates/utils/re_string_interner/Cargo.toml b/crates/utils/re_string_interner/Cargo.toml index 3609890ca6f0..376a72bdc25f 100644 --- a/crates/utils/re_string_interner/Cargo.toml +++ b/crates/utils/re_string_interner/Cargo.toml @@ -19,17 +19,12 @@ workspace = true all-features = true -[features] -serde = ["dep:serde"] - - [dependencies] re_byte_size.workspace = true ahash.workspace = true nohash-hasher.workspace = true parking_lot.workspace = true +paste.workspace = true +serde = { workspace = true, features = ["serde_derive"] } static_assertions.workspace = true - -# Optional dependencies -serde = { workspace = true, features = ["serde_derive"], optional = true } diff --git a/crates/utils/re_string_interner/src/lib.rs b/crates/utils/re_string_interner/src/lib.rs index 507169e67b20..e2126056b3a5 100644 --- a/crates/utils/re_string_interner/src/lib.rs +++ b/crates/utils/re_string_interner/src/lib.rs @@ -10,7 +10,7 @@ pub mod external { pub use nohash_hasher; - #[cfg(feature = "serde")] + pub use paste; pub use serde; } @@ -27,7 +27,7 @@ fn hash(value: impl std::hash::Hash) -> u64 { // ---------------------------------------------------------------------------- -#[derive(Copy, Clone, Eq)] +#[derive(Copy, Clone, Eq, re_byte_size::SizeBytes)] pub struct InternedString { hash: u64, // TODO(emilk): consider removing the hash from the `InternedString` (benchmark!) string: &'static str, @@ -132,7 +132,6 @@ impl std::fmt::Display for InternedString { } } -#[cfg(feature = "serde")] impl serde::Serialize for InternedString { #[inline] fn serialize(&self, serializer: S) -> Result { @@ -140,7 +139,6 @@ impl serde::Serialize for InternedString { } } -#[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for InternedString { #[inline] fn deserialize>(deserializer: D) -> Result { @@ -189,6 +187,69 @@ impl StringInterner { // ---------------------------------------------------------------------------- +/// Intern a string literal once and return the cached value on subsequent calls. +/// +/// Use for hot paths that produce the same interned identifier every call. Without this, +/// each call hashes the literal and locks the global interner, which adds up on per-frame +/// invocations from visualizer `execute` methods, codegen accessors, etc. +/// +/// `$ty` must be a type declared via [`declare_new_type!`] (or anything constructible via +/// `From<&'static str>`). +/// +/// ```ignore +/// fn identifier() -> ViewSystemIdentifier { +/// re_string_interner::intern_static!(ViewSystemIdentifier, "Ellipsoids3D") +/// } +/// ``` +#[macro_export] +macro_rules! intern_static { + ($ty:ty, $lit:literal) => {{ + static CACHED: ::std::sync::LazyLock<$ty> = + ::std::sync::LazyLock::new(|| <$ty as ::std::convert::From<&str>>::from($lit)); + *CACHED + }}; +} + +/// Like [`intern_static!`], but for types declared via [`declare_new_type_nonempty!`]. +/// +/// Those types have no infallible `From<&str>`; instead they expose +/// `from_static_str`. The empty string is rejected **at compile time** here (the +/// literal is checked in a `const` context), so an empty literal is a build error +/// rather than a runtime panic. +/// +/// ```ignore +/// fn identifier() -> ViewSystemIdentifier { +/// re_string_interner::intern_static_nonempty!(ViewSystemIdentifier, "Ellipsoids3D") +/// } +/// ``` +/// +/// A non-empty literal compiles: +/// ``` +/// re_string_interner::declare_new_type_nonempty!( +/// /// A test identifier. +/// pub struct MyString; +/// ); +/// let _ = re_string_interner::intern_static_nonempty!(MyString, "non_empty"); +/// ``` +/// +/// An empty literal fails to compile: +/// ```compile_fail +/// re_string_interner::declare_new_type_nonempty!( +/// /// A test identifier. +/// pub struct MyString; +/// ); +/// let _ = re_string_interner::intern_static_nonempty!(MyString, ""); +/// ``` +#[macro_export] +macro_rules! intern_static_nonempty { + ($ty:ty, $lit:literal) => {{ + const _: () = assert!(!$lit.is_empty(), "empty string literal"); + static CACHED: ::std::sync::LazyLock<$ty> = + ::std::sync::LazyLock::new(|| <$ty>::from_static_str($lit)); + *CACHED + }}; +} + /// Declare a newtype wrapper around [`InternedString`] with /// all the convenience methods you would want. /// @@ -296,14 +357,232 @@ macro_rules! declare_new_type { } impl re_byte_size::SizeBytes for $StructName { + const IS_POD: bool = true; + #[inline] fn heap_size_bytes(&self) -> u64 { 0 } + } + }; +} - #[inline] - fn is_pod() -> bool { - true +/// Like [`declare_new_type!`], but the string is validated. +/// +/// Currently the only rule is that the string must not be empty, but validation is centralized in +/// one place (a private `validate` fn) so further rules (e.g. no whitespace) can be added later +/// without changing the public API. The generated `InvalidError` carries the reason +/// the string was rejected. +/// +/// Compared to [`declare_new_type!`], this: +/// - does **not** implement the infallible `From<&str>` (any lifetime) / `From`, nor an +/// infallible `new`; +/// - instead exposes fallible `try_new(impl AsRef)` (for any borrowed or owned string) and +/// `TryFrom`, returning an `InvalidError` on an invalid string; +/// - generates that `InvalidError` error type (implements [`std::error::Error`]); +/// - exposes `from_static_str(&'static str)` which **panics** on an invalid string, for use with +/// [`intern_static_nonempty!`] and other trusted compile-time literals; +/// - implements `From<&'static str>` (delegating to `from_static_str`, so it **panics** on empty), +/// which keeps `impl Into` parameters ergonomic for trusted string literals/consts +/// while still forcing dynamic `&str`/`String` through the fallible constructors; +/// - implements a validating [`serde::Deserialize`] (empty string ⇒ error), so empty values cannot +/// sneak back in through deserialization. **Do not** add a `serde::Deserialize`/`serde::Serialize` +/// derive in the passed-in attributes — they are provided here. +/// +/// Usage: +/// ``` +/// re_string_interner::declare_new_type_nonempty!( +/// /// My non-empty typesafe string +/// pub struct MyString; +/// ); +/// assert!(MyString::try_new("").is_err()); +/// assert_eq!(MyString::try_new("hi").unwrap().as_str(), "hi"); +/// assert_eq!(MyString::from("hi").as_str(), "hi"); // `From<&'static str>`, for `impl Into` ergonomics +/// ``` +#[macro_export] +macro_rules! declare_new_type_nonempty { + ( + $(#[$meta:meta])* // capture docstrings; see https://stackoverflow.com/questions/33999341/generating-documentation-in-macros + $vis:vis struct $StructName:ident; + ) => { + $crate::external::paste::paste! { + $(#[$meta])* + #[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] + pub struct $StructName($crate::InternedString); + + #[doc = "Error returned when constructing an invalid [`" $StructName "`]."] + #[derive(Clone, Copy, PartialEq, Eq)] + pub struct [] { + /// Why the string was rejected, e.g. `"must not be empty"`. + reason: &'static str, + } + + impl std::fmt::Display for [] { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, concat!("Invalid `", stringify!($StructName), "`: {}"), self.reason) + } + } + + impl std::fmt::Debug for [] { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, concat!("Invalid", stringify!($StructName), "Error({:?})"), self.reason) + } + } + + impl std::error::Error for [] {} + + impl $StructName { + /// The single place where the naming rules are enforced. + /// + /// Currently only forbids the empty string, but this is where future rules + /// (e.g. no whitespace) would go. + #[inline] + fn validate(string: &str) -> Result<(), []> { + if string.is_empty() { + return Err([] { reason: "must not be empty" }); + } + Ok(()) + } + + /// Create a new instance, failing if the string is invalid (e.g. empty). + #[inline] + pub fn try_new(string: impl AsRef) -> Result]> { + let string = string.as_ref(); + Self::validate(string)?; + Ok(Self($crate::InternedString::new(string))) + } + + /// Create from a trusted compile-time string literal. + /// + /// # Panics + /// Panics if `string` is invalid (e.g. empty). + #[inline] + pub fn from_static_str(string: &'static str) -> Self { + match Self::validate(string) { + Ok(()) => Self($crate::InternedString::new(string)), + Err(err) => panic!("{err} (got {string:?})"), + } + } + + #[inline] + pub fn as_str(&self) -> &'static str { + self.0.as_str() + } + + /// Precomputed hash of the string. + #[inline] + pub fn hash(&self) -> u64 { + self.0.hash() + } + } + + impl $crate::external::nohash_hasher::IsEnabled for $StructName {} + + // NOTE: no `TryFrom<&str>` / `TryFrom<&String>`: those would collide with the blanket + // `impl> TryFrom for T` in `core` once we implement `From<&'static str>` + // below. Use the inherent `try_new` for fallible construction from borrowed strings. + impl TryFrom for $StructName { + type Error = []; + + #[inline] + fn try_from(string: String) -> Result { + Self::try_new(string) + } + } + + // Only `&'static str` (string literals / consts), so `impl Into` parameters stay + // ergonomic for trusted compile-time values. Dynamic `&str`/`String` must go through + // the fallible `try_new`/`TryFrom` instead. + impl From<&'static str> for $StructName { + /// # Panics + /// Panics if `string` is empty. + #[inline] + fn from(string: &'static str) -> Self { + Self::from_static_str(string) + } + } + + impl AsRef for $StructName { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } + } + + impl std::ops::Deref for $StructName { + type Target = str; + + #[inline] + fn deref(&self) -> &str { + self.as_str() + } + } + + impl std::fmt::Debug for $StructName { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_str().fmt(f) + } + } + + impl std::fmt::Display for $StructName { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_str().fmt(f) + } + } + + impl<'a> PartialEq<&'a str> for $StructName { + #[inline] + fn eq(&self, other: &&'a str) -> bool { + self.as_str() == *other + } + } + + impl<'a> PartialEq<&'a str> for &$StructName { + #[inline] + fn eq(&self, other: &&'a str) -> bool { + self.as_str() == *other + } + } + + impl<'a> PartialEq<$StructName> for &'a str { + #[inline] + fn eq(&self, other: &$StructName) -> bool { + *self == other.as_str() + } + } + + impl re_byte_size::SizeBytes for $StructName { + const IS_POD: bool = true; + + #[inline] + fn heap_size_bytes(&self) -> u64 { + 0 + } + } + + impl $crate::external::serde::Serialize for $StructName { + #[inline] + fn serialize( + &self, + serializer: S, + ) -> Result { + $crate::external::serde::Serialize::serialize(self.as_str(), serializer) + } + } + + impl<'de> $crate::external::serde::Deserialize<'de> for $StructName { + #[inline] + fn deserialize>( + deserializer: D, + ) -> Result { + use $crate::external::serde::de::Error as _; + let string = ::deserialize( + deserializer, + )?; + Self::try_new(string).map_err(D::Error::custom) + } } } }; @@ -368,3 +647,72 @@ fn do_not_implement_borrow() { ); static_assertions::assert_not_impl_any!(MyString: std::borrow::Borrow); } + +#[test] +fn test_nonempty_newtype_macro() { + declare_new_type_nonempty!( + /// My non-empty typesafe string + pub struct MyNonEmptyString; + ); + + // Empty is rejected via the fallible entry points: + assert!(MyNonEmptyString::try_new("").is_err()); + assert!(MyNonEmptyString::try_new(String::new()).is_err()); + assert!(MyNonEmptyString::try_from(String::new()).is_err()); + + // Non-empty round-trips and interns: + let a = MyNonEmptyString::try_new("test").expect("non-empty"); + let b = MyNonEmptyString::try_from("test".to_owned()).expect("non-empty"); + assert_eq!(a, b); + assert_eq!(a.as_str(), "test"); + assert_eq!(a, "test"); + assert_eq!("test", a); + + // Trusted literal path: + let c = MyNonEmptyString::from_static_str("test"); + assert_eq!(a, c); + + // `From<&'static str>` keeps `impl Into<_>` ergonomic: + let d: MyNonEmptyString = "test".into(); + assert_eq!(a, d); + + fn takes(_: impl Into) {} + takes("test"); + + // The error type is a real `std::error::Error` and reports why it was rejected: + let err = MyNonEmptyString::try_new("").unwrap_err(); + let msg = std::string::ToString::to_string(&err); + assert!(msg.contains("MyNonEmptyString"), "{msg:?}"); + assert!(msg.contains("must not be empty"), "{msg:?}"); + let _: &dyn std::error::Error = &err; +} + +#[test] +#[should_panic(expected = "must not be empty")] +fn test_nonempty_from_static_str_panics_on_empty() { + declare_new_type_nonempty!( + /// My non-empty typesafe string + pub struct MyNonEmptyString; + ); + let _ = MyNonEmptyString::from_static_str(""); +} + +#[test] +#[should_panic(expected = "must not be empty")] +fn test_nonempty_from_empty_static_str_panics() { + declare_new_type_nonempty!( + /// My non-empty typesafe string + pub struct MyNonEmptyString; + ); + let _val: MyNonEmptyString = "".into(); +} + +// This should never implement `Borrow` (same as the plain macro). +#[test] +fn nonempty_do_not_implement_borrow() { + declare_new_type_nonempty!( + /// My non-empty typesafe string + pub struct MyNonEmptyString; + ); + static_assertions::assert_not_impl_any!(MyNonEmptyString: std::borrow::Borrow); +} diff --git a/crates/utils/re_test_mocks/Cargo.toml b/crates/utils/re_test_mocks/Cargo.toml new file mode 100644 index 000000000000..d0a3edd8081a --- /dev/null +++ b/crates/utils/re_test_mocks/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "re_test_mocks" +authors.workspace = true +description = "In-process server doubles (`MockOtlpCollector`, `MockPostHog`) used by tests that need to capture outbound OTel/PostHog traffic." +edition.workspace = true +homepage.workspace = true +include.workspace = true +license.workspace = true +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + + +[dependencies] +axum.workspace = true +opentelemetry-proto = { workspace = true, features = ["gen-tonic", "trace"] } +parking_lot.workspace = true +serde_json.workspace = true +tokio.workspace = true +tokio-stream = { workspace = true, features = ["net"] } +tonic = { workspace = true, features = ["router", "transport", "gzip"] } + + +[dev-dependencies] +reqwest.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/utils/re_test_mocks/README.md b/crates/utils/re_test_mocks/README.md new file mode 100644 index 000000000000..cb9335d29fe8 --- /dev/null +++ b/crates/utils/re_test_mocks/README.md @@ -0,0 +1,7 @@ +# re_test_mocks + +In-process server doubles (`MockOtlpCollector`, `MockPostHog`) used by tests that need to capture outbound OTel/PostHog traffic. + +Both mocks are full implementations of the wire protocols they stand in for — a tonic gRPC `TraceService` for OTLP and an axum HTTP handler for PostHog's `/batch` endpoint. They run on ephemeral ports in the test process, capture every request, and expose notification-driven `wait_for(…)` and `received()` accessors so tests don't have to poll. The `assert_sink_empty!` macro is the companion no-traffic assertion. + +The crate root re-exports nothing; consumers reach into the submodules directly via `re_test_mocks::otlp::MockOtlpCollector` and `re_test_mocks::posthog::MockPostHog`. diff --git a/crates/utils/re_test_mocks/src/assert.rs b/crates/utils/re_test_mocks/src/assert.rs new file mode 100644 index 000000000000..2d524efc1ed9 --- /dev/null +++ b/crates/utils/re_test_mocks/src/assert.rs @@ -0,0 +1,22 @@ +//! Test assertion macros for the mock sinks in this crate. + +/// Assert that a mock sink has received no requests. +/// +/// Works on any type that exposes `fn received(&self) -> Vec` where `T: Debug`, +/// e.g. [`crate::otlp::MockOtlpCollector`] or [`crate::posthog::MockPostHog`]. +/// Panics with the contents of the buffer if any requests are present, so failures +/// show exactly what arrived unexpectedly. +/// +/// Pass by reference: `assert_sink_empty!(&collector)`. +#[macro_export] +macro_rules! assert_sink_empty { + ($sink:expr $(,)?) => {{ + let __received = $sink.received(); + assert!( + __received.is_empty(), + "expected empty, got {} request(s):\n{:#?}", + __received.len(), + __received, + ); + }}; +} diff --git a/crates/utils/re_test_mocks/src/lib.rs b/crates/utils/re_test_mocks/src/lib.rs new file mode 100644 index 000000000000..a83a31429031 --- /dev/null +++ b/crates/utils/re_test_mocks/src/lib.rs @@ -0,0 +1,6 @@ +//! In-process server doubles for tests that need to capture outbound +//! OTel/PostHog traffic from production code. + +pub mod assert; +pub mod otlp; +pub mod posthog; diff --git a/crates/utils/re_test_mocks/src/otlp.rs b/crates/utils/re_test_mocks/src/otlp.rs new file mode 100644 index 000000000000..4ecffef779b8 --- /dev/null +++ b/crates/utils/re_test_mocks/src/otlp.rs @@ -0,0 +1,487 @@ +//! In-memory OTLP `TraceService::Export` sink for tests. +//! +//! Spawns a real tonic server bound to an OS-assigned `127.0.0.1` port, +//! records every incoming `Export` request (with its gRPC metadata) into a +//! shared buffer, and exposes a notification-driven `wait_for` helper so +//! tests don't need sleep-based polling. + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use opentelemetry_proto::tonic::collector::trace::v1::{ + ExportTraceServiceRequest, ExportTraceServiceResponse, + trace_service_server::{TraceService, TraceServiceServer}, +}; +use opentelemetry_proto::tonic::common::v1::InstrumentationScope; +use opentelemetry_proto::tonic::resource::v1::Resource; +use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans, Span}; +use parking_lot::Mutex; +use tokio::sync::{Notify, oneshot}; +use tonic::transport::Server; +use tonic::{Request, Response, Status}; + +/// A single span observed by the sink, flattened out of the +/// `ResourceSpans`/`ScopeSpans` nesting that the OTLP wire format imposes. +/// +/// One incoming `Export` RPC carrying N spans produces N +/// [`ReceivedSpan`]s, each with the same `metadata` clone but its own +/// `resource`/`scope`/`span`. This is the granularity tests reason about: +/// a `wait_for` predicate looks at one span at a time, and pops exactly +/// that one when it matches — leaving any siblings from the same batch in +/// the buffer for follow-up matches. +/// +/// The bad-request case present in [`super::posthog::ReceivedEvent`] does +/// not exist here: tonic decodes proto bodies upstream of our handler, so +/// any malformed `Export` is rejected with `InvalidArgument` before we +/// ever see it. +#[derive(Clone, Debug)] +pub struct ReceivedSpan { + pub metadata: tonic::metadata::MetadataMap, + pub resource: Option, + pub scope: Option, + pub span: Span, +} + +#[derive(Default)] +struct State { + received: Mutex>, + notify: Notify, +} + +struct CollectorService { + state: Arc, +} + +// IMPORTANT: this handler records the request *before* returning its response, +// so by the time a client's `c.export(…).await` returns Ok, every +// [`ReceivedSpan`] flattened out of that request is already in the buffer. +// Tests can assert on `received()` immediately after the client `.await` — +// no `wait_for` needed. +#[tonic::async_trait] +impl TraceService for CollectorService { + async fn export( + &self, + request: Request, + ) -> Result, Status> { + let (metadata, _ext, payload) = request.into_parts(); + { + // Flatten the `ResourceSpans`→`ScopeSpans`→`spans` nesting into + // one [`ReceivedSpan`] per individual span. Push the whole batch + // under a single lock so observers never see a partially-applied + // export; notify once at the end. + let mut buffer = self.state.received.lock(); + for ResourceSpans { + resource, + scope_spans, + .. + } in payload.resource_spans + { + for ScopeSpans { scope, spans, .. } in scope_spans { + for span in spans { + buffer.push(ReceivedSpan { + metadata: metadata.clone(), + resource: resource.clone(), + scope: scope.clone(), + span, + }); + } + } + } + } + self.state.notify.notify_waiters(); + Ok(Response::new(ExportTraceServiceResponse::default())) + } +} + +/// In-process OTLP `TraceService` server that records every received `Export` +/// request for test assertions. +/// +/// Drop is fire-and-forget; use [`Self::shutdown`] for graceful teardown +/// that awaits the server task. +pub struct MockOtlpCollector { + addr: SocketAddr, + state: Arc, + shutdown: Option>, + join: Option>, +} + +/// Returned by [`MockOtlpCollector::wait_for`] when no buffered span +/// matched within the timeout. +/// +/// `snapshot` is a clone of whatever spans were in the buffer at the +/// timeout instant — the buffer itself is left untouched, so a follow-up +/// `wait_for` call can resume waiting against the same buffer. +#[derive(Debug)] +pub struct OtlpWaitTimeout { + pub snapshot: Vec, +} + +impl std::fmt::Display for OtlpWaitTimeout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "timed out waiting for matching span; buffer holds {} span(s)", + self.snapshot.len() + ) + } +} + +impl std::error::Error for OtlpWaitTimeout {} + +impl MockOtlpCollector { + /// Bind to an OS-assigned port on `127.0.0.1` and start serving. + /// + /// Returns only after the server task has begun executing, so subsequent + /// requests are not racing the spawned task's first poll. + pub async fn spawn() -> Self { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind 127.0.0.1:0"); + let addr = listener.local_addr().expect("local_addr"); + let state = Arc::new(State::default()); + let service = CollectorService { + state: state.clone(), + }; + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let (ready_tx, ready_rx) = oneshot::channel(); + let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener); + let join = tokio::spawn(async move { + // Signal that the task has begun executing; the very next thing + // we do is await the serve future, which begins polling the + // incoming stream. By the time `spawn()` returns, tonic is about + // to (or already is) accepting from the kernel queue. + _ = ready_tx.send(()); + drop( + Server::builder() + .add_service( + TraceServiceServer::new(service) + .accept_compressed(tonic::codec::CompressionEncoding::Gzip) + .send_compressed(tonic::codec::CompressionEncoding::Gzip), + ) + .serve_with_incoming_shutdown(incoming, async { + drop(shutdown_rx.await); + }) + .await, + ); + }); + _ = ready_rx.await; + + Self { + addr, + state, + shutdown: Some(shutdown_tx), + join: Some(join), + } + } + + pub fn addr(&self) -> SocketAddr { + self.addr + } + + /// `http://127.0.0.1:PORT`, suitable for tonic / OTLP exporter config. + /// + /// Plaintext gRPC only — no TLS support. If a caller wraps an HTTPS client + /// around this, connection will fail; that's a test setup bug, not a + /// mock limitation. + pub fn endpoint(&self) -> String { + format!("http://{}", self.addr) + } + + /// Snapshot of all buffered spans (cloned out, buffer untouched). + pub fn received(&self) -> Vec { + self.state.received.lock().clone() + } + + /// `true` if no spans are currently buffered (initial state, fully + /// drained, or [`Self::clear`]'d). + pub fn is_empty(&self) -> bool { + self.state.received.lock().is_empty() + } + + pub fn clear(&self) { + self.state.received.lock().clear(); + } + + /// Wait for, and consume, the next buffered span that satisfies + /// `predicate`. On success the matched span is `remove`d from the + /// buffer in arrival order and returned; siblings in the same batch + /// stay in place. On timeout the buffer is left untouched and the + /// failure surfaces the current buffer contents through + /// [`OtlpWaitTimeout::snapshot`]. + /// + /// Notification-driven: returns ~immediately once a matching span is + /// in the buffer. If multiple buffered spans match, the earliest one + /// wins; subsequent calls can pop the next match. + /// + /// The pop is the consumption point: anything left after a sequence + /// of `wait_for` calls is genuinely surplus — a stray retransmit, an + /// unexpected span the test forgot to assert on, etc. — so closing a + /// test with [`crate::assert_sink_empty!`] is a meaningful check. + pub async fn wait_for( + &self, + predicate: F, + timeout: Duration, + ) -> Result + where + F: Fn(&ReceivedSpan) -> bool, + { + let deadline = tokio::time::Instant::now() + timeout; + loop { + // Arm the waiter *before* the buffer scan so a push racing + // between scan and await cannot be missed. + let notified = self.state.notify.notified(); + tokio::pin!(notified); + notified.as_mut().enable(); + + { + let mut buffer = self.state.received.lock(); + if let Some(pos) = buffer.iter().position(&predicate) { + return Ok(buffer.remove(pos)); + } + } + + if tokio::time::timeout_at(deadline, notified).await.is_err() { + return Err(OtlpWaitTimeout { + snapshot: self.received(), + }); + } + } + } + + /// Graceful teardown: signal shutdown and await the server task to + /// complete. Use this at the end of tests that send fire-and-forget + /// requests to guarantee in-flight responses are fully processed before + /// the tokio runtime tears down. + pub async fn shutdown(mut self) { + if let Some(tx) = self.shutdown.take() { + _ = tx.send(()); + } + if let Some(join) = self.join.take() { + _ = join.await; + } + } +} + +impl Drop for MockOtlpCollector { + fn drop(&mut self) { + if let Some(tx) = self.shutdown.take() { + _ = tx.send(()); + } + // `join` is dropped; tokio detaches the task to run to completion. + // Tests that need to verify graceful shutdown should call + // `shutdown().await` explicitly instead of relying on drop. + } +} + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry_proto::tonic::collector::trace::v1::trace_service_client::TraceServiceClient; + use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue, any_value::Value}; + use opentelemetry_proto::tonic::resource::v1::Resource; + use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans, Span}; + + /// Build a request carrying `names.len()` spans in a single + /// `ScopeSpans`, so a single `Export` produces N `ReceivedSpan`s in + /// arrival order — exercises the flatten path. + fn export_with_span_names(names: &[&str]) -> ExportTraceServiceRequest { + ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + resource: Some(Resource { + attributes: vec![KeyValue { + key: "service.name".into(), + value: Some(AnyValue { + value: Some(Value::StringValue("test".into())), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + entity_refs: vec![], + }), + scope_spans: vec![ScopeSpans { + scope: None, + spans: names + .iter() + .map(|n| Span { + name: (*n).into(), + ..Default::default() + }) + .collect(), + schema_url: String::new(), + }], + schema_url: String::new(), + }], + } + } + + fn span_with_name(name: &str) -> ExportTraceServiceRequest { + export_with_span_names(&[name]) + } + + async fn client(endpoint: String) -> TraceServiceClient { + TraceServiceClient::connect(endpoint).await.unwrap() + } + + #[tokio::test] + async fn records_one_received_span_per_proto_span() { + let collector = MockOtlpCollector::spawn().await; + let mut c = client(collector.endpoint()).await; + // One Export with three spans must flatten into three buffered items. + c.export(export_with_span_names(&["a", "b", "c"])) + .await + .unwrap(); + + let got = collector.received(); + assert_eq!(got.len(), 3); + assert_eq!(got[0].span.name, "a"); + assert_eq!(got[1].span.name, "b"); + assert_eq!(got[2].span.name, "c"); + // Resource and scope propagate to every flattened span. + for received in &got { + assert!(received.resource.is_some()); + } + } + + #[tokio::test] + async fn records_request_metadata_on_every_flattened_span() { + let collector = MockOtlpCollector::spawn().await; + let mut c = client(collector.endpoint()).await; + + let mut req = Request::new(export_with_span_names(&["x", "y"])); + req.metadata_mut() + .insert("x-test-tag", "abc-123".parse().unwrap()); + c.export(req).await.unwrap(); + + let got = collector.received(); + assert_eq!(got.len(), 2); + for received in &got { + assert_eq!( + received + .metadata + .get("x-test-tag") + .map(|v| v.to_str().unwrap()), + Some("abc-123"), + ); + } + } + + #[tokio::test] + async fn wait_for_pops_only_the_matched_span() { + let collector = MockOtlpCollector::spawn().await; + let mut c = client(collector.endpoint()).await; + // Three spans, one batch — predicate-targeted pop must leave the + // other two behind. + c.export(export_with_span_names(&["a", "b", "c"])) + .await + .unwrap(); + + let got = collector + .wait_for(|s| s.span.name == "b", Duration::from_secs(5)) + .await + .unwrap(); + assert_eq!(got.span.name, "b"); + + let remaining = collector.received(); + let names: Vec<&str> = remaining.iter().map(|s| s.span.name.as_str()).collect(); + assert_eq!(names, vec!["a", "c"]); + } + + #[tokio::test] + async fn wait_for_returns_when_matching_span_arrives() { + let collector = MockOtlpCollector::spawn().await; + let endpoint = collector.endpoint(); + + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(50)).await; + let mut c = client(endpoint).await; + c.export(span_with_name("delayed")).await.unwrap(); + }); + + let start = std::time::Instant::now(); + let got = collector + .wait_for(|s| s.span.name == "delayed", Duration::from_secs(5)) + .await + .unwrap(); + assert_eq!(got.span.name, "delayed"); + // Sanity: notification-driven, so this should be well under the 5s budget. + assert!(start.elapsed() < Duration::from_secs(1)); + } + + #[tokio::test] + async fn wait_for_times_out_when_predicate_never_matches() { + let collector = MockOtlpCollector::spawn().await; + let err = collector + .wait_for(|_| false, Duration::from_millis(150)) + .await + .unwrap_err(); + assert!(err.snapshot.is_empty()); + } + + #[tokio::test] + async fn wait_for_timeout_leaves_buffer_untouched() { + let collector = MockOtlpCollector::spawn().await; + let mut c = client(collector.endpoint()).await; + c.export(span_with_name("kept")).await.unwrap(); + + // No span named "missing" → predicate never matches → timeout. + let err = collector + .wait_for(|s| s.span.name == "missing", Duration::from_millis(150)) + .await + .unwrap_err(); + // The diagnostic snapshot must surface what's in the buffer, and + // the buffer itself must still hold the span — a follow-up call + // can recover. + assert_eq!(err.snapshot.len(), 1); + assert_eq!(err.snapshot[0].span.name, "kept"); + assert_eq!(collector.received().len(), 1); + } + + #[tokio::test] + async fn assert_sink_empty_passes_when_empty() { + let collector = MockOtlpCollector::spawn().await; + crate::assert_sink_empty!(&collector); + } + + #[tokio::test] + #[should_panic(expected = "expected empty, got 1 request(s)")] + async fn assert_sink_empty_panics_with_diagnostic() { + let collector = MockOtlpCollector::spawn().await; + let mut c = client(collector.endpoint()).await; + c.export(span_with_name("unexpected")).await.unwrap(); + crate::assert_sink_empty!(&collector); + } + + #[tokio::test] + async fn is_empty_reflects_state() { + let collector = MockOtlpCollector::spawn().await; + assert!(collector.is_empty()); + + let mut c = client(collector.endpoint()).await; + c.export(span_with_name("first")).await.unwrap(); + assert!(!collector.is_empty()); + + collector.clear(); + assert!(collector.is_empty()); + } + + #[tokio::test] + async fn clear_resets_buffer() { + let collector = MockOtlpCollector::spawn().await; + let mut c = client(collector.endpoint()).await; + c.export(span_with_name("first")).await.unwrap(); + assert_eq!(collector.received().len(), 1); + collector.clear(); + assert!(collector.received().is_empty()); + c.export(span_with_name("second")).await.unwrap(); + assert_eq!(collector.received().len(), 1); + } + + #[tokio::test] + async fn shutdown_completes_cleanly() { + let collector = MockOtlpCollector::spawn().await; + let mut c = client(collector.endpoint()).await; + c.export(span_with_name("first")).await.unwrap(); + collector.shutdown().await; + } +} diff --git a/crates/utils/re_test_mocks/src/posthog.rs b/crates/utils/re_test_mocks/src/posthog.rs new file mode 100644 index 000000000000..8bc9a778f0ce --- /dev/null +++ b/crates/utils/re_test_mocks/src/posthog.rs @@ -0,0 +1,583 @@ +//! In-memory HTTP sink that mimics the `PostHog` capture endpoint. +//! +//! Spawns an `axum` server on a random `127.0.0.1` port. Every received POST +//! is parsed and its `/batch` array is flattened into one [`ReceivedEvent`] +//! per entry, recorded with the request headers. Mirrors +//! [`super::otlp::MockOtlpCollector`] in shape (same `spawn` / `received` / +//! `wait_for` / `is_empty` / `clear` / `shutdown` surface) so tests read +//! consistently. + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use axum::Router; +use axum::extract::State; +use axum::http::{HeaderMap, StatusCode}; +use axum::routing::post; +use parking_lot::Mutex; +use tokio::sync::{Notify, oneshot}; + +/// A single `PostHog` capture event observed by the sink, flattened out +/// of the wire-format `{ "batch": […] }` envelope. +/// +/// One incoming `POST` carrying N events in its `/batch` array produces N +/// [`ReceivedEvent`]s, each with the same `headers` clone but its own +/// `event` JSON. This is the granularity tests reason about: a `wait_for` +/// predicate looks at one event at a time, and pops exactly that one when +/// it matches — leaving any siblings from the same batch in the buffer +/// for follow-up matches. +/// +/// If the request body is unparsable JSON, or parses but doesn't carry +/// a `/batch` array, the request lands as a single [`ReceivedEvent`] with +/// [`EventBody::BadRequest`] and the handler returns `400`. +#[derive(Clone, Debug)] +pub struct ReceivedEvent { + pub headers: HeaderMap, + pub event: EventBody, +} + +/// One element from a parsed `/batch` array, or the diagnostic for a +/// request the handler rejected. +#[derive(Clone, Debug)] +pub enum EventBody { + Ok(serde_json::Value), + BadRequest { raw: Vec, error: String }, +} + +impl EventBody { + /// `Some(&value)` if the event was a parsed `/batch` entry; `None` + /// otherwise. + /// + /// Use this in `wait_for` predicates and other Option-shaped contexts. + /// For direct test assertions, prefer [`Self::expect_parsed`]. + pub fn as_ok(&self) -> Option<&serde_json::Value> { + if let Self::Ok(v) = self { + Some(v) + } else { + None + } + } + + /// Returns the parsed event JSON, panicking with the raw body and + /// error if the request had been rejected. Use this in direct test + /// assertions where a bad request is unambiguously a test failure. + pub fn expect_parsed(&self) -> &serde_json::Value { + match self { + Self::Ok(v) => v, + Self::BadRequest { raw, error } => panic!( + "expected a parsed PostHog batch entry, got BadRequest:\n error: {error}\n raw ({} bytes): {}", + raw.len(), + String::from_utf8_lossy(raw), + ), + } + } +} + +#[derive(Default)] +struct Inner { + received: Mutex>, + notify: Notify, +} + +// IMPORTANT: this handler records every event flattened out of the request +// *before* returning its response, so by the time a client's `.send().await` +// returns Ok, every [`ReceivedEvent`] for that request is already in the +// buffer. Tests can assert on `received()` immediately after the client +// `.await` — no `wait_for` needed. +async fn handler( + State(inner): State>, + headers: HeaderMap, + body: axum::body::Bytes, +) -> StatusCode { + let parsed = match serde_json::from_slice::(&body) { + Ok(parsed) => parsed, + Err(err) => { + inner.received.lock().push(ReceivedEvent { + headers, + event: EventBody::BadRequest { + raw: body.to_vec(), + error: err.to_string(), + }, + }); + inner.notify.notify_waiters(); + return StatusCode::BAD_REQUEST; + } + }; + + let Some(batch) = parsed.pointer("/batch").and_then(|v| v.as_array()) else { + // Parses as JSON but isn't the PostHog `{ "batch": […] }` + // envelope — record once as BadRequest so tests can diagnose, and + // surface the protocol violation through the HTTP status. + inner.received.lock().push(ReceivedEvent { + headers, + event: EventBody::BadRequest { + raw: body.to_vec(), + error: "missing or non-array `/batch` field".to_owned(), + }, + }); + inner.notify.notify_waiters(); + return StatusCode::BAD_REQUEST; + }; + + { + // Flatten under a single lock so observers never see a partially- + // applied request; notify once at the end. + let mut buffer = inner.received.lock(); + for entry in batch { + buffer.push(ReceivedEvent { + headers: headers.clone(), + event: EventBody::Ok(entry.clone()), + }); + } + } + inner.notify.notify_waiters(); + StatusCode::OK +} + +/// In-process HTTP server that flattens each POST's `/batch` array into +/// one [`ReceivedEvent`] per entry. +/// +/// Only the root path (`/`) and POST requests are routed to the handler; +/// other methods or paths get the axum default (405 / 404) and are not +/// recorded. Drop is fire-and-forget; use [`Self::shutdown`] for graceful +/// teardown that awaits the server task. +pub struct MockPostHog { + addr: SocketAddr, + inner: Arc, + shutdown: Option>, + join: Option>, +} + +/// Returned by [`MockPostHog::wait_for`] when no buffered event matched +/// within the timeout. +/// +/// `snapshot` is a clone of whatever events were in the buffer at the +/// timeout instant — the buffer itself is left untouched, so a follow-up +/// `wait_for` call can resume waiting against the same buffer. +#[derive(Debug)] +pub struct PosthogWaitTimeout { + pub snapshot: Vec, +} + +impl std::fmt::Display for PosthogWaitTimeout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "timed out waiting for matching event; buffer holds {} event(s)", + self.snapshot.len() + ) + } +} + +impl std::error::Error for PosthogWaitTimeout {} + +impl MockPostHog { + /// Bind to an OS-assigned port on `127.0.0.1` and start serving. + /// + /// Returns only after the server task has begun executing, so subsequent + /// requests are not racing the spawned task's first poll. + pub async fn spawn() -> Self { + let inner = Arc::new(Inner::default()); + let app = Router::new() + .route("/", post(handler)) + .with_state(inner.clone()); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind 127.0.0.1:0"); + let addr = listener.local_addr().expect("local_addr"); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let (ready_tx, ready_rx) = oneshot::channel(); + let join = tokio::spawn(async move { + // Signal that the task has begun executing; the very next thing + // we do is await the serve future, which begins polling the + // listener. By the time `spawn()` returns to the caller, axum is + // about to (or already is) accepting from the kernel queue. + _ = ready_tx.send(()); + drop( + axum::serve(listener, app) + .with_graceful_shutdown(async { + drop(shutdown_rx.await); + }) + .await, + ); + }); + _ = ready_rx.await; + + Self { + addr, + inner, + shutdown: Some(shutdown_tx), + join: Some(join), + } + } + + pub fn addr(&self) -> SocketAddr { + self.addr + } + + /// Convenience: `http://127.0.0.1:PORT`, suitable for `PostHogClient::with_url`. + pub fn endpoint(&self) -> String { + format!("http://{}", self.addr) + } + + /// Snapshot of all buffered events (cloned out, buffer untouched). + pub fn received(&self) -> Vec { + self.inner.received.lock().clone() + } + + /// `true` if no events are currently buffered (initial state, fully + /// drained, or [`Self::clear`]'d). + pub fn is_empty(&self) -> bool { + self.inner.received.lock().is_empty() + } + + pub fn clear(&self) { + self.inner.received.lock().clear(); + } + + /// Wait for, and consume, the next buffered event that satisfies + /// `predicate`. On success the matched event is `remove`d from the + /// buffer in arrival order and returned; siblings from the same + /// `/batch` stay in place. On timeout the buffer is left untouched + /// and the failure surfaces the current buffer contents through + /// [`PosthogWaitTimeout::snapshot`]. + /// + /// Notification-driven: returns ~immediately once a matching event + /// is in the buffer. If multiple buffered events match, the earliest + /// one wins; subsequent calls can pop the next match. + /// + /// The pop is the consumption point: anything left after a sequence + /// of `wait_for` calls is genuinely surplus, so closing a test with + /// [`crate::assert_sink_empty!`] is a meaningful check. + pub async fn wait_for( + &self, + predicate: F, + timeout: Duration, + ) -> Result + where + F: Fn(&ReceivedEvent) -> bool, + { + let deadline = tokio::time::Instant::now() + timeout; + loop { + // Arm the waiter *before* the buffer scan so a push racing + // between scan and await cannot be missed. + let notified = self.inner.notify.notified(); + tokio::pin!(notified); + notified.as_mut().enable(); + + { + let mut buffer = self.inner.received.lock(); + if let Some(pos) = buffer.iter().position(&predicate) { + return Ok(buffer.remove(pos)); + } + } + + if tokio::time::timeout_at(deadline, notified).await.is_err() { + return Err(PosthogWaitTimeout { + snapshot: self.received(), + }); + } + } + } + + /// Graceful teardown: signal shutdown and await the server task to + /// complete. Use this at the end of tests that send fire-and-forget + /// requests to guarantee in-flight responses are fully processed before + /// the tokio runtime tears down. + pub async fn shutdown(mut self) { + if let Some(tx) = self.shutdown.take() { + _ = tx.send(()); + } + if let Some(join) = self.join.take() { + _ = join.await; + } + } +} + +impl Drop for MockPostHog { + fn drop(&mut self) { + if let Some(tx) = self.shutdown.take() { + _ = tx.send(()); + } + // `join` is dropped; tokio detaches the task to run to completion. + // Tests that need to verify graceful shutdown should call + // `shutdown().await` explicitly instead of relying on drop. + } +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn post_json(url: &str, json: serde_json::Value) -> reqwest::Response { + reqwest::Client::new() + .post(url) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&json).unwrap()) + .send() + .await + .unwrap() + } + + /// Helper: wrap one or more event objects in the `PostHog` + /// `{ "batch": […] }` envelope that the handler expects. + fn batch(events: &[serde_json::Value]) -> serde_json::Value { + serde_json::json!({"api_key": "k", "batch": events}) + } + + #[tokio::test] + async fn records_one_event_per_batch_entry() { + let collector = MockPostHog::spawn().await; + let resp = post_json( + &collector.endpoint(), + batch(&[ + serde_json::json!({"event": "a"}), + serde_json::json!({"event": "b"}), + serde_json::json!({"event": "c"}), + ]), + ) + .await; + assert!(resp.status().is_success()); + + let got = collector.received(); + assert_eq!(got.len(), 3); + assert_eq!(got[0].event.expect_parsed()["event"], "a"); + assert_eq!(got[1].event.expect_parsed()["event"], "b"); + assert_eq!(got[2].event.expect_parsed()["event"], "c"); + } + + #[tokio::test] + async fn records_request_headers_on_every_flattened_event() { + let collector = MockPostHog::spawn().await; + let resp = reqwest::Client::new() + .post(collector.endpoint()) + .header("Content-Type", "application/json") + .header("X-Custom-Header", "value-123") + .body( + serde_json::to_string(&batch(&[ + serde_json::json!({"event": "a"}), + serde_json::json!({"event": "b"}), + ])) + .unwrap(), + ) + .send() + .await + .unwrap(); + assert!(resp.status().is_success()); + + let got = collector.received(); + assert_eq!(got.len(), 2); + for received in &got { + assert_eq!( + received + .headers + .get("x-custom-header") + .and_then(|v| v.to_str().ok()), + Some("value-123"), + ); + } + } + + #[tokio::test] + async fn records_malformed_json_as_bad_request() { + let collector = MockPostHog::spawn().await; + let resp = reqwest::Client::new() + .post(collector.endpoint()) + .header("Content-Type", "application/json") + .body("not-json") + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 400); + + let got = collector.received(); + assert_eq!(got.len(), 1); + match &got[0].event { + EventBody::Ok(v) => panic!("expected BadRequest, got Ok({v:?})"), + EventBody::BadRequest { raw, error } => { + assert_eq!(raw, b"not-json"); + assert!(!error.is_empty()); + } + } + } + + #[tokio::test] + async fn records_missing_batch_as_bad_request() { + let collector = MockPostHog::spawn().await; + // Parseable JSON but no `/batch` envelope — must surface as a + // protocol violation, not silently accepted. + let resp = post_json(&collector.endpoint(), serde_json::json!({"x": 1})).await; + assert_eq!(resp.status(), 400); + + let got = collector.received(); + assert_eq!(got.len(), 1); + match &got[0].event { + EventBody::Ok(v) => panic!("expected BadRequest, got Ok({v:?})"), + EventBody::BadRequest { error, .. } => { + assert!( + error.contains("/batch"), + "error should mention /batch: {error}" + ); + } + } + } + + #[tokio::test] + async fn rejects_non_post_methods() { + let collector = MockPostHog::spawn().await; + let resp = reqwest::Client::new() + .get(collector.endpoint()) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 405); + assert!(collector.is_empty()); + } + + #[tokio::test] + async fn wait_for_pops_only_the_matched_event() { + let collector = MockPostHog::spawn().await; + post_json( + &collector.endpoint(), + batch(&[ + serde_json::json!({"event": "a"}), + serde_json::json!({"event": "b"}), + serde_json::json!({"event": "c"}), + ]), + ) + .await; + + let got = collector + .wait_for( + |e| e.event.as_ok().map(|v| v["event"] == "b").unwrap_or(false), + Duration::from_secs(5), + ) + .await + .unwrap(); + assert_eq!(got.event.expect_parsed()["event"], "b"); + + let remaining = collector.received(); + let names: Vec<&str> = remaining + .iter() + .filter_map(|e| e.event.as_ok()) + .filter_map(|v| v["event"].as_str()) + .collect(); + assert_eq!(names, vec!["a", "c"]); + } + + #[tokio::test] + async fn wait_for_returns_when_matching_event_arrives() { + let collector = MockPostHog::spawn().await; + let endpoint = collector.endpoint(); + + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(50)).await; + post_json(&endpoint, batch(&[serde_json::json!({"event": "delayed"})])).await; + }); + + let start = std::time::Instant::now(); + let got = collector + .wait_for( + |e| { + e.event + .as_ok() + .map(|v| v["event"] == "delayed") + .unwrap_or(false) + }, + Duration::from_secs(5), + ) + .await + .unwrap(); + assert_eq!(got.event.expect_parsed()["event"], "delayed"); + assert!(start.elapsed() < Duration::from_secs(1)); + } + + #[tokio::test] + async fn wait_for_times_out_when_predicate_never_matches() { + let collector = MockPostHog::spawn().await; + let err = collector + .wait_for(|_| false, Duration::from_millis(150)) + .await + .unwrap_err(); + assert!(err.snapshot.is_empty()); + } + + #[tokio::test] + async fn clear_resets_buffer() { + let collector = MockPostHog::spawn().await; + post_json( + &collector.endpoint(), + batch(&[serde_json::json!({"event": "1"})]), + ) + .await; + assert_eq!(collector.received().len(), 1); + collector.clear(); + assert!(collector.received().is_empty()); + post_json( + &collector.endpoint(), + batch(&[serde_json::json!({"event": "2"})]), + ) + .await; + assert_eq!(collector.received().len(), 1); + } + + #[tokio::test] + async fn is_empty_reflects_state() { + let collector = MockPostHog::spawn().await; + assert!(collector.is_empty()); + + post_json( + &collector.endpoint(), + batch(&[serde_json::json!({"event": "x"})]), + ) + .await; + assert!(!collector.is_empty()); + + collector.clear(); + assert!(collector.is_empty()); + } + + #[tokio::test] + async fn assert_sink_empty_passes_when_empty() { + let collector = MockPostHog::spawn().await; + crate::assert_sink_empty!(&collector); + } + + #[tokio::test] + #[should_panic(expected = "expected empty, got 1 request(s)")] + async fn assert_sink_empty_panics_with_diagnostic() { + let collector = MockPostHog::spawn().await; + // Handler records before responding, so the recording is visible by + // the time `post_json` returns. No `wait_for` needed. + post_json( + &collector.endpoint(), + batch(&[serde_json::json!({"event": "unexpected"})]), + ) + .await; + crate::assert_sink_empty!(&collector); + } + + #[tokio::test] + #[should_panic(expected = "expected a parsed PostHog batch entry, got BadRequest")] + async fn expect_parsed_panics_on_bad_request() { + let collector = MockPostHog::spawn().await; + reqwest::Client::new() + .post(collector.endpoint()) + .body("not-json") + .send() + .await + .unwrap(); + let got = collector.received(); + assert_eq!(got.len(), 1); + let _ = got[0].event.expect_parsed(); + } + + #[tokio::test] + async fn shutdown_completes_cleanly() { + let collector = MockPostHog::spawn().await; + post_json(&collector.endpoint(), serde_json::json!({"x": 1})).await; + collector.shutdown().await; + } +} diff --git a/crates/utils/re_tracing/src/server.rs b/crates/utils/re_tracing/src/server.rs index 1cf135820d78..4b5a4d10527f 100644 --- a/crates/utils/re_tracing/src/server.rs +++ b/crates/utils/re_tracing/src/server.rs @@ -51,7 +51,7 @@ fn start_puffin_viewer() { .spawn(); if let Err(err) = child { - let cmd = format!("cargo install puffin_viewer && puffin_viewer --url {url}",); + let cmd = format!("cargo install puffin_viewer && puffin_viewer --url {url}"); re_log::warn!("Failed to start puffin_viewer: {err}. Try connecting manually with: {cmd}"); rfd::MessageDialog::new() diff --git a/crates/utils/re_tuid/Cargo.toml b/crates/utils/re_tuid/Cargo.toml index 83c22a1b8b90..cdbe58f5eb55 100644 --- a/crates/utils/re_tuid/Cargo.toml +++ b/crates/utils/re_tuid/Cargo.toml @@ -25,9 +25,6 @@ default = [] ## Enable bytemuck support. bytemuck = ["dep:bytemuck"] -## Enable (de)serialization using serde. -serde = ["dep:serde"] - [dependencies] re_byte_size.workspace = true @@ -35,11 +32,12 @@ re_log.workspace = true document-features.workspace = true getrandom.workspace = true +quiver.workspace = true +serde = { workspace = true, features = ["derive"] } web-time.workspace = true # Optional dependencies bytemuck = { workspace = true, optional = true, features = ["derive"] } -serde = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] criterion.workspace = true diff --git a/crates/utils/re_tuid/benches/bench_tuid.rs b/crates/utils/re_tuid/benches/bench_tuid.rs index eb74eb0c6be0..e549683c3d08 100644 --- a/crates/utils/re_tuid/benches/bench_tuid.rs +++ b/crates/utils/re_tuid/benches/bench_tuid.rs @@ -4,7 +4,7 @@ fn bench_tuid(c: &mut Criterion) { let mut group = c.benchmark_group("tuid"); group.throughput(criterion::Throughput::Elements(1)); group.bench_function("Tuid::new", |b| { - b.iter(|| criterion::black_box(re_tuid::Tuid::new())); + b.iter(|| std::hint::black_box(re_tuid::Tuid::new())); }); group.throughput(criterion::Throughput::Elements(1_000)); @@ -12,7 +12,7 @@ fn bench_tuid(c: &mut Criterion) { use rand::prelude::*; let mut ids = (0..2_000).map(|_| re_tuid::Tuid::new()).collect::>(); ids.shuffle(&mut rand::rng()); - b.iter(|| criterion::black_box(ids[0..1_000].cmp(&ids[1_000..2_000]))); + b.iter(|| std::hint::black_box(ids[0..1_000].cmp(&ids[1_000..2_000]))); }); } diff --git a/crates/utils/re_tuid/src/lib.rs b/crates/utils/re_tuid/src/lib.rs index 772d1c248f13..6bff571ec9bc 100644 --- a/crates/utils/re_tuid/src/lib.rs +++ b/crates/utils/re_tuid/src/lib.rs @@ -42,7 +42,7 @@ /// The raw bytes of the `Tuid` sorts in time order as the `Tuid` itself, /// and the `Tuid` is byte-aligned so you can just transmute between `Tuid` and raw bytes. #[repr(C, align(1))] -#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, re_byte_size::SizeBytes)] #[cfg_attr( feature = "bytemuck", derive(bytemuck::AnyBitPattern, bytemuck::NoUninit) @@ -97,6 +97,23 @@ impl std::fmt::Debug for Tuid { } } +impl From<[u8; 16]> for Tuid { + #[inline] + fn from(bytes: [u8; 16]) -> Self { + Self::from_bytes(bytes) + } +} + +impl From for [u8; 16] { + #[inline] + fn from(tuid: Tuid) -> Self { + tuid.as_bytes() + } +} + +// Make `quiver::Column` work (backed by a big-endian `FixedSizeBinary(16)` column): +quiver::newtype_datatype!(Tuid, quiver::FixedSizeBinary<16>); + impl From for std::borrow::Cow<'_, Tuid> { #[inline] fn from(value: Tuid) -> Self { @@ -291,18 +308,6 @@ fn random_u64() -> u64 { u64::from_be_bytes(bytes) } -impl re_byte_size::SizeBytes for Tuid { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} - #[test] fn test_tuid() { use std::collections::{BTreeSet, HashSet}; @@ -356,14 +361,12 @@ fn test_tuid_formatting() { // ------------------------------------------------------------------------------- // For backwards compatibility with our MsgPack encoder/decoder -#[cfg(feature = "serde")] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(serde::Deserialize, serde::Serialize)] struct LegacyTuid { time_nanos: u64, inc: u64, } -#[cfg(feature = "serde")] impl serde::Serialize for Tuid { fn serialize(&self, serializer: S) -> Result where @@ -377,7 +380,6 @@ impl serde::Serialize for Tuid { } } -#[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for Tuid { fn deserialize(deserializer: D) -> Result where diff --git a/crates/utils/re_video/Cargo.toml b/crates/utils/re_video/Cargo.toml index 78da8f32a32d..5190ffda70af 100644 --- a/crates/utils/re_video/Cargo.toml +++ b/crates/utils/re_video/Cargo.toml @@ -23,9 +23,6 @@ all-features = true [features] default = ["av1", "ffmpeg"] -## Enable serialization for data structures that support it. -serde = ["dep:serde"] - ## Native AV1 decoding. av1 = ["dep:dav1d"] @@ -40,11 +37,15 @@ nasm = [ ] +[package.metadata.cargo-shear] +ignored = ["getrandom"] + [dependencies] re_byte_size.workspace = true re_log.workspace = true re_mutex.workspace = true re_quota_channel.workspace = true +re_rvl.workspace = true re_span.workspace = true re_tracing.workspace = true re_tuid.workspace = true @@ -61,11 +62,15 @@ saturating_cast.workspace = true scuffle-av1.workspace = true scuffle-bytes-util.workspace = true smallvec.workspace = true +serde = { workspace = true, features = ["derive"] } thiserror.workspace = true web-time.workspace = true ffmpeg-sidecar = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +# On web we only need PNG for non-8-bit PNGs, since there is no browser decoder path that preserves them. +# Other web images should go through browser decoding instead. +image = { workspace = true, default-features = false, features = ["png"] } +bytemuck.workspace = true # We enable re_rav1d on native, UNLESS we're on Linux Arm64 # See https://github.com/rerun-io/rerun/issues/7755 @@ -81,18 +86,18 @@ dav1d = { workspace = true, optional = true, default-features = false, features ] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -image = { workspace = true, default-features = false, features = ["png", "jpeg"] } -bytemuck.workspace = true - +image = { workspace = true, default-features = false, features = ["jpeg"] } # web [target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { workspace = true, features = ["wasm_js"] } js-sys.workspace = true wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true web-sys = { workspace = true, features = [ "Blob", "BlobPropertyBag", + "CodecState", "DomException", "EncodedVideoChunk", "EncodedVideoChunkInit", diff --git a/crates/utils/re_video/benches/video_load_bench.rs b/crates/utils/re_video/benches/video_load_bench.rs index afd54d43417e..76f5a4aaa783 100644 --- a/crates/utils/re_video/benches/video_load_bench.rs +++ b/crates/utils/re_video/benches/video_load_bench.rs @@ -19,7 +19,6 @@ fn video_load(c: &mut Criterion) { &video, "video/mp4", "Big_Buck_Bunny_1080_10s_av1.mp4", - re_tuid::Tuid::new(), ) }, criterion::BatchSize::LargeInput, diff --git a/crates/utils/re_video/examples/frames.rs b/crates/utils/re_video/examples/frames.rs index ffc39c656a4f..2aa2e5581595 100644 --- a/crates/utils/re_video/examples/frames.rs +++ b/crates/utils/re_video/examples/frames.rs @@ -10,6 +10,7 @@ use std::time::{Duration, Instant}; use indicatif::ProgressBar; use re_mutex::Mutex; +use re_video::player::VideoSliceSource; fn main() { re_log::setup_logging(); @@ -25,8 +26,7 @@ fn main() { println!("Decoding {video_path}"); let video_blob = std::fs::read(video_path).expect("failed to read video"); - let source_id = re_tuid::Tuid::new(); - let video = re_video::VideoDataDescription::load_mp4(&video_blob, video_path, source_id) + let video = re_video::VideoDataDescription::load_mp4(&video_blob, video_path) .expect("failed to load video"); println!( @@ -84,7 +84,9 @@ fn main() { continue; }; - let chunk = sample.get(&|_| &video_blob, sample_idx).unwrap(); + let chunk = sample + .get(&VideoSliceSource(&video_blob), sample_idx) + .unwrap(); decoder.submit_chunk(chunk).expect("Failed to submit chunk"); } decoder.end_of_video().expect("Failed to end of video"); @@ -124,8 +126,10 @@ fn main() { re_video::PixelFormat::Yuv { .. } => { re_log::error_once!("YUV frame writing is not supported"); } - re_video::PixelFormat::L8 | re_video::PixelFormat::L16 => { - re_log::error_once!("L8 & L16 frame writing is not supported"); + re_video::PixelFormat::L8 + | re_video::PixelFormat::L16 + | re_video::PixelFormat::R32Float => { + re_log::error_once!("L8 & L16 & R32Float frame writing is not supported"); } } } diff --git a/crates/utils/re_video/src/av1.rs b/crates/utils/re_video/src/av1.rs index 2d0e906d2e56..36fe8e2ae2bf 100644 --- a/crates/utils/re_video/src/av1.rs +++ b/crates/utils/re_video/src/av1.rs @@ -1,5 +1,5 @@ use std::io; -use std::io::{Cursor, SeekFrom}; +use std::io::Cursor; use scuffle_av1::seq::SequenceHeaderObu; use scuffle_av1::{ObuHeader, ObuType}; @@ -39,6 +39,7 @@ pub fn detect_av1_keyframe_start(data: &[u8]) -> Result Result @@ -78,8 +77,16 @@ pub fn detect_av1_keyframe_start(data: &[u8]) -> Result Result(reader: &mut R, obu_size: u64) -> io::Result<()> { - let offset = i64::try_from(obu_size).map_err(|err| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!("payload size exceeds seek limits: {err}"), - ) - })?; - - reader.seek(SeekFrom::Current(offset))?; - Ok(()) -} - /// Determine if the frame is a keyframe based on the OBU type and its content. #[inline] fn is_keyframe(reader: &mut R) -> io::Result { @@ -196,6 +191,32 @@ mod test { assert!(matches!(result, Ok(GopStartDetection::NotStartOfGop))); } + /// AV1 sample with three sequential Frame OBUs and no Sequence Header. + /// + /// Regression fixture for the OBU walker cursor-drift bug. + const AV1_MULTI_FRAME_DRIFT_REPRO: &[u8] = &[ + 0x32, 0x30, 0x28, 0x9C, 0xC2, 0x05, 0x69, 0x7B, 0x24, 0x6A, 0x04, 0x1C, 0x71, 0xC7, 0x03, + 0x00, 0x01, 0x00, 0x20, 0x04, 0x80, 0x60, 0xC0, 0x00, 0x00, 0x62, 0x73, 0x15, 0x73, 0x05, + 0x58, 0x72, 0x84, 0xD9, 0xD5, 0xF4, 0x6A, 0xBB, 0xF3, 0xB5, 0x5E, 0xF0, 0xF6, 0xC2, 0x6B, + 0x38, 0x6B, 0x70, 0xD9, 0x4C, 0x32, 0x1B, 0x28, 0x94, 0x60, 0x05, 0x69, 0x7B, 0x24, 0x72, + 0x04, 0x1E, 0x79, 0xE7, 0x83, 0x00, 0x01, 0x00, 0x20, 0x04, 0x80, 0x60, 0xC0, 0x00, 0x94, + 0xB2, 0x5B, 0xEA, 0xFA, 0x32, 0x4B, 0x31, 0x26, 0x80, 0x0A, 0xD2, 0xF6, 0x48, 0xE4, 0x08, + 0x3C, 0xF3, 0xCF, 0x06, 0x00, 0x02, 0x00, 0x40, 0x09, 0x00, 0xC1, 0x80, 0x00, 0x7B, 0x4D, + 0x69, 0xD2, 0xD7, 0xC7, 0x6D, 0x2F, 0xB4, 0xF2, 0xDA, 0xD1, 0xDC, 0x5A, 0xD9, 0x45, 0x0F, + 0xA2, 0xB7, 0x98, 0xD0, 0x19, 0xF6, 0x3C, 0x42, 0xBB, 0x5F, 0x5E, 0xE1, 0xF3, 0xEB, 0xF2, + 0xCE, 0x63, 0x4D, 0xD7, 0xC5, 0xEA, 0x0A, 0xDE, 0xFA, 0x76, 0x15, 0xAC, 0xB8, 0x85, 0x88, + 0x9C, 0x7D, 0x59, 0x63, 0x45, 0xA8, + ]; + + #[test] + fn test_detect_av1_multi_frame_does_not_drift() { + let result = detect_av1_keyframe_start(AV1_MULTI_FRAME_DRIFT_REPRO); + assert!( + matches!(result, Ok(GopStartDetection::NotStartOfGop)), + "expected NotStartOfGop, got {result:?}" + ); + } + #[test] fn test_detect_av1_non_keyframe() { let result = detect_av1_keyframe_start(super::AV1_TEST_INTER_FRAME); diff --git a/crates/utils/re_video/src/decode/av1.rs b/crates/utils/re_video/src/decode/av1.rs index 9ac4a6ed75d3..50aaad990146 100644 --- a/crates/utils/re_video/src/decode/av1.rs +++ b/crates/utils/re_video/src/decode/av1.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use dav1d::{PixelLayout, PlanarImageComponent}; use re_log::debug_assert; -use super::async_decoder_wrapper::SyncDecoder; +use super::sync_decoder_wrapper::SyncDecoder; use super::{ Chunk, DecodeError, Frame, FrameContent, FrameInfo, PixelFormat, Result, YuvMatrixCoefficients, YuvPixelLayout, YuvRange, diff --git a/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs b/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs index a33e72d4b0de..e1310d871707 100644 --- a/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs +++ b/crates/utils/re_video/src/decode/ffmpeg_cli/ffmpeg.rs @@ -12,6 +12,7 @@ use h264_reader::nal::UnitType; use re_log::debug_assert; use re_quota_channel::{Receiver, SendError, Sender}; +use super::ivf::write_chunk_to_ivf_stream; use super::version::FFmpegVersionParseError; use crate::decode::ffmpeg_cli::{ FFMPEG_MINIMUM_VERSION_MAJOR, FFMPEG_MINIMUM_VERSION_MINOR, FFmpegVersion, @@ -62,6 +63,11 @@ pub enum Error { #[error("Bad video data: {0}")] BadVideoData(String), + #[error( + "This FFmpeg build has no usable encoder for {codec:?}. Install a build with the matching encoder (e.g. libvpx for VP8/VP9, libsvtav1/libaom for AV1) or choose a different output codec." + )] + NoEncoderForCodec { codec: crate::VideoCodec }, + #[error("FFmpeg error: {0}")] Ffmpeg(String), @@ -110,7 +116,7 @@ impl From for Error { } /// ffmpeg does not tell us the timestamp/duration of a given frame, so we need to remember it. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, re_byte_size::SizeBytes)] struct FFmpegFrameInfo { /// The start of a new group of pictures? /// @@ -139,26 +145,12 @@ struct FFmpegFrameInfo { decode_timestamp: Time, } -impl re_byte_size::SizeBytes for FFmpegFrameInfo { - fn heap_size_bytes(&self) -> u64 { - 0 - } -} - +#[derive(re_byte_size::SizeBytes)] enum FFmpegFrameData { Chunk(Chunk), Quit, } -impl re_byte_size::SizeBytes for FFmpegFrameData { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::Chunk(chunk) => chunk.heap_size_bytes(), - Self::Quit => 0, - } - } -} - /// Wraps an stdin with a shared shutdown boolean. struct StdinWithShutdown { shutdown: Arc, @@ -295,6 +287,7 @@ impl FFmpegProcessAndListener { let codec_str = match codec { crate::VideoCodec::H264 => "h264", crate::VideoCodec::H265 => "hevc", + crate::VideoCodec::VP8 | crate::VideoCodec::VP9 => "ivf", _ => unreachable!(), }; @@ -325,8 +318,8 @@ impl FFmpegProcessAndListener { .format(codec_str) // TODO(andreas): should we check ahead of time whether this is available? //.fps_mode("0") .input("-") // stdin is our input! - // h264 bitstreams doesn't have timestamp information. Whatever ffmpeg tries to make up about timing & framerates is wrong! - // If we don't tell it to just pass the frames through, variable framerate (VFR) video will just not play at all. + // h264 bitstreams doesn't have timestamp information. Whatever ffmpeg tries to make up about timing & frame rates is wrong! + // If we don't tell it to just pass the frames through, variable frame rate (VFR) video will just not play at all. .fps_mode("passthrough") .pix_fmt(ffmpeg_pix_fmt) // ffmpeg-sidecar's .rawvideo() sets pix_fmt to rgb24, we don't want that. @@ -382,10 +375,7 @@ impl FFmpegProcessAndListener { }) .expect("Failed to spawn ffmpeg listener thread"); - let codec_meta = encoding_details - .and_then(|e| e.stsd.as_ref()) - .and_then(CodecMeta::from_stsd) - .unwrap_or(CodecMeta::RawBytestream); + let mut codec_meta = CodecMeta::for_decoder(codec, encoding_details); // Writes video data to the ffmpeg process: let write_thread = std::thread::Builder::new() @@ -402,7 +392,7 @@ impl FFmpegProcessAndListener { &mut ffmpeg_stdin, &frame_data_rx, &output_sender, - &codec_meta, + &mut codec_meta, ); } }) @@ -526,24 +516,13 @@ fn write_ffmpeg_input( ffmpeg_stdin: &mut dyn std::io::Write, frame_data_rx: &Receiver, output_sender: &OutputSender, - codec_meta: &CodecMeta, + codec_meta: &mut CodecMeta, ) { - let mut state = AnnexBStreamState::default(); - while let Ok(data) = frame_data_rx.recv() { let chunk = match data { FFmpegFrameData::Chunk(chunk) => chunk, FFmpegFrameData::Quit => { - // Try to flush out the last frames from ffmpeg with an EndSequence/EndStream NAL units. - // Unfortunatelt this doesn't help, at least not for https://github.com/rerun-io/rerun/issues/8073 - let end_nals: Vec = [ - ANNEXB_NAL_START_CODE, - &[UnitType::EndOfSeq.id()], - ANNEXB_NAL_START_CODE, - &[UnitType::EndOfStream.id()], - ] - .concat(); - write_bytes(ffmpeg_stdin, &end_nals).ok(); + codec_meta.write_end_of_stream(ffmpeg_stdin).ok(); // NOTE(emilk): I've also tried writing `NalUnitType::AccessUnitDelimiter` here, but to no avail. @@ -553,17 +532,7 @@ fn write_ffmpeg_input( } }; - let write_result = match codec_meta { - CodecMeta::Avc(avcc) => { - write_avc_chunk_to_nalu_stream(avcc, ffmpeg_stdin, &chunk, &mut state) - .map_err(Error::from) - } - CodecMeta::Hevc(hvcc) => { - write_hevc_chunk_to_nalu_stream(hvcc, ffmpeg_stdin, &chunk, &mut state) - .map_err(Error::from) - } - CodecMeta::RawBytestream => write_bytes(ffmpeg_stdin, &chunk.data), - }; + let write_result = codec_meta.write_chunk(ffmpeg_stdin, &chunk); if let Err(err) = write_result { let write_error = matches!(err, Error::FailedToWriteToFfmpeg(_)); @@ -952,7 +921,7 @@ impl FFmpegCliDecoder { } } -fn check_ffmpeg_version( +pub(super) fn check_ffmpeg_version( ffmpeg_version_result: Result, ) -> Result<(), Error> { match ffmpeg_version_result { @@ -1048,7 +1017,7 @@ fn should_ignore_log_msg(msg: &str) -> bool { // This is supported by experimentation yielding that it shows only up when using the `-colorspace` parameter. // (color range and yuvj formats are fine though!) "No accelerated colorspace conversion found from yuv420p to bgr24", - // We actually don't even want it to estimate a framerate! + // We actually don't even want it to estimate a frame rate! "not enough frames to estimate rate", // Similar: we don't want it to be able to estimate any of these things and we set those values explicitly, see invocation. // Observed on Windows FFmpeg 7.1, but not with the same version on Mac with the same video. @@ -1094,24 +1063,151 @@ fn sanitize_ffmpeg_log_message(msg: &str) -> String { msg } -#[derive(Clone)] enum CodecMeta { - RawBytestream, // generic “pass-through” label for any format that’s ready to feed to the decoder as-is. - Avc(re_mp4::Avc1Box), - Hevc(re_mp4::HevcBox), + /// Pass `chunk.data` through verbatim. Used for streamed H.264/H.265 (already Annex-B), + /// or any format that is ready to feed to the decoder as-is. + RawBytestream, + + /// H.264 in MP4: prepend SPS/PPS to each IDR, otherwise length-prefix → Annex-B. + Avc { + avcc: re_mp4::Avc1Box, + state: AnnexBStreamState, + }, + + /// H.265 in MP4: prepend VPS/SPS/PPS to each IRAP, otherwise length-prefix → Annex-B. + Hevc { + hvcc: re_mp4::HevcBox, + state: AnnexBStreamState, + }, + + /// VP8 / VP9: wrap each chunk in an IVF file/frame header, since their raw bitstream + /// has no self-delimiting framing and `FFmpeg` has no `vp8`/`vp9` raw demuxer. + /// See . + Ivf { + /// `b"VP80"` for VP8, `b"VP90"` for VP9. + fourcc: [u8; 4], + width: u16, + height: u16, + + /// Whether we already wrote the 32-byte file header to `FFmpeg`'s stdin. + file_header_written: bool, + + /// Frame counter used as the per-frame IVF PTS. + /// + /// `FFmpeg` is run with `-fps_mode passthrough`, so absolute PTS values + /// are immaterial as long as they are monotonically increasing. + frame_idx: u64, + }, } impl CodecMeta { - fn from_stsd(stsd: &re_mp4::StsdBox) -> Option { - use re_mp4::StsdBoxContent::{Avc1, Hev1, Hvc1}; + fn for_decoder( + codec: &crate::VideoCodec, + encoding_details: Option<&VideoEncodingDetails>, + ) -> Self { + match codec { + crate::VideoCodec::VP8 | crate::VideoCodec::VP9 => { + let fourcc = if matches!(codec, crate::VideoCodec::VP8) { + *b"VP80" + } else { + *b"VP90" + }; + let (width, height) = encoding_details + .map(|e| (e.coded_dimensions[0], e.coded_dimensions[1])) + .unwrap_or((0, 0)); + Self::Ivf { + fourcc, + width, + height, + file_header_written: false, + frame_idx: 0, + } + } + _ => encoding_details + .and_then(|e| e.stsd.as_ref()) + .and_then(Self::from_stsd) + .unwrap_or(Self::RawBytestream), + } + } + fn from_stsd(stsd: &re_mp4::StsdBox) -> Option { match &stsd.contents { - Avc1(avc) => Some(Self::Avc(avc.clone())), - Hev1(hevc) | Hvc1(hevc) => Some(Self::Hevc(hevc.clone())), - + re_mp4::StsdBoxContent::Avc1(avcc) => Some(Self::Avc { + avcc: avcc.clone(), + state: AnnexBStreamState::default(), + }), + re_mp4::StsdBoxContent::Hev1(hvcc) | re_mp4::StsdBoxContent::Hvc1(hvcc) => { + Some(Self::Hevc { + hvcc: hvcc.clone(), + state: AnnexBStreamState::default(), + }) + } + re_mp4::StsdBoxContent::Vp08(vp8) => Some(Self::Ivf { + fourcc: *b"VP80", + width: vp8.width, + height: vp8.height, + file_header_written: false, + frame_idx: 0, + }), + re_mp4::StsdBoxContent::Vp09(vp9) => Some(Self::Ivf { + fourcc: *b"VP90", + width: vp9.width, + height: vp9.height, + file_header_written: false, + frame_idx: 0, + }), _ => None, } } + + fn write_chunk(&mut self, out: &mut dyn std::io::Write, chunk: &Chunk) -> Result<(), Error> { + match self { + Self::RawBytestream => write_bytes(out, &chunk.data), + + Self::Avc { avcc, state } => { + write_avc_chunk_to_nalu_stream(avcc, out, chunk, state).map_err(Error::from) + } + + Self::Hevc { hvcc, state } => { + write_hevc_chunk_to_nalu_stream(hvcc, out, chunk, state).map_err(Error::from) + } + + Self::Ivf { + fourcc, + width, + height, + file_header_written, + frame_idx, + } => write_chunk_to_ivf_stream( + fourcc, + width, + height, + file_header_written, + frame_idx, + out, + chunk, + ), + } + } + + fn write_end_of_stream(&self, out: &mut dyn std::io::Write) -> Result<(), Error> { + match self { + Self::Ivf { .. } => Ok(()), + Self::RawBytestream | Self::Avc { .. } | Self::Hevc { .. } => { + // Try to flush out the last frames from ffmpeg with EndSequence/EndStream NAL units. + // Unfortunately this doesn't help, at least not for https://github.com/rerun-io/rerun/issues/8073 + let end_nals: Vec = [ + ANNEXB_NAL_START_CODE, + &[UnitType::EndOfSeq.id()], + ANNEXB_NAL_START_CODE, + &[UnitType::EndOfStream.id()], + ] + .concat(); + + write_bytes(out, &end_nals) + } + } + } } #[cfg(test)] diff --git a/crates/utils/re_video/src/decode/ffmpeg_cli/ivf.rs b/crates/utils/re_video/src/decode/ffmpeg_cli/ivf.rs new file mode 100644 index 000000000000..0bbb11019011 --- /dev/null +++ b/crates/utils/re_video/src/decode/ffmpeg_cli/ivf.rs @@ -0,0 +1,62 @@ +use crate::Chunk; + +use super::ffmpeg::Error; + +pub fn write_chunk_to_ivf_stream( + fourcc: &[u8; 4], + width: &u16, + height: &u16, + file_header_written: &mut bool, + frame_idx: &mut u64, + out: &mut dyn std::io::Write, + chunk: &Chunk, +) -> Result<(), Error> { + if !*file_header_written { + write_ivf_file_header(out, fourcc, width, height)?; + *file_header_written = true; + } + + write_ivf_frame_header(out, chunk.data.len() as u32, *frame_idx)?; + out.write_all(&chunk.data) + .map_err(Error::FailedToWriteToFfmpeg)?; + + *frame_idx += 1; + + Ok(()) +} + +/// Write a 32-byte IVF file header. See . +fn write_ivf_file_header( + out: &mut dyn std::io::Write, + fourcc: &[u8; 4], + width: &u16, + height: &u16, +) -> Result<(), Error> { + let mut hdr = [0u8; 32]; + hdr[0..4].copy_from_slice(b"DKIF"); + // version=0 left implicit (zeros). + hdr[6..8].copy_from_slice(&32u16.to_le_bytes()); // header length + hdr[8..12].copy_from_slice(fourcc); + hdr[12..14].copy_from_slice(&width.to_le_bytes()); + hdr[14..16].copy_from_slice(&height.to_le_bytes()); + // Placeholder timebase: 1/1000. We run FFmpeg with `-fps_mode passthrough`, + // so absolute PTS values are immaterial as long as they are monotonic. + hdr[16..20].copy_from_slice(&60u32.to_le_bytes()); // timebase denominator + hdr[20..24].copy_from_slice(&1u32.to_le_bytes()); // timebase numerator + // Advertise an open-ended stream. Some IVF demuxers treat zero as an empty file. + hdr[24..28].copy_from_slice(&u32::MAX.to_le_bytes()); + // unused=0 left implicit (zeros). + out.write_all(&hdr).map_err(Error::FailedToWriteToFfmpeg) +} + +/// Write a 12-byte IVF per-frame header. See . +fn write_ivf_frame_header( + out: &mut dyn std::io::Write, + frame_size: u32, + pts: u64, +) -> Result<(), Error> { + let mut hdr = [0u8; 12]; + hdr[0..4].copy_from_slice(&frame_size.to_le_bytes()); + hdr[4..12].copy_from_slice(&pts.to_le_bytes()); + out.write_all(&hdr).map_err(Error::FailedToWriteToFfmpeg) +} diff --git a/crates/utils/re_video/src/decode/ffmpeg_cli/mod.rs b/crates/utils/re_video/src/decode/ffmpeg_cli/mod.rs index 63eedb24c593..2cd528f1e4eb 100644 --- a/crates/utils/re_video/src/decode/ffmpeg_cli/mod.rs +++ b/crates/utils/re_video/src/decode/ffmpeg_cli/mod.rs @@ -1,7 +1,10 @@ mod ffmpeg; +mod ivf; +mod transcode; mod version; pub use ffmpeg::{Error, FFmpegCliDecoder}; +pub use transcode::{TranscodedMp4, transcode_mp4}; pub use version::{ FFMPEG_MINIMUM_VERSION_MAJOR, FFMPEG_MINIMUM_VERSION_MINOR, FFmpegVersion, FFmpegVersionParseError, diff --git a/crates/utils/re_video/src/decode/ffmpeg_cli/transcode.rs b/crates/utils/re_video/src/decode/ffmpeg_cli/transcode.rs new file mode 100644 index 000000000000..9a1ed2fa9806 --- /dev/null +++ b/crates/utils/re_video/src/decode/ffmpeg_cli/transcode.rs @@ -0,0 +1,485 @@ +//! Transcode an mp4 through the `ffmpeg` CLI. +//! +//! ffmpeg reads the (seekable) source file directly — an mp4's `moov` sample +//! tables can trail its `mdat`, so a non-seekable stdin pipe can't be demuxed — +//! and writes a **fragmented** mp4 to stdout: `empty_moov` puts a demuxable init +//! segment up front, and `frag_keyframe` starts a new self-contained fragment at +//! each keyframe. [`TranscodedMp4`] yields those bytes as they are produced, so +//! nothing is buffered and the caller can demux one GOP at a time. + +use std::collections::{BTreeSet, VecDeque}; +use std::path::Path; + +use ffmpeg_sidecar::child::FfmpegChild; +use ffmpeg_sidecar::command::FfmpegCommand; +use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel}; +use ffmpeg_sidecar::iter::FfmpegIterator; + +use super::FFmpegVersion; +use super::ffmpeg::{Error, check_ffmpeg_version}; +use crate::{HwAccel, Mp4TranscodeOptions, VideoCodec}; + +/// How many trailing error/fatal stderr lines to keep for error reporting. +const STDERR_TAIL_LINES: usize = 40; + +/// A streaming iterator over the transcoded, fragmented mp4 that `ffmpeg` +/// writes to stdout, yielded as raw byte chunks that do *not* align to mp4 box +/// or GOP boundaries (the caller must reframe them). +pub struct TranscodedMp4 { + child: FfmpegChild, + events: FfmpegIterator, + + /// Bounded tail of ffmpeg's error/fatal log lines, kept so a non-zero exit + /// can report why it failed. + stderr_tail: VecDeque, + + debug_name: String, + done: bool, +} + +impl Iterator for TranscodedMp4 { + /// One chunk of the fragmented-mp4 output, or a terminal error. + type Item = Result, Error>; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + + // Drive the shared event stream until we have a chunk to hand back or the + // process is done. Metadata, progress, and benign logs are skipped; the + // internal reader thread blocks on its rendezvous channel between our + // calls, so this keeps only one chunk resident at a time. + loop { + match self.events.next() { + Some(FfmpegEvent::OutputChunk(chunk)) => return Some(Ok(chunk)), + + // ffmpeg's own diagnostics: keep a bounded tail of the serious + // ones so a non-zero exit can explain itself. + Some(FfmpegEvent::Log(LogLevel::Error | LogLevel::Fatal, line)) => { + self.push_tail(line); + } + + // An error from `ffmpeg_sidecar` itself, rather than from ffmpeg. + Some(FfmpegEvent::Error(err)) => { + self.done = true; + return Some(Err(Error::FfmpegSidecar(err))); + } + + // stdout closed (`Done`) or both reader threads finished (`None`): + // ffmpeg is done writing. Reap it and surface a non-zero exit. + Some(FfmpegEvent::Done) | None => { + self.done = true; + return self.finish().err().map(Err); + } + + // Metadata, progress, `LogEOF`, and benign logs: nothing to emit. + Some(_) => {} + } + } + } +} + +impl TranscodedMp4 { + fn push_tail(&mut self, line: String) { + if self.stderr_tail.len() >= STDERR_TAIL_LINES { + self.stderr_tail.pop_front(); + } + self.stderr_tail.push_back(line); + } + + /// Reap ffmpeg once its output has ended; `Err` if it exited non-zero. + fn finish(&mut self) -> Result<(), Error> { + let status = self + .child + .wait() + .map_err(|err| Error::FfmpegSidecar(format!("failed to await ffmpeg: {err}")))?; + if status.success() { + return Ok(()); + } + let tail = Vec::from(std::mem::take(&mut self.stderr_tail)).join("\n"); + Err(Error::Ffmpeg(format!( + "ffmpeg exited with {status} while transcoding {debug_name}:\n{tail}", + debug_name = self.debug_name, + ))) + } +} + +impl Drop for TranscodedMp4 { + fn drop(&mut self) { + if !self.done { + // Consumer stopped early (or an error aborted us): kill ffmpeg so its + // reader threads unblock and it doesn't linger on a full stdout pipe. + // The threads are detached and exit on the resulting pipe EOF. + self.child.kill().ok(); + self.child.wait().ok(); + } + } +} + +/// Re-encode the mp4 at `input_path` into a B-frame-free, stream-friendly +/// fragmented mp4 (one `frag_keyframe` fragment per GOP, `empty_moov` init +/// segment up front), applying the transforms in `options`. +/// +/// The re-encode is done at a visually-lossless quality and preserves the source pixel format where +/// possible, so the output is a faithful — not bit-exact — copy of the input. +/// +/// Returns [`Error::FFmpegNotInstalled`] if no usable `ffmpeg` executable is +/// found, or [`Error::NoEncoderForCodec`] if this ffmpeg build has no encoder for +/// the requested output codec. +pub fn transcode_mp4( + input_path: &Path, + source_codec: VideoCodec, + options: &Mp4TranscodeOptions, + debug_name: &str, +) -> Result { + re_tracing::profile_function!(); + + let ffmpeg_path = options.ffmpeg_override.as_deref(); + + // Surfaces `Error::FFmpegNotInstalled` / `Error::UnsupportedFFmpegVersion`, + // exactly like the decoder, and before the encoder probe below so a bogus + // override fails here deterministically. Safe to block: we're off the GUI thread. + check_ffmpeg_version(FFmpegVersion::for_executable_blocking(ffmpeg_path))?; + + let target = options.output_codec.clone().unwrap_or(source_codec); + let available = available_encoders(ffmpeg_path); + let spec = resolve_encoder(&target, options.hardware_acceleration, &available)?; + + let mut command = ffmpeg_command(ffmpeg_path); + + // ffmpeg seeks the source itself; no stdin piping (mp4 can't be demuxed from a pipe). + command.input(input_path.to_string_lossy().as_ref()); + command.args(["-c:v", spec.name]); + command.args(spec.rate_control.iter().map(String::as_str)); + if spec.needs_bf0 { + // No B-frames in H.26x output: `VideoStream` can't model `DTS != PTS` + // (#10090). AV1/VP8/VP9 are inherently `DTS == PTS`, so they don't need it. + command.args(["-bf", "0"]); + } + if let Some(gop) = options.gop_size { + // `-g` sets the max keyframe interval; `-force_key_frames` guarantees a + // keyframe exactly every `gop` frames, codec-agnostically (unlike the + // x264-only `-sc_threshold 0`), so the GOP length — and thus the per-GOP + // fragmentation below — is deterministic. + command.args(["-g", &gop.to_string()]); + command.args(["-force_key_frames", &format!("expr:gte(n,n_forced*{gop})")]); + } + command.args(spec.extra_output_args.iter().map(String::as_str)); + + let mut child = command + // Deliberately no `-pix_fmt`: ffmpeg then negotiates the encoder's format from + // the decoded input, preserving the source bit depth / chroma subsampling + // (10-bit, 4:2:2, 4:4:4) instead of forcing a downconvert to 8-bit 4:2:0. + .fps_mode("passthrough") // keep every frame (no dupes/drops), at its original PTS. + .args(["-an"]) // `VideoStream` carries no audio. + // Fragmented mp4: `empty_moov` puts a demuxable init segment up front and + // `frag_keyframe` starts a new self-contained fragment at each keyframe, so + // the output is streamable and splits cleanly into one GOP per fragment. + .args(["-movflags", "frag_keyframe+empty_moov+default_base_moof"]) + .args(["-f", "mp4"]) + .output("pipe:1") + .spawn() + .map_err(Error::FailedToStartFfmpeg)?; + + // The same event iterator the decoder consumes: it spawns the stdout/stderr + // reader threads internally and delivers their output over a rendezvous + // channel, so we neither manage threads nor buffer the whole stream. + let events = child + .iter() + .map_err(|err| Error::NoIterator(err.to_string()))?; + + Ok(TranscodedMp4 { + child, + events, + stderr_tail: VecDeque::with_capacity(STDERR_TAIL_LINES), + debug_name: debug_name.to_owned(), + done: false, + }) +} + +/// A resolved ffmpeg encoder plus the args needed to drive it for B-frame-free, +/// stream-friendly output. +struct EncoderSpec { + /// The ffmpeg `-c:v` encoder name. + name: &'static str, + + /// Rate-control args (e.g. `-crf 18`, or `-cq 23` for a GPU encoder). + rate_control: Vec, + + /// Extra codec/muxer args (e.g. `-strict experimental` so the mp4 muxer will + /// write a VP8 track). + extra_output_args: Vec, + + /// Whether `-bf 0` must be passed. Only H.26x need it; AV1/VP8/VP9 are + /// inherently `DTS == PTS`. + needs_bf0: bool, +} + +impl EncoderSpec { + fn new(name: &'static str, rate_control: &[&str], needs_bf0: bool) -> Self { + Self { + name, + rate_control: rate_control.iter().map(|s| (*s).to_owned()).collect(), + extra_output_args: Vec::new(), + needs_bf0, + } + } + + fn with_extra(mut self, extra: &[&str]) -> Self { + self.extra_output_args = extra.iter().map(|s| (*s).to_owned()).collect(); + self + } +} + +/// Pick the ffmpeg encoder (and its rate-control flags) for `target`, preferring +/// a hardware encoder when `hw == Auto` and one is `available`, otherwise falling +/// back to software. Returns [`Error::NoEncoderForCodec`] if none is available. +/// +/// This is the single maintenance point for encoder/flag choices — the quality +/// defaults and GPU flags are best-effort. Kept pure (takes the already-probed +/// `available` set) so it is unit-testable without spawning ffmpeg. +fn resolve_encoder( + target: &VideoCodec, + hw: HwAccel, + available: &BTreeSet, +) -> Result { + // (gpu candidates in priority order, software candidates in priority order). + // GPU is limited to the NVENC + VideoToolbox families (defined rate-control); + // QSV/VAAPI are deferred. VP8/VP9's only GPU encoders are the deferred Intel + // ones (`vp8_vaapi`/`vp9_vaapi`/`vp9_qsv`), so their GPU list is empty here. + let (gpu, sw): (Vec, Vec) = match target { + VideoCodec::H264 => ( + vec![ + EncoderSpec::new("h264_nvenc", &["-rc", "vbr", "-cq", "23"], true), + EncoderSpec::new("h264_videotoolbox", &["-q:v", "55"], true), + ], + vec![EncoderSpec::new("libx264", &["-crf", "18"], true)], + ), + VideoCodec::H265 => ( + vec![ + EncoderSpec::new("hevc_nvenc", &["-rc", "vbr", "-cq", "23"], true), + EncoderSpec::new("hevc_videotoolbox", &["-q:v", "55"], true), + ], + vec![EncoderSpec::new("libx265", &["-crf", "20"], true)], + ), + VideoCodec::AV1 => ( + vec![EncoderSpec::new( + "av1_nvenc", + &["-rc", "vbr", "-cq", "30"], + false, + )], + vec![ + EncoderSpec::new("libsvtav1", &["-crf", "30"], false), + EncoderSpec::new("libaom-av1", &["-crf", "30", "-b:v", "0"], false), + ], + ), + VideoCodec::VP9 => ( + Vec::new(), + // libvpx constant-quality needs `-b:v 0`. + vec![EncoderSpec::new( + "libvpx-vp9", + &["-crf", "31", "-b:v", "0"], + false, + )], + ), + VideoCodec::VP8 => ( + Vec::new(), + // VP8 CQ needs a bitrate cap, and the mp4 muxer needs `-strict experimental`. + vec![ + EncoderSpec::new("libvpx", &["-crf", "10", "-b:v", "2M"], false) + .with_extra(&["-strict", "experimental"]), + ], + ), + VideoCodec::ImageSequence(_) => { + // The reader rejects an image-sequence target before we get here; this + // is a defensive backstop. + return Err(Error::BadVideoData(format!( + "Cannot transcode to a non-video (image-sequence) codec {target:?}" + ))); + } + }; + + if hw == HwAccel::Auto { + if let Some(spec) = gpu.into_iter().find(|c| available.contains(c.name)) { + return Ok(spec); + } + re_log::warn_once!( + "No hardware encoder available in this FFmpeg build for {target:?}; using a software encoder" + ); + } + if let Some(spec) = sw.into_iter().find(|c| available.contains(c.name)) { + return Ok(spec); + } + Err(Error::NoEncoderForCodec { + codec: target.clone(), + }) +} + +/// Build an [`FfmpegCommand`] for the given override (or `PATH`). +/// +/// The single place the ffmpeg binary is resolved, so the encoder probe and the +/// transcode itself never end up pointing at different executables. +fn ffmpeg_command(ffmpeg_path: Option<&Path>) -> FfmpegCommand { + match ffmpeg_path { + Some(path) => FfmpegCommand::new_with_path(path), + None => FfmpegCommand::new(), + } +} + +/// The set of encoder names this ffmpeg build reports via `ffmpeg -encoders`. +/// +/// Returns an empty set if ffmpeg can't be run. +fn available_encoders(ffmpeg_path: Option<&Path>) -> BTreeSet { + // `new_with_path` already pipes stdout; silence stderr (the `-loglevel` line). + let output = match ffmpeg_command(ffmpeg_path) + .as_inner_mut() + .args(["-hide_banner", "-encoders"]) + .stderr(std::process::Stdio::null()) + .output() + { + Ok(output) => output, + Err(err) => { + re_log::warn_once!("Failed to probe FFmpeg encoders: {err}"); + return BTreeSet::new(); + } + }; + parse_encoder_names(&String::from_utf8_lossy(&output.stdout)) +} + +/// Parse the encoder names out of `ffmpeg -encoders` stdout. +/// +/// The listing is a header, a `------` separator, then one line per encoder: +/// a 6-char flag column (first char `V`/`A`/`S`) then the encoder name, e.g. +/// ` V....D h264_nvenc NVIDIA NVENC H.264 encoder`. We keep the names of the +/// video (`V`) encoders. +fn parse_encoder_names(stdout: &str) -> BTreeSet { + // Everything after the `------` separator (or the whole text if not found). + let body = stdout.split_once("------").map_or(stdout, |(_, rest)| rest); + body.lines() + .filter_map(|line| { + let mut parts = line.split_whitespace(); + let flags = parts.next()?; + (flags.len() == 6 && flags.starts_with('V')) + .then(|| parts.next()) + .flatten() + .map(str::to_owned) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::{parse_encoder_names, resolve_encoder}; + use crate::{HwAccel, VideoCodec}; + + fn available_set(names: &[&str]) -> std::collections::BTreeSet { + names.iter().map(|s| (*s).to_owned()).collect() + } + + #[test] + fn software_specs_per_codec() { + // Every software encoder present; assert the expected name, rate-control, + // and whether `-bf 0` is needed. + let all = available_set(&["libx264", "libx265", "libsvtav1", "libvpx-vp9", "libvpx"]); + let cases = [ + (VideoCodec::H264, "libx264", vec!["-crf", "18"], true), + (VideoCodec::H265, "libx265", vec!["-crf", "20"], true), + (VideoCodec::AV1, "libsvtav1", vec!["-crf", "30"], false), + ( + VideoCodec::VP9, + "libvpx-vp9", + vec!["-crf", "31", "-b:v", "0"], + false, + ), + ( + VideoCodec::VP8, + "libvpx", + vec!["-crf", "10", "-b:v", "2M"], + false, + ), + ]; + for (codec, name, rc, needs_bf0) in cases { + let spec = resolve_encoder(&codec, HwAccel::Off, &all).expect("encoder available"); + assert_eq!(spec.name, name, "{codec:?}"); + assert_eq!(spec.rate_control, rc, "{codec:?}"); + assert_eq!(spec.needs_bf0, needs_bf0, "{codec:?}"); + } + } + + #[test] + fn vp8_carries_strict_experimental() { + let spec = resolve_encoder(&VideoCodec::VP8, HwAccel::Off, &available_set(&["libvpx"])) + .expect("libvpx available"); + assert_eq!(spec.extra_output_args, vec!["-strict", "experimental"]); + } + + #[test] + fn auto_prefers_hardware_then_falls_back_to_software() { + // GPU present → picked. + let with_gpu = available_set(&["h264_nvenc", "libx264"]); + let spec = resolve_encoder(&VideoCodec::H264, HwAccel::Auto, &with_gpu).unwrap(); + assert_eq!(spec.name, "h264_nvenc"); + + // GPU absent → software fallback. + let sw_only = available_set(&["libx264"]); + let spec = resolve_encoder(&VideoCodec::H264, HwAccel::Auto, &sw_only).unwrap(); + assert_eq!(spec.name, "libx264"); + } + + #[test] + fn auto_for_codec_without_gpu_encoder_uses_software() { + // VP9's only GPU encoders are the deferred Intel ones, so within scope it + // has none; `Auto` must still resolve to software. + let spec = resolve_encoder( + &VideoCodec::VP9, + HwAccel::Auto, + &available_set(&["libvpx-vp9"]), + ) + .unwrap(); + assert_eq!(spec.name, "libvpx-vp9"); + } + + #[test] + fn av1_falls_back_to_libaom_when_svtav1_missing() { + let spec = resolve_encoder( + &VideoCodec::AV1, + HwAccel::Off, + &available_set(&["libaom-av1"]), + ) + .unwrap(); + assert_eq!(spec.name, "libaom-av1"); + assert_eq!(spec.rate_control, vec!["-crf", "30", "-b:v", "0"]); + } + + #[test] + fn no_encoder_available_is_an_error() { + let err = resolve_encoder(&VideoCodec::VP9, HwAccel::Off, &available_set(&[])); + assert!(matches!( + err, + Err(super::Error::NoEncoderForCodec { + codec: VideoCodec::VP9 + }) + )); + } + + #[test] + fn parse_encoder_names_extracts_video_encoders() { + let sample = "\ +Encoders: + V..... = Video + A..... = Audio + ------ + V....D libx264 libx264 H.264 / AVC + V....D h264_nvenc NVIDIA NVENC H.264 encoder + A....D aac AAC (Advanced Audio Coding) + V....D libvpx-vp9 libvpx VP9 +"; + let got = parse_encoder_names(sample); + assert!(got.contains("libx264")); + assert!(got.contains("h264_nvenc")); + assert!(got.contains("libvpx-vp9")); + // Audio encoders are not video, so they're excluded. + assert!(!got.contains("aac")); + } +} diff --git a/crates/utils/re_video/src/decode/image_decoder.rs b/crates/utils/re_video/src/decode/image_decoder.rs index 59becf424711..6c11141b6ca0 100644 --- a/crates/utils/re_video/src/decode/image_decoder.rs +++ b/crates/utils/re_video/src/decode/image_decoder.rs @@ -1,9 +1,14 @@ -use crate::{PixelFormat, decode::async_decoder_wrapper::SyncDecoder}; +use crate::{DecodedFrameContent, PixelFormat}; +#[cfg(not(target_arch = "wasm32"))] +use crate::decode::sync_decoder_wrapper::SyncDecoder; + +#[cfg(not(target_arch = "wasm32"))] pub struct SyncImageDecoder { image_format: image::ImageFormat, } +#[cfg(not(target_arch = "wasm32"))] impl SyncImageDecoder { pub fn try_new(descr: &crate::VideoDataDescription) -> Option { Some(Self { @@ -16,6 +21,7 @@ impl SyncImageDecoder { } } +#[cfg(not(target_arch = "wasm32"))] impl SyncDecoder for SyncImageDecoder { // TODO(isse): We could potentially cache decoded blobs, but that's missing some things: // - A way to purge the cache, i.e have a purge function on video decoders that gets called from `VideoStreamCache`? @@ -37,7 +43,7 @@ impl SyncDecoder for SyncImageDecoder { reader.set_format(self.image_format); - let content = match decode_to_frame_content(reader) { + let content = match decode_to_decoded_frame_content(reader) { Ok(content) => content, Err(err) => { let _send_error = output_sender.send(crate::FrameResult::Err(err)); @@ -65,9 +71,9 @@ impl SyncDecoder for SyncImageDecoder { } } -fn decode_to_frame_content( +pub(crate) fn decode_to_decoded_frame_content( reader: image::ImageReader>>, -) -> Result { +) -> Result { let dynamic_image = reader .decode() .map_err(|err| crate::DecodeError::ImageDecoder(err.to_string()))?; @@ -123,7 +129,7 @@ fn decode_to_frame_content( } }; - Ok(crate::FrameContent { + Ok(DecodedFrameContent { data: data.to_owned(), width, height, diff --git a/crates/utils/re_video/src/decode/mod.rs b/crates/utils/re_video/src/decode/mod.rs index 1ffd2e5db98e..974367672e78 100644 --- a/crates/utils/re_video/src/decode/mod.rs +++ b/crates/utils/re_video/src/decode/mod.rs @@ -77,9 +77,12 @@ //! supporting HDR content at which point more properties will be important! //! -#[cfg(not(target_arch = "wasm32"))] -mod async_decoder_wrapper; -#[cfg(not(target_arch = "wasm32"))] +mod sync_decoder; + +#[cfg_attr(target_arch = "wasm32", path = "sync_decoder_wrapper_wasm.rs")] +#[cfg_attr(not(target_arch = "wasm32"), path = "sync_decoder_wrapper_native.rs")] +mod sync_decoder_wrapper; + mod image_decoder; #[cfg(with_dav1d)] @@ -92,7 +95,8 @@ mod ffmpeg_cli; pub use ffmpeg_cli::FFmpegCliDecoder; #[cfg(with_ffmpeg)] pub use ffmpeg_cli::{ - Error as FFmpegError, FFmpegVersion, FFmpegVersionParseError, ffmpeg_download_url, + Error as FFmpegError, FFmpegVersion, FFmpegVersionParseError, TranscodedMp4, + ffmpeg_download_url, transcode_mp4, }; #[cfg(target_arch = "wasm32")] @@ -100,19 +104,28 @@ mod web_image_decoder; #[cfg(target_arch = "wasm32")] mod webcodecs; +#[cfg(target_arch = "wasm32")] +pub use webcodecs::WebVideoFrame; + +mod rvl_decoder; + use crate::{SampleIndex, Time, VideoDataDescription, player::VideoPlaybackIssueSeverity}; -#[derive(thiserror::Error, Debug, Clone)] +#[derive(thiserror::Error, Debug, Clone, re_byte_size::SizeBytes)] pub enum DecodeError { #[error("Waiting for encoding details")] WaitingForCodecDetails, #[error("Unsupported codec: {0}")] - UnsupportedCodec(String), + UnsupportedCodec(#[size_bytes(ignore)] String), #[cfg(with_dav1d)] #[error("dav1d: {0}")] - Dav1d(#[from] dav1d::Error), + Dav1d( + #[from] + #[size_bytes(ignore)] + dav1d::Error, + ), #[error("To enabled native AV1 decoding, compile Rerun with the `nasm` feature enabled.")] Dav1dWithoutNasm, @@ -122,26 +135,26 @@ pub enum DecodeError { )] NoDav1dOnLinuxArm64, - #[cfg(not(target_arch = "wasm32"))] #[error("Image decode error: {0}")] - ImageDecoder(String), + ImageDecoder(#[size_bytes(ignore)] String), + + #[error(transparent)] + RvlDecoder(#[size_bytes(ignore)] re_rvl::RvlDecodeError), #[cfg(target_arch = "wasm32")] #[error(transparent)] - WebDecoder(#[from] webcodecs::WebError), + WebDecoder( + #[from] + #[size_bytes(ignore)] + webcodecs::WebError, + ), #[cfg(with_ffmpeg)] #[error(transparent)] - Ffmpeg(std::sync::Arc), + Ffmpeg(#[size_bytes(ignore)] std::sync::Arc), #[error("Unsupported bits per component: {0}")] - BadBitsPerComponent(usize), -} - -impl re_byte_size::SizeBytes for DecodeError { - fn heap_size_bytes(&self) -> u64 { - 0 - } + BadBitsPerComponent(#[size_bytes(ignore)] usize), } impl DecodeError { @@ -153,13 +166,13 @@ impl DecodeError { Self::WaitingForCodecDetails | Self::UnsupportedCodec(_) | Self::Dav1dWithoutNasm - | Self::NoDav1dOnLinuxArm64 => false, + | Self::NoDav1dOnLinuxArm64 + | Self::RvlDecoder(_) => false, // Issue with AV1 decoding. #[cfg(with_dav1d)] Self::Dav1d(_) => true, - #[cfg(not(target_arch = "wasm32"))] Self::ImageDecoder(_) => false, // Issue with WebCodecs decoding. @@ -177,13 +190,12 @@ impl DecodeError { pub fn severity(&self) -> VideoPlaybackIssueSeverity { match self { - Self::WaitingForCodecDetails => VideoPlaybackIssueSeverity::Loading, + Self::WaitingForCodecDetails => VideoPlaybackIssueSeverity::Informational, #[cfg(with_dav1d)] Self::Dav1d(err) => match err { dav1d::Error::Again => VideoPlaybackIssueSeverity::Loading, _ => VideoPlaybackIssueSeverity::Error, }, - #[cfg(not(target_arch = "wasm32"))] Self::ImageDecoder(_) => VideoPlaybackIssueSeverity::Error, #[cfg(target_arch = "wasm32")] Self::WebDecoder(err) => err.severity(), @@ -193,7 +205,8 @@ impl DecodeError { Self::UnsupportedCodec(_) | Self::Dav1dWithoutNasm | Self::NoDav1dOnLinuxArm64 - | Self::BadBitsPerComponent(_) => VideoPlaybackIssueSeverity::Error, + | Self::BadBitsPerComponent(_) + | Self::RvlDecoder(_) => VideoPlaybackIssueSeverity::Error, } } } @@ -265,9 +278,15 @@ pub fn new_decoder( #[cfg(target_arch = "wasm32")] { - return match video.codec { - crate::VideoCodec::ImageSequence(_) => { - if let Some(decoder) = + return match &video.codec { + crate::VideoCodec::ImageSequence(codec) => { + if codec.as_deref() == Some("application/rvl") { + Ok(Box::new(sync_decoder_wrapper::SyncDecoderWrapper::new( + "rvl decoder".to_owned(), + Box::new(rvl_decoder::RvlDecoder), + output_sender, + ))) + } else if let Some(decoder) = web_image_decoder::WebImageDecoder::try_new(video, output_sender.clone()) { Ok(Box::new(decoder)) @@ -295,7 +314,7 @@ pub fn new_decoder( #[cfg(with_dav1d)] { re_log::trace!("Decoding AV1…"); - return Ok(Box::new(async_decoder_wrapper::AsyncDecoderWrapper::new( + return Ok(Box::new(sync_decoder_wrapper::SyncDecoderWrapper::new( debug_name.to_owned(), Box::new(av1::SyncDav1dDecoder::new(debug_name.to_owned())?), output_sender, @@ -304,7 +323,10 @@ pub fn new_decoder( } #[cfg(with_ffmpeg)] - crate::VideoCodec::H264 | crate::VideoCodec::H265 => Ok(Box::new(FFmpegCliDecoder::new( + crate::VideoCodec::H264 + | crate::VideoCodec::H265 + | crate::VideoCodec::VP8 + | crate::VideoCodec::VP9 => Ok(Box::new(FFmpegCliDecoder::new( debug_name.to_owned(), video.encoding_details.as_ref(), output_sender, @@ -321,8 +343,14 @@ pub fn new_decoder( )?)), crate::VideoCodec::ImageSequence(codec) => { - if let Some(decoder) = image_decoder::SyncImageDecoder::try_new(video) { - Ok(Box::new(async_decoder_wrapper::AsyncDecoderWrapper::new( + if codec.as_deref() == Some("application/rvl") { + Ok(Box::new(sync_decoder_wrapper::SyncDecoderWrapper::new( + "rvl decoder".to_owned(), + Box::new(rvl_decoder::RvlDecoder), + output_sender, + ))) + } else if let Some(decoder) = image_decoder::SyncImageDecoder::try_new(video) { + Ok(Box::new(sync_decoder_wrapper::SyncDecoderWrapper::new( format!("image decoder ({})", decoder.mime_type()), Box::new(decoder), output_sender, @@ -332,6 +360,7 @@ pub fn new_decoder( } } + #[cfg(not(all(feature = "av1", with_ffmpeg)))] _ => Err(DecodeError::UnsupportedCodec( video.human_readable_codec_string(), )), @@ -343,6 +372,7 @@ pub fn new_decoder( /// For details on how to interpret the data, see [`crate::SampleMetadata`]. /// /// In MP4, one sample is one frame. +#[derive(re_byte_size::SizeBytes)] pub struct Chunk { /// The start of a new group of pictures? /// @@ -390,45 +420,17 @@ pub struct Chunk { pub duration: Option(ctx.blueprint_db(), ctx.blueprint_query(), ctx.view_id); + let view_property = ViewProperty::from_archetype::(ctx); view_property_ui_impl(ctx, ui, &view_property, None); } @@ -30,8 +29,7 @@ pub fn view_property_ui_with_redirect( redirect_component: ComponentIdentifier, redirect_with_view_id: re_viewer_context::ViewId, ) { - let view_property = - ViewProperty::from_archetype::(ctx.blueprint_db(), ctx.blueprint_query(), ctx.view_id); + let view_property = ViewProperty::from_archetype::(ctx); view_property_ui_impl( ctx, ui, @@ -46,9 +44,8 @@ pub fn view_property_ui_with_redirect( view_state: ctx.view_state, query_result: &re_viewer_context::DataQueryResult::default(), }, - view_property: ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), + view_property: ViewProperty::from_archetype_for_view::( + ctx.viewer_ctx, redirect_with_view_id, ), }), diff --git a/crates/viewer/re_view_bar_chart/Cargo.toml b/crates/viewer/re_view_bar_chart/Cargo.toml index 20728e0610ec..e21aca981975 100644 --- a/crates/viewer/re_view_bar_chart/Cargo.toml +++ b/crates/viewer/re_view_bar_chart/Cargo.toml @@ -34,6 +34,7 @@ ahash.workspace = true arrow.workspace = true egui_plot.workspace = true egui.workspace = true +itertools.workspace = true [dev-dependencies] re_test_viewport.workspace = true diff --git a/crates/viewer/re_view_bar_chart/src/view_class.rs b/crates/viewer/re_view_bar_chart/src/view_class.rs index ee329234366d..b05dbb540467 100644 --- a/crates/viewer/re_view_bar_chart/src/view_class.rs +++ b/crates/viewer/re_view_bar_chart/src/view_class.rs @@ -1,5 +1,6 @@ use ahash::HashMap; use egui::NumExt as _; +use itertools::izip; use re_log_types::{EntityPath, EntityPathHash}; use re_sdk_types::blueprint::archetypes::{PlotBackground, PlotLegend}; use re_sdk_types::blueprint::components::{Corner2D, Enabled}; @@ -165,20 +166,15 @@ impl ViewClass for BarChartView { let state = state.downcast_mut::<()>()?; - let blueprint_db = ctx.blueprint_db(); let view_id = query.view_id; let charts = system_output - .visualizer_data::>( + .visualizer_data_or_default::>( BarChartVisualizerSystem::identifier(), )?; let ctx = self.view_context(ctx, view_id, state, query.space_origin); - let background = ViewProperty::from_archetype::( - blueprint_db, - ctx.blueprint_query(), - view_id, - ); + let background = ViewProperty::from_archetype::(&ctx); let background_color = background .component_or_fallback::(&ctx, PlotBackground::descriptor_color().component)?; let show_grid = background.component_or_fallback::( @@ -186,11 +182,7 @@ impl ViewClass for BarChartView { PlotBackground::descriptor_show_grid().component, )?; - let plot_legend = ViewProperty::from_archetype::( - blueprint_db, - ctx.blueprint_query(), - view_id, - ); + let plot_legend = ViewProperty::from_archetype::(&ctx); let legend_visible: Visible = plot_legend.component_or_fallback(&ctx, PlotLegend::descriptor_visible().component)?; let legend_corner: Corner2D = @@ -230,7 +222,7 @@ impl ViewClass for BarChartView { color, widths, }, - ) in charts + ) in charts.iter() { let arg: ::arrow::buffer::ScalarBuffer = match &abscissa.buffer { TensorBuffer::U8(data) => data.iter().map(|v| *v as f64).collect(), @@ -261,11 +253,8 @@ impl ViewClass for BarChartView { }; let egui_color: egui::Color32 = color.0.into(); - let bars: Vec<(f64, f64, f64)> = arg - .iter() - .zip(widths.iter()) - .zip(data.iter()) - .map(|((index, width), value)| { + let bars: Vec<(f64, f64, f64)> = izip!(&arg, widths, &data) + .map(|(index, width, value)| { let center_x = index + (0.5 * *width as f64); (center_x, *width as f64, *value) }) diff --git a/crates/viewer/re_view_bar_chart/src/visualizer_system.rs b/crates/viewer/re_view_bar_chart/src/visualizer_system.rs index b2a919663b02..237c0f579a35 100644 --- a/crates/viewer/re_view_bar_chart/src/visualizer_system.rs +++ b/crates/viewer/re_view_bar_chart/src/visualizer_system.rs @@ -16,7 +16,7 @@ use re_viewer_context::{ VisualizerExecutionOutput, VisualizerQueryInfo, VisualizerSystem, typed_fallback_for, }; -#[derive(Default)] +#[derive(Default, Clone)] pub struct BarChartData { pub abscissa: datatypes::TensorData, pub widths: Vec, @@ -30,7 +30,10 @@ pub struct BarChartVisualizerSystem; impl IdentifiedViewSystem for BarChartVisualizerSystem { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "BarChart".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "BarChart" + ) } } diff --git a/crates/viewer/re_view_bar_chart/tests/snapshots/bar_chart_1d.png b/crates/viewer/re_view_bar_chart/tests/snapshots/bar_chart_1d.png index af59184f2a92..8f781130de09 100644 --- a/crates/viewer/re_view_bar_chart/tests/snapshots/bar_chart_1d.png +++ b/crates/viewer/re_view_bar_chart/tests/snapshots/bar_chart_1d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c39cc1cdb1452fdf10e15c9ccfdf0e5935a260243018c8696e339d102d10bac7 -size 19844 +oid sha256:f8ca215268b46a0e1f2aade21efff6bfe927af1874033d3722216083c080e2a2 +size 19620 diff --git a/crates/viewer/re_view_bar_chart/tests/snapshots/help_view_bar_chart_view_mac.png b/crates/viewer/re_view_bar_chart/tests/snapshots/help_view_bar_chart_view_mac.png index 14d051183211..bcb6d5b2191e 100644 --- a/crates/viewer/re_view_bar_chart/tests/snapshots/help_view_bar_chart_view_mac.png +++ b/crates/viewer/re_view_bar_chart/tests/snapshots/help_view_bar_chart_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71b22244534deed5c08815349b4ee02ad91f0fe4aa84e77dc8a82afc8542477c -size 19419 +oid sha256:faf9002dc4fa4c0fa173ac06f8ff8695c7ef0929e31be795b4c7869799db6e2c +size 19186 diff --git a/crates/viewer/re_view_bar_chart/tests/snapshots/help_view_bar_chart_view_windows.png b/crates/viewer/re_view_bar_chart/tests/snapshots/help_view_bar_chart_view_windows.png index a9dd196bfb6f..1300da7c65d0 100644 --- a/crates/viewer/re_view_bar_chart/tests/snapshots/help_view_bar_chart_view_windows.png +++ b/crates/viewer/re_view_bar_chart/tests/snapshots/help_view_bar_chart_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b58c04a1fbf1a71c44a2cc429ada6c1c872de0183a33d88f5b17760125063e0 -size 20608 +oid sha256:1fa7909ead2a605585beff43e3a14085e3297134cf884f2f4750260eb3608ce6 +size 20833 diff --git a/crates/viewer/re_view_dataframe/Cargo.toml b/crates/viewer/re_view_dataframe/Cargo.toml index b7f9ad0f2493..70356447d5aa 100644 --- a/crates/viewer/re_view_dataframe/Cargo.toml +++ b/crates/viewer/re_view_dataframe/Cargo.toml @@ -16,6 +16,7 @@ include.workspace = true workspace = true [dependencies] +re_byte_size = { workspace = true, features = ["egui"] } re_chunk_store.workspace = true re_dataframe.workspace = true re_dataframe_ui.workspace = true diff --git a/crates/viewer/re_view_dataframe/src/dataframe_ui.rs b/crates/viewer/re_view_dataframe/src/dataframe_ui.rs index b2022a05dcf2..1f518c2ba0ae 100644 --- a/crates/viewer/re_view_dataframe/src/dataframe_ui.rs +++ b/crates/viewer/re_view_dataframe/src/dataframe_ui.rs @@ -13,7 +13,7 @@ use re_dataframe_ui::{ColumnBlueprint, DisplayRecordBatch, DisplayRecordBatchErr use re_log_types::{EntityPath, TimeInt, TimelineName}; use re_sdk_types::ComponentDescriptor; use re_sdk_types::reflection::ComponentDescriptorExt as _; -use re_ui::UiExt as _; +use re_ui::{UiExt as _, UiLayout}; use re_viewer_context::{StoreViewContext, TimeControlCommand, ViewId}; use crate::expanded_rows::{ExpandedRows, ExpandedRowsCache}; @@ -36,7 +36,7 @@ pub(crate) enum HideColumnAction { pub(crate) fn dataframe_ui( ctx: &StoreViewContext<'_>, ui: &mut egui::Ui, - query_handle: &re_dataframe::QueryHandle, + query_handle: &mut re_dataframe::QueryHandle, expanded_rows_cache: &mut ExpandedRowsCache, view_id: &ViewId, time_cursor_row: Option, @@ -163,20 +163,17 @@ impl RowsDisplayData { row_indices: &Range, row_data: Vec>, selected_columns: &[ColumnDescriptor], - query_timeline: &TimelineName, + query_timeline: Option<&TimelineName>, ) -> Result { - let display_record_batches = row_data + let display_record_batches: Vec<_> = row_data .into_iter() .map(|data| { DisplayRecordBatch::try_new( - selected_columns - .iter() - .map(|desc| desc.into()) - .zip(data) + std::iter::zip(selected_columns.iter().map(|desc| desc.into()), data) .map(|(desc, data)| (desc, ColumnBlueprint::default_ref(), data)), ) }) - .collect::, _>>()?; + .try_collect()?; let mut batch_ref_from_row = BTreeMap::new(); let mut offset = row_indices.start; @@ -193,7 +190,8 @@ impl RowsDisplayData { .iter() .find_position(|desc| { if let ColumnDescriptor::Time(time_column_desc) = desc { - time_column_desc.timeline_name() == *query_timeline + query_timeline + .is_some_and(|timeline| time_column_desc.timeline_name() == *timeline) } else { false } @@ -212,7 +210,7 @@ impl RowsDisplayData { struct DataframeTableDelegate<'a> { ctx: &'a StoreViewContext<'a>, table_style: re_ui::TableStyle, - query_handle: &'a QueryHandle, + query_handle: &'a mut QueryHandle, selected_columns: &'a [ColumnDescriptor], header_entity_paths: Vec>, display_data: anyhow::Result, @@ -235,12 +233,7 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { fn prepare(&mut self, info: &egui_table::PrefetchInfo) { re_tracing::profile_function!(); - // TODO(ab): actual static-only support - let filtered_index = self - .query_handle - .query() - .filtered_index - .unwrap_or_else(|| TimelineName::new("")); + let filtered_index = self.query_handle.query().filtered_index; self.query_handle .seek_to_row(info.visible_rows.start as usize); @@ -252,7 +245,7 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { &info.visible_rows, data, self.selected_columns, - &filtered_index, + filtered_index.as_ref(), ); self.display_data = data.context("Failed to create display data"); @@ -311,22 +304,16 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { && column.archetype_name().is_some() }); - // TODO(ab): actual static-only support - let filtered_index = self - .query_handle - .query() - .filtered_index - .unwrap_or_else(|| TimelineName::new("")); + let filtered_index = self.query_handle.query().filtered_index; // if this column can actually be hidden, then that's the corresponding action let hide_action = match column { ColumnDescriptor::RowId(_) => Some(HideColumnAction::RowId), - ColumnDescriptor::Time(desc) => { - (desc.timeline_name() != filtered_index).then(|| HideColumnAction::Time { + ColumnDescriptor::Time(desc) => (Some(desc.timeline_name()) != filtered_index) + .then(|| HideColumnAction::Time { timeline_name: desc.timeline_name(), - }) - } + }), ColumnDescriptor::Component(desc) => Some(HideColumnAction::Component { entity_path: desc.entity_path.clone(), @@ -468,15 +455,10 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { }) .unwrap_or(TimeInt::MAX); - // TODO(ab): actual static-only support - let filtered_index = self - .query_handle - .query() - .filtered_index - .unwrap_or_else(|| TimelineName::new("")); - let mut time_ctrl_at_time = self.ctx.time_ctrl.clone(); - time_ctrl_at_time.set_time_cursor_ad_hoc(filtered_index, timestamp.into()); + if let Some(filtered_index) = self.query_handle.query().filtered_index { + time_ctrl_at_time.set_time_cursor_ad_hoc(filtered_index, timestamp.into()); + } let ctx_at_time = self.ctx.with_time_ctrl(&time_ctrl_at_time); ui.set_truncate_style(); @@ -489,7 +471,10 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { // Iterate over the top row (the summary, thus the `None`), and all additional rows. // Note: we must iterate over all rows regardless of the actual number of instances so that // the zebra stripes are properly drawn. - let instance_indices = std::iter::once(None).chain((0..additional_lines).map(Option::Some)); + let instance_indices = std::iter::chain( + std::iter::once(None), + (0..additional_lines).map(Option::Some), + ); { re_tracing::profile_scope!("rows"); @@ -509,7 +494,13 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { // This is called when data actually needs to be drawn (as opposed to summaries like // "N instances" or "N more…"). let data_content = |ui: &mut egui::Ui| { - column.data_ui(&ctx_at_time, ui, batch_row_idx, instance_index); + column.data_ui( + &ctx_at_time, + ui, + batch_row_idx, + instance_index, + UiLayout::List, + ); }; // Draw the cell content with some margin. diff --git a/crates/viewer/re_view_dataframe/src/expanded_rows.rs b/crates/viewer/re_view_dataframe/src/expanded_rows.rs index c9ce3dde6d89..a99e23dc7714 100644 --- a/crates/viewer/re_view_dataframe/src/expanded_rows.rs +++ b/crates/viewer/re_view_dataframe/src/expanded_rows.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; /// /// Note: each view should store its own cache. Using a [`re_viewer_context::ViewState`] is a /// good way to do this. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, re_byte_size::SizeBytes)] pub(crate) struct ExpandedRowsCache { /// Maps "table row number" to "additional lines". /// diff --git a/crates/viewer/re_view_dataframe/src/view_class.rs b/crates/viewer/re_view_dataframe/src/view_class.rs index eff7fab23d00..e8dd0c09a6e8 100644 --- a/crates/viewer/re_view_dataframe/src/view_class.rs +++ b/crates/viewer/re_view_dataframe/src/view_class.rs @@ -16,7 +16,7 @@ use crate::expanded_rows::ExpandedRowsCache; use crate::view_query; use crate::visualizer_system::EmptySystem; -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] struct DataframeViewState { /// Cache for the expanded rows. expanded_rows_cache: ExpandedRowsCache, @@ -36,6 +36,10 @@ impl ViewState for DataframeViewState { fn as_any_mut(&mut self) -> &mut dyn Any { self } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } } #[derive(Default)] @@ -43,7 +47,10 @@ pub struct DataframeView; impl ViewClass for DataframeView { fn identifier() -> ViewClassIdentifier { - "Dataframe".into() + re_viewer_context::external::re_string_interner::intern_static_nonempty!( + ViewClassIdentifier, + "Dataframe" + ) } fn recommendation_order(&self) -> i32 { @@ -175,7 +182,7 @@ Configure in the selection panel: let (view_columns, selection) = view_query.apply_column_selection(ctx, &view_columns)?; dataframe_query.selection = Some(selection); - let query_handle = query_engine.query(dataframe_query); + let mut query_handle = query_engine.query(dataframe_query); // Time cursor row — always computed when timelines match (for the visual indicator) let timelines_match = timeline.name() == ctx.time_ctrl.timeline_name(); @@ -204,7 +211,7 @@ Configure in the selection panel: let hide_column_actions = dataframe_ui( &ctx.active_recording_store_view_context(), ui, - &query_handle, + &mut query_handle, &mut state.expanded_rows_cache, &query.view_id, time_cursor_row, diff --git a/crates/viewer/re_view_dataframe/src/view_query/blueprint.rs b/crates/viewer/re_view_dataframe/src/view_query/blueprint.rs index ba9dd779c6a9..6689ddce1aeb 100644 --- a/crates/viewer/re_view_dataframe/src/view_query/blueprint.rs +++ b/crates/viewer/re_view_dataframe/src/view_query/blueprint.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use re_chunk_store::ColumnDescriptor; +use re_log::ResultExt as _; use re_log_types::{AbsoluteTimeRange, EntityPath, Timeline, TimelineName}; use re_sdk_types::blueprint::archetypes::DataframeQuery; use re_sdk_types::blueprint::{components, datatypes}; @@ -25,9 +26,12 @@ impl Query { DataframeQuery::descriptor_timeline().component, )?; - // if the timeline is unset, we "freeze" it to the current time panel timeline - if let Some(timeline_name) = timeline_name { - Ok(timeline_name.into()) + // if the timeline is unset (or invalid), we "freeze" it to the current time panel timeline + if let Some(timeline_name) = timeline_name + .as_ref() + .and_then(|timeline_name| timeline_name.try_into().ok_or_log_error_once()) + { + Ok(timeline_name) } else { let timeline_name = *ctx.time_ctrl.timeline_name(); self.save_timeline_name(ctx, &timeline_name); @@ -245,7 +249,9 @@ impl Query { let selected_time_columns: HashSet = time_columns .iter() - .map(|timeline_name| timeline_name.as_str().into()) + .filter_map(|timeline_name| { + TimelineName::try_new(timeline_name.as_str()).ok_or_log_error_once() + }) .collect(); let selected_component_columns = component_columns .iter() @@ -435,7 +441,7 @@ mod test { use super::{Query, reorder_columns_by_entity}; - fn make_component_column(entity: &str, component: &str) -> ColumnDescriptor { + fn make_component_column(entity: &str, component: &'static str) -> ColumnDescriptor { ColumnDescriptor::Component(ComponentColumnDescriptor { entity_path: entity.into(), component: ComponentIdentifier::from(component), diff --git a/crates/viewer/re_view_dataframe/src/view_query/mod.rs b/crates/viewer/re_view_dataframe/src/view_query/mod.rs index d1a304a352d2..8f64a60cdbfe 100644 --- a/crates/viewer/re_view_dataframe/src/view_query/mod.rs +++ b/crates/viewer/re_view_dataframe/src/view_query/mod.rs @@ -17,10 +17,8 @@ impl Query { /// See the `blueprint_io` module for more related accessors. pub fn from_blueprint(ctx: &ViewerContext<'_>, view_id: ViewId) -> Self { Self { - query_property: ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view_id, + query_property: ViewProperty::from_archetype_for_view::( + ctx, view_id, ), } } diff --git a/crates/viewer/re_view_dataframe/src/view_query/ui.rs b/crates/viewer/re_view_dataframe/src/view_query/ui.rs index cb0e89f22588..f9d44c9632a7 100644 --- a/crates/viewer/re_view_dataframe/src/view_query/ui.rs +++ b/crates/viewer/re_view_dataframe/src/view_query/ui.rs @@ -10,12 +10,15 @@ use re_sdk_types::blueprint::components; use re_sorbet::ColumnSelector; use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{TimeDragValue, UiExt as _, icons, list_item}; -use re_viewer_context::{TimeControlCommand, ViewId, ViewSystemExecutionError, ViewerContext}; +use re_viewer_context::{ + TimeControlCommand, TimeRangeHighlight, TimeRangeHighlightKind, ViewId, + ViewSystemExecutionError, ViewerContext, +}; use crate::view_query::Query; /// A group of component columns belonging to the same entity path, used for drag-and-drop reordering. -#[derive(Hash)] +#[derive(Hash, Debug)] struct EntityGroup { entity_path: EntityPath, columns: Vec, @@ -161,9 +164,12 @@ impl Query { if should_display_time_range && timeline.is_some_and(|t| t.name() == ctx.time_ctrl.timeline_name()) { - ctx.send_time_commands([TimeControlCommand::HighlightRange(AbsoluteTimeRange::new( - start, end, - ))]); + ctx.send_time_commands([TimeControlCommand::HighlightRange(TimeRangeHighlight { + range: AbsoluteTimeRange::new(start, end), + timeline: *ctx.time_ctrl.timeline_name(), + kind: TimeRangeHighlightKind::TimeRangeConfiguration, + color: None, + })]); } Ok(()) @@ -258,8 +264,7 @@ impl Query { all_components .iter() .copied() - .any(|component| component.as_str() == component_sel.component) - .then_some(component_sel.component.into()) + .find(|component| component.as_str() == component_sel.component) }) .or_else(|| all_components.iter().next().copied()); @@ -609,7 +614,7 @@ fn all_pov_entities_for_view( let comp_for_entity = ctx .recording_engine() .store() - .all_components_on_timeline(timeline, &node.data_result.entity_path); + .all_components_on_timeline(Some(timeline), &node.data_result.entity_path); if comp_for_entity.is_some_and(|components| !components.is_empty()) { all_entities.insert(node.data_result.entity_path.clone()); } diff --git a/crates/viewer/re_view_dataframe/src/visualizer_system.rs b/crates/viewer/re_view_dataframe/src/visualizer_system.rs index 74dc5a7b58ad..c77ab11745c7 100644 --- a/crates/viewer/re_view_dataframe/src/visualizer_system.rs +++ b/crates/viewer/re_view_dataframe/src/visualizer_system.rs @@ -9,7 +9,10 @@ pub struct EmptySystem {} impl IdentifiedViewSystem for EmptySystem { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Empty".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Empty" + ) } } diff --git a/crates/viewer/re_view_dataframe/tests/snapshots/help_view_dataframe_view_mac.png b/crates/viewer/re_view_dataframe/tests/snapshots/help_view_dataframe_view_mac.png index 8603a11e6db1..a86ea94feaa8 100644 --- a/crates/viewer/re_view_dataframe/tests/snapshots/help_view_dataframe_view_mac.png +++ b/crates/viewer/re_view_dataframe/tests/snapshots/help_view_dataframe_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8600884077d2e85f0f68a83214af0e8a5f5ef676a5987efcdfc65926ea1456bd -size 22328 +oid sha256:506d47bd4b8796c2ebd88ee32cd0e02797270960a9e92fb3e6b636cf548fcff1 +size 22227 diff --git a/crates/viewer/re_view_dataframe/tests/snapshots/help_view_dataframe_view_windows.png b/crates/viewer/re_view_dataframe/tests/snapshots/help_view_dataframe_view_windows.png index 8603a11e6db1..a86ea94feaa8 100644 --- a/crates/viewer/re_view_dataframe/tests/snapshots/help_view_dataframe_view_windows.png +++ b/crates/viewer/re_view_dataframe/tests/snapshots/help_view_dataframe_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8600884077d2e85f0f68a83214af0e8a5f5ef676a5987efcdfc65926ea1456bd -size 22328 +oid sha256:506d47bd4b8796c2ebd88ee32cd0e02797270960a9e92fb3e6b636cf548fcff1 +size 22227 diff --git a/crates/viewer/re_view_dataframe/tests/snapshots/null_timeline.png b/crates/viewer/re_view_dataframe/tests/snapshots/null_timeline.png index 880e16e79ba1..87052a3acd07 100644 --- a/crates/viewer/re_view_dataframe/tests/snapshots/null_timeline.png +++ b/crates/viewer/re_view_dataframe/tests/snapshots/null_timeline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1848973c0f94d93df4fe346a18ff39624c812200fd9924fefa3ee4ac83a5523 -size 10815 +oid sha256:f3d4f0a96a453439b660acdc98ca4a36b04917aebf7ce8c3e89342aa5808fac8 +size 10768 diff --git a/crates/viewer/re_view_dataframe/tests/snapshots/unknown_timeline_selection_panel_ui.png b/crates/viewer/re_view_dataframe/tests/snapshots/unknown_timeline_selection_panel_ui.png index ea5aa5b84d37..6acce46bd250 100644 --- a/crates/viewer/re_view_dataframe/tests/snapshots/unknown_timeline_selection_panel_ui.png +++ b/crates/viewer/re_view_dataframe/tests/snapshots/unknown_timeline_selection_panel_ui.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03f3b34a9849bb1b137dcb9c65ebf1aacb35be9fe0d53eb0653af271535a2341 -size 29617 +oid sha256:ee2f2c086e6b43ac906013ad38eac8d87e10207b1dc8a5a8780092b837d4f2da +size 30151 diff --git a/crates/viewer/re_view_dataframe/tests/snapshots/unknown_timeline_view_ui.png b/crates/viewer/re_view_dataframe/tests/snapshots/unknown_timeline_view_ui.png index 5a0b04e3cd98..acbe8959ef94 100644 --- a/crates/viewer/re_view_dataframe/tests/snapshots/unknown_timeline_view_ui.png +++ b/crates/viewer/re_view_dataframe/tests/snapshots/unknown_timeline_view_ui.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f55cfdb251909203625e0f10139dc7f999db824f6dac7310b8ed6fbc5da30ab -size 21730 +oid sha256:bf27562249b4ca507ab57b4cf0b26e7b378b90ff667818d203b03596523399bb +size 21957 diff --git a/crates/viewer/re_view_graph/Cargo.toml b/crates/viewer/re_view_graph/Cargo.toml index b0f80ed86fce..9e4698c5b85a 100644 --- a/crates/viewer/re_view_graph/Cargo.toml +++ b/crates/viewer/re_view_graph/Cargo.toml @@ -16,6 +16,7 @@ include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] workspace = true [dependencies] +re_byte_size = { workspace = true, features = ["egui"] } re_data_ui.workspace = true re_chunk.workspace = true re_entity_db.workspace = true diff --git a/crates/viewer/re_view_graph/src/graph/hash.rs b/crates/viewer/re_view_graph/src/graph/hash.rs index eb39b432c651..0aedbffcbd90 100644 --- a/crates/viewer/re_view_graph/src/graph/hash.rs +++ b/crates/viewer/re_view_graph/src/graph/hash.rs @@ -2,7 +2,7 @@ use re_log_types::hash::Hash64; use re_sdk_types::components; /// A 64 bit hash of [`components::GraphNode`] with very small risk of collision. -#[derive(Copy, Clone, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Eq, PartialOrd, Ord, re_byte_size::SizeBytes)] pub struct GraphNodeHash(Hash64); impl nohash_hasher::IsEnabled for GraphNodeHash {} diff --git a/crates/viewer/re_view_graph/src/graph/ids.rs b/crates/viewer/re_view_graph/src/graph/ids.rs index 1b40dd9cba6e..6a4317d36de4 100644 --- a/crates/viewer/re_view_graph/src/graph/ids.rs +++ b/crates/viewer/re_view_graph/src/graph/ids.rs @@ -3,7 +3,7 @@ use re_sdk_types::components; use super::GraphNodeHash; -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, re_byte_size::SizeBytes)] pub struct NodeId { pub entity_hash: EntityPathHash, pub node_hash: GraphNodeHash, @@ -35,7 +35,7 @@ impl std::fmt::Debug for NodeId { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, re_byte_size::SizeBytes)] pub struct EdgeId { // TODO(grtlr): Consider something more storage efficient here pub source: NodeId, diff --git a/crates/viewer/re_view_graph/src/layout/geometry.rs b/crates/viewer/re_view_graph/src/layout/geometry.rs index 52498c900049..2cbbd1223922 100644 --- a/crates/viewer/re_view_graph/src/layout/geometry.rs +++ b/crates/viewer/re_view_graph/src/layout/geometry.rs @@ -2,7 +2,7 @@ use egui::{Pos2, Rect, Vec2}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, re_byte_size::SizeBytes)] pub enum PathGeometry { /// A simple straight edge. Line { source: Pos2, target: Pos2 }, @@ -18,7 +18,7 @@ pub enum PathGeometry { // We could add other geometries, such as `Orthogonal` here too. } -#[derive(Debug)] +#[derive(Debug, re_byte_size::SizeBytes)] pub struct EdgeGeometry { pub target_arrow: bool, pub path: PathGeometry, diff --git a/crates/viewer/re_view_graph/src/layout/params.rs b/crates/viewer/re_view_graph/src/layout/params.rs index d46fe7974120..1581bb7f1a89 100644 --- a/crates/viewer/re_view_graph/src/layout/params.rs +++ b/crates/viewer/re_view_graph/src/layout/params.rs @@ -8,7 +8,7 @@ use re_sdk_types::{Archetype, Component}; use re_viewer_context::ViewContext; use re_viewport_blueprint::{ViewProperty, ViewPropertyQueryError}; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, re_byte_size::SizeBytes)] pub struct ForceLayoutParams { // Link pub(super) force_link_enabled: Enabled, @@ -39,11 +39,7 @@ struct QueryArchetype<'a, T> { impl<'a, T: Archetype> QueryArchetype<'a, T> { fn new(ctx: &'a ViewContext<'a>) -> Self { - let property = ViewProperty::from_archetype::( - ctx.viewer_ctx.blueprint_db(), - ctx.blueprint_query(), - ctx.view_id, - ); + let property = ViewProperty::from_archetype::(ctx); Self { ctx, property, diff --git a/crates/viewer/re_view_graph/src/layout/provider.rs b/crates/viewer/re_view_graph/src/layout/provider.rs index 61d50419eddf..cdf62ec0f537 100644 --- a/crates/viewer/re_view_graph/src/layout/provider.rs +++ b/crates/viewer/re_view_graph/src/layout/provider.rs @@ -95,8 +95,11 @@ pub fn update_simulation( simulation } +#[derive(re_byte_size::SizeBytes)] pub struct ForceLayoutProvider { // If all nodes are fixed, we can skip the simulation. + // `fjadra::Simulation` keeps its internals private; count layout inputs we own. + #[size_bytes(ignore)] simulation: Option, pub request: LayoutRequest, } diff --git a/crates/viewer/re_view_graph/src/layout/request.rs b/crates/viewer/re_view_graph/src/layout/request.rs index 1b5973ce33c0..55ecdcf021ee 100644 --- a/crates/viewer/re_view_graph/src/layout/request.rs +++ b/crates/viewer/re_view_graph/src/layout/request.rs @@ -13,20 +13,20 @@ use re_chunk::EntityPath; use crate::graph::{EdgeId, Graph, NodeId}; -#[derive(PartialEq)] +#[derive(PartialEq, re_byte_size::SizeBytes)] pub(super) struct NodeTemplate { pub(super) size: Vec2, pub(super) fixed_position: Option, } -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, re_byte_size::SizeBytes)] pub struct EdgeTemplate { pub source: NodeId, pub target: NodeId, pub target_arrow: bool, } -#[derive(Default, PartialEq)] +#[derive(Default, PartialEq, re_byte_size::SizeBytes)] pub(super) struct GraphTemplate { pub(super) nodes: BTreeMap, @@ -39,7 +39,7 @@ pub(super) struct GraphTemplate { /// A [`LayoutRequest`] encapsulates all the information that is considered when computing a layout. /// /// It implements [`PartialEq`] to check if a layout is up-to-date, or if it needs to be recomputed. -#[derive(PartialEq)] +#[derive(PartialEq, re_byte_size::SizeBytes)] pub struct LayoutRequest { pub(super) graphs: BTreeMap, } diff --git a/crates/viewer/re_view_graph/src/layout/result.rs b/crates/viewer/re_view_graph/src/layout/result.rs index 862f33da221e..fa8a2e7ecfbe 100644 --- a/crates/viewer/re_view_graph/src/layout/result.rs +++ b/crates/viewer/re_view_graph/src/layout/result.rs @@ -6,7 +6,7 @@ use re_chunk::EntityPath; use super::EdgeGeometry; use crate::graph::{EdgeId, NodeId}; -#[derive(Debug)] +#[derive(Debug, re_byte_size::SizeBytes)] pub struct Layout { pub(super) nodes: ahash::HashMap, pub(super) edges: ahash::HashMap>, diff --git a/crates/viewer/re_view_graph/src/ui/selection.rs b/crates/viewer/re_view_graph/src/ui/selection.rs index 4b0f81eb2aaa..7199fbc1596b 100644 --- a/crates/viewer/re_view_graph/src/ui/selection.rs +++ b/crates/viewer/re_view_graph/src/ui/selection.rs @@ -12,8 +12,7 @@ pub fn view_property_force_ui( ctx: &ViewContext<'_>, ui: &mut egui::Ui, ) { - let property = - ViewProperty::from_archetype::(ctx.blueprint_db(), ctx.blueprint_query(), ctx.view_id); + let property = ViewProperty::from_archetype::(ctx); let reflection = ctx.viewer_ctx.reflection(); let Some(reflection) = reflection.archetypes.get(&property.archetype_name) else { diff --git a/crates/viewer/re_view_graph/src/ui/state.rs b/crates/viewer/re_view_graph/src/ui/state.rs index b1ec4d14d2ea..2dcf12b3410d 100644 --- a/crates/viewer/re_view_graph/src/ui/state.rs +++ b/crates/viewer/re_view_graph/src/ui/state.rs @@ -9,7 +9,7 @@ use crate::layout::{ForceLayoutParams, ForceLayoutProvider, Layout, LayoutReques /// View state for the custom view. /// /// This state is preserved between frames, but not across Viewer sessions. -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] pub struct GraphViewState { pub layout_state: LayoutState, pub visual_bounds: Option, @@ -25,8 +25,8 @@ impl GraphViewState { ui.vertical(|ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); let egui::Rect { min, max } = rect; - ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x),)); - ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y),)); + ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x))); + ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y))); }); ui.end_row(); } @@ -46,12 +46,16 @@ impl ViewState for GraphViewState { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } } /// The following is a simple state machine that keeps track of the different /// layouts and if they need to be recomputed. It also holds the state of the /// force-based simulation. -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] pub enum LayoutState { #[default] None, diff --git a/crates/viewer/re_view_graph/src/view.rs b/crates/viewer/re_view_graph/src/view.rs index 4b026e505d28..c03b18b5c2c3 100644 --- a/crates/viewer/re_view_graph/src/view.rs +++ b/crates/viewer/re_view_graph/src/view.rs @@ -28,7 +28,10 @@ impl ViewClass for GraphView { // State type as described above. fn identifier() -> ViewClassIdentifier { - "Graph".into() + re_viewer_context::external::re_string_interner::intern_static_nonempty!( + ViewClassIdentifier, + "Graph" + ) } fn display_name(&self) -> &'static str { @@ -194,20 +197,16 @@ impl ViewClass for GraphView { ) -> Result<(), ViewSystemExecutionError> { re_tracing::profile_function!(); - let empty_node_data = ahash::HashMap::default(); - let empty_edge_data = ahash::HashMap::default(); let node_data = system_output - .visualizer_data::>( + .visualizer_data_or_default::>( NodeVisualizer::identifier(), - ) - .unwrap_or(&empty_node_data); + )?; let edge_data = system_output - .visualizer_data::>( + .visualizer_data_or_default::>( EdgesVisualizer::identifier(), - ) - .unwrap_or(&empty_edge_data); + )?; - let graphs = merge(node_data, edge_data) + let graphs = merge(&node_data, &edge_data) .map(|(ent, nodes, edges)| Graph::new(ui, ent.clone(), nodes, edges)) .collect::>(); @@ -216,21 +215,13 @@ impl ViewClass for GraphView { let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); let params = ForceLayoutParams::get(&view_ctx)?; - let background = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let background = ViewProperty::from_archetype::(&view_ctx); let background_color = background.component_or_fallback::( &view_ctx, GraphBackground::descriptor_color().component, )?; - let bounds_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let bounds_property = ViewProperty::from_archetype::(&view_ctx); let rect_in_scene: blueprint::components::VisualBounds2D = bounds_property .component_or_fallback(&view_ctx, VisualBounds2D::descriptor_range().component)?; diff --git a/crates/viewer/re_view_graph/src/visualizers/edges.rs b/crates/viewer/re_view_graph/src/visualizers/edges.rs index 33c52e24b126..d83794047048 100644 --- a/crates/viewer/re_view_graph/src/visualizers/edges.rs +++ b/crates/viewer/re_view_graph/src/visualizers/edges.rs @@ -15,6 +15,7 @@ use crate::graph::NodeId; #[derive(Default)] pub struct EdgesVisualizer; +#[derive(Clone)] pub struct EdgeInstance { // We will need this in the future, when we want to select individual edges. pub instance: Instance, @@ -24,6 +25,7 @@ pub struct EdgeInstance { pub target_index: NodeId, } +#[derive(Clone)] pub struct EdgeData { pub graph_type: components::GraphType, pub edges: Vec, @@ -31,7 +33,10 @@ pub struct EdgeData { impl IdentifiedViewSystem for EdgesVisualizer { fn identifier() -> ViewSystemIdentifier { - "GraphEdges".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "GraphEdges" + ) } } diff --git a/crates/viewer/re_view_graph/src/visualizers/mod.rs b/crates/viewer/re_view_graph/src/visualizers/mod.rs index 04d74e36bdf9..c327ff298eee 100644 --- a/crates/viewer/re_view_graph/src/visualizers/mod.rs +++ b/crates/viewer/re_view_graph/src/visualizers/mod.rs @@ -13,10 +13,8 @@ pub fn merge<'a>( edge_data: &'a ahash::HashMap, ) -> impl Iterator, Option<&'a EdgeData>)> + 'a { // We sort the entities to ensure that we always process them in the same order. - let unique_entities = node_data - .keys() - .chain(edge_data.keys()) - .collect::>(); + let unique_entities = + std::iter::chain(node_data.keys(), edge_data.keys()).collect::>(); unique_entities.into_iter().map(|entity| { let nodes = node_data.get(entity); diff --git a/crates/viewer/re_view_graph/src/visualizers/nodes.rs b/crates/viewer/re_view_graph/src/visualizers/nodes.rs index 6baa22af458d..e0c213a6bad7 100644 --- a/crates/viewer/re_view_graph/src/visualizers/nodes.rs +++ b/crates/viewer/re_view_graph/src/visualizers/nodes.rs @@ -45,6 +45,7 @@ pub struct NodeInstance { pub label: Label, } +#[derive(Clone)] pub struct NodeData { pub visualizer_instruction_id: VisualizerInstructionId, pub nodes: Vec, @@ -52,7 +53,10 @@ pub struct NodeData { impl IdentifiedViewSystem for NodeVisualizer { fn identifier() -> ViewSystemIdentifier { - "GraphNodes".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "GraphNodes" + ) } } diff --git a/crates/viewer/re_view_graph/tests/snapshots/coincident_nodes.png b/crates/viewer/re_view_graph/tests/snapshots/coincident_nodes.png index 7deef239c2fe..0e6b5bc46587 100644 --- a/crates/viewer/re_view_graph/tests/snapshots/coincident_nodes.png +++ b/crates/viewer/re_view_graph/tests/snapshots/coincident_nodes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93b0ee6de7179e4138beb48d1bc84357ed2aed6523e53b347c56042831eaa5af -size 821 +oid sha256:2042665338ede900bbd3f69a58ba92af3a31fa233431a62f802ebd03c7671b01 +size 825 diff --git a/crates/viewer/re_view_graph/tests/snapshots/help_view_graph_view_mac.png b/crates/viewer/re_view_graph/tests/snapshots/help_view_graph_view_mac.png index e5c5ffd46e6b..2359ee293251 100644 --- a/crates/viewer/re_view_graph/tests/snapshots/help_view_graph_view_mac.png +++ b/crates/viewer/re_view_graph/tests/snapshots/help_view_graph_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fd27804fb0a3108d2a6266812994591f93a4f67b37eed8c59d1717c55bae01e -size 8466 +oid sha256:7e5b76f76a9b1e94705892391d67a64fa378a0d42a8182603b107042d5babf62 +size 8444 diff --git a/crates/viewer/re_view_graph/tests/snapshots/help_view_graph_view_windows.png b/crates/viewer/re_view_graph/tests/snapshots/help_view_graph_view_windows.png index 4ac086f204bb..e17f54e16a79 100644 --- a/crates/viewer/re_view_graph/tests/snapshots/help_view_graph_view_windows.png +++ b/crates/viewer/re_view_graph/tests/snapshots/help_view_graph_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45a6c1c6743174380d6a2c6e0599a09cbd2a4bc46da87c2a9bf27196131ebd7c -size 8506 +oid sha256:21657043ff9da403387c3cf912bbd444e2a22870b0a981152d47fe03abdc3b21 +size 8488 diff --git a/crates/viewer/re_view_graph/tests/snapshots/multi_graphs.png b/crates/viewer/re_view_graph/tests/snapshots/multi_graphs.png index 4b332d903073..1d2cc33ee12c 100644 --- a/crates/viewer/re_view_graph/tests/snapshots/multi_graphs.png +++ b/crates/viewer/re_view_graph/tests/snapshots/multi_graphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4d0bcdee4b5f8a4f52c31e3ffe1555bcd9eaa8c6169c671eab0f2fcd84b8c02 -size 6751 +oid sha256:0214f136d3e6931149207e8461fe87694481a07ed322317af19afec9a7ab3cd9 +size 6792 diff --git a/crates/viewer/re_view_graph/tests/snapshots/self_and_multi_edges.png b/crates/viewer/re_view_graph/tests/snapshots/self_and_multi_edges.png index dd4b10190fb7..106491415565 100644 --- a/crates/viewer/re_view_graph/tests/snapshots/self_and_multi_edges.png +++ b/crates/viewer/re_view_graph/tests/snapshots/self_and_multi_edges.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71dd8f1515d2bd26ae263245e0ea0afeb8afbc364cc652d30f9750ffdf75b16c -size 19696 +oid sha256:69ce9fabb2ea47958d2f276a83f4e08735169e0f4773baa1351c49049129865a +size 19856 diff --git a/crates/viewer/re_view_map/Cargo.toml b/crates/viewer/re_view_map/Cargo.toml index f0e6663562ec..40b1c815e2ed 100644 --- a/crates/viewer/re_view_map/Cargo.toml +++ b/crates/viewer/re_view_map/Cargo.toml @@ -19,6 +19,7 @@ workspace = true all-features = true [dependencies] +re_byte_size.workspace = true re_data_ui.workspace = true re_entity_db.workspace = true re_log.workspace = true diff --git a/crates/viewer/re_view_map/src/map_view.rs b/crates/viewer/re_view_map/src/map_view.rs index 9f4c178491a3..f136ab250e30 100644 --- a/crates/viewer/re_view_map/src/map_view.rs +++ b/crates/viewer/re_view_map/src/map_view.rs @@ -1,3 +1,5 @@ +use std::mem::size_of; + use egui::{Modifiers, NumExt as _, Rect, Response}; use re_data_ui::{DataUi as _, item_ui}; use re_entity_db::InstancePathHash; @@ -52,6 +54,36 @@ impl Default for MapViewState { } } +impl re_byte_size::SizeBytes for MapViewState { + fn heap_size_bytes(&self) -> u64 { + let Self { + tiles, + map_memory: _, + selected_provider, + last_center_position: _, + last_gpu_picking_result: _, + } = self; + + // `walkers::HttpTiles` keeps its tile cache private, so this is a best-effort estimate + // based on walkers 0.53 internals: an LRU cache with capacity 256 plus the currently queued + // download ids. + // - `TilesIo::new`: + // - `HttpTiles::stats`: + // Texture memory itself is tracked by egui/wgpu. + let tiles = tiles.as_ref().map_or(0, |tiles| { + const WALKERS_TILE_CACHE_CAPACITY: usize = 256; + + let tile_cache = WALKERS_TILE_CACHE_CAPACITY + * (size_of::<(walkers::TileId, Option)>() + 1); + let in_progress = tiles.stats().in_progress * size_of::(); + + (tile_cache + in_progress) as u64 + }); + + tiles + selected_provider.heap_size_bytes() + } +} + impl MapViewState { // This method ensures that tiles is initialized and returns mutable references to tiles and map_memory. pub fn ensure_and_get_mut_refs( @@ -81,6 +113,10 @@ impl ViewState for MapViewState { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } } #[derive(Default)] @@ -196,32 +232,21 @@ impl ViewClass for MapView { system_output: SystemExecutionOutput, ) -> Result<(), ViewSystemExecutionError> { let state = state.downcast_mut::()?; - let map_background = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); - - let map_zoom = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); + let map_background = ViewProperty::from_archetype::(&view_ctx); + let map_zoom = ViewProperty::from_archetype::(&view_ctx); - let empty_geo_points = GeoPointsOutput::default(); - let empty_geo_line_strings = GeoLineStringsOutput::default(); let geo_points_visualizer = system_output - .visualizer_data::(GeoPointsVisualizer::identifier()) - .unwrap_or(&empty_geo_points); + .visualizer_data_or_default::(GeoPointsVisualizer::identifier())?; let geo_line_strings_visualizers = system_output - .visualizer_data::(GeoLineStringsVisualizer::identifier()) - .unwrap_or(&empty_geo_line_strings); + .visualizer_data_or_default::( + GeoLineStringsVisualizer::identifier(), + )?; // // Map Provider // - let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); let map_provider = map_background .component_or_fallback(&view_ctx, MapBackground::descriptor_provider().component)?; if state.selected_provider != map_provider { @@ -321,8 +346,14 @@ impl ViewClass for MapView { map_rect, ); - let mut view_builder = - create_view_builder(ctx, ui.ctx(), map_rect, &query.highlights, picking_config)?; + let mut view_builder = create_view_builder( + ctx, + ui.ctx(), + query.view_id.render_view_id(), + map_rect, + &query.highlights, + picking_config, + )?; geo_line_strings_visualizers.queue_draw_data( ctx.render_ctx(), @@ -360,6 +391,7 @@ impl ViewClass for MapView { fn create_view_builder( ctx: &ViewerContext<'_>, egui_ctx: &egui::Context, + view_id: re_renderer::ViewBuilderId, view_rect: Rect, highlights: &ViewHighlights, picking_config: Option, @@ -399,6 +431,7 @@ fn create_view_builder( picking_config, }, + view_id, ) } diff --git a/crates/viewer/re_view_map/src/visualizers/geo_line_strings.rs b/crates/viewer/re_view_map/src/visualizers/geo_line_strings.rs index ac20d8cbaecf..0f124460c102 100644 --- a/crates/viewer/re_view_map/src/visualizers/geo_line_strings.rs +++ b/crates/viewer/re_view_map/src/visualizers/geo_line_strings.rs @@ -11,7 +11,7 @@ use re_viewer_context::{ typed_fallback_for, }; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] struct GeoLineStringsBatch { lines: Vec>, radii: Vec, @@ -20,7 +20,7 @@ struct GeoLineStringsBatch { } /// Output data from [`GeoLineStringsVisualizer`]. -#[derive(Default)] +#[derive(Default, Clone)] pub struct GeoLineStringsOutput { batches: Vec<(EntityPath, GeoLineStringsBatch)>, } @@ -31,7 +31,10 @@ pub struct GeoLineStringsVisualizer; impl IdentifiedViewSystem for GeoLineStringsVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "GeoLineStrings".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "GeoLineStrings" + ) } } @@ -103,8 +106,8 @@ impl VisualizerSystem for GeoLineStringsVisualizer { // iterate over all instances for (instance_index, (line, color, radius)) in itertools::izip!( lines, - colors.iter().chain(std::iter::repeat(&last_color)), - radii.iter().chain(std::iter::repeat(&last_radii)), + std::iter::chain(colors, std::iter::repeat(&last_color)), + std::iter::chain(radii, std::iter::repeat(&last_radii)), ) .enumerate() { diff --git a/crates/viewer/re_view_map/src/visualizers/geo_points.rs b/crates/viewer/re_view_map/src/visualizers/geo_points.rs index 5e52246846c0..cba555f371db 100644 --- a/crates/viewer/re_view_map/src/visualizers/geo_points.rs +++ b/crates/viewer/re_view_map/src/visualizers/geo_points.rs @@ -14,7 +14,7 @@ use re_viewer_context::{ typed_fallback_for, }; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct GeoPointBatch { pub positions: Vec, pub radii: Vec, @@ -23,7 +23,7 @@ pub struct GeoPointBatch { } /// Output data from [`GeoPointsVisualizer`]. -#[derive(Default)] +#[derive(Default, Clone)] pub struct GeoPointsOutput { pub batches: Vec<(EntityPath, GeoPointBatch)>, } @@ -34,7 +34,10 @@ pub struct GeoPointsVisualizer; impl IdentifiedViewSystem for GeoPointsVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "GeoPoints".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "GeoPoints" + ) } } @@ -117,8 +120,8 @@ impl VisualizerSystem for GeoPointsVisualizer { // iterate over all instances for (instance_index, (position, color, radius)) in itertools::izip!( positions, - colors.iter(), - radii.iter().chain(std::iter::repeat(&last_radii)), + &colors, + std::iter::chain(radii, std::iter::repeat(&last_radii)), ) .enumerate() { @@ -163,16 +166,14 @@ impl GeoPointsOutput { // so boosting the outline radius would make it erreously large. for (entity_path, batch) in &self.batches { - let (positions, radii): (Vec<_>, Vec<_>) = batch - .positions - .iter() - .zip(&batch.radii) - .map(|(pos, radius)| { - let size = super::radius_to_size(*radius, projector, *pos); - let ui_position = projector.project(*pos); - (glam::vec3(ui_position.x, ui_position.y, 0.0), size) - }) - .unzip(); + let (positions, radii): (Vec<_>, Vec<_>) = + std::iter::zip(&batch.positions, &batch.radii) + .map(|(pos, radius)| { + let size = super::radius_to_size(*radius, projector, *pos); + let ui_position = projector.project(*pos); + (glam::vec3(ui_position.x, ui_position.y, 0.0), size) + }) + .unzip(); let outline = highlight.entity_outline_mask(entity_path.hash()); diff --git a/crates/viewer/re_view_map/tests/snapshots/help_view_map_view_mac.png b/crates/viewer/re_view_map/tests/snapshots/help_view_map_view_mac.png index 4c7dddaee927..8da347526c0a 100644 --- a/crates/viewer/re_view_map/tests/snapshots/help_view_map_view_mac.png +++ b/crates/viewer/re_view_map/tests/snapshots/help_view_map_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edb2c04c80c866ef072dc08eec6c4641ed7e57971a2c1240196041c3e9c5fe38 -size 8219 +oid sha256:79484c979cb65e261427a03aae92307a7deb875d60c4dc0be8db5d4db5825b1e +size 8196 diff --git a/crates/viewer/re_view_map/tests/snapshots/help_view_map_view_windows.png b/crates/viewer/re_view_map/tests/snapshots/help_view_map_view_windows.png index 57e811a87ff3..6fe087814c57 100644 --- a/crates/viewer/re_view_map/tests/snapshots/help_view_map_view_windows.png +++ b/crates/viewer/re_view_map/tests/snapshots/help_view_map_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6504c98826fde1b40367024fc37e8ac8f1b83058c9a8797dcd72f19c96a137e -size 8259 +oid sha256:74f0144fc3098f4545376b1780904d916d536a9f8b56c56d055a08373a3ac68c +size 8240 diff --git a/crates/viewer/re_view_spatial/Cargo.toml b/crates/viewer/re_view_spatial/Cargo.toml index b710004b139f..ee06e2783db9 100644 --- a/crates/viewer/re_view_spatial/Cargo.toml +++ b/crates/viewer/re_view_spatial/Cargo.toml @@ -25,13 +25,13 @@ nasm = ["re_video/nasm"] [dependencies] -re_arrow_util.workspace = true -re_byte_size = { workspace = true, features = ["ecolor", "glam"] } +re_byte_size = { workspace = true, features = ["ecolor", "egui", "glam", "macaw"] } re_chunk_store.workspace = true re_data_ui.workspace = true re_entity_db.workspace = true re_error.workspace = true re_format.workspace = true +re_gamepad.workspace = true re_log.workspace = true re_log_types.workspace = true re_query.workspace = true @@ -66,6 +66,7 @@ itertools.workspace = true macaw = { workspace = true, features = ["serde"] } nohash-hasher.workspace = true ordered-float.workspace = true +parking_lot.workspace = true saturating_cast.workspace = true serde.workspace = true smallvec = { workspace = true, features = ["serde"] } diff --git a/crates/viewer/re_view_spatial/src/caches/mesh_cache.rs b/crates/viewer/re_view_spatial/src/caches/mesh_cache.rs index 77f6916c2379..75b2b0c49e51 100644 --- a/crates/viewer/re_view_spatial/src/caches/mesh_cache.rs +++ b/crates/viewer/re_view_spatial/src/caches/mesh_cache.rs @@ -23,37 +23,21 @@ use crate::mesh_loader::{LoadedMesh, NativeAsset3D, NativeMesh3D}; // // TODO(andreas): Maybe these should be different concerns? // Blobs need costly unpacking/reading/parsing, regular meshes don't. -#[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, re_byte_size::SizeBytes)] pub struct MeshCacheKey { pub versioned_instance_path_hash: VersionedInstancePathHash, pub query_result_hash: Hash64, pub media_type: Option, } -impl re_byte_size::SizeBytes for MeshCacheKey { - fn heap_size_bytes(&self) -> u64 { - let Self { - versioned_instance_path_hash: _, - query_result_hash: _, - media_type, - } = self; - media_type.heap_size_bytes() - } -} - +#[derive(re_byte_size::SizeBytes)] struct MeshEntry { mesh: Option>, last_used_generation: u64, } -impl re_byte_size::SizeBytes for MeshEntry { - fn heap_size_bytes(&self) -> u64 { - self.mesh.heap_size_bytes() - } -} - /// Caches meshes based on their [`MeshCacheKey`]. -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] pub struct MeshCache { cache: HashMap>, generation: u64, @@ -77,7 +61,7 @@ pub enum AnyMesh<'a> { impl MeshCache { pub fn entry( &mut self, - name: &str, + name: &dyn std::fmt::Display, key: MeshCacheKey, mesh: AnyMesh<'_>, render_ctx: &RenderContext, @@ -89,9 +73,10 @@ impl MeshCache { .entry(key) .or_insert_with(|| { re_tracing::profile_scope!("MeshCache-miss"); + let name = name.to_string(); re_log::trace!("Loading CPU mesh {name:?}…"); - let result = LoadedMesh::load(name.to_owned(), mesh, render_ctx); + let result = LoadedMesh::load(name.clone(), mesh, render_ctx); match result { Ok(cpu_mesh) => MeshEntry { @@ -198,16 +183,6 @@ impl Cache for MeshCache { } } -impl re_byte_size::SizeBytes for MeshCache { - fn heap_size_bytes(&self) -> u64 { - let Self { - cache, - generation: _, - } = self; - cache.heap_size_bytes() - } -} - impl re_byte_size::MemUsageTreeCapture for MeshCache { fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree { let mut node = re_byte_size::MemUsageNode::new(); diff --git a/crates/viewer/re_view_spatial/src/contexts/depth_offsets.rs b/crates/viewer/re_view_spatial/src/contexts/depth_offsets.rs index d304c6d3e072..36ec65a58f82 100644 --- a/crates/viewer/re_view_spatial/src/contexts/depth_offsets.rs +++ b/crates/viewer/re_view_spatial/src/contexts/depth_offsets.rs @@ -20,7 +20,10 @@ pub struct EntityDepthOffsets { impl IdentifiedViewSystem for EntityDepthOffsets { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "EntityDepthOffsets".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "EntityDepthOffsets" + ) } } diff --git a/crates/viewer/re_view_spatial/src/contexts/transform_tree_context.rs b/crates/viewer/re_view_spatial/src/contexts/transform_tree_context.rs index 6869215d5634..edd2e9b8fb5e 100644 --- a/crates/viewer/re_view_spatial/src/contexts/transform_tree_context.rs +++ b/crates/viewer/re_view_spatial/src/contexts/transform_tree_context.rs @@ -149,11 +149,17 @@ struct EntityTransformIdMapping { /// /// Does *not* contain any implicit transform frame id. entity_path_to_transform_frame_id: IntMap, + + /// Entities whose logged coordinate frame was empty and therefore fell back to their implicit frame. + empty_coordinate_frames: IntSet, } impl IdentifiedViewSystem for TransformTreeContext { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "TransformContext".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "TransformContext" + ) } } @@ -246,11 +252,8 @@ impl ViewContextSystem for TransformTreeContext { self.target_frame = { re_tracing::profile_scope!("target_frame"); - let spatial_info_prop = ViewProperty::from_archetype::< - blueprint::archetypes::SpatialInformation, - >( - ctx.blueprint_db(), ctx.blueprint_query(), ctx.view_id - ); + let spatial_info_prop = + ViewProperty::from_archetype::(ctx); let target_frame_component = spatial_info_prop .component_or_fallback::( @@ -286,12 +289,17 @@ impl ViewContextSystem for TransformTreeContext { re_tracing::profile_scope!("add-overrides"); // Add overrides to the additional frame id hash map so we can get back the id for errors. for results in frame_id_results { - let Some(frame) = - results.get_mono(archetypes::CoordinateFrame::descriptor_frame().component) - else { + let Some(frame) = results.get_mono::( + archetypes::CoordinateFrame::descriptor_frame().component, + ) else { continue; }; + // Empty coordinate frames resolve to implicit frames, so don't register the empty ID for diagnostics. + if frame.as_str().is_empty() { + continue; + } + let frame_hash = TransformFrameIdHash::new(&frame); // This may be a frame id we've never heard of, so make add them to our internal lookup. @@ -510,6 +518,17 @@ impl TransformTreeContext { .unwrap_or_else(|| TransformFrameIdHash::from_entity_path_hash(entity_path)) } + /// Returns whether the entity's empty coordinate frame was replaced with its implicit frame. + #[inline] + pub fn uses_implicit_frame_for_empty_coordinate_frame( + &self, + entity_path: EntityPathHash, + ) -> bool { + self.entity_transform_id_mapping + .empty_coordinate_frames + .contains(&entity_path) + } + /// Returns all reachable frame for the current root. #[inline] pub fn child_frames_for_entity( @@ -591,6 +610,7 @@ fn lookup_image_plane_distance( .recording_engine() .cache() .latest_at( + re_chunk_store::ChunkTrackingMode::Report, latest_at_query, &data_result.entity_path, [plane_dist_component], @@ -666,34 +686,42 @@ impl EntityTransformIdMapping { let transform_frame_id_component = archetypes::CoordinateFrame::descriptor_frame().component; - let frame_id = results - .get_mono::(transform_frame_id_component) - .map_or_else( - || { - let fallback = - TransformFrameIdHash::from_entity_path(results.entity_path()); - // Make sure this is the same as the fallback provider (which is a lot slower to run) - re_log::debug_assert_eq!( - TransformFrameIdHash::new(&typed_fallback_for::( - results.query_context(), - transform_frame_id_component - )), - fallback - ); - fallback - }, - |frame_id| { - let is_mono = results.get_raw_cell(transform_frame_id_component).is_some_and(|array| array.len() == 1); - if !is_mono { - re_log::warn_once!( - "Entity {:?} has multiple coordinate frame instances, which is not supported. Using the first one.", - results.entity_path(), - ); - } - TransformFrameIdHash::new(&frame_id)}, + let entity_path_hash = results.entity_path().hash(); + // Missing coordinate frames use the implicit frame derived from the entity path. + let fallback = || { + let fallback = TransformFrameIdHash::from_entity_path(results.entity_path()); + // Make sure this is the same as the fallback provider (which is a lot slower to run) + re_log::debug_assert_eq!( + TransformFrameIdHash::new(&typed_fallback_for::( + results.query_context(), + transform_frame_id_component + )), + fallback ); + fallback + }; + let frame_id = match results.get_mono::(transform_frame_id_component) { + None => fallback(), + Some(frame_id) => { + let is_mono = results + .get_raw_cell(transform_frame_id_component) + .is_some_and(|array| array.len() == 1); + if !is_mono { + re_log::warn_once!( + "Entity {:?} has multiple coordinate frame instances, which is not supported. Using the first one.", + results.entity_path(), + ); + } - let entity_path_hash = results.entity_path().hash(); + if frame_id.as_str().is_empty() { + // Treat an empty value like an absent CoordinateFrame, but remember it so visualizers can warn. + self.empty_coordinate_frames.insert(entity_path_hash); + fallback() + } else { + TransformFrameIdHash::new(&frame_id) + } + } + }; match self.transform_frame_id_to_entity_path.entry(frame_id) { std::collections::hash_map::Entry::Vacant(entry) => { @@ -788,9 +816,9 @@ mod tests { let view_id = blueprint.add_view_at_root(ViewBlueprint::new(class_id, RecommendedView::root())); - let property = ViewProperty::from_archetype::< + let property = ViewProperty::from_archetype_for_view::< re_sdk_types::blueprint::archetypes::SpatialInformation, - >(ctx.blueprint_db(), ctx.blueprint_query(), view_id); + >(ctx, view_id); property.save_blueprint_component( ctx, &re_sdk_types::blueprint::archetypes::SpatialInformation::descriptor_target_frame(), @@ -890,9 +918,9 @@ mod tests { let view_id = blueprint.add_view_at_root(ViewBlueprint::new(class_id, RecommendedView::root())); - let property = ViewProperty::from_archetype::< + let property = ViewProperty::from_archetype_for_view::< re_sdk_types::blueprint::archetypes::SpatialInformation, - >(ctx.blueprint_db(), ctx.blueprint_query(), view_id); + >(ctx, view_id); property.save_blueprint_component( ctx, &re_sdk_types::blueprint::archetypes::SpatialInformation::descriptor_target_frame(), diff --git a/crates/viewer/re_view_spatial/src/eye.rs b/crates/viewer/re_view_spatial/src/eye.rs index 4a02f7ba4afc..3a89f4f2a293 100644 --- a/crates/viewer/re_view_spatial/src/eye.rs +++ b/crates/viewer/re_view_spatial/src/eye.rs @@ -20,7 +20,9 @@ use crate::scene_bounding_boxes::SceneBoundingBoxes; /// Note: we prefer the word "eye" to not confuse it with logged cameras. /// /// Our view-space uses RUB (X=Right, Y=Up, Z=Back). -#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive( + Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize, re_byte_size::SizeBytes, +)] pub struct Eye { pub world_from_rub_view: IsoTransform, @@ -152,7 +154,7 @@ impl Eye { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] struct EyeInterpolation { elapsed_time: f32, start: Eye, @@ -183,14 +185,16 @@ impl EyeInterpolation { /// Some non-persistent state for the eye. /// /// Note: we use "eye" so we don't confuse this with logged camera. -#[derive(Default, Clone, Debug, PartialEq)] +#[derive(Default, Clone, Debug, PartialEq, re_byte_size::SizeBytes)] pub struct EyeState { /// Vertical field of view in radians. fov_y: Option, velocity: Vec3, - /// The lasst tracked entity. + gamepad_interaction: Option, + + /// The last tracked entity. /// /// This should not be used to get the current tracked entity, get that /// via view properties instead. @@ -212,6 +216,20 @@ pub struct EyeState { pub last_eye_up: Option, } +#[derive(Clone, Debug, PartialEq, re_byte_size::SizeBytes)] +struct GamepadInteraction { + pos: Vec3, + look_target: Vec3, + eye_up: Vec3, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum GamepadNavigationStatus { + Disconnected, + ConnectedInactive, + Active, +} + /// Utility struct for handling eye control parameter changes, /// e.g. via user input or blueprint. pub(crate) struct EyeController { @@ -232,6 +250,17 @@ impl EyeController { /// Avoids breaking the view by zooming in too far. pub const MIN_ORBIT_DISTANCE: f32 = Eye::PERSPECTIVE_NEAR_PLANE * 2.0; + /// Cap on the orbital camera radius, as a multiple of the scene bounding box diagonal. + /// + /// Only applied when scroll-to-zoom wants to grow the radius further. Zoom-in, rotate, + /// pan, WASD, and every other form of motion are left alone — this is intentionally a + /// local restriction on orbital zoom-out only, not a general movement envelope. The 2D + /// view has its own, separate zoom-out cap (see `ui_2d::MAX_ZOOM_OUT_FACTOR`). + /// + /// If the radius already exceeds this (e.g. right after loading) the current radius is + /// used as the cap instead, so the camera isn't pulled back in. + const MAX_ORBITAL_ZOOM_OUT_FACTOR: f32 = 5.0; + fn get_eye(&self) -> Eye { Eye { world_from_rub_view: IsoTransform::look_at_rh( @@ -397,9 +426,11 @@ impl EyeController { /// Rotate based on a certain number of pixel delta. pub fn rotate(&mut self, delta: egui::Vec2) { let sensitivity = 0.004; // radians-per-point. TODO(emilk): take fov_y and canvas size into account + self.rotate_radians(sensitivity * delta); + } - let delta = sensitivity * delta; - + /// Rotate based on a yaw/pitch delta in radians. + fn rotate_radians(&mut self, delta: egui::Vec2) { let mut rot = self.rotation(); let radius = self.radius(); @@ -482,7 +513,7 @@ impl EyeController { } /// Handle zoom/scroll input. - fn handle_zoom(&mut self, egui_ctx: &egui::Context) { + fn handle_zoom(&mut self, egui_ctx: &egui::Context, scene_bounding_box: &macaw::BoundingBox) { let zoom_factor = egui_ctx.input(|input| { // egui's default horizontal_scroll_modifier is shift, which is also our speed-up modifier. // This means that a user who wants to speed up scroll-to-zoom will generate a horizontal scroll delta. @@ -498,16 +529,18 @@ impl EyeController { match self.kind { Eye3DKind::Orbital => { let radius = self.pos.distance(self.look_target); - let new_radius = (radius / zoom_factor).at_least(Self::MIN_ORBIT_DISTANCE); + + // Cap zoom-out against the scene bounding box. If we're already past the cap + // (e.g. right after loading) use the current radius instead — no snap-back. + let max_radius = max_orbital_radius(scene_bounding_box).max(radius); + let new_radius = (radius / zoom_factor).clamp(Self::MIN_ORBIT_DISTANCE, max_radius); // The user may be scrolling to move the camera closer, but are not realizing // the radius is now tiny. // TODO(emilk): inform the users somehow that scrolling won't help, and that they should use WSAD instead. // It might be tempting to start moving the camera here on scroll, but that would is bad for other reasons. - // Don't let radius go too small or too big because this might cause infinity/nan in some calculations. - // Max value is chosen with some generous margin of an observed crash due to infinity. - if f32::MIN_POSITIVE < new_radius && new_radius < 1.0e17 { + if f32::MIN_POSITIVE < new_radius { self.pos = self.look_target - self.fwd() * new_radius; self.did_interact = true; } @@ -569,12 +602,73 @@ impl EyeController { } } + /// Listen to an active gamepad to move the eye. + fn handle_gamepad_navigation( + &mut self, + eye_state: &mut EyeState, + egui_ctx: &egui::Context, + enabled: bool, + ) -> GamepadNavigationStatus { + if !enabled { + return GamepadNavigationStatus::Disconnected; + } + + let repaint_ctx = egui_ctx.clone(); + re_gamepad::set_event_waker(move || repaint_ctx.request_repaint()); + + let dt = egui_ctx.input(|input| input.stable_dt.at_most(0.1)); + let Some(navigation) = re_gamepad::navigation_from_active_gamepad(dt) else { + return GamepadNavigationStatus::Disconnected; + }; + + if !navigation.is_active() { + eye_state.velocity = Vec3::ZERO; + return GamepadNavigationStatus::ConnectedInactive; + } + + let local_movement = navigation.local_movement; + let speed = (self.speed as f32) * navigation.speed_multiplier; + let world_movement = self.rotation() * (speed * local_movement); + + eye_state.velocity = if local_movement == Vec3::ZERO { + Vec3::ZERO + } else { + egui::lerp( + eye_state.velocity..=world_movement, + egui::emath::exponential_smooth_factor(0.90, 0.2, dt), + ) + }; + let delta = eye_state.velocity * dt; + + self.pos += delta; + self.look_target += delta; + + if navigation.look_delta_radians.length_squared() > 1.0e-6 { + self.rotate_radians(egui::vec2( + navigation.look_delta_radians.x, + navigation.look_delta_radians.y, + )); + } + + self.did_interact |= navigation.is_active(); + let requires_repaint = + navigation.is_active() || eye_state.velocity.length() > 0.01 * self.speed as f32; + + if requires_repaint { + egui_ctx.request_repaint(); + } + + GamepadNavigationStatus::Active + } + fn handle_input( &mut self, eye_state: &mut EyeState, response: &egui::Response, drag_threshold: f32, - ) { + scene_bounding_box: &macaw::BoundingBox, + enable_gamepad_navigation: bool, + ) -> GamepadNavigationStatus { // Modify speed based on modifiers: let os = response.ctx.os(); response.ctx.input(|input| { @@ -593,7 +687,7 @@ impl EyeController { self.handle_drag(response, drag_threshold); if response.hovered() { - self.handle_zoom(&response.ctx); + self.handle_zoom(&response.ctx, scene_bounding_box); } if response.has_focus() { @@ -602,12 +696,36 @@ impl EyeController { response.request_focus(); } + let gamepad_navigation_status = + self.handle_gamepad_navigation(eye_state, &response.ctx, enable_gamepad_navigation); + if self.did_interact { eye_state.last_interaction_time = Some(response.ctx.time()); } + + gamepad_navigation_status } } +/// Cap on the orbital zoom-out radius, derived from the scene bounding box diagonal. +/// +/// Returns `1.0e17` (a large fallback that avoids infinities downstream) when no usable scene +/// bounding box is available. +fn max_orbital_radius(scene_bounding_box: &macaw::BoundingBox) -> f32 { + // `1.0e17` fallback is chosen with generous margin of an observed crash due to infinity. + let fallback = 1.0e17; + + if !scene_bounding_box.is_finite() || scene_bounding_box.is_nothing() { + return fallback; + } + let scene_diagonal = scene_bounding_box.size().length(); + if !scene_diagonal.is_finite() || scene_diagonal <= 0.0 { + return fallback; + } + (scene_diagonal * EyeController::MAX_ORBITAL_ZOOM_OUT_FACTOR) + .max(EyeController::MIN_ORBIT_DISTANCE) +} + pub fn find_camera(cameras: &[PinholeWrapper], needle: &EntityPath) -> Option { let mut found_camera = None; @@ -653,6 +771,7 @@ impl EyeState { response: &egui::Response, cameras: &[PinholeWrapper], bounding_boxes: &SceneBoundingBoxes, + enable_gamepad_navigation: bool, ) -> Result { let mut eye_controller = EyeController::from_blueprint(ctx, eye_property, self.fov_y)?; @@ -664,19 +783,19 @@ impl EyeState { .. } = eye_controller; + if let Some(gamepad_interaction) = &self.gamepad_interaction { + eye_controller.pos = gamepad_interaction.pos; + eye_controller.look_target = gamepad_interaction.look_target; + eye_controller.eye_up = gamepad_interaction.eye_up; + } + let mut drag_threshold = 0.0; let tracking_entity = eye_property .component_or_empty::( EyeControls3D::descriptor_tracking_entity().component, )? - .and_then(|tracking_entity| { - if tracking_entity.is_empty() { - None - } else { - Some(tracking_entity) - } - }); + .filter(|tracking_entity| !tracking_entity.is_empty()); if let Some(tracking_entity) = &tracking_entity { let tracking_entity = EntityPath::from(tracking_entity.as_str()); @@ -690,20 +809,47 @@ impl EyeState { // We do input before tracking entity, because the input can cause the eye // to stop tracking. - eye_controller.handle_input(self, response, drag_threshold); + let gamepad_navigation_status = eye_controller.handle_input( + self, + response, + drag_threshold, + &bounding_boxes.current, + enable_gamepad_navigation, + ); + + match gamepad_navigation_status { + GamepadNavigationStatus::Active => { + self.gamepad_interaction = Some(GamepadInteraction { + pos: eye_controller.pos, + look_target: eye_controller.look_target, + eye_up: eye_controller.eye_up, + }); + } + GamepadNavigationStatus::ConnectedInactive | GamepadNavigationStatus::Disconnected => { + if self.gamepad_interaction.take().is_some() { + eye_controller.did_interact = true; + } + } + } // If we interacted we write to the blueprint so reset spin offset. if eye_controller.did_interact { self.spin = None; } - eye_controller.save_to_blueprint( - ctx.viewer_ctx, - eye_property, - old_pos, - old_look_target, - old_eye_up, - ); + // Gamepad input is not tracked by egui's `is_interacting` undo heuristic. Avoid creating + // one undo point per poll frame by only writing the final pose when the gamepad returns to + // neutral or disconnects. + // TODO(michael): find a nicer way to handle this, e.g. through a dedicated `interacting()` function. + if gamepad_navigation_status != GamepadNavigationStatus::Active { + eye_controller.save_to_blueprint( + ctx.viewer_ctx, + eye_property, + old_pos, + old_look_target, + old_eye_up, + ); + } if let Some(tracked_eye) = self.handle_tracking_entity( ctx, @@ -729,7 +875,6 @@ impl EyeState { /// Handles both tracking and clearing tracked entity. /// /// If we are tracking an entity, this will return the current eye we should use. - #[expect(clippy::too_many_arguments)] fn handle_tracking_entity( &mut self, ctx: &ViewContext<'_>, @@ -1042,12 +1187,9 @@ impl EyeState { response: &egui::Response, pinhole_cameras: &[PinholeWrapper], bounding_boxes: &SceneBoundingBoxes, + enable_gamepad_navigation: bool, ) -> Result { - let eye_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - ctx.view_id, - ); + let eye_property = ViewProperty::from_archetype::(ctx); let target_eye = self.control_and_sync_with_blueprint( ctx, @@ -1055,6 +1197,7 @@ impl EyeState { response, pinhole_cameras, bounding_boxes, + enable_gamepad_navigation, )?; // If we use fallbacks for position and look target, continue to diff --git a/crates/viewer/re_view_spatial/src/lib.rs b/crates/viewer/re_view_spatial/src/lib.rs index dc03a7d323dd..e106c3308595 100644 --- a/crates/viewer/re_view_spatial/src/lib.rs +++ b/crates/viewer/re_view_spatial/src/lib.rs @@ -38,17 +38,14 @@ pub use view_2d::SpatialView2D; pub use view_3d::SpatialView3D; // Export some other types that are useful for extensions. -pub use contexts::TransformTreeContext; +pub use contexts::{EntityDepthOffsets, TransformTreeContext}; -mod view_kind { - /// Whether a spatial visualizer prefers 2D or 3D views. - /// - /// Used by heuristics to determine which entities belong to which spatial view kind. - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum SpatialViewKind { - TwoD, - ThreeD, - } +/// Whether a space is 2D or 3D. +/// Also used for subspaces, e.g. 2D pinhole subspace in 3D. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpaceKind { + TwoD, + ThreeD, } pub fn configure_background( diff --git a/crates/viewer/re_view_spatial/src/max_image_dimension_subscriber.rs b/crates/viewer/re_view_spatial/src/max_image_dimension_subscriber.rs index e1d9ef85c2f3..a370a4392f43 100644 --- a/crates/viewer/re_view_spatial/src/max_image_dimension_subscriber.rs +++ b/crates/viewer/re_view_spatial/src/max_image_dimension_subscriber.rs @@ -23,7 +23,16 @@ bitflags::bitflags! { } } -#[derive(Debug, Clone, Default)] +impl re_byte_size::SizeBytes for ImageTypes { + const IS_POD: bool = true; + + #[inline] + fn heap_size_bytes(&self) -> u64 { + 0 + } +} + +#[derive(Debug, Clone, Default, re_byte_size::SizeBytes)] pub struct MaxDimensions { pub width: u32, pub height: u32, @@ -67,18 +76,6 @@ impl MaxImageDimensionsStoreSubscriber { } } -impl re_byte_size::SizeBytes for MaxDimensions { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} - impl re_byte_size::MemUsageTreeCapture for MaxImageDimensionsStoreSubscriber { fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree { use re_byte_size::SizeBytes as _; @@ -207,10 +204,10 @@ impl PerStoreChunkSubscriber for MaxImageDimensionsStoreSubscriber { }); for (blob, media_type) in itertools::izip!( blobs, - media_types - .into_iter() - .map(Some) - .chain(std::iter::repeat(None)) + std::iter::chain( + media_types.into_iter().map(Some), + std::iter::repeat(None) + ) ) { let Some(blob) = blob.first() else { continue; @@ -283,15 +280,10 @@ fn try_size_from_blob( re_tracing::profile_scope!("video asset"); let media_type = components::MediaType::or_guess_from_data(media_type, blob)?; - re_video::VideoDataDescription::load_from_bytes( - blob, - media_type.as_str(), - debug_name, - re_log_types::external::re_tuid::Tuid::new(), - ) - .ok() - .and_then(|video| video.encoding_details.map(|e| e.coded_dimensions)) - .map(|[w, h]| [w as _, h as _]) + re_video::VideoDataDescription::load_from_bytes(blob, media_type.as_str(), debug_name) + .ok() + .and_then(|video| video.encoding_details.map(|e| e.coded_dimensions)) + .map(|[w, h]| [w as _, h as _]) } else { None } @@ -321,6 +313,8 @@ fn try_size_from_video_stream_sample( components::VideoCodec::H264 => re_video::VideoCodec::H264, components::VideoCodec::H265 => re_video::VideoCodec::H265, components::VideoCodec::AV1 => re_video::VideoCodec::AV1, + components::VideoCodec::VP8 => re_video::VideoCodec::VP8, + components::VideoCodec::VP9 => re_video::VideoCodec::VP9, }; match re_video::detect_gop_start(sample, codec).ok()? { diff --git a/crates/viewer/re_view_spatial/src/mesh_loader.rs b/crates/viewer/re_view_spatial/src/mesh_loader.rs index 8bed747e8b95..5613e27e77a3 100644 --- a/crates/viewer/re_view_spatial/src/mesh_loader.rs +++ b/crates/viewer/re_view_spatial/src/mesh_loader.rs @@ -31,22 +31,21 @@ pub struct NativeMesh3D<'a> { pub albedo_texture_format: Option, } +// Mostly VRAM, not counted here. +#[derive(re_byte_size::SizeBytes)] pub struct LoadedMesh { + #[size_bytes(ignore)] name: String, // TODO(andreas): We should only have MeshHandles here (which are generated by the MeshManager!) // Can't do that right now because it's too hard to pass the render context through. + #[size_bytes(ignore)] pub mesh_instances: Vec, + #[size_bytes(ignore)] bbox: macaw::BoundingBox, } -impl re_byte_size::SizeBytes for LoadedMesh { - fn heap_size_bytes(&self) -> u64 { - 0 // Mostly VRAM, not counted here. - } -} - impl LoadedMesh { pub fn load( name: String, @@ -139,12 +138,14 @@ impl LoadedMesh { let vertex_colors = if let Some(vertex_colors) = vertex_colors { re_tracing::profile_scope!("copy_colors"); - vertex_colors - .iter() - .map(|c| re_renderer::Rgba32Unmul::from_rgba_unmul_array(c.to_array())) - .chain(std::iter::repeat(re_renderer::Rgba32Unmul::WHITE)) - .take(num_positions) - .collect::>() + std::iter::chain( + vertex_colors + .iter() + .map(|c| re_renderer::Rgba32Unmul::from_rgba_unmul_array(c.to_array())), + std::iter::repeat(re_renderer::Rgba32Unmul::WHITE), + ) + .take(num_positions) + .collect::>() } else { vec![re_renderer::Rgba32Unmul::WHITE; num_positions] }; diff --git a/crates/viewer/re_view_spatial/src/pickable_textured_rect.rs b/crates/viewer/re_view_spatial/src/pickable_textured_rect.rs index 7497ed362e36..f41994ec71a8 100644 --- a/crates/viewer/re_view_spatial/src/pickable_textured_rect.rs +++ b/crates/viewer/re_view_spatial/src/pickable_textured_rect.rs @@ -13,7 +13,7 @@ pub enum PickableRectSourceData { }, /// The rectangle is a frame in a video. - Video, + Video { depth_meter: Option }, /// The rectangle represents a placeholder icon. Placeholder, diff --git a/crates/viewer/re_view_spatial/src/picking.rs b/crates/viewer/re_view_spatial/src/picking.rs index 538f54047420..305a9c09f77c 100644 --- a/crates/viewer/re_view_spatial/src/picking.rs +++ b/crates/viewer/re_view_spatial/src/picking.rs @@ -9,7 +9,7 @@ use re_renderer::PickingLayerProcessor; use crate::PickableTexturedRect; use crate::eye::Eye; -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, re_byte_size::SizeBytes)] pub enum PickingHitType { /// The hit was a textured rect. TexturedRect, @@ -21,11 +21,12 @@ pub enum PickingHitType { GuiOverlay, } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, re_byte_size::SizeBytes)] pub struct PickingRayHit { /// What entity or instance got hit by the picking ray. /// /// The ray hit position may not actually be on this entity, as we allow snapping to closest entity! + // `InstancePathHash` doesn't impl `SizeBytes`; it's all POD (no heap). pub instance_path_hash: InstancePathHash, /// Where the ray hit the entity. @@ -37,7 +38,7 @@ pub struct PickingRayHit { pub hit_type: PickingHitType, } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, re_byte_size::SizeBytes)] pub struct PickingResult { /// Picking ray hits. /// diff --git a/crates/viewer/re_view_spatial/src/picking_ui.rs b/crates/viewer/re_view_spatial/src/picking_ui.rs index 2490eb4da5ab..516825e4a9e5 100644 --- a/crates/viewer/re_view_spatial/src/picking_ui.rs +++ b/crates/viewer/re_view_spatial/src/picking_ui.rs @@ -11,13 +11,15 @@ use re_viewer_context::{ UiLayout, ViewQuery, ViewSystemExecutionError, ViewerContext, }; +use crate::TransformTreeContext; use crate::visualizers::DepthImageProcessResult; use crate::{ - PickableRectSourceData, PickableTexturedRect, + PickableRectSourceData, PickableTexturedRect, SpaceKind, picking::{PickableUiRect, PickingContext, PickingHitType}, - picking_ui_pixel::{PickedPixelInfo, textured_rect_hover_ui}, + picking_ui_pixel::{ + PickedPixelInfo, TextureInteractionId, depth_value_from_gpu_texture, textured_rect_hover_ui, + }, ui::SpatialViewState, - view_kind::SpatialViewKind, visualizers::{ CamerasVisualizer, CamerasVisualizerOutput, DepthImageVisualizer, DepthImageVisualizerOutput, EncodedDepthImageVisualizer, EncodedDepthImageVisualizerOutput, @@ -25,7 +27,6 @@ use crate::{ }, }; -#[expect(clippy::too_many_arguments)] pub fn picking( ctx: &ViewerContext<'_>, missing_chunk_reporter: &MissingChunkReporter, @@ -36,7 +37,7 @@ pub fn picking( system_output: &re_viewer_context::SystemExecutionOutput, ui_rects: &[PickableUiRect], query: &ViewQuery<'_>, - spatial_kind: SpatialViewKind, + spatial_kind: SpaceKind, ) -> Result<(egui::Response, Option), ViewSystemExecutionError> { re_tracing::profile_function!(); @@ -108,17 +109,39 @@ pub fn picking( } response = if let Some(picked_pixel) = get_pixel_picking_info(system_output, hit) { - if let PickableRectSourceData::Image { - depth_meter: Some(meter), - image, - } = &picked_pixel.source_data - { - let [x, y] = picked_pixel.pixel_coordinates; - if let Some(raw_value) = image.get_xyc(x, y, 0) { - let raw_value = raw_value.as_f64(); - let depth_in_meters = raw_value / *meter.0 as f64; - depth_at_pointer = Some(depth_in_meters as f32); + match &picked_pixel.source_data { + PickableRectSourceData::Image { + depth_meter: Some(meter), + image, + } => { + let [x, y] = picked_pixel.pixel_coordinates; + if let Some(raw_value) = image.get_xyc(x, y, 0) { + let raw_value = raw_value.as_f64(); + let depth_in_meters = raw_value / *meter.0 as f64; + depth_at_pointer = Some(depth_in_meters as f32); + } + } + PickableRectSourceData::Video { + depth_meter: Some(meter), + } => { + // For video-decoded depth images, read the depth value back from the GPU. + let interaction_id = TextureInteractionId { + entity_path: &instance_path.entity_path, + interaction_idx: hit_idx as u32, + }; + let [x, y] = picked_pixel.pixel_coordinates; + if let Some(raw_value) = depth_value_from_gpu_texture( + ctx.egui_ctx(), + ctx.render_ctx(), + &picked_pixel.texture.texture, + &interaction_id, + [x, y], + ) { + let depth_in_meters = raw_value / *meter.0 as f64; + depth_at_pointer = Some(depth_in_meters as f32); + } } + _ => {} } response @@ -201,31 +224,33 @@ pub fn picking( ItemCollection::from_items_and_context(hovered_items.into_iter().map(|item| (item, None))); if let Some((_, context)) = hovered_items.iter_mut().next() { + let transforms = system_output + .context_systems + .get_and_report_missing::(missing_chunk_reporter)?; *context = Some(match spatial_kind { - SpatialViewKind::TwoD => ItemContext::TwoD { - space_2d: query.space_origin.clone(), + SpaceKind::TwoD => ItemContext::TwoD { + space_2d_target_frame: transforms.target_frame(), pos: picking_context .pointer_in_camera_plane .extend(depth_at_pointer.unwrap_or(f32::INFINITY)), }, - SpatialViewKind::ThreeD => { + SpaceKind::ThreeD => { let hovered_point = picking_result.space_position(); - let empty_cameras = Vec::new(); - let pinhole_cameras = system_output - .visualizer_data::(CamerasVisualizer::identifier()) - .ok() - .map(|d| &d.pinhole_cameras) - .unwrap_or(&empty_cameras); + let cameras = system_output.visualizer_data_or_default::( + CamerasVisualizer::identifier(), + )?; + + let pinhole_cameras = &cameras.pinhole_cameras; ItemContext::ThreeD { - space_3d: query.space_origin.clone(), + space_3d_target_frame: transforms.target_frame(), pos: hovered_point, tracked_entity: state.last_tracked_entity().cloned(), - point_in_space_cameras: pinhole_cameras + point_in_2d_spaces: pinhole_cameras .iter() .map(|cam| { ( - cam.ent_path.clone(), + cam.pinhole_child_frame_id, hovered_point.map(|pos| cam.project_onto_2d(pos)), ) }) @@ -243,7 +268,7 @@ pub fn picking( fn iter_pickable_rects( system_output: &re_viewer_context::SystemExecutionOutput, ) -> impl Iterator { - iter_spatial_data(system_output).flat_map(|(_affinity, data)| data.pickable_rects.iter()) + iter_spatial_data(system_output).flat_map(|data| data.pickable_rects.iter()) } /// If available, finds pixel info for a picking hit. @@ -255,12 +280,12 @@ fn get_pixel_picking_info( ) -> Option { let depth_visualizer_output = system_output .visualizer_data::(DepthImageVisualizer::identifier()) - .ok(); + .ok()?; let encoded_depth_visualizer_output = system_output .visualizer_data::( EncodedDepthImageVisualizer::identifier(), ) - .ok(); + .ok()?; if hit.hit_type == PickingHitType::TexturedRect { iter_pickable_rects(system_output) @@ -298,15 +323,26 @@ fn get_pixel_picking_info( .get(&hit.instance_path_hash.entity_path_hash) }) { + let width = image_info + .as_ref() + .map(|i| i.width()) + .unwrap_or_else(|| colormap.width_height()[0]); let pixel_coordinates = hit .instance_path_hash .instance - .to_2d_image_coordinate(image_info.width()); - Some(PickedPixelInfo { - source_data: PickableRectSourceData::Image { - image: image_info.clone(), + .to_2d_image_coordinate(width); + let source_data = if let Some(image) = image_info { + PickableRectSourceData::Image { + image: image.clone(), depth_meter: Some(*depth_meter), - }, + } + } else { + PickableRectSourceData::Video { + depth_meter: Some(*depth_meter), + } + }; + Some(PickedPixelInfo { + source_data, texture: colormap.clone(), pixel_coordinates, }) diff --git a/crates/viewer/re_view_spatial/src/picking_ui_pixel.rs b/crates/viewer/re_view_spatial/src/picking_ui_pixel.rs index 5e7be253900f..90c540425a30 100644 --- a/crates/viewer/re_view_spatial/src/picking_ui_pixel.rs +++ b/crates/viewer/re_view_spatial/src/picking_ui_pixel.rs @@ -10,7 +10,7 @@ use re_view::AnnotationSceneContext; use re_viewer_context::{Annotations, ImageInfo, StoreViewContext, ViewQuery, gpu_bridge}; use crate::PickableRectSourceData; -use crate::view_kind::SpatialViewKind; +use crate::SpaceKind; pub struct PickedPixelInfo { pub source_data: PickableRectSourceData, @@ -18,13 +18,12 @@ pub struct PickedPixelInfo { pub pixel_coordinates: [u32; 2], } -#[expect(clippy::too_many_arguments)] pub fn textured_rect_hover_ui( ctx: &StoreViewContext<'_>, ui: &mut egui::Ui, instance_path: &re_entity_db::InstancePath, query: &ViewQuery<'_>, - spatial_kind: SpatialViewKind, + spatial_kind: SpaceKind, ui_pan_and_zoom_from_ui: egui::emath::RectTransform, annotations: &AnnotationSceneContext, picked_pixel_info: PickedPixelInfo, @@ -37,8 +36,8 @@ pub fn textured_rect_hover_ui( } = picked_pixel_info; let depth_meter = match &source_data { - PickableRectSourceData::Image { depth_meter, .. } => *depth_meter, - PickableRectSourceData::Video => None, + PickableRectSourceData::Image { depth_meter, .. } + | PickableRectSourceData::Video { depth_meter } => *depth_meter, PickableRectSourceData::Placeholder => { // No point in zooming into a placeholder! return; @@ -55,7 +54,7 @@ pub fn textured_rect_hover_ui( let [w, h] = texture.width_height(); let (w, h) = (w as f32, h as f32); - if spatial_kind == SpatialViewKind::TwoD { + if spatial_kind == SpaceKind::TwoD { let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, h)); show_zoomed_image_region_area_outline( @@ -158,10 +157,16 @@ impl TextureInteractionId<'_> { pub fn gpu_readback_id(&self) -> re_renderer::GpuReadbackIdentifier { re_log_types::hash::Hash64::hash((self.entity_path, self.interaction_idx)).hash64() } + + fn render_view_id(&self, topic: &str) -> re_renderer::ViewBuilderId { + re_renderer::ViewBuilderId::new( + re_log_types::hash::Hash64::hash((self.entity_path, self.interaction_idx, topic)) + .hash64(), + ) + } } /// `meter`: iff this is a depth map, how long is one meter? -#[expect(clippy::too_many_arguments)] pub fn show_zoomed_image_region( render_ctx: &re_renderer::RenderContext, ui: &mut egui::Ui, @@ -187,7 +192,6 @@ pub fn show_zoomed_image_region( } /// `meter`: iff this is a depth map, how long is one meter? -#[expect(clippy::too_many_arguments)] fn try_show_zoomed_image_region( render_ctx: &re_renderer::RenderContext, ui: &mut egui::Ui, @@ -226,6 +230,7 @@ fn try_show_zoomed_image_region( image_rect_on_screen, colormapped_texture.clone(), egui::TextureOptions::NEAREST, + interaction_id.render_view_id("zoomed_region"), interaction_id.debug_label("zoomed_region"), )?; } @@ -285,6 +290,7 @@ fn try_show_zoomed_image_region( image_rect_on_screen, colormapped_texture, egui::TextureOptions::NEAREST, + interaction_id.render_view_id("single_pixel"), interaction_id.debug_label("single_pixel"), ) }) @@ -348,17 +354,18 @@ fn pixel_value_ui( if let Some(meter) = meter && let Some(raw_value) = image.get_xyc(x, y, 0) { - let raw_value = raw_value.as_f64(); - let meters = raw_value / (meter as f64); - ui.label("Depth:"); - if meters < 1.0 { - ui.monospace(format!("{:.1} mm", meters * 1e3)); - } else { - ui.monospace(format!("{meters:.3} m")); - } + show_depth_at_hover(ui, raw_value.as_f64(), meter); } } + if let PixelValueSource::GpuTexture(texture) = &pixel_value_source + && let Some(meter) = meter + && let Some(raw_value) = + depth_value_from_gpu_texture(ui.ctx(), render_ctx, texture, interaction_id, [x, y]) + { + show_depth_at_hover(ui, raw_value, meter); + } + let text = match pixel_value_source { PixelValueSource::Image(image) => pixel_value_string_from_image(image, x, y), PixelValueSource::GpuTexture(texture) => pixel_value_string_from_gpu_texture( @@ -379,6 +386,16 @@ fn pixel_value_ui( }); } +fn show_depth_at_hover(ui: &mut egui::Ui, raw_value: f64, meter: f32) { + let meters = raw_value / (meter as f64); + ui.label("Depth:"); + if meters < 1.0 { + ui.monospace(format!("{:.1} mm", meters * 1e3)); + } else { + ui.monospace(format!("{meters:.3} m")); + } +} + fn format_pixel_value( image_kind: ImageKind, color_model: ColorModel, @@ -552,21 +569,22 @@ struct TextureReadbackUserdata { buffer_info: re_renderer::Texture2DBufferInfo, } -fn pixel_value_string_from_gpu_texture( +/// Read back raw pixel bytes from a GPU texture at the given coordinates. +/// +/// Schedules a 64x64 region readback around the cursor and polls for results +/// from a previous frame. Returns the raw bytes for the single pixel, or `None` +/// if no result is available yet. +fn readback_pixel_from_gpu_texture( egui_ctx: &egui::Context, render_ctx: &re_renderer::RenderContext, texture: &GpuTexture2D, interaction_id: &TextureInteractionId<'_>, [x, y]: [u32; 2], -) -> Option<(String, String)> { +) -> Option> { // TODO(andreas): Should parts of this be a utility in re_renderer? // Note that before this was implemented the readback belt was private to `re_renderer` because it is fairly advanced in its usage. - // Only support Rgb8Unorm textures for now. - // We could support more here but that needs more handling code and it doesn't look like we have to right now. - if texture.format() != wgpu::TextureFormat::Rgba8Unorm { - return None; - } + let bytes_per_pixel: u32 = texture.format().block_copy_size(None)?; let readback_id = interaction_id.gpu_readback_id(); @@ -578,7 +596,7 @@ fn pixel_value_string_from_gpu_texture( // First check if we have a result ready to read. // Keep in mind that copy operation may have required row-padding, use `buffer_info` to get the right values. // Readbacks from GPU might come in bursts for all sort of reasons. So make sure we only look at the latest result. - let readback_result_rgb = readback_belt.readback_newest_available( + let readback_result = readback_belt.readback_newest_available( readback_id, |data, userdata: Box| { re_log::debug_assert!(data.len() == userdata.buffer_info.buffer_size_padded as usize); @@ -592,27 +610,25 @@ fn pixel_value_string_from_gpu_texture( userdata.readback_rect.extent.as_ivec2() - glam::IVec2::ONE, ) .as_uvec2(); - let start_index = - (data_pos.x * 4 + userdata.buffer_info.bytes_per_row_padded * data_pos.y) as usize; - - [ - data[start_index], - data[start_index + 1], - data[start_index + 2], - ] + let start_index = (data_pos.x * bytes_per_pixel + + userdata.buffer_info.bytes_per_row_padded * data_pos.y) + as usize; + let end_index = start_index + bytes_per_pixel as usize; + + data[start_index..end_index].to_vec() }, ); // Unfortunately, it can happen that GPU readbacks come in bursts one frame and we get thing in the next. // Therefore, we have to keep around the previous result and use that until we get a new one. - let readback_result_rgb = { + let readback_result = { let frame_nr = egui_ctx.cumulative_frame_nr(); #[derive(Clone)] struct PreviousReadbackResult { frame_nr: u64, interaction_id: re_renderer::GpuReadbackIdentifier, - readback_result_rgb: [u8; 3], + pixel_bytes: Vec, } // Only use the interaction *index* to identify the memory itself so we don't accumulate data indefinitely. @@ -620,31 +636,30 @@ fn pixel_value_string_from_gpu_texture( let memory_id = egui::Id::new(interaction_id.interaction_idx); let interaction_id = interaction_id.gpu_readback_id(); - if let Some(readback_result_rgb) = readback_result_rgb { + if let Some(pixel_bytes) = readback_result { egui_ctx.memory_mut(|m| { m.data.insert_temp( memory_id, PreviousReadbackResult { frame_nr, interaction_id, - readback_result_rgb, + pixel_bytes: pixel_bytes.clone(), }, ); }); - Some(readback_result_rgb) + Some(pixel_bytes) } else { const MAX_FRAMES_WITHOUT_GPU_READBACK: u64 = 3; - let cached: PreviousReadbackResult = egui_ctx.memory(|m| m.data.get_temp(memory_id))?; + let cached: Option = + egui_ctx.memory(|m| m.data.get_temp(memory_id)); - if cached.interaction_id == interaction_id - && cached.frame_nr + MAX_FRAMES_WITHOUT_GPU_READBACK >= frame_nr - { - Some(cached.readback_result_rgb) - } else { - None - } + cached.and_then(|cached| { + (cached.interaction_id == interaction_id + && cached.frame_nr + MAX_FRAMES_WITHOUT_GPU_READBACK >= frame_nr) + .then_some(cached.pixel_bytes) + }) } }; @@ -711,11 +726,76 @@ fn pixel_value_string_from_gpu_texture( } } - let rgb = readback_result_rgb?; - let rgb = [ - TensorElement::U8(rgb[0]), - TensorElement::U8(rgb[1]), - TensorElement::U8(rgb[2]), - ]; - format_pixel_value(ImageKind::Color, ColorModel::RGB, &rgb) + readback_result +} + +/// Read back a pixel value from a GPU texture and format it as a string. +fn pixel_value_string_from_gpu_texture( + egui_ctx: &egui::Context, + render_ctx: &re_renderer::RenderContext, + texture: &GpuTexture2D, + interaction_id: &TextureInteractionId<'_>, + [x, y]: [u32; 2], +) -> Option<(String, String)> { + let pixel_bytes = + readback_pixel_from_gpu_texture(egui_ctx, render_ctx, texture, interaction_id, [x, y])?; + + match texture.format() { + wgpu::TextureFormat::Rgba8Unorm => { + let elements = [ + TensorElement::U8(pixel_bytes[0]), + TensorElement::U8(pixel_bytes[1]), + TensorElement::U8(pixel_bytes[2]), + ]; + format_pixel_value(ImageKind::Color, ColorModel::RGB, &elements) + } + wgpu::TextureFormat::R8Unorm => { + let elements = [TensorElement::U8(pixel_bytes[0])]; + format_pixel_value(ImageKind::Depth, ColorModel::L, &elements) + } + wgpu::TextureFormat::R16Uint => { + let value = u16::from_le_bytes([pixel_bytes[0], pixel_bytes[1]]); + let elements = [TensorElement::U16(value)]; + format_pixel_value(ImageKind::Depth, ColorModel::L, &elements) + } + wgpu::TextureFormat::R32Float => { + let value = f32::from_le_bytes([ + pixel_bytes[0], + pixel_bytes[1], + pixel_bytes[2], + pixel_bytes[3], + ]); + let elements = [TensorElement::F32(value)]; + format_pixel_value(ImageKind::Depth, ColorModel::L, &elements) + } + _ => None, + } +} + +/// Read back a raw depth value from a GPU texture at the given pixel. +/// +/// Returns the depth value as f64, suitable for division by `depth_meter`. +pub fn depth_value_from_gpu_texture( + egui_ctx: &egui::Context, + render_ctx: &re_renderer::RenderContext, + texture: &GpuTexture2D, + interaction_id: &TextureInteractionId<'_>, + [x, y]: [u32; 2], +) -> Option { + let pixel_bytes = + readback_pixel_from_gpu_texture(egui_ctx, render_ctx, texture, interaction_id, [x, y])?; + + match texture.format() { + wgpu::TextureFormat::R8Unorm => Some(pixel_bytes[0] as f64), + wgpu::TextureFormat::R16Uint => { + Some(u16::from_le_bytes([pixel_bytes[0], pixel_bytes[1]]) as f64) + } + wgpu::TextureFormat::R32Float => Some(f32::from_le_bytes([ + pixel_bytes[0], + pixel_bytes[1], + pixel_bytes[2], + pixel_bytes[3], + ]) as f64), + _ => None, + } } diff --git a/crates/viewer/re_view_spatial/src/pinhole.rs b/crates/viewer/re_view_spatial/src/pinhole.rs index 2bd827fb8c0d..f913721d1062 100644 --- a/crates/viewer/re_view_spatial/src/pinhole.rs +++ b/crates/viewer/re_view_spatial/src/pinhole.rs @@ -1,7 +1,7 @@ /// A pinhole camera model. /// /// Corresponds roughly to the [`re_sdk_types::archetypes::Pinhole`] archetype, but uses render-friendly types. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, re_byte_size::SizeBytes)] pub struct Pinhole { pub image_from_camera: glam::Mat3, pub resolution: glam::Vec2, diff --git a/crates/viewer/re_view_spatial/src/pinhole_wrapper.rs b/crates/viewer/re_view_spatial/src/pinhole_wrapper.rs index ecc8fe06837f..a52bf7944019 100644 --- a/crates/viewer/re_view_spatial/src/pinhole_wrapper.rs +++ b/crates/viewer/re_view_spatial/src/pinhole_wrapper.rs @@ -2,16 +2,17 @@ use glam::Vec3; use macaw::IsoTransform; use re_log_types::EntityPath; use re_sdk_types::components::ViewCoordinates; -use re_tf::image_view_coordinates; +use re_tf::{TransformFrameIdHash, image_view_coordinates}; use crate::Pinhole; /// A logged pinhole camera with some extra information. -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] pub struct PinholeWrapper { - /// Path to the entity which has the projection (pinhole, ortho or otherwise) transforms. - /// - /// We expect the camera transform to apply to this instance and every path below it. + /// The child frame of the pinhole camera, i.e. the 2D frame into which the pinhole transforms into. + pub pinhole_child_frame_id: TransformFrameIdHash, + + /// Path to the entity which has the pinhole projection. pub ent_path: EntityPath, /// The coordinate system of the pinhole entity ("view-space"). diff --git a/crates/viewer/re_view_spatial/src/proc_mesh.rs b/crates/viewer/re_view_spatial/src/proc_mesh.rs index 5ae530380dd0..baa439e3c90b 100644 --- a/crates/viewer/re_view_spatial/src/proc_mesh.rs +++ b/crates/viewer/re_view_spatial/src/proc_mesh.rs @@ -21,7 +21,7 @@ use smallvec::smallvec; /// Description of a mesh that can be procedurally generated. /// /// Obtain the actual mesh by passing this to [`WireframeCache`] or [`SolidCache`]. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, re_byte_size::SizeBytes)] pub enum ProcMeshKey { /// A unit cube, centered; its bounds are ±0.5. Cube, @@ -57,6 +57,7 @@ pub enum ProcMeshKey { // can have parts which are independently offset, thus allowing us to stretch a // single sphere/capsule mesh into an arbitrary length and radius capsule. // (Tapered capsules will still need distinct meshes.) + #[size_bytes(ignore)] // `NotNan` doesn't impl `SizeBytes`. length: NotNan, /// Number of triangle subdivisions to use to create a finer, rounder mesh. @@ -84,12 +85,6 @@ pub enum ProcMeshKey { }, } -impl re_byte_size::SizeBytes for ProcMeshKey { - fn heap_size_bytes(&self) -> u64 { - 0 - } -} - impl ProcMeshKey { /// Returns the bounding box which can be computed from the mathematical shape, /// without regard for its exact approximation as a mesh. @@ -124,12 +119,11 @@ impl ProcMeshKey { /// A renderable mesh generated from a [`ProcMeshKey`] by the [`WireframeCache`], /// which is to be drawn as lines rather than triangles. -#[derive(Debug)] +#[derive(Debug, re_byte_size::SizeBytes)] pub struct WireframeMesh { - #[expect(unused)] + #[size_bytes(ignore)] pub bbox: macaw::BoundingBox, - #[expect(unused)] pub vertex_count: usize, /// Collection of line strips making up the wireframe. @@ -140,20 +134,6 @@ pub struct WireframeMesh { pub line_strips: Vec>, } -impl re_byte_size::SizeBytes for WireframeMesh { - fn heap_size_bytes(&self) -> u64 { - let Self { - bbox: _, - vertex_count: _, - line_strips, - } = self; - line_strips - .iter() - .map(|strip| strip.len() * std::mem::size_of::()) - .sum::() as _ - } -} - /// A renderable mesh generated from a [`ProcMeshKey`] by the [`SolidCache`], /// which is to be drawn as triangles rather than lines. /// diff --git a/crates/viewer/re_view_spatial/src/scene_bounding_boxes.rs b/crates/viewer/re_view_spatial/src/scene_bounding_boxes.rs index 8a824bd4ae5d..5b38734f8f3d 100644 --- a/crates/viewer/re_view_spatial/src/scene_bounding_boxes.rs +++ b/crates/viewer/re_view_spatial/src/scene_bounding_boxes.rs @@ -1,12 +1,12 @@ use egui::NumExt as _; use nohash_hasher::IntMap; use re_log_types::EntityPathHash; -use re_viewer_context::{SystemExecutionOutput, ViewClass as _}; +use re_viewer_context::SystemExecutionOutput; -use crate::view_kind::SpatialViewKind; +use crate::SpaceKind; use crate::visualizers::iter_spatial_data; -#[derive(Clone)] +#[derive(Clone, re_byte_size::SizeBytes)] pub struct SceneBoundingBoxes { /// Overall bounding box of the scene for the current query. pub current: macaw::BoundingBox, @@ -46,7 +46,7 @@ impl SceneBoundingBoxes { &mut self, ui: &egui::Ui, system_output: &SystemExecutionOutput, - space_kind: SpatialViewKind, + space_kind: SpaceKind, ) { re_tracing::profile_function!(); @@ -56,28 +56,36 @@ impl SceneBoundingBoxes { self.region_of_interest_current = macaw::BoundingBox::nothing(); self.region_of_interest_per_entity.clear(); - for (affinity, data) in iter_spatial_data(system_output) { - // If we're in a 3D space, but the visualizer is distinctly 2D, don't count it towards the bounding box. - // These visualizers show up when we're on a pinhole camera plane which itself is heuristically fed by the - // bounding box, creating a feedback loop if we were to add it here. - if space_kind == SpatialViewKind::ThreeD - && affinity == Some(crate::SpatialView2D::identifier()) - { - continue; - } + for data in iter_spatial_data(system_output) { + for bounding_box in data.iter_bounding_boxes() { + // 2D objects under a pinhole are placed on its image plane. Since the image plane + // distance may depend on the scene bounds, including them could create a feedback loop. + if space_kind == SpaceKind::ThreeD && bounding_box.subspace == SpaceKind::TwoD { + continue; + } - for (entity, bbox) in data.iter_bounding_boxes() { self.per_entity - .entry(*entity) - .and_modify(|bbox_entry| *bbox_entry = bbox_entry.union(*bbox)) - .or_insert(*bbox); + .entry(bounding_box.entity_path_hash) + .and_modify(|bbox_entry| { + *bbox_entry = bbox_entry.union(bounding_box.bounding_box); + }) + .or_insert(bounding_box.bounding_box); } - for (entity, region_of_interest) in data.iter_regions_of_interest() { + for region_of_interest in data.iter_regions_of_interest() { + // 2D objects under a pinhole are placed on its image plane. Since the image plane + // distance may depend on the region of interest, including them could create a feedback loop. + if space_kind == SpaceKind::ThreeD && region_of_interest.subspace == SpaceKind::TwoD + { + continue; + } + self.region_of_interest_per_entity - .entry(*entity) - .and_modify(|entry| *entry = entry.union(*region_of_interest)) - .or_insert(*region_of_interest); + .entry(region_of_interest.entity_path_hash) + .and_modify(|entry| { + *entry = entry.union(region_of_interest.bounding_box); + }) + .or_insert(region_of_interest.bounding_box); } } diff --git a/crates/viewer/re_view_spatial/src/shared_fallbacks.rs b/crates/viewer/re_view_spatial/src/shared_fallbacks.rs index b55d718d7b47..69256d953c60 100644 --- a/crates/viewer/re_view_spatial/src/shared_fallbacks.rs +++ b/crates/viewer/re_view_spatial/src/shared_fallbacks.rs @@ -52,6 +52,12 @@ pub fn register_fallbacks(system_registry: &mut re_viewer_context::ViewSystemReg .register_fallback_provider(component, |_ctx| components::Radius::new_ui_points(0.5)); } + // VideoReference + system_registry.register_fallback_provider( + archetypes::VideoFrameReference::descriptor_video_reference().component, + |ctx| components::EntityPath::from(ctx.target_entity_path), + ); + // Pinhole system_registry.register_fallback_provider( archetypes::Pinhole::descriptor_image_plane_distance().component, @@ -181,21 +187,26 @@ pub fn register_fallbacks(system_registry: &mut re_viewer_context::ViewSystemReg None, ); + // Note: an empty coordinate frame is treated as invalid and falls through to the implicit frame. if let Some(frame_id) = results.get_mono::( archetypes::CoordinateFrame::descriptor_frame().component, - ) { + ) && !frame_id.as_str().is_empty() + { return frame_id; } } 'scope: { + // This path only works if `TransformTreeContext` already built the transform forest. + // Creating `TransformDatabaseStoreCache` here would initialize the frame id registry, + // but still wouldn't build a useful transform forest, so a non-creating read is enough. let caches = ctx.store_ctx().caches; - let (frame_id_registry, transform_forest) = - caches.memoizer(|c: &mut re_viewer_context::TransformDatabaseStoreCache| { - (c.frame_id_registry(ctx.recording()), c.transform_forest()) - }); - - let Some(transform_forest) = transform_forest else { + let Some((frame_id_registry, transform_forest)) = caches + .memoizer_read::(|c| { + Some((c.cached_frame_id_registry()?, c.transform_forest()?)) + }) + .flatten() + else { break 'scope; }; @@ -223,6 +234,8 @@ pub fn register_fallbacks(system_registry: &mut re_viewer_context::ViewSystemReg .get_mono::( archetypes::CoordinateFrame::descriptor_frame().component, ) + // Empty coordinate frames fall back to implicit frames in transform retrieval and therefore provide no explicit root candidate. + .filter(|frame| !frame.as_str().is_empty()) .and_then(|frame| { transform_forest .root_from_frame(re_tf::TransformFrameIdHash::new(&frame)) diff --git a/crates/viewer/re_view_spatial/src/spatial_topology.rs b/crates/viewer/re_view_spatial/src/spatial_topology.rs index 9bdebde8a943..757aa88717a0 100644 --- a/crates/viewer/re_view_spatial/src/spatial_topology.rs +++ b/crates/viewer/re_view_spatial/src/spatial_topology.rs @@ -3,7 +3,7 @@ use std::sync::OnceLock; use ahash::HashMap; use nohash_hasher::{IntMap, IntSet}; use re_chunk_store::{ - ChunkStore, ChunkStoreEvent, ChunkStoreSubscriber, ChunkStoreSubscriberHandle, + ChunkStore, ChunkStoreDiff, ChunkStoreEvent, ChunkStoreSubscriber, ChunkStoreSubscriberHandle, }; use re_log::debug_assert; use re_log_types::{EntityPath, EntityPathHash, StoreId}; @@ -17,6 +17,15 @@ bitflags::bitflags! { } } +impl re_byte_size::SizeBytes for SubSpaceConnectionFlags { + const IS_POD: bool = true; + + #[inline] + fn heap_size_bytes(&self) -> u64 { + 0 + } +} + bitflags::bitflags! { /// Marks entities that are of special interest for heuristics. #[derive(PartialEq, Eq, Debug, Copy, Clone)] @@ -25,6 +34,15 @@ bitflags::bitflags! { } } +impl re_byte_size::SizeBytes for HeuristicHints { + const IS_POD: bool = true; + + #[inline] + fn heap_size_bytes(&self) -> u64 { + 0 + } +} + /// Spatial subspace within we typically expect a homogeneous dimensionality without any projections. /// /// Subspaces are separated by projections. @@ -35,7 +53,7 @@ bitflags::bitflags! { /// Within the tree of all subspaces, every entity is contained in exactly one subspace. /// The subtree at (and including) the `origin` minus the /// subtrees of all child spaces are considered to be contained in a subspace. -#[derive(Debug)] +#[derive(Debug, re_byte_size::SizeBytes)] pub struct SubSpace { /// The transform root of this subspace. /// @@ -157,20 +175,21 @@ impl ChunkStoreSubscriber for SpatialTopologyStoreSubscriber { re_tracing::profile_function!(); for event in events { - let Some(add) = event.to_addition() else { - // Topology is only additive, don't care about removals. + let ChunkStoreDiff::SchemaAddition(add) = &event.diff else { continue; }; - // Possible optimization: - // only update topologies if an entity is logged the first time or a new relevant component was added. - self.topologies - .entry(event.store_id.clone()) - .or_default() - .on_store_diff( - add.delta_chunk().entity_path(), - add.delta_chunk().component_descriptors(), - ); + for meta in &add.new_columns { + self.topologies + .entry(event.store_id.clone()) + .or_default() + .on_store_diff( + &meta.entity_path, + meta.components + .iter() + .map(|component| &component.descriptor), + ); + } } } } @@ -184,6 +203,7 @@ impl ChunkStoreSubscriber for SpatialTopologyStoreSubscriber { /// /// Spatial topology is time independent but may change as new data comes in. /// Generally, the assumption is that topological cuts stay constant over time. +#[derive(Debug, re_byte_size::SizeBytes)] pub struct SpatialTopology { /// All subspaces, identified by their origin-hash. subspaces: IntMap, @@ -198,58 +218,6 @@ pub struct SpatialTopology { has_explicit_coordinate_frame: bool, } -impl re_byte_size::SizeBytes for SubSpaceConnectionFlags { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} - -impl re_byte_size::SizeBytes for HeuristicHints { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } - - #[inline] - fn is_pod() -> bool { - true - } -} - -impl re_byte_size::SizeBytes for SubSpace { - fn heap_size_bytes(&self) -> u64 { - let Self { - origin, - entities, - child_spaces, - parent_space: _, - connection_to_parent: _, - heuristic_hints, - } = self; - origin.heap_size_bytes() - + entities.heap_size_bytes() - + child_spaces.heap_size_bytes() - + heuristic_hints.heap_size_bytes() - } -} - -impl re_byte_size::SizeBytes for SpatialTopology { - fn heap_size_bytes(&self) -> u64 { - let Self { - subspaces, - subspace_origin_per_logged_entity, - has_explicit_coordinate_frame: _, - } = self; - subspaces.heap_size_bytes() + subspace_origin_per_logged_entity.heap_size_bytes() - } -} - impl Default for SpatialTopology { fn default() -> Self { Self { diff --git a/crates/viewer/re_view_spatial/src/ui.rs b/crates/viewer/re_view_spatial/src/ui.rs index cb5998aeddf7..9ae5482544eb 100644 --- a/crates/viewer/re_view_spatial/src/ui.rs +++ b/crates/viewer/re_view_spatial/src/ui.rs @@ -1,10 +1,11 @@ -use egui::emath::OrderedFloat; use egui::text::TextWrapping; use egui::{NumExt as _, WidgetText}; +use egui::{emath::OrderedFloat, epaint::text::ByteRangeExt as _, text::ByteRange}; use macaw::BoundingBox; use re_format::format_f32; use re_sdk_types::blueprint::archetypes::EyeControls3D; use re_sdk_types::blueprint::components::VisualBounds2D; +use re_sdk_types::components::Radius; use re_sdk_types::image::ImageKind; use re_ui::UiExt as _; use re_viewer_context::{ @@ -15,11 +16,11 @@ use re_viewport_blueprint::ViewProperty; use super::eye::Eye; use super::ui_3d::View3DState; use crate::Pinhole; +use crate::SpaceKind; use crate::pickable_textured_rect::PickableRectSourceData; use crate::picking::{PickableUiRect, PickingResult}; use crate::scene_bounding_boxes::SceneBoundingBoxes; -use crate::view_kind::SpatialViewKind; -use crate::visualizers::{UiLabel, UiLabelStyle, UiLabelTarget, iter_spatial_data}; +use crate::visualizers::{Axes, UiLabel, UiLabelStyle, UiLabelTarget, iter_spatial_data}; #[derive(Clone, Copy, PartialEq, Eq)] pub enum AutoSizeUnit { @@ -39,7 +40,7 @@ impl From for WidgetText { } /// Number of images per image kind. -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, re_byte_size::SizeBytes)] pub struct ImageCounts { pub segmentation: usize, pub color: usize, @@ -47,9 +48,13 @@ pub struct ImageCounts { } /// TODO(andreas): Should turn this "inside out" - [`SpatialViewState`] should be used by `View3DState`, not the other way round. -#[derive(Clone, Default)] +#[derive(Clone, Default, re_byte_size::SizeBytes)] pub struct SpatialViewState { pub bounding_boxes: SceneBoundingBoxes, + pub show_bounding_box: bool, + + pub show_smoothed_bbox: bool, + pub show_per_entity_bbox: bool, /// Number of images per image kind processed last frame. pub image_counts_last_frame: ImageCounts, @@ -73,6 +78,10 @@ impl ViewState for SpatialViewState { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } } impl SpatialViewState { @@ -81,7 +90,7 @@ impl SpatialViewState { &mut self, ui: &egui::Ui, system_output: &re_viewer_context::SystemExecutionOutput, - space_kind: SpatialViewKind, + space_kind: SpaceKind, ) { re_tracing::profile_function!(); @@ -90,7 +99,7 @@ impl SpatialViewState { // Reset the counts and start over. self.image_counts_last_frame = Default::default(); - for (_affinity, data) in iter_spatial_data(system_output) { + for data in iter_spatial_data(system_output) { for pickable_rect in &data.pickable_rects { match &pickable_rect.source_data { PickableRectSourceData::Image { @@ -101,7 +110,7 @@ impl SpatialViewState { ImageKind::Color => self.image_counts_last_frame.color += 1, ImageKind::Depth => self.image_counts_last_frame.depth += 1, }, - PickableRectSourceData::Video => { + PickableRectSourceData::Video { .. } => { self.image_counts_last_frame.color += 1; } PickableRectSourceData::Placeholder => {} @@ -110,7 +119,7 @@ impl SpatialViewState { } } - pub fn bounding_box_ui(&self, ui: &mut egui::Ui, spatial_kind: SpatialViewKind) { + pub fn bounding_box_ui(&self, ui: &mut egui::Ui, spatial_kind: SpaceKind) { ui.grid_left_hand_label("Bounding box") .on_hover_text("The bounding box encompassing all Entities in the view right now"); ui.vertical(|ui| { @@ -120,10 +129,10 @@ impl SpatialViewState { if self.bounding_boxes.current.is_nothing() { ui.label(egui::RichText::new("empty").italics()); } else { - ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x),)); - ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y),)); - if spatial_kind == SpatialViewKind::ThreeD { - ui.label(format!("z [{} - {}]", format_f32(min.z), format_f32(max.z),)); + ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x))); + ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y))); + if spatial_kind == SpaceKind::ThreeD { + ui.label(format!("z [{} - {}]", format_f32(min.z), format_f32(max.z))); } } }); @@ -132,11 +141,7 @@ impl SpatialViewState { // Say the name out loud. It is fun! pub fn view_eye_ui(&mut self, ui: &mut egui::Ui, ctx: &ViewerContext<'_>, view_id: ViewId) { - let eye_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view_id, - ); + let eye_property = ViewProperty::from_archetype_for_view::(ctx, view_id); if ui .button("Reset") @@ -197,7 +202,7 @@ pub fn create_labels( eye3d: &Eye, parent_ui: &egui::Ui, highlights: &ViewHighlights, - spatial_kind: SpatialViewKind, + spatial_kind: SpaceKind, ) -> (Vec, Vec) { re_tracing::profile_function!(); @@ -237,7 +242,7 @@ pub fn create_labels( fonts.layout_job(egui::text::LayoutJob { sections: vec![egui::text::LayoutSection { leading_space: 0.0, - byte_range: 0..text.len(), + byte_range: ByteRange::full(text), format: format.clone(), }], text: text.to_owned(), @@ -356,7 +361,7 @@ fn resolve_label_positions( labels: &[UiLabel], ui_from_scene: &egui::emath::RectTransform, ui_from_world_3d: &glam::Mat4, - spatial_kind: SpatialViewKind, + spatial_kind: SpaceKind, ) -> Vec<(UiLabel, f32, egui::Pos2)> { let viewport = ui_from_scene.to().expand(100.0); @@ -364,7 +369,7 @@ fn resolve_label_positions( for label in labels { let (wrap_width, text_anchor_pos) = match label.target { UiLabelTarget::Rect(rect) => { - if spatial_kind == SpatialViewKind::ThreeD { + if spatial_kind == SpaceKind::ThreeD { continue; // TODO(#1640): 2D labels are not visible in 3D for now. } let rect_in_ui = ui_from_scene.transform_rect(rect); @@ -374,14 +379,14 @@ fn resolve_label_positions( ) } UiLabelTarget::Point2D(pos) => { - if spatial_kind == SpatialViewKind::ThreeD { + if spatial_kind == SpaceKind::ThreeD { continue; // TODO(#1640): 2D labels are not visible in 3D for now. } let pos_in_ui = ui_from_scene.transform_pos(pos); (f32::INFINITY, pos_in_ui) } UiLabelTarget::Position3D(pos) => { - if spatial_kind == SpatialViewKind::TwoD { + if spatial_kind == SpaceKind::TwoD { continue; // TODO(#1640): 3D labels are not visible in 2D for now. } let pos_in_ui = *ui_from_world_3d * pos.extend(1.0); @@ -453,7 +458,7 @@ pub fn paint_loading_indicators( let ui_from_world_3d = eye3d.ui_from_world(*ui_from_scene.to()); - for (_affinity, data) in iter_spatial_data(system_output) { + for data in iter_spatial_data(system_output) { for crate::visualizers::LoadingIndicator { center, half_extent_u, @@ -505,3 +510,71 @@ pub fn paint_loading_indicators( } } } + +/// UI for the debug-build-only bounding box controls. +#[cfg(debug_assertions)] +pub fn bbox_debug_ui(ui: &mut egui::Ui, state: &mut SpatialViewState) { + ui.re_checkbox(&mut state.show_smoothed_bbox, "Smoothed bbox"); + ui.re_checkbox(&mut state.show_per_entity_bbox, "Per-entity bboxes"); +} + +/// Draws the origin axes gizmo of a spatial view. +pub fn draw_origin_axes( + tokens: &re_ui::DesignTokens, + line_builder: &mut re_renderer::LineDrawableBuilder<'_>, + state: &mut SpatialViewState, + axes: Axes, +) { + let axis_length = 1.0; // The axes are also a measuring stick + crate::visualizers::add_axis_arrows( + tokens, + line_builder, + glam::Affine3A::IDENTITY, + None, + axis_length, + axes, + re_renderer::OutlineMaskPreference::NONE, + re_log_types::Instance::ALL.get(), + ); + + // If we are showing the axes for the space, then add the space origin to the region of interest, but not the scene bounding box. + state + .bounding_boxes + .region_of_interest_current + .extend(glam::Vec3::ZERO); +} + +/// Draws the enabled bounding boxes for a spatial view. +pub fn draw_bounding_boxes( + tokens: &re_ui::DesignTokens, + line_builder: &mut re_renderer::LineDrawableBuilder<'_>, + state: &SpatialViewState, +) { + // TODO(andreas): Make configurable. Could pick up default radius for this view? + let box_line_radius = re_renderer::Size(*Radius::default().0); + + // TODO(andreas): Make this an enum so the user can choose between showing + // the bounding box (all entities), the region of interest, or per-entity bounding boxes. + if state.show_bounding_box { + line_builder + .batch("scene_bbox_current") + .add_box_outline(&state.bounding_boxes.current) + .map(|lines| lines.radius(box_line_radius).color(tokens.frustum_color)); + } + + if state.show_smoothed_bbox { + line_builder + .batch("scene_region_of_interest_smoothed") + .add_box_outline(&state.bounding_boxes.region_of_interest_smoothed) + .map(|lines| lines.radius(box_line_radius).color(tokens.frustum_color)); + } + + if state.show_per_entity_bbox { + let mut batch = line_builder.batch("per_entity_regions_of_interest"); + for region_of_interest in state.bounding_boxes.region_of_interest_per_entity.values() { + batch + .add_box_outline(region_of_interest) + .map(|lines| lines.radius(box_line_radius).color(egui::Color32::YELLOW)); + } + } +} diff --git a/crates/viewer/re_view_spatial/src/ui_2d.rs b/crates/viewer/re_view_spatial/src/ui_2d.rs index b402c2045d28..b0d871193580 100644 --- a/crates/viewer/re_view_spatial/src/ui_2d.rs +++ b/crates/viewer/re_view_spatial/src/ui_2d.rs @@ -2,27 +2,29 @@ use egui::emath::RectTransform; use egui::{Align2, Pos2, Rect, Shape, Vec2, pos2, vec2}; use macaw::IsoTransform; use re_chunk_store::MissingChunkReporter; -use re_entity_db::EntityPath; use re_log::ResultExt as _; -use re_renderer::ViewPickingConfiguration; use re_renderer::view_builder::{TargetConfiguration, ViewBuilder}; -use re_sdk_types::blueprint::archetypes::{Background, NearClipPlane, VisualBounds2D}; -use re_sdk_types::blueprint::components as blueprint_components; +use re_renderer::{LineDrawableBuilder, ViewPickingConfiguration}; +use re_sdk_types::blueprint::archetypes::{ + Background, NearClipPlane, SpatialInformation, VisualBounds2D, +}; +use re_sdk_types::blueprint::components::{self as blueprint_components, Enabled}; use re_sdk_types::{Archetype as _, archetypes}; +use re_tf::TransformFrameIdHash; use re_ui::{ContextExt as _, Help, MouseButtonText, icons}; use re_view::controls::DRAG_PAN2D_BUTTON; use re_viewer_context::{ - ItemContext, QueryContext, ViewClass as _, ViewClassExt as _, ViewContext, ViewQuery, - ViewSystemExecutionError, ViewerContext, gpu_bridge, typed_fallback_for, + ItemContext, QueryContext, ViewClassExt as _, ViewContext, ViewQuery, ViewSystemExecutionError, + ViewerContext, gpu_bridge, typed_fallback_for, }; use re_viewport_blueprint::ViewProperty; use super::eye::Eye; -use super::ui::create_labels; +use super::ui::{create_labels, draw_bounding_boxes, draw_origin_axes}; +use crate::SpaceKind; use crate::contexts::TransformTreeContext; use crate::ui::SpatialViewState; -use crate::view_kind::SpatialViewKind; -use crate::visualizers::collect_ui_labels; +use crate::visualizers::{Axes, collect_ui_labels}; use crate::{Pinhole, SpatialView2D}; // --- @@ -87,11 +89,18 @@ fn ui_from_scene( .inverse() .transform_pos(zoom_center_in_ui) .to_vec2(); - bounds_rect = scale_rect( + let candidate = scale_rect( bounds_rect.translate(-zoom_center_in_scene), Vec2::splat(1.0) / zoom_delta, ) .translate(zoom_center_in_scene); + + bounds_rect = clamp_zoom_out( + bounds_rect, + candidate, + zoom_center_in_scene, + &view_state.bounding_boxes.current, + ); } } @@ -120,6 +129,55 @@ fn scale_rect(rect: Rect, factor: Vec2) -> Rect { ) } +/// Cap on how large the 2D visible area is allowed to grow, measured per-axis as a multiple +/// of the matching scene bounding box extent. +/// +/// Applied to zoom-out only: width is clamped against `scene_size.x * factor` and height +/// against `scene_size.y * factor`. An axis that is already past the limit is pinned at its +/// current size, never pulled back in; zoom-in is never restricted. +const MAX_ZOOM_OUT_FACTOR: f32 = 5.0; + +/// Cap zoom-out against the scene bounding box. +/// +/// If we're already past the cap (e.g. right after loading) use the current size instead — no +/// snap-back. Zooming in is never restricted. +fn clamp_zoom_out( + current: Rect, + candidate: Rect, + zoom_center: Vec2, + scene_bbox: &macaw::BoundingBox, +) -> Rect { + // `1.0e17` fallback is chosen with generous margin of an observed crash due to infinity. + let fallback = Vec2::splat(1.0e17); + + let max_size = if scene_bbox.is_finite() && !scene_bbox.is_nothing() { + let scene_size = scene_bbox.size(); + let max_size = vec2(scene_size.x, scene_size.y) * MAX_ZOOM_OUT_FACTOR; + if max_size.x.is_finite() && max_size.x > 0.0 && max_size.y.is_finite() && max_size.y > 0.0 + { + max_size + } else { + fallback + } + } else { + fallback + } + .max(current.size()); + + let candidate_size = candidate.size(); + let clamped_size = candidate_size.min(max_size); + + if clamped_size == candidate_size { + candidate + } else { + scale_rect( + current.translate(-zoom_center), + clamped_size / current.size(), + ) + .translate(zoom_center) + } +} + pub fn help(os: egui::os::OperatingSystem) -> Help { let egui::InputOptions { zoom_modifier, .. } = egui::InputOptions::default(); // This is OK, since we don't allow the user to change this modifier. @@ -150,67 +208,53 @@ impl SpatialView2D { return Ok(()); } - // TODO(andreas): Why don't we have this already? - let view_ctx = ViewContext { - viewer_ctx: ctx, - view_id: query.view_id, - view_class_identifier: Self::identifier(), - space_origin: query.space_origin, - view_state: state, - query_result: ctx.lookup_query_result(query.view_id), - }; - // TODO(emilk): some way to visualize the resolution rectangle of the pinhole camera (in case there is no image logged). let transforms = system_output .context_systems .get_and_report_missing::(missing_chunk_reporter)?; - state.pinhole_at_origin = transforms - .pinhole_tree_root_info(transforms.target_frame()) - .map(|pinhole_at_root| { - let pinhole = &pinhole_at_root.pinhole_projection; - - let query_ctx = QueryContext { - view_ctx: &view_ctx, - target_entity_path: query.space_origin, - instruction_id: None, - archetype_name: Some(archetypes::Pinhole::name()), - query: query.latest_at_query(), - }; - Pinhole { - image_from_camera: pinhole.image_from_camera.0.into(), - resolution: pinhole - .resolution - .unwrap_or_else(|| { - typed_fallback_for( - &query_ctx, - archetypes::Pinhole::descriptor_resolution().component, - ) - }) - .into(), - } - }); + let view_target_frame = transforms.target_frame(); + state.pinhole_at_origin = + transforms + .pinhole_tree_root_info(view_target_frame) + .map(|pinhole_at_root| { + let pinhole = &pinhole_at_root.pinhole_projection; + + let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); + let query_ctx = QueryContext { + view_ctx: &view_ctx, + target_entity_path: query.space_origin, + instruction_id: None, + archetype_name: Some(archetypes::Pinhole::name()), + query: query.latest_at_query(), + }; + Pinhole { + image_from_camera: pinhole.image_from_camera.0.into(), + resolution: pinhole + .resolution + .unwrap_or_else(|| { + typed_fallback_for( + &query_ctx, + archetypes::Pinhole::descriptor_resolution().component, + ) + }) + .into(), + } + }); let (response, painter) = ui.allocate_painter(ui.available_size(), egui::Sense::click_and_drag()); let ui_rect = response.rect; - let bounds_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); - let clip_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); + let bounds_property = ViewProperty::from_archetype::(&view_ctx); + let clip_property = ViewProperty::from_archetype::(&view_ctx); // Convert ui coordinates to/from scene coordinates. let ui_from_scene = { - let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); let mut new_state = state.clone(); let ui_from_scene = ui_from_scene(&view_ctx, &response, &mut new_state, &bounds_property); + *state = new_state; ui_from_scene @@ -242,7 +286,7 @@ impl SpatialView2D { &eye, ui, &query.highlights, - SpatialViewKind::TwoD, + SpaceKind::TwoD, ); let picking_config = if let Some(pointer_pos_ui) = response.hover_pos() { @@ -262,7 +306,7 @@ impl SpatialView2D { &system_output, &label_ui_rects, query, - SpatialViewKind::TwoD, + SpaceKind::TwoD, )?; picking_config } else { @@ -284,19 +328,42 @@ impl SpatialView2D { ) else { return Ok(()); }; - let mut view_builder = ViewBuilder::new(ctx.render_ctx(), target_config)?; + let mut view_builder = ViewBuilder::new( + ctx.render_ctx(), + target_config, + query.view_id.render_view_id(), + )?; + + let (show_axes, show_bounding_box) = { + let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); + let information_property = + ViewProperty::from_archetype::(&view_ctx); + let show_axes = **information_property.component_or_fallback::( + &view_ctx, + SpatialInformation::descriptor_show_axes().component, + )?; + let show_bounding_box = **information_property.component_or_fallback::( + &view_ctx, + SpatialInformation::descriptor_show_bounding_box().component, + )?; + (show_axes, show_bounding_box) + }; + state.show_bounding_box = show_bounding_box; - let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); // Recreate view state to handle context editing during picking. + let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx()); + + if show_axes { + draw_origin_axes(ctx.tokens(), &mut line_builder, state, Axes::Xy); + } + draw_bounding_boxes(ctx.tokens(), &mut line_builder, state); for draw_data in system_output.drain_draw_data() { view_builder.queue_draw(ctx.render_ctx(), draw_data); } + view_builder.queue_draw(ctx.render_ctx(), line_builder.into_draw_data()?); - let background = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); + let background = ViewProperty::from_archetype::(&view_ctx); let (background_drawable, clear_color) = crate::configure_background(&view_ctx, &background)?; if let Some(background_drawable) = background_drawable { @@ -317,7 +384,7 @@ impl SpatialView2D { for selected_context in ctx.selection_state().selection_item_contexts() { painter.extend(show_projections_from_3d_space( ui, - query.space_origin, + view_target_frame, &ui_from_scene, selected_context, ui.selection_stroke().color, @@ -326,7 +393,7 @@ impl SpatialView2D { if let Some(hovered_context) = ctx.selection_state().hovered_item_context() { painter.extend(show_projections_from_3d_space( ui, - query.space_origin, + view_target_frame, &ui_from_scene, hovered_context, ui.hover_stroke().color, @@ -343,7 +410,6 @@ impl SpatialView2D { } } -#[expect(clippy::too_many_arguments)] fn setup_target_config( render_mode: re_renderer::RenderMode, egui_painter: &egui::Painter, @@ -461,19 +527,18 @@ fn re_render_rect_from_egui_rect(rect: egui::Rect) -> re_renderer::RectF32 { fn show_projections_from_3d_space( ui: &egui::Ui, - space: &EntityPath, + target_frame: TransformFrameIdHash, ui_from_scene: &RectTransform, item_context: &ItemContext, circle_fill_color: egui::Color32, ) -> Vec { let mut shapes = Vec::new(); if let ItemContext::ThreeD { - point_in_space_cameras: target_spaces, - .. + point_in_2d_spaces, .. } = item_context { - for (space_2d, pos_2d) in target_spaces { - if space_2d == space + for (space_2d_root, pos_2d) in point_in_2d_spaces { + if *space_2d_root == target_frame && let Some(pos_2d) = pos_2d { // User is hovering a 2D point inside a 3D view. diff --git a/crates/viewer/re_view_spatial/src/ui_3d.rs b/crates/viewer/re_view_spatial/src/ui_3d.rs index 624171e40417..335943e37c2d 100644 --- a/crates/viewer/re_view_spatial/src/ui_3d.rs +++ b/crates/viewer/re_view_spatial/src/ui_3d.rs @@ -3,7 +3,6 @@ use egui::{Modifiers, NumExt as _}; use glam::Vec3; use macaw::BoundingBox; use re_chunk_store::MissingChunkReporter; -use re_log_types::Instance; use re_renderer::view_builder::{Projection, TargetConfiguration, ViewBuilder}; use re_renderer::{LineDrawableBuilder, Size}; use re_sdk_types::blueprint::archetypes::{ @@ -24,16 +23,16 @@ use re_viewer_context::{ use re_viewport_blueprint::ViewProperty; use super::eye::{Eye, EyeState}; +use crate::SpaceKind; use crate::SpatialView3D; use crate::eye::find_camera; use crate::pinhole_wrapper::PinholeWrapper; -use crate::ui::{SpatialViewState, create_labels}; -use crate::view_kind::SpatialViewKind; -use crate::visualizers::{CamerasVisualizerOutput, collect_ui_labels}; +use crate::ui::{SpatialViewState, create_labels, draw_bounding_boxes, draw_origin_axes}; +use crate::visualizers::{Axes, CamerasVisualizerOutput, collect_ui_labels}; // --- -#[derive(Clone)] +#[derive(Clone, re_byte_size::SizeBytes)] pub struct View3DState { pub eye_state: EyeState, @@ -43,9 +42,6 @@ pub struct View3DState { eye_interact_fade_in: bool, eye_interact_fade_change_time: f64, - - pub show_smoothed_bbox: bool, - pub show_per_entity_bbox: bool, } impl Default for View3DState { @@ -55,8 +51,6 @@ impl Default for View3DState { scene_view_coordinates: None, eye_interact_fade_in: false, eye_interact_fade_change_time: f64::NEG_INFINITY, - show_smoothed_bbox: false, - show_per_entity_bbox: false, } } } @@ -132,13 +126,11 @@ impl SpatialView3D { re_tracing::profile_function!(); let highlights = &query.highlights; - let empty_cameras = Vec::new(); - let space_cameras = system_output - .visualizer_data::( - crate::visualizers::CamerasVisualizer::identifier(), - ) - .map(|d| &d.pinhole_cameras) - .unwrap_or(&empty_cameras); + let cameras = system_output.visualizer_data_or_default::( + crate::visualizers::CamerasVisualizer::identifier(), + )?; + let space_cameras = &cameras.pinhole_cameras; + let scene_view_coordinates = query_view_coordinates_at_closest_ancestor( query.space_origin, ctx.recording(), @@ -156,11 +148,8 @@ impl SpatialView3D { let view_context = self.view_context(ctx, query.view_id, state, query.space_origin); - let information_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let information_property = + ViewProperty::from_archetype::(&view_context); let show_axes = **information_property.component_or_fallback::( &view_context, @@ -172,14 +161,25 @@ impl SpatialView3D { )?; state_3d.update(scene_view_coordinates); + let is_selected_view = ctx + .selection_state() + .selected_items() + .single_item() + .and_then(Item::view_id) + == Some(query.view_id); + let enable_gamepad_navigation = + ctx.app_options().experimental.gamepad_navigation && is_selected_view; + let eye = state_3d.eye_state.update( &view_context, &response, space_cameras, &state.bounding_boxes, + enable_gamepad_navigation, )?; state.state_3d = state_3d; + state.show_bounding_box = show_bounding_box; // Determine view port resolution and position. let resolution_in_pixel = @@ -197,27 +197,10 @@ impl SpatialView3D { line_builder.reserve_strips(32)?; line_builder.reserve_vertices(64)?; - // Origin gizmo if requested. - // TODO(andreas): Move this to the transform3d_arrow scene part. - // As of #2522 state is now longer accessible there, move the property to a context? if show_axes { - let axis_length = 1.0; // The axes are also a measuring stick - crate::visualizers::add_axis_arrows( - ctx.tokens(), - &mut line_builder, - glam::Affine3A::IDENTITY, - None, - axis_length, - re_renderer::OutlineMaskPreference::NONE, - Instance::ALL.get(), - ); - - // If we are showing the axes for the space, then add the space origin to the region of interest, but not the scene bounding box. - state - .bounding_boxes - .region_of_interest_current - .extend(glam::Vec3::ZERO); + draw_origin_axes(ctx.tokens(), &mut line_builder, state, Axes::Xyz); } + draw_bounding_boxes(ctx.tokens(), &mut line_builder, state); // Create labels now since their shapes participate are added to scene.ui for picking. let (label_shapes, ui_rects) = create_labels( @@ -226,7 +209,7 @@ impl SpatialView3D { &eye, ui, highlights, - SpatialViewKind::ThreeD, + SpaceKind::ThreeD, ); let (response, picking_config) = if let Some(pointer_pos_ui) = response.hover_pos() { @@ -249,7 +232,7 @@ impl SpatialView3D { &system_output, &ui_rects, query, - SpatialViewKind::ThreeD, + SpaceKind::ThreeD, )? } else { state.previous_picking_result = None; @@ -280,13 +263,14 @@ impl SpatialView3D { picking_config, }; - let mut view_builder = ViewBuilder::new(ctx.render_ctx(), target_config)?; + let mut view_builder = ViewBuilder::new( + ctx.render_ctx(), + target_config, + query.view_id.render_view_id(), + )?; - let eye_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let eye_property = + ViewProperty::from_archetype_for_view::(ctx, query.view_id); // Track focused entity if any. if let Some(focused_item) = ctx.focused_item() { @@ -386,40 +370,6 @@ impl SpatialView3D { ); } - // TODO(andreas): Make configurable. Could pick up default radius for this view? - let box_line_radius = Size(*re_sdk_types::components::Radius::default().0); - - // TODO(andreas): Make this an enum so the user can choose between showing - // the bounding box (all entities), the region of interest, or per-entity bounding boxes. - if show_bounding_box { - line_builder - .batch("scene_bbox_current") - .add_box_outline(&state.bounding_boxes.current) - .map(|lines| { - lines - .radius(box_line_radius) - .color(ui.tokens().frustum_color) - }); - } - if state.state_3d.show_smoothed_bbox { - line_builder - .batch("scene_region_of_interest_smoothed") - .add_box_outline(&state.bounding_boxes.region_of_interest_smoothed) - .map(|lines| { - lines - .radius(box_line_radius) - .color(ctx.tokens().frustum_color) - }); - } - if state.state_3d.show_per_entity_bbox { - let mut batch = line_builder.batch("per_entity_regions_of_interest"); - for region_of_interest in state.bounding_boxes.region_of_interest_per_entity.values() { - batch - .add_box_outline(region_of_interest) - .map(|lines| lines.radius(box_line_radius).color(egui::Color32::YELLOW)); - } - } - show_orbit_eye_center( ui.ctx(), &mut state.state_3d, @@ -434,11 +384,7 @@ impl SpatialView3D { let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); // Optional 3D line grid. - let grid_config = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let grid_config = ViewProperty::from_archetype::(&view_ctx); if let Some(draw_data) = Self::setup_grid_3d(&view_ctx, &grid_config)? { view_builder.queue_draw(ctx.render_ctx(), draw_data); } @@ -446,11 +392,7 @@ impl SpatialView3D { // Commit ui induced lines. view_builder.queue_draw(ctx.render_ctx(), line_builder.into_draw_data()?); - let background = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let background = ViewProperty::from_archetype::(&view_ctx); let (background_drawable, clear_color) = crate::configure_background(&view_ctx, &background)?; if let Some(background_drawable) = background_drawable { @@ -627,8 +569,14 @@ fn show_projections_from_2d_space( ray_color: egui::Color32, ) { match item_context { - ItemContext::TwoD { space_2d, pos } => { - if let Some(cam) = cameras.iter().find(|cam| &cam.ent_path == space_2d) { + ItemContext::TwoD { + space_2d_target_frame, + pos, + } => { + if let Some(cam) = cameras + .iter() + .find(|cam| &cam.pinhole_child_frame_id == space_2d_target_frame) + { // Render a thick line to the actual z value if any and a weaker one as an extension // If we don't have a z value, we only render the thick one. let depth = if 0.0 < pos.z && pos.z.is_finite() { diff --git a/crates/viewer/re_view_spatial/src/view_2d.rs b/crates/viewer/re_view_spatial/src/view_2d.rs index deff53d5e3b1..9895ad183eec 100644 --- a/crates/viewer/re_view_spatial/src/view_2d.rs +++ b/crates/viewer/re_view_spatial/src/view_2d.rs @@ -2,7 +2,9 @@ use nohash_hasher::{IntMap, IntSet}; use re_chunk_store::MissingChunkReporter; use re_entity_db::{EntityDb, EntityTree}; use re_log_types::EntityPath; -use re_sdk_types::blueprint::archetypes::{Background, NearClipPlane, VisualBounds2D}; +use re_sdk_types::blueprint::archetypes::{ + Background, NearClipPlane, SpatialInformation, VisualBounds2D, +}; use re_sdk_types::{View as _, ViewClassIdentifier}; use re_ui::{Help, UiExt as _}; use re_view::view_property_ui; @@ -11,13 +13,15 @@ use re_viewer_context::{ ViewSpawnHeuristics, ViewState, ViewStateExt as _, ViewSystemExecutionError, ViewerContext, }; +use crate::SpaceKind; use crate::contexts::register_spatial_contexts; use crate::heuristics::IndicatedVisualizableEntities; use crate::max_image_dimension_subscriber::{ImageTypes, MaxDimensions}; use crate::shared_fallbacks; use crate::spatial_topology::{SpatialTopology, SubSpaceConnectionFlags}; use crate::ui::SpatialViewState; -use crate::view_kind::SpatialViewKind; +#[cfg(debug_assertions)] +use crate::ui::bbox_debug_ui; use crate::visualizers::register_2d_spatial_visualizers; #[derive(Default)] @@ -237,11 +241,15 @@ impl ViewClass for SpatialView2D { let state = state.downcast_mut::()?; // TODO(andreas): list_item'ify the rest ui.selection_grid("spatial_settings_ui").show(ui, |ui| { - state.bounding_box_ui(ui, SpatialViewKind::TwoD); + state.bounding_box_ui(ui, SpaceKind::TwoD); + + #[cfg(debug_assertions)] + bbox_debug_ui(ui, state); }); re_ui::list_item::list_item_scope(ui, "spatial_view2d_selection_ui", |ui| { let view_ctx = self.view_context(ctx, view_id, state, space_origin); + view_property_ui::(&view_ctx, ui); view_property_ui::(&view_ctx, ui); view_property_ui::(&view_ctx, ui); view_property_ui::(&view_ctx, ui); @@ -262,7 +270,7 @@ impl ViewClass for SpatialView2D { re_tracing::profile_function!(); let state = state.downcast_mut::()?; - state.update_frame_statistics(ui, &system_output, SpatialViewKind::TwoD); + state.update_frame_statistics(ui, &system_output, SpaceKind::TwoD); self.view_2d(ctx, missing_chunk_reporter, ui, state, query, system_output) } diff --git a/crates/viewer/re_view_spatial/src/view_3d.rs b/crates/viewer/re_view_spatial/src/view_3d.rs index 91c9af976fa9..8577b111b250 100644 --- a/crates/viewer/re_view_spatial/src/view_3d.rs +++ b/crates/viewer/re_view_spatial/src/view_3d.rs @@ -25,12 +25,14 @@ use re_viewer_context::{ }; use re_viewport_blueprint::ViewProperty; +use crate::SpaceKind; use crate::contexts::register_spatial_contexts; use crate::heuristics::IndicatedVisualizableEntities; use crate::shared_fallbacks; use crate::spatial_topology::{HeuristicHints, SpatialTopology, SubSpaceConnectionFlags}; use crate::ui::SpatialViewState; -use crate::view_kind::SpatialViewKind; +#[cfg(debug_assertions)] +use crate::ui::bbox_debug_ui; use crate::visualizers::{ CamerasVisualizer, TransformAxes3DVisualizer, register_3d_spatial_visualizers, }; @@ -105,11 +107,7 @@ impl ViewClass for SpatialView3D { ); fn eye_property(ctx: &QueryContext<'_>) -> ViewProperty { - ViewProperty::from_archetype::( - ctx.view_ctx.blueprint_db(), - ctx.view_ctx.blueprint_query(), - ctx.view_ctx.view_id, - ) + ViewProperty::from_archetype::(ctx.view_ctx) } system_registry.register_fallback_provider( @@ -516,16 +514,10 @@ impl ViewClass for SpatialView3D { }); ui.end_row(); - state.bounding_box_ui(ui, SpatialViewKind::ThreeD); + state.bounding_box_ui(ui, SpaceKind::ThreeD); #[cfg(debug_assertions)] - { - ui.re_checkbox(&mut state.state_3d.show_smoothed_bbox, "Smoothed bbox"); - ui.re_checkbox( - &mut state.state_3d.show_per_entity_bbox, - "Per-entity bboxes", - ); - } + bbox_debug_ui(ui, state); }); re_ui::list_item::list_item_scope(ui, "spatial_view3d_selection_ui", |ui| { @@ -551,7 +543,7 @@ impl ViewClass for SpatialView3D { re_tracing::profile_function!(); let state = state.downcast_mut::()?; - state.update_frame_statistics(ui, &system_output, SpatialViewKind::ThreeD); + state.update_frame_statistics(ui, &system_output, SpaceKind::ThreeD); self.view_3d(ctx, missing_chunk_reporter, ui, state, query, system_output) } @@ -561,11 +553,7 @@ impl ViewClass for SpatialView3D { // is suitable for the most part. However, as of writing the alpha color picker doesn't handle alpha // which we need here. fn view_property_ui_grid3d(ctx: &ViewContext<'_>, ui: &mut egui::Ui) { - let property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - ctx.view_id, - ); + let property = ViewProperty::from_archetype::(ctx); let reflection = ctx.viewer_ctx.reflection(); let Some(reflection) = reflection.archetypes.get(&property.archetype_name) else { ui.error_label(format!( diff --git a/crates/viewer/re_view_spatial/src/visualizers/arrows2d.rs b/crates/viewer/re_view_spatial/src/visualizers/arrows2d.rs index eaf0ed39feb9..66d9dbbfa688 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/arrows2d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/arrows2d.rs @@ -109,7 +109,7 @@ impl Arrows2DVisualizer { obj_space_bounding_box.extend(end.extend(0.0)); } - data.add_bounding_box(entity_path.hash(), obj_space_bounding_box, world_from_obj); + data.add_bounding_box_2d(entity_path.hash(), obj_space_bounding_box, world_from_obj); data.ui_labels.extend(process_labels_2d( LabeledBatch { @@ -157,7 +157,10 @@ struct Arrows2DComponentData<'a> { impl IdentifiedViewSystem for Arrows2DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Arrows2D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Arrows2D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/arrows3d.rs b/crates/viewer/re_view_spatial/src/visualizers/arrows3d.rs index 91e142856dd9..cbe690b167f3 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/arrows3d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/arrows3d.rs @@ -106,7 +106,7 @@ impl Arrows3DVisualizer { obj_space_bounding_box.extend(end); } - data.add_bounding_box(entity_path.hash(), obj_space_bounding_box, world_from_obj); + data.add_bounding_box_3d(entity_path.hash(), obj_space_bounding_box, world_from_obj); { let instance_positions = { @@ -159,7 +159,10 @@ struct Arrows3DComponentData<'a> { impl IdentifiedViewSystem for Arrows3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Arrows3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Arrows3D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/assets3d.rs b/crates/viewer/re_view_spatial/src/visualizers/assets3d.rs index 7eb18aa091e9..26c89f680965 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/assets3d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/assets3d.rs @@ -55,7 +55,7 @@ impl Asset3DVisualizer { }; c.entry( - &entity_path.to_string(), + entity_path, key.clone(), AnyMesh::Asset { asset: crate::mesh_loader::NativeAsset3D { @@ -92,7 +92,7 @@ impl Asset3DVisualizer { } })); - data.add_bounding_box(entity_path.hash(), mesh.bbox(), world_from_pose); + data.add_bounding_box_3d(entity_path.hash(), mesh.bbox(), world_from_pose); } } } @@ -101,7 +101,10 @@ impl Asset3DVisualizer { impl IdentifiedViewSystem for Asset3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Asset3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Asset3D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/boxes2d.rs b/crates/viewer/re_view_spatial/src/visualizers/boxes2d.rs index e0c81fe94c0b..6a10663f30b0 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/boxes2d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/boxes2d.rs @@ -103,7 +103,11 @@ impl Boxes2DVisualizer { } } - view_data.add_bounding_box(entity_path.hash(), obj_space_bounding_box, world_from_obj); + view_data.add_bounding_box_2d( + entity_path.hash(), + obj_space_bounding_box, + world_from_obj, + ); view_data.ui_labels.extend(process_labels( LabeledBatch { @@ -113,19 +117,18 @@ impl Boxes2DVisualizer { overall_position: UiLabelTarget::Point2D( <[f32; 2]>::from(obj_space_bounding_box.center().truncate()).into(), ), - instance_positions: data - .half_sizes - .iter() - .copied() - .zip(clamped_or(data.centers, &Position2D::ZERO).copied()) - .map(|(half_size, center)| { - let min = half_size.box_min(center); - let max = half_size.box_max(center); - UiLabelTarget::Rect(egui::Rect::from_min_max( - egui::pos2(min.x, min.y), - egui::pos2(max.x, max.y), - )) - }), + instance_positions: std::iter::zip( + data.half_sizes.iter().copied(), + clamped_or(data.centers, &Position2D::ZERO).copied(), + ) + .map(|(half_size, center)| { + let min = half_size.box_min(center); + let max = half_size.box_max(center); + UiLabelTarget::Rect(egui::Rect::from_min_max( + egui::pos2(min.x, min.y), + egui::pos2(max.x, max.y), + )) + }), labels: &data.labels, colors: &colors, show_labels: data.show_labels.unwrap_or_else(|| { @@ -158,7 +161,10 @@ struct Boxes2DComponentData<'a> { impl IdentifiedViewSystem for Boxes2DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Boxes2D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Boxes2D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/boxes3d.rs b/crates/viewer/re_view_spatial/src/visualizers/boxes3d.rs index e1feca09b275..3e01719ac7c8 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/boxes3d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/boxes3d.rs @@ -90,7 +90,10 @@ struct Boxes3DComponentData<'a> { impl IdentifiedViewSystem for Boxes3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Boxes3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Boxes3D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/cameras.rs b/crates/viewer/re_view_spatial/src/visualizers/cameras.rs index bc1b31606ed8..9e69ef5dbd2c 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/cameras.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/cameras.rs @@ -12,12 +12,13 @@ use re_viewer_context::{ }; use super::SpatialViewVisualizerData; +use crate::SpaceKind; use crate::contexts::TransformTreeContext; use crate::pinhole_wrapper::PinholeWrapper; -use crate::view_kind::SpatialViewKind; use crate::visualizers::process_radius; use crate::visualizers::utilities::spatial_view_kind_from_view_class; +#[derive(Default, Clone)] pub struct CamerasVisualizerOutput { pub pinhole_cameras: Vec, } @@ -27,7 +28,10 @@ pub struct CamerasVisualizer; impl IdentifiedViewSystem for CamerasVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Cameras".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Cameras" + ) } } @@ -41,7 +45,6 @@ struct CameraComponentDataWithFallbacks { } impl CamerasVisualizer { - #[expect(clippy::too_many_arguments)] fn visit_instance( data: &mut SpatialViewVisualizerData, pinhole_cameras: &mut Vec, @@ -50,7 +53,7 @@ impl CamerasVisualizer { transforms: &TransformTreeContext, pinhole_properties: &CameraComponentDataWithFallbacks, entity_highlight: &ViewOutlineMasks, - view_kind: SpatialViewKind, + view_kind: SpaceKind, ) -> Result<(), String> { let instance = Instance::from(0); let ent_path = ctx.target_entity_path; @@ -109,9 +112,9 @@ impl CamerasVisualizer { }; // If the camera is the target frame of a 2D view, there is nothing for us to display. - if transforms.target_frame() == pinhole_child_frame_id && view_kind == SpatialViewKind::TwoD - { + if transforms.target_frame() == pinhole_child_frame_id && view_kind == SpaceKind::TwoD { pinhole_cameras.push(PinholeWrapper { + pinhole_child_frame_id, ent_path: ent_path.clone(), pinhole_view_coordinates: pinhole_properties.camera_xyz, world_from_camera: macaw::IsoTransform::IDENTITY, @@ -136,6 +139,7 @@ impl CamerasVisualizer { re_log::debug_assert!(world_from_camera_iso.is_finite()); pinhole_cameras.push(PinholeWrapper { + pinhole_child_frame_id, ent_path: ent_path.clone(), pinhole_view_coordinates: pinhole_properties.camera_xyz, world_from_camera: world_from_camera_iso, @@ -217,7 +221,7 @@ impl CamerasVisualizer { } // world_from_camera is the transform to the pinhole origin. - data.add_bounding_box(ent_path.hash(), macaw::BoundingBox::ZERO, world_from_camera); + data.add_bounding_box_3d(ent_path.hash(), macaw::BoundingBox::ZERO, world_from_camera); Ok(()) } diff --git a/crates/viewer/re_view_spatial/src/visualizers/capsules3d.rs b/crates/viewer/re_view_spatial/src/visualizers/capsules3d.rs index 93a493474f37..ffde2dc42b68 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/capsules3d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/capsules3d.rs @@ -58,9 +58,8 @@ impl Capsules3DVisualizer { let axes_only = batch.fill_mode.axes_only(); - let meshes = lengths_iter - .zip(radii.iter()) - .map(|(Length(length), &Radius(radius))| { + let meshes = + std::iter::zip(lengths_iter, &radii).map(|(Length(length), &Radius(radius))| { let ratio = clean_length(length.0 / radius.0); // Avoid generating extremely similar meshes by rounding the ratio. @@ -127,7 +126,10 @@ struct Capsules3DComponentData<'a> { impl IdentifiedViewSystem for Capsules3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Capsules3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Capsules3D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/cylinders3d.rs b/crates/viewer/re_view_spatial/src/visualizers/cylinders3d.rs index d132a3325652..56e8f091bcc5 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/cylinders3d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/cylinders3d.rs @@ -49,8 +49,7 @@ impl Cylinders3DVisualizer { }) .take(num_instances); - let half_sizes: Vec = lengths_iter - .zip(radii_iter) + let half_sizes: Vec = std::iter::zip(lengths_iter, radii_iter) .map(|(Length(length), Radius(radius))| { let radius = clean_length(radius.0); // Cylinder radius is already half the diameter, so we can use it directly. @@ -122,7 +121,10 @@ struct Cylinders3DComponentData<'a> { impl IdentifiedViewSystem for Cylinders3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Cylinders3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Cylinders3D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/depth_images.rs b/crates/viewer/re_view_spatial/src/visualizers/depth_images.rs index 31f4271b1aee..1064946f0e28 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/depth_images.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/depth_images.rs @@ -21,11 +21,13 @@ use super::entity_iterator::process_archetype; use super::{SpatialViewVisualizerData, textured_rect_from_image}; use crate::contexts::{SpatialSceneVisualizerInstructionContext, TransformTreeContext}; use crate::visualizers::first_copied; -use crate::{PickableRectSourceData, PickableTexturedRect, SpatialView3D}; +use crate::{PickableRectSourceData, PickableTexturedRect, SpaceKind, SpatialView3D}; use re_sdk_types::reflection::Enum as _; pub struct DepthImageProcessResult { - pub image_info: ImageInfo, + /// Raw image data for pixel-level picking. + /// `None` for video-decoded depth images where raw pixel data isn't available. + pub image_info: Option, pub depth_meter: DepthMeter, pub colormap: ColormappedTexture, } @@ -46,7 +48,6 @@ pub struct DepthImageComponentData { pub magnification_filter: MagnificationFilter, } -#[expect(clippy::too_many_arguments)] pub fn process_depth_image_data( ctx: &QueryContext<'_>, ent_context: &SpatialSceneVisualizerInstructionContext<'_>, @@ -81,9 +82,9 @@ pub fn process_depth_image_data( .map(|r| [r[0] as f32, r[1] as f32]) .unwrap_or_else(|| { // Don't use fallback provider since it has to query information we already have. - let image_stats = ctx - .store_ctx() - .memoizer(|c: &mut ImageStatsCache| c.entry(&image_info)); + let store_ctx = ctx.store_ctx(); + let image_stats = + store_ctx.memoizer_read_or_compute::(&image_info); ColormapWithRange::default_range_for_depth_images(&image_stats) }); let colormap_with_range = ColormapWithRange { @@ -134,7 +135,7 @@ pub fn process_depth_image_data( fill_ratio, &textured_rect.colormapped_texture, ); - data_store.add_bounding_box( + data_store.add_bounding_box_3d( entity_path.hash(), cloud.world_space_bbox(), glam::Affine3A::IDENTITY, @@ -142,7 +143,7 @@ pub fn process_depth_image_data( depth_cloud_entities.insert( entity_path.hash(), DepthImageProcessResult { - image_info, + image_info: Some(image_info), depth_meter, colormap: textured_rect.colormapped_texture, }, @@ -164,7 +165,7 @@ pub fn process_depth_image_data( depth_meter: Some(depth_meter), }, }, - ent_context.view_class_identifier, + SpaceKind::TwoD, ); } } @@ -187,6 +188,8 @@ fn process_entity_view_as_depth_cloud( let dimensions = glam::UVec2::from_array(depth_texture.texture.width_height()); + // Depth meter defines how many texture units we need for a single world unit. + // Therefore, the scaling factor for texture depth -> world depth is the inverse of that: let world_depth_from_texture_depth = 1.0 / *depth_meter.0; // We want point radius to be defined in a scale where the radius of a point @@ -221,7 +224,10 @@ fn process_entity_view_as_depth_cloud( impl IdentifiedViewSystem for DepthImageVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "DepthImage".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "DepthImage" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/ellipses2d.rs b/crates/viewer/re_view_spatial/src/visualizers/ellipses2d.rs new file mode 100644 index 000000000000..0c56618ce65e --- /dev/null +++ b/crates/viewer/re_view_spatial/src/visualizers/ellipses2d.rs @@ -0,0 +1,294 @@ +use re_log_types::Instance; +use re_renderer::{LineDrawableBuilder, PickingLayerInstanceId}; +use re_sdk_types::archetypes::Ellipses2D; +use re_sdk_types::components::{ClassId, Color, HalfSize2D, Position2D, Radius, ShowLabels}; +use re_sdk_types::{Archetype as _, ArrowString}; +use re_view::{clamped_or, process_annotation_slices, process_color_slice}; +use re_viewer_context::{ + IdentifiedViewSystem, QueryContext, ViewClass as _, ViewContext, ViewContextCollection, + ViewQuery, ViewSystemExecutionError, VisualizerExecutionOutput, VisualizerQueryInfo, + VisualizerSystem, typed_fallback_for, +}; + +use super::utilities::{LabeledBatch, process_labels}; +use super::{SpatialViewVisualizerData, process_radius_slice}; +use crate::contexts::SpatialSceneVisualizerInstructionContext; +use crate::visualizers::UiLabelTarget; + +// --- + +/// Number of segments used to tessellate each ellipse outline. +/// +/// The polyline closes, so we emit `ELLIPSE_SEGMENTS + 1` vertices per ellipse. +const ELLIPSE_SEGMENTS: usize = 64; + +/// Extra padding added around the tight ellipse extents when computing the scene bounding box, +/// so auto-fit views don't clip the outline at the edges. +const BOUNDING_BOX_PADDING_FACTOR: f32 = 1.1; + +#[derive(Default)] +pub struct Ellipses2DVisualizer; + +// NOTE: Do not put profile scopes in these methods. They are called for all entities and all +// timestamps within a time range -- it's _a lot_. +impl Ellipses2DVisualizer { + fn process_data<'a>( + view_data: &mut SpatialViewVisualizerData, + ctx: &QueryContext<'_>, + line_builder: &mut LineDrawableBuilder<'_>, + view_query: &ViewQuery<'_>, + ent_context: &SpatialSceneVisualizerInstructionContext<'_>, + data: impl Iterator>, + ) { + let entity_path = ctx.target_entity_path; + + for data in data { + let num_instances = data.half_sizes.len(); + if num_instances == 0 { + continue; + } + + let annotation_infos = process_annotation_slices( + view_query.latest_at, + num_instances, + data.class_ids, + &ent_context.annotations, + ); + + let radii = process_radius_slice( + ctx, + entity_path, + num_instances, + data.line_radii, + Ellipses2D::descriptor_line_radii().component, + ); + let colors = process_color_slice( + ctx, + Ellipses2D::descriptor_colors().component, + num_instances, + &annotation_infos, + data.colors, + ); + + let world_from_obj = ent_context + .transform_info + .single_transform_required_for_entity(entity_path, Ellipses2D::name()) + .as_affine3a(); + + let mut line_batch = line_builder + .batch("ellipses2d") + .depth_offset(ent_context.depth_offset) + .world_from_obj(world_from_obj) + .outline_mask_ids(ent_context.highlight.overall) + .picking_object_id(re_renderer::PickingLayerObjectId(entity_path.hash64())); + + let mut obj_space_bounding_box = macaw::BoundingBox::nothing(); + + let centers = clamped_or(data.centers, &Position2D::ZERO); + + for (i, (half_size, center, radius, &color)) in + itertools::izip!(data.half_sizes, centers, radii, &colors).enumerate() + { + let cx = center.x(); + let cy = center.y(); + let rx = half_size.x(); + let ry = half_size.y(); + + let padded_half = glam::vec2(rx, ry) * BOUNDING_BOX_PADDING_FACTOR; + let bbox_center = glam::vec2(cx, cy); + obj_space_bounding_box.extend((bbox_center - padded_half).extend(0.0)); + obj_space_bounding_box.extend((bbox_center + padded_half).extend(0.0)); + + let delta = std::f32::consts::TAU / ELLIPSE_SEGMENTS as f32; + let points = (0..ELLIPSE_SEGMENTS + 1).map(|n| { + let theta = n as f32 * delta; + glam::vec2(cx + rx * theta.cos(), cy + ry * theta.sin()) + }); + + let ellipse = line_batch + .add_strip_2d(points) + .flags(LineDrawableBuilder::default_shape_flags()) + .color(color) + .radius(radius) + .picking_instance_id(PickingLayerInstanceId(i as _)); + if let Some(outline_mask_ids) = ent_context + .highlight + .instances + .get(&Instance::from(i as u64)) + { + ellipse.outline_mask_ids(*outline_mask_ids); + } + } + + view_data.add_bounding_box_2d( + entity_path.hash(), + obj_space_bounding_box, + world_from_obj, + ); + + view_data.ui_labels.extend(process_labels( + LabeledBatch { + entity_path, + visualizer_instruction: ent_context.visualizer_instruction, + num_instances, + overall_position: UiLabelTarget::Point2D( + <[f32; 2]>::from(obj_space_bounding_box.center().truncate()).into(), + ), + instance_positions: clamped_or(data.centers, &Position2D::ZERO) + .map(|center| UiLabelTarget::Point2D(egui::pos2(center.x(), center.y()))), + labels: &data.labels, + colors: &colors, + show_labels: data.show_labels.unwrap_or_else(|| { + typed_fallback_for(ctx, Ellipses2D::descriptor_show_labels().component) + }), + annotation_infos: &annotation_infos, + }, + std::convert::identity, + )); + } + } +} + +// --- + +struct Ellipses2DComponentData<'a> { + // Point of views + half_sizes: &'a [HalfSize2D], + + // Clamped to edge + centers: &'a [Position2D], + colors: &'a [Color], + line_radii: &'a [Radius], + labels: Vec, + class_ids: &'a [ClassId], + + // Non-repeated + show_labels: Option, +} + +impl IdentifiedViewSystem for Ellipses2DVisualizer { + fn identifier() -> re_viewer_context::ViewSystemIdentifier { + "Ellipses2D".into() + } +} + +impl VisualizerSystem for Ellipses2DVisualizer { + fn visualizer_query_info( + &self, + _app_options: &re_viewer_context::AppOptions, + ) -> VisualizerQueryInfo { + VisualizerQueryInfo::single_required_component::( + &Ellipses2D::descriptor_half_sizes(), + &Ellipses2D::all_components(), + ) + } + + fn affinity(&self) -> Option { + Some(crate::SpatialView2D::identifier()) + } + + fn execute( + &self, + ctx: &ViewContext<'_>, + view_query: &ViewQuery<'_>, + context_systems: &ViewContextCollection, + ) -> Result { + let mut view_data = SpatialViewVisualizerData::default(); + let output = VisualizerExecutionOutput::default(); + let mut line_builder = LineDrawableBuilder::new(ctx.viewer_ctx.render_ctx()); + line_builder.radius_boost_in_ui_points_for_outlines( + re_view::SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES, + ); + + use super::entity_iterator::process_archetype; + process_archetype::( + ctx, + view_query, + context_systems, + &output, + self, + |ctx, spatial_ctx, results| { + let all_half_sizes = + results.iter_required(Ellipses2D::descriptor_half_sizes().component); + if all_half_sizes.is_empty() { + return Ok(()); + } + + let num_ellipses: usize = all_half_sizes + .chunks() + .iter() + .flat_map(|chunk| chunk.iter_slices::<[f32; 2]>()) + .map(|vectors| vectors.len()) + .sum(); + if num_ellipses == 0 { + return Ok(()); + } + + // Each ellipse is one closed strip with `ELLIPSE_SEGMENTS + 1` vertices. + line_builder.reserve_strips(num_ellipses)?; + line_builder.reserve_vertices(num_ellipses * (ELLIPSE_SEGMENTS + 1))?; + + let all_centers = results.iter_optional(Ellipses2D::descriptor_centers().component); + let all_colors = results.iter_optional(Ellipses2D::descriptor_colors().component); + let all_line_radii = + results.iter_optional(Ellipses2D::descriptor_line_radii().component); + let all_labels = results.iter_optional(Ellipses2D::descriptor_labels().component); + let all_class_ids = + results.iter_optional(Ellipses2D::descriptor_class_ids().component); + let all_show_labels = + results.iter_optional(Ellipses2D::descriptor_show_labels().component); + + let results_iter = re_query::range_zip_1x6( + all_half_sizes.slice::<[f32; 2]>(), + all_centers.slice::<[f32; 2]>(), + all_colors.slice::(), + all_line_radii.slice::(), + all_labels.slice::(), + all_class_ids.slice::(), + all_show_labels.slice::(), + ) + .map( + |( + _index, + half_sizes, + centers, + colors, + line_radii, + labels, + class_ids, + show_labels, + )| { + Ellipses2DComponentData { + half_sizes: bytemuck::cast_slice(half_sizes), + centers: centers.map_or(&[], |centers| bytemuck::cast_slice(centers)), + colors: colors.map_or(&[], |colors| bytemuck::cast_slice(colors)), + line_radii: line_radii + .map_or(&[], |line_radii| bytemuck::cast_slice(line_radii)), + labels: labels.unwrap_or_default(), + class_ids: class_ids + .map_or(&[], |class_ids| bytemuck::cast_slice(class_ids)), + show_labels: show_labels + .map(|b| !b.is_empty() && b.value(0)) + .map(Into::into), + } + }, + ); + + Self::process_data( + &mut view_data, + ctx, + &mut line_builder, + view_query, + spatial_ctx, + results_iter, + ); + + Ok(()) + }, + )?; + + Ok(output + .with_draw_data([(line_builder.into_draw_data()?.into())]) + .with_visualizer_data(view_data)) + } +} diff --git a/crates/viewer/re_view_spatial/src/visualizers/ellipsoids.rs b/crates/viewer/re_view_spatial/src/visualizers/ellipsoids.rs index 3847f874912d..9535556cac3f 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/ellipsoids.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/ellipsoids.rs @@ -92,7 +92,10 @@ struct Ellipsoids3DComponentData<'a> { impl IdentifiedViewSystem for Ellipsoids3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Ellipsoids3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Ellipsoids3D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/encoded_depth_image.rs b/crates/viewer/re_view_spatial/src/visualizers/encoded_depth_image.rs deleted file mode 100644 index 9211e18b908c..000000000000 --- a/crates/viewer/re_view_spatial/src/visualizers/encoded_depth_image.rs +++ /dev/null @@ -1,183 +0,0 @@ -use nohash_hasher::IntMap; - -use re_log_types::EntityPathHash; -use re_sdk_types::{ - Archetype as _, - archetypes::EncodedDepthImage, - components::{Blob, Colormap, MagnificationFilter, MediaType}, -}; -use re_viewer_context::{ - IdentifiedViewSystem, ImageDecodeCache, ViewClass as _, ViewContext, ViewContextCollection, - ViewQuery, ViewSystemExecutionError, VisualizerExecutionOutput, VisualizerQueryInfo, - VisualizerReportSeverity, VisualizerSystem, -}; - -use super::entity_iterator::process_archetype; -use super::{ - SpatialViewVisualizerData, - depth_images::{DepthImageComponentData, process_depth_image_data}, -}; -use crate::{ - contexts::TransformTreeContext, - visualizers::{ - depth_images::{DepthImageProcessResult, populate_depth_visualizer_execution_result}, - first_copied, - }, -}; -use re_sdk_types::reflection::Enum as _; - -pub struct EncodedDepthImageVisualizerOutput { - pub depth_cloud_entities: IntMap, -} - -#[derive(Default)] -pub struct EncodedDepthImageVisualizer; - -impl IdentifiedViewSystem for EncodedDepthImageVisualizer { - fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "EncodedDepthImage".into() - } -} - -impl VisualizerSystem for EncodedDepthImageVisualizer { - fn visualizer_query_info( - &self, - _app_options: &re_viewer_context::AppOptions, - ) -> VisualizerQueryInfo { - VisualizerQueryInfo::single_required_component::( - &EncodedDepthImage::descriptor_blob(), - &EncodedDepthImage::all_components(), - ) - } - - fn affinity(&self) -> Option { - Some(crate::SpatialView2D::identifier()) - } - - fn execute( - &self, - ctx: &ViewContext<'_>, - view_query: &ViewQuery<'_>, - context_systems: &ViewContextCollection, - ) -> Result { - let output = VisualizerExecutionOutput::default(); - let mut depth_clouds = Vec::new(); - let mut data = SpatialViewVisualizerData::default(); - let mut depth_cloud_entities = IntMap::default(); - - let transforms = context_systems.get::(&output)?; - - process_archetype::( - ctx, - view_query, - context_systems, - &output, - self, - |ctx, spatial_ctx, results| { - let all_blobs = - results.iter_required(EncodedDepthImage::descriptor_blob().component); - if all_blobs.is_empty() { - return Ok(()); - } - let all_media_types = - results.iter_optional(EncodedDepthImage::descriptor_media_type().component); - let all_colormaps = - results.iter_optional(EncodedDepthImage::descriptor_colormap().component); - let all_value_ranges = - results.iter_optional(EncodedDepthImage::descriptor_depth_range().component); - let all_depth_meters = - results.iter_optional(EncodedDepthImage::descriptor_meter().component); - let all_fill_ratios = results - .iter_optional(EncodedDepthImage::descriptor_point_fill_ratio().component); - let all_magnification_filters = results - .iter_optional(EncodedDepthImage::descriptor_magnification_filter().component); - - for ( - (_time, row_id), - blobs, - media_type, - colormap, - value_range, - depth_meter, - fill_ratio, - magnification_filter, - ) in re_query::range_zip_1x6( - all_blobs.slice::<&[u8]>(), - all_media_types.slice::(), - all_colormaps.slice::(), - all_value_ranges.slice::<[f64; 2]>(), - all_depth_meters.slice::(), - all_fill_ratios.slice::(), - all_magnification_filters.slice::(), - ) { - let Some(blob) = blobs.first() else { - // If missing we already reported an error. - continue; - }; - - let media_type = media_type - .and_then(|types| types.first().cloned()) - .map(|mt| MediaType(mt.into())); - - let image = match ctx.store_ctx().memoizer(|c: &mut ImageDecodeCache| { - c.entry_encoded_depth( - row_id, - EncodedDepthImage::descriptor_blob().component, - blob, - media_type.as_ref(), - ) - }) { - Ok(image) => image, - Err(err) => { - results.report_for_component( - EncodedDepthImage::descriptor_blob().component, - VisualizerReportSeverity::Error, - format!("Failed to decode EncodedDepthImage blob: {err}"), - ); - continue; - } - }; - - let component_data = DepthImageComponentData { - image, - depth_meter: first_copied(depth_meter).map(Into::into), - fill_ratio: first_copied(fill_ratio).map(Into::into), - colormap: colormap.and_then(|s| Colormap::from_integer_slice(s).next()?), - value_range: first_copied(value_range), - magnification_filter: first_copied(magnification_filter) - .and_then(MagnificationFilter::from_u8) - .unwrap_or_default(), - }; - - let mut report_error = |error: String| { - results.report_unspecified_source(VisualizerReportSeverity::Error, error); - }; - - process_depth_image_data( - ctx, - spatial_ctx, - &mut data, - &mut depth_cloud_entities, - &mut depth_clouds, - transforms, - component_data, - EncodedDepthImage::name(), - EncodedDepthImage::descriptor_meter().component, - EncodedDepthImage::descriptor_colormap().component, - &mut report_error, - ); - } - - Ok(()) - }, - )?; - - populate_depth_visualizer_execution_result(ctx, &data, depth_clouds, output).map(|output| { - output.with_visualizer_data(data).with_visualizer_data( - EncodedDepthImageVisualizerOutput { - depth_cloud_entities, - }, - ) - }) - } -} diff --git a/crates/viewer/re_view_spatial/src/visualizers/grid_map.rs b/crates/viewer/re_view_spatial/src/visualizers/grid_map.rs index 2ed815f2f949..8859c3153555 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/grid_map.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/grid_map.rs @@ -6,7 +6,7 @@ use re_sdk_types::components::{ CellSize, Colormap, ImageBuffer, ImageFormat, Opacity, RotationAxisAngle, RotationQuat, Translation3D, }; -use re_sdk_types::datatypes::{ColorModel, Quaternion}; +use re_sdk_types::datatypes::ColorModel; use re_sdk_types::image::ImageKind; use re_sdk_types::reflection::Enum as _; use re_viewer_context::{ @@ -19,7 +19,7 @@ use re_viewer_context::{ use super::SpatialViewVisualizerData; use super::entity_iterator::process_archetype; use crate::contexts::SpatialSceneVisualizerInstructionContext; -use crate::{PickableRectSourceData, PickableTexturedRect}; +use crate::{PickableRectSourceData, PickableTexturedRect, SpaceKind}; #[derive(Default)] pub struct GridMapVisualizer; @@ -45,12 +45,15 @@ struct GridMapComponentData { rotation_axis_angle: Option, quaternion: Option, opacity: Option, - colormap: Option, + colormap: Colormap, } impl IdentifiedViewSystem for GridMapVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "GridMap".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "GridMap" + ) } } @@ -177,7 +180,10 @@ impl GridMapVisualizer { opacity: opacities.and_then(|o| o.first().copied()).map(Into::into), colormap: colormaps .and_then(|c| c.first().copied()) - .and_then(Colormap::try_from_integer), + .and_then(Colormap::try_from_integer) + .unwrap_or_else(|| { + typed_fallback_for(ctx, GridMap::descriptor_colormap().component) + }), }) }, ); @@ -192,7 +198,7 @@ impl GridMapVisualizer { component_data, &color_mode, ) { - data.add_bounding_box( + data.add_bounding_box_3d( entity_path.hash(), textured_rect.bounding_box(), glam::Affine3A::IDENTITY, @@ -206,7 +212,7 @@ impl GridMapVisualizer { depth_meter: None, }, }, - spatial_ctx.view_class_identifier, + SpaceKind::ThreeD, // The bounding box is flat, but this is distinctively a 3D object in a 3D space! ); } } @@ -240,11 +246,9 @@ impl GridMapVisualizer { return None; } - let image_stats = ctx - .viewer_ctx() - .store_context - .caches - .memoizer(|c: &mut re_viewer_context::ImageStatsCache| c.entry(&image)); + let caches = ctx.viewer_ctx().store_context.caches; + let image_stats = + caches.memoizer_read_or_compute::(&image); let colormapped_texture = match gpu_bridge::image_to_gpu( ctx.viewer_ctx().render_ctx(), @@ -270,67 +274,17 @@ impl GridMapVisualizer { .single_transform_required_for_entity(entity_path, GridMap::name()) .as_affine3a(); - let translation = if let Some(translation) = translation { - translation.into() - } else { - glam::Affine3A::IDENTITY - }; - - let rotation = match (quaternion, rotation_axis_angle) { - (Some(quaternion), Some(rotation_axis_angle)) - if quaternion.0 != Quaternion::IDENTITY - && rotation_axis_angle != RotationAxisAngle::IDENTITY => - { - // Match the behavior documented in the archetype definition: - // if both are set, the quaternion takes precedence. - results.report_for_component( - GridMap::descriptor_quaternion().component, - VisualizerReportSeverity::Warning, - format!( - "GridMap {entity_path} has both quaternion and rotation_axis_angle set; using quaternion." - ), - ); - - if let Ok(rotation) = glam::Affine3A::try_from(quaternion) { - rotation - } else { - results.report_for_component( - GridMap::descriptor_quaternion().component, - VisualizerReportSeverity::Error, - "invalid rotation quaternion", - ); - return None; - } - } - (Some(quaternion), _) => { - if let Ok(rotation) = glam::Affine3A::try_from(quaternion) { - rotation - } else { - results.report_for_component( - GridMap::descriptor_quaternion().component, - VisualizerReportSeverity::Error, - "invalid rotation quaternion", - ); - return None; - } - } - (_, Some(rotation_axis_angle)) => { - if let Ok(rotation) = glam::Affine3A::try_from(rotation_axis_angle) { - rotation - } else { - results.report_for_component( - GridMap::descriptor_rotation_axis_angle().component, - VisualizerReportSeverity::Error, - "invalid rotation axis-angle", - ); - return None; - } - } - (None, None) => glam::Affine3A::IDENTITY, - }; - - let grid_from_entity = translation * rotation; - let world_from_grid = world_from_entity * grid_from_entity; + let entity_from_grid = super::entity_from_grid_transform( + results, + entity_path, + "GridMap", + translation, + rotation_axis_angle, + quaternion, + GridMap::descriptor_quaternion().component, + GridMap::descriptor_rotation_axis_angle().component, + )?; + let world_from_grid = world_from_entity * entity_from_grid; let [width, height] = image.width_height_f32(); let extent_u = world_from_grid.transform_vector3(Vec3::X * width * cell_size); @@ -375,14 +329,10 @@ impl GridMapVisualizer { results: &re_view::VisualizerInstructionQueryResults<'_>, component_data: &GridMapComponentData, ) -> GridMapColorMode { - let Some(colormap) = component_data.colormap else { - return GridMapColorMode::NoColormap; - }; - if component_data.image.format.color_model() != ColorModel::L { results.report_for_component( GridMap::descriptor_colormap().component, - VisualizerReportSeverity::Warning, + VisualizerReportSeverity::Info, format!( "GridMap colormaps only apply to single-channel images; ignoring colormap for {:?} data.", component_data.image.format.color_model() @@ -391,12 +341,13 @@ impl GridMapVisualizer { return GridMapColorMode::NoColormap; } - if matches!(colormap, Colormap::RvizMap | Colormap::RvizCostmap) - && !matches!( - component_data.image.format.datatype(), - re_sdk_types::datatypes::ChannelDatatype::U8 - ) - { + if matches!( + component_data.colormap, + Colormap::RvizMap | Colormap::RvizCostmap + ) && !matches!( + component_data.image.format.datatype(), + re_sdk_types::datatypes::ChannelDatatype::U8 + ) { results.report_for_component( GridMap::descriptor_colormap().component, VisualizerReportSeverity::Warning, @@ -408,12 +359,16 @@ impl GridMapVisualizer { return GridMapColorMode::NoColormap; } - let image_stats = - ctx.viewer_ctx().store_context.caches.memoizer( - |c: &mut re_viewer_context::ImageStatsCache| c.entry(&component_data.image), + let caches = ctx.viewer_ctx().store_context.caches; + let image_stats = caches + .memoizer_read_or_compute::( + &component_data.image, ); - let value_range = if matches!(colormap, Colormap::RvizMap | Colormap::RvizCostmap) { + let value_range = if matches!( + component_data.colormap, + Colormap::RvizMap | Colormap::RvizCostmap + ) { // RViz grid-map colormaps are discrete mappings for u8 values, not continuous gradients. [0.0, 255.0] } else { @@ -424,7 +379,7 @@ impl GridMapVisualizer { }; GridMapColorMode::Colormapped(ColormapWithRange { - colormap, + colormap: component_data.colormap, value_range, }) } diff --git a/crates/viewer/re_view_spatial/src/visualizers/images.rs b/crates/viewer/re_view_spatial/src/visualizers/images.rs index 89a659648589..e55b9ed59f2f 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/images.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/images.rs @@ -12,7 +12,7 @@ use super::SpatialViewVisualizerData; use super::entity_iterator::process_archetype; use crate::contexts::SpatialSceneVisualizerInstructionContext; use crate::visualizers::{first_copied, textured_rect_from_image}; -use crate::{PickableRectSourceData, PickableTexturedRect}; +use crate::{PickableRectSourceData, PickableTexturedRect, SpaceKind}; #[derive(Default)] pub struct ImageVisualizer; @@ -25,7 +25,10 @@ struct ImageComponentData { impl IdentifiedViewSystem for ImageVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Image".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Image" + ) } } @@ -159,7 +162,7 @@ impl ImageVisualizer { depth_meter: None, }, }, - spatial_ctx.view_class_identifier, + SpaceKind::TwoD, ); } Err(err) => { diff --git a/crates/viewer/re_view_spatial/src/visualizers/lines2d.rs b/crates/viewer/re_view_spatial/src/visualizers/lines2d.rs index 5fd6714fd951..916479da94c0 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/lines2d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/lines2d.rs @@ -74,7 +74,7 @@ impl Lines2DVisualizer { let mut obj_space_bounding_box = macaw::BoundingBox::nothing(); for (i, (strip, radius, &color)) in - itertools::izip!(ent_data.strips.iter(), radii, &colors).enumerate() + itertools::izip!(&ent_data.strips, radii, &colors).enumerate() { let lines = line_batch .add_strip_2d(strip.iter().copied().map(Into::into)) @@ -97,7 +97,7 @@ impl Lines2DVisualizer { } } - data.add_bounding_box(entity_path.hash(), obj_space_bounding_box, world_from_obj); + data.add_bounding_box_2d(entity_path.hash(), obj_space_bounding_box, world_from_obj); data.ui_labels.extend(process_labels_2d( LabeledBatch { @@ -144,7 +144,10 @@ struct Lines2DComponentData<'a> { impl IdentifiedViewSystem for Lines2DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Lines2D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Lines2D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/lines3d.rs b/crates/viewer/re_view_spatial/src/visualizers/lines3d.rs index 7f5dff9e66fa..a3e3da8076b5 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/lines3d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/lines3d.rs @@ -77,7 +77,7 @@ impl Lines3DVisualizer { let mut num_rendered_strips = 0usize; for (i, (strip, radius, &color)) in - itertools::izip!(ent_data.strips.iter(), radii, &colors).enumerate() + itertools::izip!(&ent_data.strips, radii, &colors).enumerate() { let lines = line_batch .add_strip(strip.iter().copied().map(Into::into)) @@ -108,7 +108,7 @@ impl Lines3DVisualizer { ent_data.strips.len() ); - data.add_bounding_box(entity_path.hash(), obj_space_bounding_box, world_from_obj); + data.add_bounding_box_3d(entity_path.hash(), obj_space_bounding_box, world_from_obj); data.ui_labels.extend(process_labels_3d( LabeledBatch { @@ -155,7 +155,10 @@ struct Lines3DComponentData<'a> { impl IdentifiedViewSystem for Lines3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Lines3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Lines3D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/meshes.rs b/crates/viewer/re_view_spatial/src/visualizers/meshes.rs index 3119d2dd56d1..f62f3c233cf2 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/meshes.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/meshes.rs @@ -62,7 +62,7 @@ impl Mesh3DVisualizer { }; c.entry( - &entity_path.to_string(), + entity_path, key.clone(), AnyMesh::Mesh { mesh: mesh_entry.native_mesh, @@ -93,7 +93,7 @@ impl Mesh3DVisualizer { } })); - data.add_bounding_box(entity_path.hash(), mesh.bbox(), world_from_instance); + data.add_bounding_box_3d(entity_path.hash(), mesh.bbox(), world_from_instance); } } } @@ -102,7 +102,10 @@ impl Mesh3DVisualizer { impl IdentifiedViewSystem for Mesh3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Mesh3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Mesh3D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/mod.rs b/crates/viewer/re_view_spatial/src/visualizers/mod.rs index 0ec2af1621d0..d647de93b4d3 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/mod.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/mod.rs @@ -9,8 +9,8 @@ mod cameras; mod capsules3d; mod cylinders3d; mod depth_images; +mod ellipses2d; mod ellipsoids; -mod encoded_depth_image; mod grid_map; mod images; mod lines2d; @@ -22,16 +22,17 @@ mod segmentation_images; mod transform_axes_3d; pub mod utilities; mod video; +mod voxel_grid_map; pub use cameras::{CamerasVisualizer, CamerasVisualizerOutput}; pub use depth_images::{DepthImageProcessResult, DepthImageVisualizer, DepthImageVisualizerOutput}; -pub use encoded_depth_image::{EncodedDepthImageVisualizer, EncodedDepthImageVisualizerOutput}; use re_sdk_types::{ComponentDescriptor, ComponentIdentifier, archetypes}; -pub use transform_axes_3d::{TransformAxes3DVisualizer, add_axis_arrows}; +pub use transform_axes_3d::{Axes, TransformAxes3DVisualizer, add_axis_arrows}; pub use utilities::{ - SpatialViewVisualizerData, UiLabel, UiLabelStyle, UiLabelTarget, entity_iterator, - iter_spatial_data, process_labels_3d, textured_rect_from_image, + SpatialViewVisualizerData, UiLabel, UiLabelStyle, UiLabelTarget, entity_from_grid_transform, + entity_iterator, iter_spatial_data, process_labels_3d, textured_rect_from_image, }; +pub use video::{EncodedDepthImageVisualizer, EncodedDepthImageVisualizerOutput}; /// Shows a loading animation in a spatial view. /// @@ -78,7 +79,8 @@ pub fn register_2d_spatial_visualizers( system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; - system_registry.register_visualizer::()?; + system_registry.register_visualizer::()?; + system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; @@ -107,7 +109,8 @@ pub fn register_3d_spatial_visualizers( system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; - system_registry.register_visualizer::()?; + system_registry.register_visualizer::()?; + system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; @@ -118,6 +121,7 @@ pub fn register_3d_spatial_visualizers( system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; + system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; system_registry.register_visualizer::()?; Ok(()) @@ -141,7 +145,11 @@ pub fn visualizers_processing_draw_order() archetypes::DepthImage::descriptor_draw_order(), ), ( - encoded_depth_image::EncodedDepthImageVisualizer::identifier(), + ellipses2d::Ellipses2DVisualizer::identifier(), + archetypes::Ellipses2D::descriptor_draw_order(), + ), + ( + video::EncodedDepthImageVisualizer::identifier(), archetypes::EncodedDepthImage::descriptor_draw_order(), ), ( @@ -182,7 +190,7 @@ pub fn visualizers_processing_draw_order() pub fn collect_ui_labels(system_output: &SystemExecutionOutput) -> Vec { iter_spatial_data(system_output) - .flat_map(|(_affinity, data)| data.ui_labels.iter().cloned()) + .flat_map(|data| data.ui_labels.iter().cloned()) .collect() } diff --git a/crates/viewer/re_view_spatial/src/visualizers/points2d.rs b/crates/viewer/re_view_spatial/src/visualizers/points2d.rs index 04cf3d579a94..4f44f07965be 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/points2d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/points2d.rs @@ -12,6 +12,7 @@ use re_viewer_context::{ use super::SpatialViewVisualizerData; use super::utilities::{LabeledBatch, process_labels_2d}; +use crate::SpaceKind; use crate::contexts::SpatialSceneVisualizerInstructionContext; use crate::visualizers::{load_keypoint_connections, process_radius_slice}; @@ -34,6 +35,13 @@ impl Points2DVisualizer { ) -> Result<(), ViewSystemExecutionError> { let entity_path = ctx.target_entity_path; + // Opt-in due to the cost of CPU-sorting transparent point clouds every frame. + let transparency_enabled = ctx + .viewer_ctx() + .app_options() + .experimental + .point_cloud_transparency; + for data in data { let num_instances = data.positions.len(); @@ -76,6 +84,9 @@ impl Points2DVisualizer { .single_transform_required_for_entity(entity_path, Points2D::name()) .as_affine3a(); + let has_transparency = transparency_enabled && colors.iter().any(|c| !c.is_opaque()); + let point_cloud_bounds = re_renderer::util::point_cloud_bounds(&positions); + { let point_batch = point_builder .batch(entity_path.to_string()) @@ -84,7 +95,9 @@ impl Points2DVisualizer { re_renderer::renderer::PointCloudBatchFlags::FLAG_DRAW_AS_CIRCLES | re_renderer::renderer::PointCloudBatchFlags::FLAG_ENABLE_SHADING, ) + .enable_alpha_blending(has_transparency) .world_from_obj(world_from_obj) + .object_space_bounding_box(point_cloud_bounds.bbox) .outline_mask_ids(ent_context.highlight.overall) .picking_object_id(re_renderer::PickingLayerObjectId(entity_path.hash64())); @@ -110,12 +123,12 @@ impl Points2DVisualizer { } } - let point_cloud_bounds = re_renderer::util::point_cloud_bounds(&positions); view_data.add_bounding_box_and_region_of_interest( entity_path.hash(), point_cloud_bounds.bbox, point_cloud_bounds.region_of_interest, world_from_obj, + SpaceKind::TwoD, ); load_keypoint_connections( @@ -168,7 +181,10 @@ pub struct Points2DComponentData<'a> { impl IdentifiedViewSystem for Points2DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Points2D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Points2D" + ) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/points3d.rs b/crates/viewer/re_view_spatial/src/visualizers/points3d.rs index 1e545e3a856d..6216b7e8ec96 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/points3d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/points3d.rs @@ -2,14 +2,21 @@ use std::sync::Arc; use itertools::Itertools as _; use nohash_hasher::IntMap; +use parking_lot::Mutex; use re_byte_size::SizeBytes as _; use re_entity_db::EntityDb; use re_log_types::hash::Hash64; -use re_renderer::{LineDrawableBuilder, PickingLayerInstanceId, PointCloudBuilder, PositionRadius}; +use re_renderer::{ + LineDrawableBuilder, PickingLayerInstanceId, PointCloudBuilder, PositionRadius, + renderer::PointCloudSortOrderCache, +}; use re_sdk_types::Archetype as _; use re_sdk_types::ArrowString; use re_sdk_types::archetypes::Points3D; -use re_sdk_types::components::{ClassId, Color, KeypointId, Position3D, Radius, ShowLabels}; +use re_sdk_types::components::{ + ClassId, Color, KeypointId, PointShading, Position3D, Radius, ShowLabels, +}; +use re_sdk_types::reflection::Enum as _; use re_view::{process_annotation_and_keypoint_slices, process_color_slice}; use re_viewer_context::{ Cache, IdentifiedViewSystem, QueryContext, ResolvedAnnotationInfos, ViewClass as _, @@ -19,6 +26,7 @@ use re_viewer_context::{ use super::utilities::LabeledBatch; use super::{Keypoints, SpatialViewVisualizerData, process_labels_3d}; +use crate::SpaceKind; use crate::contexts::SpatialSceneVisualizerInstructionContext; use crate::visualizers::{load_keypoint_connections, process_radius_slice}; @@ -43,6 +51,7 @@ struct Points3DComponentData<'a> { // Non-repeated show_labels: Option, + point_shading: Option, } /// Processed/computed point cloud data ready for rendering. @@ -50,13 +59,25 @@ struct Points3DComponentData<'a> { /// This bundles together the results of processing raw component data /// (computing annotations, colors, radii, bounding boxes, etc.) /// so that it can be memoized based on `data.query_hash`. +#[derive(re_byte_size::SizeBytes)] struct Points3DCpu { position_radii: Vec, + + #[size_bytes(ignore)] // Lives entirely on the stack. point_cloud_bounds: re_renderer::util::PointCloudBounds, + picking_ids: Vec, annotation_infos: ResolvedAnnotationInfos, keypoints: Keypoints, colors: Vec, + + /// Whether any point has a non-opaque color, requiring alpha-blended rendering. + has_transparency: bool, + + /// Scratch buffers holding the back-to-front point ordering across frames. + /// + /// Each instance transform has its own cache, which tracks ordering per rendered view. + sort_order_caches: Mutex>, } impl Points3DCpu { @@ -109,6 +130,8 @@ impl Points3DCpu { let position_radii = PositionRadius::from_many(positions, &radii); + let has_transparency = colors.iter().any(|c| !c.is_opaque()); + Self { position_radii, point_cloud_bounds, @@ -116,24 +139,15 @@ impl Points3DCpu { annotation_infos, keypoints, colors, + has_transparency, + sort_order_caches: Mutex::new(Vec::new()), } } - fn heap_size_bytes(&self) -> u64 { - let Self { - position_radii, - point_cloud_bounds: _, - picking_ids, - annotation_infos, - keypoints, - colors, - } = self; - - (position_radii.capacity() * std::mem::size_of::()) as u64 - + picking_ids.heap_size_bytes() - + annotation_infos.heap_size_bytes() - + keypoints.heap_size_bytes() - + colors.heap_size_bytes() + fn sort_order_cache(&self, transform_index: usize) -> PointCloudSortOrderCache { + let mut caches = self.sort_order_caches.lock(); + caches.resize_with(transform_index + 1, PointCloudSortOrderCache::default); + caches[transform_index].clone() } } @@ -223,7 +237,9 @@ impl re_byte_size::SizeBytes for Points3DCache { // Count the underlying data of the Arc directly instead of weighing active cache .values() - .map(|entry| entry.cpu.heap_size_bytes() + std::mem::size_of_val(&entry.cpu) as u64) + .map(|entry| { + entry.cpu.as_ref().heap_size_bytes() + std::mem::size_of_val(&entry.cpu) as u64 + }) .sum::() + (cache.capacity() * std::mem::size_of::<(Hash64, Points3DCacheEntry)>()) as u64 } @@ -250,6 +266,13 @@ impl Points3DVisualizer { re_tracing::profile_function!(); let entity_path = ctx.target_entity_path; + // Opt-in due to the cost of CPU-sorting transparent point clouds every frame. + let transparency_enabled = ctx + .viewer_ctx() + .app_options() + .experimental + .point_cloud_transparency; + for data in data { let num_instances = data.positions.len(); if num_instances == 0 { @@ -266,25 +289,38 @@ impl Points3DVisualizer { Points3DCpu::compute(ctx, entity_path, query, ent_context, &data) }) }); + let point_shading = data.point_shading.unwrap_or_else(|| { + typed_fallback_for(ctx, Points3D::descriptor_point_shading().component) + }); // TODO(grtlr): The following is a quick fix to get multiple instance poses to work // with point clouds: We sent the same point cloud multiple times to the GPU (bad // for memory) and render them with multiple draw calls across different batches (bad // for performance). - for world_from_obj in ent_context + for (transform_index, world_from_obj) in ent_context .transform_info .target_from_instances() .iter() .map(|transform| transform.as_affine3a()) + .enumerate() { re_tracing::profile_scope!("one-transform"); - let point_batch = point_builder + let alpha_blend = transparency_enabled && cpu.has_transparency; + + let mut point_batch = point_builder .batch(entity_path.to_string()) + .enable_shading(matches!(point_shading, PointShading::Gradient)) + .enable_alpha_blending(alpha_blend) .world_from_obj(world_from_obj) + .object_space_bounding_box(cpu.point_cloud_bounds.bbox) .outline_mask_ids(ent_context.highlight.overall) .picking_object_id(re_renderer::PickingLayerObjectId(entity_path.hash64())); + if alpha_blend { + point_batch = point_batch.sort_order(cpu.sort_order_cache(transform_index)); + } + let mut point_range_builder = point_batch.add_points(&cpu.position_radii, &cpu.colors, &cpu.picking_ids); @@ -310,6 +346,7 @@ impl Points3DVisualizer { cpu.point_cloud_bounds.bbox, cpu.point_cloud_bounds.region_of_interest, world_from_obj, + SpaceKind::ThreeD, ); load_keypoint_connections( @@ -345,7 +382,10 @@ impl Points3DVisualizer { impl IdentifiedViewSystem for Points3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Points3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Points3D" + ) } } @@ -426,10 +466,12 @@ impl VisualizerSystem for Points3DVisualizer { results.iter_optional(Points3D::descriptor_keypoint_ids().component); let all_show_labels = results.iter_optional(Points3D::descriptor_show_labels().component); + let all_point_shading = + results.iter_optional(Points3D::descriptor_point_shading().component); let query_result_hash = results.query_result_hash(); - let results_iter = re_query::range_zip_1x6( + let results_iter = re_query::range_zip_1x7( all_positions.slice::<[f32; 3]>(), // RowId 5 all_colors.slice::(), // RowId 7 all_radii.slice::(), @@ -437,6 +479,7 @@ impl VisualizerSystem for Points3DVisualizer { all_class_ids.slice::(), all_keypoint_ids.slice::(), all_show_labels.slice::(), + all_point_shading.slice::(), ) .map( |( @@ -448,6 +491,7 @@ impl VisualizerSystem for Points3DVisualizer { class_ids, keypoint_ids, show_labels, + point_shading, )| { Points3DComponentData { index, @@ -463,6 +507,8 @@ impl VisualizerSystem for Points3DVisualizer { show_labels: show_labels .map(|b| !b.is_empty() && b.value(0)) .map(Into::into), + point_shading: point_shading + .and_then(|s| PointShading::from_integer_slice(s).next()?), } }, ); diff --git a/crates/viewer/re_view_spatial/src/visualizers/segmentation_images.rs b/crates/viewer/re_view_spatial/src/visualizers/segmentation_images.rs index 13a9be95b3dc..71a003fe626c 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/segmentation_images.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/segmentation_images.rs @@ -10,7 +10,7 @@ use re_viewer_context::{ use super::SpatialViewVisualizerData; use crate::visualizers::textured_rect_from_image; -use crate::{PickableRectSourceData, PickableTexturedRect}; +use crate::{PickableRectSourceData, PickableTexturedRect, SpaceKind}; #[derive(Default)] pub struct SegmentationImageVisualizer; @@ -22,7 +22,10 @@ struct SegmentationImageComponentData { impl IdentifiedViewSystem for SegmentationImageVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "SegmentationImage".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "SegmentationImage" + ) } } @@ -124,7 +127,7 @@ impl VisualizerSystem for SegmentationImageVisualizer { depth_meter: None, }, }, - spatial_ctx.view_class_identifier, + SpaceKind::TwoD, ); } Err(err) => { diff --git a/crates/viewer/re_view_spatial/src/visualizers/transform_axes_3d.rs b/crates/viewer/re_view_spatial/src/visualizers/transform_axes_3d.rs index 34fe8744df9e..1949f362b593 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/transform_axes_3d.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/transform_axes_3d.rs @@ -1,3 +1,4 @@ +use itertools::chain; use re_entity_db::InstancePathHash; use re_log_types::{EntityPath, Instance}; use re_sdk_types::Archetype as _; @@ -21,7 +22,10 @@ pub struct TransformAxes3DVisualizer; impl IdentifiedViewSystem for TransformAxes3DVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "TransformAxes3D".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "TransformAxes3D" + ) } } @@ -34,12 +38,14 @@ impl VisualizerSystem for TransformAxes3DVisualizer { relevant_archetype: Some(TransformAxes3D::name()), // Make this visualizer available for any entity with Transform3D components constraints: VisualizabilityConstraints::AnyBuiltinComponent( - Transform3D::all_component_identifiers() - .chain(CoordinateFrame::all_component_identifiers()) - .chain(InstancePoses3D::all_component_identifiers()) - .chain(Pinhole::all_component_identifiers()) - .chain(TransformAxes3D::all_component_identifiers()) - .collect(), + chain!( + Transform3D::all_component_identifiers(), + CoordinateFrame::all_component_identifiers(), + InstancePoses3D::all_component_identifiers(), + Pinhole::all_component_identifiers(), + TransformAxes3D::all_component_identifiers(), + ) + .collect(), ), queried: TransformAxes3D::all_components().iter().cloned().collect(), } @@ -222,7 +228,7 @@ impl VisualizerSystem for TransformAxes3DVisualizer { } // Only add the center to the bounding box - the lines may be dependent on the bounding box, causing a feedback loop otherwise. - data.add_bounding_box( + data.add_bounding_box_3d( data_result.entity_path.hash(), macaw::BoundingBox::ZERO, *world_from_obj, @@ -248,6 +254,7 @@ impl VisualizerSystem for TransformAxes3DVisualizer { *world_from_obj, Some(&data_result.entity_path), axis_length, + Axes::Xyz, outline_mask, instance_index as u64, ); @@ -260,12 +267,20 @@ impl VisualizerSystem for TransformAxes3DVisualizer { } } +/// Which axes to draw. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Axes { + Xy, + Xyz, +} + pub fn add_axis_arrows( tokens: &re_ui::DesignTokens, line_builder: &mut re_renderer::LineDrawableBuilder<'_>, world_from_obj: glam::Affine3A, ent_path: Option<&EntityPath>, axis_length: f32, + axes: Axes, outline_mask_ids: re_renderer::OutlineMaskPreference, instance_index: u64, ) { @@ -304,13 +319,15 @@ pub fn add_axis_arrows( | LineStripFlags::STRIP_FLAG_CAP_START_ROUND, ) .picking_instance_id(picking_instance_id); - line_batch - .add_segment(glam::Vec3::ZERO, glam::Vec3::Z * axis_length) - .radius(line_radius) - .color(tokens.axis_color_z) - .flags( - LineStripFlags::STRIP_FLAG_CAP_END_TRIANGLE - | LineStripFlags::STRIP_FLAG_CAP_START_ROUND, - ) - .picking_instance_id(picking_instance_id); + if axes == Axes::Xyz { + line_batch + .add_segment(glam::Vec3::ZERO, glam::Vec3::Z * axis_length) + .radius(line_radius) + .color(tokens.axis_color_z) + .flags( + LineStripFlags::STRIP_FLAG_CAP_END_TRIANGLE + | LineStripFlags::STRIP_FLAG_CAP_START_ROUND, + ) + .picking_instance_id(picking_instance_id); + } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/utilities/labels.rs b/crates/viewer/re_view_spatial/src/visualizers/utilities/labels.rs index ed6b24f68aed..dc3d6c21bff0 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/utilities/labels.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/utilities/labels.rs @@ -157,7 +157,7 @@ pub fn process_labels<'a, P: 'a>( let labels = izip!( annotation_infos.iter(), - labels.iter().map(Some).chain(std::iter::repeat(None)) + std::iter::chain(labels.iter().map(Some), std::iter::repeat(None)) ) .map(|(annotation_info, label)| annotation_info.label(label.map(|l| l.as_str()))); diff --git a/crates/viewer/re_view_spatial/src/visualizers/utilities/mod.rs b/crates/viewer/re_view_spatial/src/visualizers/utilities/mod.rs index 5bae8bcb19fa..496fba982f9f 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/utilities/mod.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/utilities/mod.rs @@ -13,6 +13,6 @@ pub use proc_mesh_vis::{ProcMeshBatch, ProcMeshDrawableBuilder}; pub use spatial_view_visualizer::{SpatialViewVisualizerData, iter_spatial_data}; pub use textured_rect::textured_rect_from_image; pub use transform_retrieval::{ - format_transform_info_result, spatial_view_kind_from_affinity, + entity_from_grid_transform, format_transform_info_result, spatial_view_kind_from_affinity, spatial_view_kind_from_view_class, transform_info_for_archetype_or_report_error, }; diff --git a/crates/viewer/re_view_spatial/src/visualizers/utilities/proc_mesh_vis.rs b/crates/viewer/re_view_spatial/src/visualizers/utilities/proc_mesh_vis.rs index e5cc85107e1b..5420f756c8b7 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/utilities/proc_mesh_vis.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/utilities/proc_mesh_vis.rs @@ -97,10 +97,11 @@ fn combine_instance_poses_with_archetype_transforms( let mut iter_rotation_quat = clamped_or_nothing(quaternions, num_instances); let last_target_from_instances = target_from_poses.last(); - let clamped_target_from_instances = target_from_poses - .iter() - .chain(std::iter::repeat(last_target_from_instances)) - .copied(); + let clamped_target_from_instances = std::iter::chain( + target_from_poses, + std::iter::repeat(last_target_from_instances), + ) + .copied(); let target_from_instances = clamped_target_from_instances .take(num_instances) @@ -159,7 +160,6 @@ impl<'ctx> ProcMeshDrawableBuilder<'ctx> { } /// Add a batch of data to be drawn. - #[expect(clippy::too_many_arguments)] pub fn add_batch( &mut self, query_context: &QueryContext<'_>, @@ -244,12 +244,11 @@ impl<'ctx> ProcMeshDrawableBuilder<'ctx> { let mut world_space_bounding_box = macaw::BoundingBox::nothing(); - let world_from_instances = target_from_instances - .iter() - .map(|transform| transform.as_affine3a()) - .chain(std::iter::repeat( - target_from_instances.last().as_affine3a(), - )); + let world_from_instances = std::iter::chain( + &target_from_instances, + std::iter::repeat(target_from_instances.last()), + ) + .map(|transform| transform.as_affine3a()); let mut num_instances = 0; for ( @@ -259,7 +258,7 @@ impl<'ctx> ProcMeshDrawableBuilder<'ctx> { half_sizes, world_from_instances, line_radii, - colors.iter(), + &colors, batch.meshes, batch.fill_modes ) @@ -358,7 +357,7 @@ impl<'ctx> ProcMeshDrawableBuilder<'ctx> { } } - self.data.add_bounding_box( + self.data.add_bounding_box_3d( entity_path.hash(), world_space_bounding_box, glam::Affine3A::IDENTITY, @@ -370,10 +369,11 @@ impl<'ctx> ProcMeshDrawableBuilder<'ctx> { visualizer_instruction: ent_context.visualizer_instruction, num_instances, overall_position: world_space_bounding_box.center(), - instance_positions: target_from_instances - .iter() - .chain(std::iter::repeat(target_from_instances.last())) - .map(|t| t.translation.as_vec3()), + instance_positions: std::iter::chain( + &target_from_instances, + std::iter::repeat(target_from_instances.last()), + ) + .map(|t| t.translation.as_vec3()), labels: batch.labels, colors: &colors, show_labels: batch diff --git a/crates/viewer/re_view_spatial/src/visualizers/utilities/spatial_view_visualizer.rs b/crates/viewer/re_view_spatial/src/visualizers/utilities/spatial_view_visualizer.rs index 6a349bbd2ff1..6aa20a4975a2 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/utilities/spatial_view_visualizer.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/utilities/spatial_view_visualizer.rs @@ -1,10 +1,26 @@ use re_log_types::EntityPathHash; -use re_sdk_types::ViewClassIdentifier; -use re_viewer_context::{SystemExecutionOutput, ViewClass as _}; +use re_viewer_context::SystemExecutionOutput; use super::UiLabel; +use crate::PickableTexturedRect; +use crate::SpaceKind; use crate::visualizers::LoadingIndicator; -use crate::{PickableTexturedRect, SpatialView2D}; + +/// A bounding box produced by a spatial visualizer. +#[derive(Clone, Copy, Debug)] +pub struct SpatialViewBoundingBox { + pub entity_path_hash: EntityPathHash, + pub bounding_box: macaw::BoundingBox, + + /// Whether this bounding box is defined in a 2D or 3D subspace. + /// + /// If an object can only be defined in a 2D subspace (e.g. a 2D image), this will be `SpaceKind::TwoD`. + /// Note that such objects can still be placed in a 3D scene, but need a pinhole parent to do so. + /// + /// We use this information to filter out 2D objects when computing the overall scene bounding box for a 3D scene, + /// since the camera plane distance may depend on the scene bounds and including 2D objects would create a feedback loop. + pub subspace: SpaceKind, +} /// Common data struct for all spatial scene elements. /// @@ -18,26 +34,22 @@ pub struct SpatialViewVisualizerData { pub ui_labels: Vec, /// Bounding boxes of all visualizations that the visualizer showed. - bounding_boxes: Vec<(EntityPathHash, macaw::BoundingBox)>, + bounding_boxes: Vec, /// Regions of interest for all visualizations, excluding spatial outliers. /// /// Used for camera framing and other heuristics. For most visualizers this is /// identical to the bounding box. Point cloud visualizers may provide a tighter /// region that excludes outlier points. - regions_of_interest: Vec<(EntityPathHash, macaw::BoundingBox)>, + regions_of_interest: Vec, /// Textured rectangles that the visualizer produced which can be interacted with. pub pickable_rects: Vec, } impl SpatialViewVisualizerData { - pub fn add_pickable_rect( - &mut self, - pickable_rect: PickableTexturedRect, - class_identifier: ViewClassIdentifier, - ) { - self.add_pickable_rect_to_bounding_box(&pickable_rect, class_identifier); + pub fn add_pickable_rect(&mut self, pickable_rect: PickableTexturedRect, subspace: SpaceKind) { + self.add_pickable_rect_to_bounding_box(&pickable_rect, subspace); self.pickable_rects.push(pickable_rect); } @@ -45,15 +57,42 @@ impl SpatialViewVisualizerData { /// /// For most visualizers these are the same. Use [`Self::add_bounding_box_and_region_of_interest`] /// when they differ (e.g. for point clouds with outlier rejection). - pub fn add_bounding_box( + pub fn add_bounding_box_3d( + &mut self, + entity: EntityPathHash, + bbox: macaw::BoundingBox, + world_from_obj: glam::Affine3A, + ) { + self.add_bounding_box(entity, bbox, world_from_obj, SpaceKind::ThreeD); + } + + pub fn add_bounding_box_2d( &mut self, entity: EntityPathHash, bbox: macaw::BoundingBox, world_from_obj: glam::Affine3A, + ) { + self.add_bounding_box(entity, bbox, world_from_obj, SpaceKind::TwoD); + } + + fn add_bounding_box( + &mut self, + entity: EntityPathHash, + bbox: macaw::BoundingBox, + world_from_obj: glam::Affine3A, + subspace: SpaceKind, ) { let transformed = bbox.transform_affine3(&world_from_obj); - self.bounding_boxes.push((entity, transformed)); - self.regions_of_interest.push((entity, transformed)); + self.bounding_boxes.push(SpatialViewBoundingBox { + entity_path_hash: entity, + bounding_box: transformed, + subspace, + }); + self.regions_of_interest.push(SpatialViewBoundingBox { + entity_path_hash: entity, + bounding_box: transformed, + subspace, + }); } /// Adds separate bounding box and region of interest for an entity. @@ -66,59 +105,60 @@ impl SpatialViewVisualizerData { bbox: macaw::BoundingBox, region_of_interest: macaw::BoundingBox, world_from_obj: glam::Affine3A, + subspace: SpaceKind, ) { - self.bounding_boxes - .push((entity, bbox.transform_affine3(&world_from_obj))); - self.regions_of_interest.push(( - entity, - region_of_interest.transform_affine3(&world_from_obj), - )); + self.bounding_boxes.push(SpatialViewBoundingBox { + entity_path_hash: entity, + bounding_box: bbox.transform_affine3(&world_from_obj), + subspace, + }); + self.regions_of_interest.push(SpatialViewBoundingBox { + entity_path_hash: entity, + bounding_box: region_of_interest.transform_affine3(&world_from_obj), + subspace, + }); } pub fn add_pickable_rect_to_bounding_box( &mut self, pickable_rect: &PickableTexturedRect, - class_identifier: ViewClassIdentifier, + subspace: SpaceKind, ) { - // Only update the bounding box if this is a 2D view. - // This is avoids a cyclic relationship where the image plane grows - // the bounds which in turn influence the size of the image plane. - // See: https://github.com/rerun-io/rerun/issues/3728 - if class_identifier == SpatialView2D::identifier() { - let entry = ( - pickable_rect.ent_path.hash(), - pickable_rect.textured_rect.bounding_box(), - ); - self.bounding_boxes.push(entry); - self.regions_of_interest.push(entry); - } + let entity_path_hash = pickable_rect.ent_path.hash(); + let bounding_box = pickable_rect.textured_rect.bounding_box(); + self.bounding_boxes.push(SpatialViewBoundingBox { + entity_path_hash, + bounding_box, + subspace, + }); + self.regions_of_interest.push(SpatialViewBoundingBox { + entity_path_hash, + bounding_box, + subspace, + }); } - pub fn iter_bounding_boxes( - &self, - ) -> impl ExactSizeIterator { + pub fn iter_bounding_boxes(&self) -> impl ExactSizeIterator { self.bounding_boxes.iter() } pub fn iter_regions_of_interest( &self, - ) -> impl ExactSizeIterator { + ) -> impl ExactSizeIterator { self.regions_of_interest.iter() } } -/// Iterate over [`SpatialViewVisualizerData`] from all visualizer outputs, -/// paired with the affinity of the visualizer that produced it. +/// Iterate over [`SpatialViewVisualizerData`] from all visualizer outputs. pub fn iter_spatial_data( system_output: &SystemExecutionOutput, -) -> impl Iterator, &SpatialViewVisualizerData)> { +) -> impl Iterator { system_output .visualizer_execution_output .per_visualizer .values() .filter_map(|result| { let output = result.as_ref().ok()?; - let data = output.get_visualizer_data::()?; - Some((output.affinity, data)) + output.get_visualizer_data::() }) } diff --git a/crates/viewer/re_view_spatial/src/visualizers/utilities/textured_rect.rs b/crates/viewer/re_view_spatial/src/visualizers/utilities/textured_rect.rs index 45cc7867d0b6..4dfdf9d587be 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/utilities/textured_rect.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/utilities/textured_rect.rs @@ -14,7 +14,6 @@ fn mag_filter(filter: MagnificationFilter) -> renderer::TextureFilterMag { } } -#[expect(clippy::too_many_arguments)] pub fn textured_rect_from_image( ctx: &ViewerContext<'_>, ent_path: &EntityPath, @@ -30,7 +29,7 @@ pub fn textured_rect_from_image( let debug_name = ent_path.to_string(); let image_stats = ctx .store_context - .memoizer(|c: &mut ImageStatsCache| c.entry(image)); + .memoizer_read_or_compute::(image); gpu_bridge::image_to_gpu( ctx.render_ctx(), diff --git a/crates/viewer/re_view_spatial/src/visualizers/utilities/transform_retrieval.rs b/crates/viewer/re_view_spatial/src/visualizers/utilities/transform_retrieval.rs index 78a25848c868..e21f54d76dca 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/utilities/transform_retrieval.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/utilities/transform_retrieval.rs @@ -1,34 +1,106 @@ use re_log_types::EntityPath; use re_sdk_types::ViewClassIdentifier; use re_sdk_types::blueprint::components::VisualizerInstructionId; +use re_sdk_types::components::{RotationAxisAngle, RotationQuat, Translation3D}; +use re_sdk_types::datatypes::Quaternion; +use re_tf::TransformFrameIdHash; use re_viewer_context::{ViewClass as _, VisualizerExecutionOutput, VisualizerReportSeverity}; +use crate::SpaceKind; use crate::contexts::{TransformInfo, TransformTreeContext}; -use crate::view_kind::SpatialViewKind; + +/// Resolves the optional translation and rotation components of a grid-like archetype into a +/// `grid_from_entity` affine transform. +/// +/// If both a quaternion and an axis-angle rotation are set, the quaternion takes precedence (and a +/// warning is reported). Reports an error and returns `None` if a provided rotation is invalid. +/// +/// `archetype_label` is used in the warning message, e.g. `"GridMap"` or `"VoxelGridMap"`. +#[expect(clippy::too_many_arguments)] +pub fn entity_from_grid_transform( + results: &re_view::VisualizerInstructionQueryResults<'_>, + entity_path: &EntityPath, + archetype_label: &str, + translation: Option, + rotation_axis_angle: Option, + quaternion: Option, + quaternion_component: re_sdk_types::ComponentIdentifier, + rotation_axis_angle_component: re_sdk_types::ComponentIdentifier, +) -> Option { + let translation = translation.map_or(glam::Affine3A::IDENTITY, Into::into); + + let rotation = match (quaternion, rotation_axis_angle) { + (Some(quaternion), Some(rotation_axis_angle)) + if quaternion.0 != Quaternion::IDENTITY + && rotation_axis_angle != RotationAxisAngle::IDENTITY => + { + results.report_for_component( + quaternion_component, + VisualizerReportSeverity::Warning, + format!( + "{archetype_label} {entity_path} has both quaternion and rotation_axis_angle set; using quaternion." + ), + ); + + let Ok(rotation) = glam::Affine3A::try_from(quaternion) else { + results.report_for_component( + quaternion_component, + VisualizerReportSeverity::Error, + "invalid rotation quaternion", + ); + return None; + }; + rotation + } + (Some(quaternion), _) => { + let Ok(rotation) = glam::Affine3A::try_from(quaternion) else { + results.report_for_component( + quaternion_component, + VisualizerReportSeverity::Error, + "invalid rotation quaternion", + ); + return None; + }; + rotation + } + (_, Some(rotation_axis_angle)) => { + let Ok(rotation) = glam::Affine3A::try_from(rotation_axis_angle) else { + results.report_for_component( + rotation_axis_angle_component, + VisualizerReportSeverity::Error, + "invalid rotation axis-angle", + ); + return None; + }; + rotation + } + (None, None) => glam::Affine3A::IDENTITY, + }; + + Some(translation * rotation) +} /// Derive the spatial view kind from the view class identifier. -pub fn spatial_view_kind_from_view_class(class: ViewClassIdentifier) -> SpatialViewKind { +pub fn spatial_view_kind_from_view_class(class: ViewClassIdentifier) -> SpaceKind { if class == crate::SpatialView3D::identifier() { - SpatialViewKind::ThreeD + SpaceKind::ThreeD } else if class == crate::SpatialView2D::identifier() { - SpatialViewKind::TwoD + SpaceKind::TwoD } else { re_log::debug_panic!("Not a spatial view class identifier {class:?}"); - SpatialViewKind::TwoD + SpaceKind::TwoD } } /// Derive the spatial view kind from an optional view class affinity. /// /// Returns `None` if the affinity is `None` or not a spatial view class. -pub fn spatial_view_kind_from_affinity( - affinity: Option, -) -> Option { +pub fn spatial_view_kind_from_affinity(affinity: Option) -> Option { let class = affinity?; if class == crate::SpatialView3D::identifier() { - Some(SpatialViewKind::ThreeD) + Some(SpaceKind::ThreeD) } else if class == crate::SpatialView2D::identifier() { - Some(SpatialViewKind::TwoD) + Some(SpaceKind::TwoD) } else { None } @@ -38,13 +110,24 @@ pub fn spatial_view_kind_from_affinity( pub fn transform_info_for_archetype_or_report_error<'a>( entity_path: &EntityPath, transform_context: &'a TransformTreeContext, - archetype_kind: Option, - view_kind: SpatialViewKind, + archetype_kind: Option, + view_kind: SpaceKind, instruction_id: &VisualizerInstructionId, output: &VisualizerExecutionOutput, ) -> Option<&'a TransformInfo> { re_tracing::profile_function!(); + if transform_context.uses_implicit_frame_for_empty_coordinate_frame(entity_path.hash()) { + output.report_unspecified_source( + *instruction_id, + VisualizerReportSeverity::Warning, + format!( + "CoordinateFrame has an empty frame ID; falling back to the implicit frame {:?}.", + re_tf::TransformFrameId::from_entity_path(entity_path).as_str(), + ), + ); + } + let result = transform_context.target_from_entity_path(entity_path.hash()); let transform_info = match format_transform_info_result(entity_path, transform_context, result) { @@ -161,8 +244,8 @@ pub fn is_valid_space_for_content( instruction_id: &VisualizerInstructionId, transform_context: &TransformTreeContext, transform: &TransformInfo, - content_kind: Option, - view_kind: SpatialViewKind, + content_kind: Option, + view_kind: SpaceKind, output: &VisualizerExecutionOutput, ) -> bool { let Some(content_view_kind) = content_kind else { @@ -178,7 +261,7 @@ pub fn is_valid_space_for_content( // // Everything in this 3D view is technically 2D already, but we still have the 3D controls etc. // (We can however, still show some "agnostic" content like the Pinhole itself) - if view_kind == SpatialViewKind::ThreeD + if view_kind == SpaceKind::ThreeD && let Some(target_frame_pinhole_root) = target_frame_pinhole_root { let origin = if let Some(origin) = @@ -198,29 +281,53 @@ pub fn is_valid_space_for_content( .pinhole_tree_root_info(transform.tree_root()) .is_some(); + // Helper for formatting messages below. + let frame_text = |frame_hash: TransformFrameIdHash| { + if let Some(frame) = transform_context.format_frame_or_debug_warn(frame_hash, entity_path) { + format!(" ({frame:?})") + } else { + String::new() + } + }; + match content_view_kind { - SpatialViewKind::TwoD => { + SpaceKind::TwoD => { match view_kind { - SpatialViewKind::TwoD => { - // Degenerated case: 2D content is under a pinhole which itself is NOT the pinhole that the 2D view is in. - // We don't allow this since this would mean to apply a 3D->2D projection to a space that's already 2D. - if transform_has_pinhole_ancestor - && target_frame_pinhole_root.is_none_or(|target_frame_pinhole_root| { - target_frame_pinhole_root != transform.tree_root() - }) - { - output.report_unspecified_source( - *instruction_id, - VisualizerReportSeverity::Error, - "Can't visualize 2D content with a pinhole ancestor that's embedded within the 2D view. This applies a 3D → 2D projection to a space that's already regarded 2D.", - ); - false - } else { - true + SpaceKind::TwoD => { + if !transform_has_pinhole_ancestor { + return true; + } + // 2D content below a pinhole is only valid when the view targets the same pinhole-defined 2D subspace. + match target_frame_pinhole_root { + None => { + output.report_unspecified_source( + *instruction_id, + VisualizerReportSeverity::Error, + format!( + "This 2D content has a pinhole transform frame ancestor{}, but the 2D view's target frame doesn't have a pinhole root.", + frame_text(transform.tree_root()) + ), + ); + false + } + Some(target_frame_pinhole_root) + if target_frame_pinhole_root != transform.tree_root() => + { + output.report_unspecified_source( + *instruction_id, + VisualizerReportSeverity::Error, + format!( + "This 2D content has a pinhole transform frame ancestor{} that is different from the 2D view's pinhole root{}.", + frame_text(transform.tree_root()), frame_text(target_frame_pinhole_root) + ), + ); + false + } + Some(_) => true, } } - SpatialViewKind::ThreeD => { + SpaceKind::ThreeD => { // 2D content in a 3D view needs to be under a Pinhole transform. if transform_has_pinhole_ancestor { true @@ -236,7 +343,7 @@ pub fn is_valid_space_for_content( } } - SpatialViewKind::ThreeD => { + SpaceKind::ThreeD => { // View agnostic failure case for 3D content: if the 3D content is under a pinhole projection, we can't show it! if transform_has_pinhole_ancestor { output.report_unspecified_source( @@ -248,7 +355,7 @@ pub fn is_valid_space_for_content( } match view_kind { - SpatialViewKind::TwoD => { + SpaceKind::TwoD => { // 3D content in 2D works only if there's a Pinhole transform at the origin of the view. // // TODO(andreas): What's actually keeping us from allowing the 2D view to be rooted _under_ a pinhole, e.g. `/pinhole_here/some_2d_stuff`? @@ -268,7 +375,7 @@ pub fn is_valid_space_for_content( } } - SpatialViewKind::ThreeD => true, // Valid 3D content in a valid 3D view is always fine. + SpaceKind::ThreeD => true, // Valid 3D content in a valid 3D view is always fine. } } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/video/encoded_depth_image.rs b/crates/viewer/re_view_spatial/src/visualizers/video/encoded_depth_image.rs new file mode 100644 index 000000000000..bff15719f237 --- /dev/null +++ b/crates/viewer/re_view_spatial/src/visualizers/video/encoded_depth_image.rs @@ -0,0 +1,165 @@ +use nohash_hasher::IntMap; + +use re_log_types::EntityPathHash; +use re_sdk_types::Archetype as _; +use re_sdk_types::archetypes::EncodedDepthImage; +use re_sdk_types::components::{Blob, Colormap, DepthMeter, FillRatio, MediaType, ValueRange}; +use re_view::{DataResultQuery as _, latest_at_with_blueprint_resolved_data}; +use re_viewer_context::ViewClass as _; +use re_viewer_context::{ + IdentifiedViewSystem, ViewContext, ViewContextCollection, ViewQuery, ViewSystemExecutionError, + VisualizerExecutionOutput, VisualizerQueryInfo, VisualizerSystem, + gpu_bridge::colormap_to_re_renderer, typed_fallback_for, +}; + +use super::{DepthTextureConfig, SpatialViewVisualizerData, execute_video_stream_like}; +use crate::visualizers::depth_images::DepthImageProcessResult; +use crate::visualizers::video::VideoStreamCtx; + +pub struct EncodedDepthImageVisualizerOutput { + /// Depth cloud entities, keyed by entity path, for picking. + /// + /// Currently always empty; will be populated once depth cloud rendering is wired + /// up through the video path. + pub depth_cloud_entities: IntMap, +} + +#[derive(Default)] +pub struct EncodedDepthImageVisualizer; + +impl IdentifiedViewSystem for EncodedDepthImageVisualizer { + fn identifier() -> re_viewer_context::ViewSystemIdentifier { + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "EncodedDepthImage" + ) + } +} + +impl VisualizerSystem for EncodedDepthImageVisualizer { + fn visualizer_query_info( + &self, + _app_options: &re_viewer_context::AppOptions, + ) -> VisualizerQueryInfo { + VisualizerQueryInfo::single_required_component::( + &EncodedDepthImage::descriptor_blob(), + &EncodedDepthImage::all_components(), + ) + } + + fn affinity(&self) -> Option { + Some(crate::SpatialView2D::identifier()) + } + + fn execute( + &self, + ctx: &ViewContext<'_>, + view_query: &ViewQuery<'_>, + context_systems: &ViewContextCollection, + ) -> Result { + re_tracing::profile_function!(); + + let mut data = SpatialViewVisualizerData::default(); + let mut depth_cloud_entities: IntMap = + IntMap::default(); + + let arch_name = EncodedDepthImage::name(); + let sample_component = EncodedDepthImage::descriptor_blob().component; + + let get_codec: &crate::visualizers::video::GetCodecFn = + &|ctx, latest_at, data_result, instruction, output| { + let codec_component = EncodedDepthImage::descriptor_media_type().component; + let results = data_result.latest_at_with_blueprint_resolved_data_for_component( + ctx, + latest_at, + codec_component, + Some(instruction), + ); + if results.any_missing_chunks() { + output.set_missing_chunks(); + } + + let codec = results + .get_mono::(codec_component) + .map(|m| m.to_string()); + Ok(re_video::VideoCodec::ImageSequence(codec)) + }; + + let get_depth_config: &crate::visualizers::video::GetDepthConfigFn = + &|ctx, latest_at, data_result, instruction, output| { + let colormap_component = EncodedDepthImage::descriptor_colormap().component; + let value_range_component = EncodedDepthImage::descriptor_depth_range().component; + let depth_meter_component = EncodedDepthImage::descriptor_meter().component; + let fill_ratio_component = + EncodedDepthImage::descriptor_point_fill_ratio().component; + + let query_ctx = re_viewer_context::QueryContext { + view_ctx: ctx, + target_entity_path: &data_result.entity_path, + instruction_id: Some(instruction.id), + archetype_name: Some(EncodedDepthImage::name()), + query: latest_at.clone(), + }; + + let results = latest_at_with_blueprint_resolved_data( + ctx, + None, + latest_at, + data_result, + [ + colormap_component, + value_range_component, + depth_meter_component, + fill_ratio_component, + ], + Some(instruction), + ); + if results.any_missing_chunks() { + output.set_missing_chunks(); + } + + let colormap: Colormap = results + .get_mono(colormap_component) + .unwrap_or_else(|| typed_fallback_for(&query_ctx, colormap_component)); + + let value_range: ValueRange = results + .get_mono(value_range_component) + .unwrap_or_else(|| typed_fallback_for(&query_ctx, value_range_component)); + let value_range = [value_range.0.0[0] as f32, value_range.0.0[1] as f32]; + + let depth_meter: DepthMeter = results + .get_mono(depth_meter_component) + .unwrap_or_else(|| typed_fallback_for(&query_ctx, depth_meter_component)); + + let fill_ratio: FillRatio = + results.get_mono(fill_ratio_component).unwrap_or_default(); + + DepthTextureConfig { + colormap: colormap_to_re_renderer(colormap), + range: value_range, + depth_meter, + fill_ratio, + } + }; + + let ctx = VideoStreamCtx::new( + ctx, + view_query, + context_systems, + &mut data, + Self::identifier(), + arch_name, + sample_component, + &get_codec, + ) + .with_depth_handler(get_depth_config, &mut depth_cloud_entities); + + let output = execute_video_stream_like(ctx)?; + + Ok( + output.with_visualizer_data(EncodedDepthImageVisualizerOutput { + depth_cloud_entities, + }), + ) + } +} diff --git a/crates/viewer/re_view_spatial/src/visualizers/video/encoded_image.rs b/crates/viewer/re_view_spatial/src/visualizers/video/encoded_image.rs index f6ba45c9685f..f9fb81b584cf 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/video/encoded_image.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/video/encoded_image.rs @@ -1,6 +1,6 @@ use re_sdk_types::Archetype as _; use re_sdk_types::archetypes::EncodedImage; -use re_sdk_types::components::Blob; +use re_sdk_types::components::{Blob, MediaType}; use re_view::DataResultQuery as _; use re_viewer_context::{ IdentifiedViewSystem, ViewClass as _, ViewContext, ViewContextCollection, ViewQuery, @@ -8,14 +8,17 @@ use re_viewer_context::{ }; use super::SpatialViewVisualizerData; -use crate::visualizers::video::execute_video_stream_like; +use crate::visualizers::video::{VideoStreamCtx, execute_video_stream_like}; #[derive(Default)] pub struct EncodedImageVisualizer; impl IdentifiedViewSystem for EncodedImageVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "EncodedImage".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "EncodedImage" + ) } } @@ -44,47 +47,37 @@ impl VisualizerSystem for EncodedImageVisualizer { let mut data = SpatialViewVisualizerData::default(); - let arch_name = EncodedImage::name(); - let sample_component = EncodedImage::descriptor_blob().component; - let opacity_component = EncodedImage::descriptor_opacity().component; - let get_codec: &crate::visualizers::video::GetCodecFn = &|ctx, latest_at, data_result, instruction, output| { let codec_component = EncodedImage::descriptor_media_type().component; - let codec_result_wrapped = re_view::BlueprintResolvedResults::LatestAt( - latest_at.clone(), - data_result.latest_at_with_blueprint_resolved_data_for_component( - ctx, - latest_at, - codec_component, - Some(instruction), - ), - ); - let codec_result = re_view::VisualizerInstructionQueryResults::new( - instruction, - &codec_result_wrapped, - output, + let results = data_result.latest_at_with_blueprint_resolved_data_for_component( + ctx, + latest_at, + codec_component, + Some(instruction), ); + if results.any_missing_chunks() { + output.set_missing_chunks(); + } - let all_codecs = codec_result.iter_optional(codec_component); - let codec = all_codecs - .slice::() - .next() - .and_then(|((_time, _row_id), codec)| Some(codec.first()?.to_string())); - + let codec = results + .get_mono::(codec_component) + .map(|m| m.to_string()); Ok(re_video::VideoCodec::ImageSequence(codec)) }; - execute_video_stream_like( + let ctx = VideoStreamCtx::new( ctx, view_query, context_systems, &mut data, Self::identifier(), - arch_name, - sample_component, - opacity_component, + EncodedImage::name(), + EncodedImage::descriptor_blob().component, &get_codec, ) + .with_opacity_component(EncodedImage::descriptor_opacity().component); + + execute_video_stream_like(ctx) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/video/mod.rs b/crates/viewer/re_view_spatial/src/visualizers/video/mod.rs index 525b980876ad..c2675974b444 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/video/mod.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/video/mod.rs @@ -1,29 +1,33 @@ +mod encoded_depth_image; mod encoded_image; mod video_frame_reference; mod video_stream; +pub use encoded_depth_image::{EncodedDepthImageVisualizer, EncodedDepthImageVisualizerOutput}; pub use encoded_image::EncodedImageVisualizer; +use nohash_hasher::IntMap; use re_log_types::hash::Hash64; use re_log_types::{EntityPath, EntityPathHash}; use re_renderer::renderer; -use re_renderer::resource_managers::ImageDataDesc; -use re_sdk_types::ViewClassIdentifier; +use re_renderer::resource_managers::{GpuTexture2D, ImageDataDesc}; use re_sdk_types::blueprint::components::VisualizerInstructionId; use re_sdk_types::components::Opacity; use re_ui::ContextExt as _; use re_video::player::{VideoPlaybackIssueSeverity, VideoPlayerError}; use re_view::DataResultQuery as _; use re_viewer_context::{ - VideoStreamCache, VideoStreamProcessingError, ViewClass as _, ViewContext, - ViewContextCollection, ViewQuery, ViewSystemExecutionError, ViewSystemIdentifier, - VisualizerExecutionOutput, typed_fallback_for, video_stream_time_from_query, + SystemCommandSender as _, VideoStoreSource, VideoStreamCache, VideoStreamProcessingError, + ViewClass as _, ViewContext, ViewContextCollection, ViewQuery, ViewSystemExecutionError, + ViewSystemIdentifier, VisualizerExecutionOutput, typed_fallback_for, + video_stream_time_from_query, }; pub use video_frame_reference::VideoFrameReferenceVisualizer; pub use video_stream::VideoStreamVisualizer; use super::{LoadingIndicator, SpatialViewVisualizerData, UiLabel, UiLabelStyle, UiLabelTarget}; +use crate::SpaceKind; use crate::contexts::EntityDepthOffsets; -use crate::view_kind::SpatialViewKind; +use crate::visualizers::DepthImageProcessResult; use crate::visualizers::utilities::{ spatial_view_kind_from_view_class, transform_info_for_archetype_or_report_error, }; @@ -37,17 +41,128 @@ type GetCodecFn = dyn Fn( &VisualizerExecutionOutput, ) -> Result; -#[expect(clippy::too_many_arguments)] -fn execute_video_stream_like( - ctx: &ViewContext<'_>, - view_query: &ViewQuery<'_>, - context_systems: &ViewContextCollection, - data: &mut SpatialViewVisualizerData, +/// Configuration for rendering depth textures with a colormap. +/// +/// When provided to [`execute_video_stream_like`], the video frame texture +/// will be colormapped instead of treated as a regular RGBA image. +/// In 3D views with a pinhole camera, a depth cloud will be rendered instead. +pub(super) struct DepthTextureConfig { + pub colormap: re_renderer::Colormap, + pub range: [f32; 2], + pub depth_meter: re_sdk_types::components::DepthMeter, + pub fill_ratio: re_sdk_types::components::FillRatio, +} + +impl DepthTextureConfig { + fn to_colormapped_texture(&self, texture: GpuTexture2D) -> renderer::ColormappedTexture { + renderer::ColormappedTexture { + texture, + range: self.range, + decode_srgb: false, + texture_alpha: renderer::TextureAlpha::Opaque, + gamma: 1.0, + color_mapper: renderer::ColorMapper::Function(self.colormap), + shader_decoding: None, + } + } +} + +/// Callback to produce per-entity depth texture configuration. +/// +/// Called once per entity/instruction to query colormap, value range, etc. +pub(super) type GetDepthConfigFn = dyn Fn( + &ViewContext<'_>, + &re_chunk_store::LatestAtQuery, + &re_viewer_context::DataResult, + &re_viewer_context::VisualizerInstruction, + &VisualizerExecutionOutput, +) -> DepthTextureConfig; + +struct DepthHandler<'a> { + get_depth_config: &'a GetDepthConfigFn, + depth_cloud_entities: &'a mut IntMap, +} + +/// Used to pass the required context to [`execute_video_stream_like`]. +struct VideoStreamCtx<'a> { + view_context: &'a ViewContext<'a>, + view_query: &'a ViewQuery<'a>, + context_systems: &'a ViewContextCollection, + data: &'a mut SpatialViewVisualizerData, + visualizer_name: ViewSystemIdentifier, + archetype_name: re_sdk_types::ArchetypeName, sample_component: re_sdk_types::ComponentIdentifier, - opacity_component: re_sdk_types::ComponentIdentifier, - get_codec: &GetCodecFn, + opacity_component: Option, + + get_codec: &'a GetCodecFn, + + depth_handler: Option>, +} + +impl<'a> VideoStreamCtx<'a> { + pub fn new( + view_context: &'a ViewContext<'a>, + view_query: &'a ViewQuery<'a>, + context_systems: &'a ViewContextCollection, + data: &'a mut SpatialViewVisualizerData, + + visualizer_name: ViewSystemIdentifier, + + archetype_name: re_sdk_types::ArchetypeName, + sample_component: re_sdk_types::ComponentIdentifier, + + get_codec: &'a GetCodecFn, + ) -> Self { + Self { + view_context, + view_query, + context_systems, + data, + visualizer_name, + archetype_name, + sample_component, + opacity_component: None, + get_codec, + depth_handler: None, + } + } + + pub fn with_opacity_component( + mut self, + opacity_component: re_sdk_types::ComponentIdentifier, + ) -> Self { + self.opacity_component = Some(opacity_component); + + self + } + + pub fn with_depth_handler( + mut self, + get_depth_config: &'a GetDepthConfigFn, + depth_cloud_entities: &'a mut IntMap, + ) -> Self { + self.depth_handler = Some(DepthHandler { + get_depth_config, + depth_cloud_entities, + }); + + self + } +} + +impl<'a> std::ops::Deref for VideoStreamCtx<'a> { + type Target = ViewContext<'a>; + + #[inline] + fn deref(&self) -> &Self::Target { + self.view_context + } +} + +fn execute_video_stream_like( + mut ctx: VideoStreamCtx<'_>, ) -> Result { re_tracing::profile_function!(); @@ -55,11 +170,15 @@ fn execute_video_stream_like( let viewer_ctx = ctx.viewer_ctx; let view_kind = spatial_view_kind_from_view_class(ctx.view_class_identifier); - let transforms = context_systems.get::(&output)?; - let depth_offsets = context_systems.get::(&output)?; - let latest_at = view_query.latest_at_query(); - - for (data_result, instruction) in view_query.iter_visualizer_instruction_for(visualizer_name) { + let transforms = ctx.context_systems.get::(&output)?; + let depth_offsets = ctx.context_systems.get::(&output)?; + let latest_at = ctx.view_query.latest_at_query(); + let mut depth_clouds = Vec::new(); + + for (data_result, instruction) in ctx + .view_query + .iter_visualizer_instruction_for(ctx.visualizer_name) + { let entity_path = &data_result.entity_path; re_tracing::profile_scope!("Entity", entity_path.to_string().as_str()); @@ -67,7 +186,7 @@ fn execute_video_stream_like( let Some(transform_info) = transform_info_for_archetype_or_report_error( entity_path, transforms, - Some(SpatialViewKind::TwoD), + Some(SpaceKind::TwoD), view_kind, &instruction.id, &output, @@ -76,10 +195,11 @@ fn execute_video_stream_like( }; let world_from_entity = transform_info - .single_transform_required_for_entity(entity_path, archetype_name) + .single_transform_required_for_entity(entity_path, ctx.archetype_name) .as_affine3a(); let query_context = ctx.query_context(data_result, latest_at.clone(), instruction.id); - let highlight = view_query + let highlight = ctx + .view_query .highlights .entity_outline_mask(entity_path.hash()); @@ -89,48 +209,53 @@ fn execute_video_stream_like( // Note that this area is also used for the bounding box which is important for the 2D view to determine default bounds. let mut video_resolution = glam::vec2(1280.0, 720.0); - let opacity_result_wrapped = re_view::BlueprintResolvedResults::LatestAt( - latest_at.clone(), - data_result.latest_at_with_blueprint_resolved_data_for_component( - ctx, + let opacity = ctx.opacity_component.map(|opacity_component| { + let results = data_result.latest_at_with_blueprint_resolved_data_for_component( + &ctx, &latest_at, opacity_component, Some(instruction), - ), - ); - - let opacity_result = re_view::VisualizerInstructionQueryResults::new( - instruction, - &opacity_result_wrapped, - &output, - ); - - let all_opacities = opacity_result.iter_optional(opacity_component); - let opacity = all_opacities - .slice::() - .next() - .and_then(|((_time, _row_id), opacity)| opacity.first()) - .copied() - .map(Opacity::from); + ); + if results.any_missing_chunks() { + output.set_missing_chunks(); + } + results + .get_mono::(opacity_component) + .unwrap_or_else(|| { + typed_fallback_for( + &re_viewer_context::QueryContext { + view_ctx: &ctx, + target_entity_path: entity_path, + instruction_id: Some(instruction.id), + archetype_name: Some(ctx.archetype_name), + query: latest_at.clone(), + }, + opacity_component, + ) + }) + }); // Perform a latest-at query for the sample component to give the video stream cache something to hook onto. let _sample_result = data_result.latest_at_with_blueprint_resolved_data_for_component( - ctx, + &ctx, &latest_at, - sample_component, + ctx.sample_component, Some(instruction), ); let video = match viewer_ctx .store_context .memoizer(|c: &mut VideoStreamCache| { + let new_codec = + (ctx.get_codec)(&ctx, &latest_at, data_result, instruction, &output)?; + c.entry( viewer_ctx.recording(), entity_path, - view_query.timeline, + ctx.view_query.timeline, viewer_ctx.app_options().video_decoder_settings(), - sample_component, - &|| get_codec(ctx, &latest_at, data_result, instruction, &output), + ctx.sample_component, + new_codec, ) }) { Ok(video) => video, @@ -156,8 +281,8 @@ fn execute_video_stream_like( }; show_video_frame( - ctx, - data, + ctx.view_context, + ctx.data, entity_path, world_from_entity, highlight, @@ -165,6 +290,8 @@ fn execute_video_stream_like( instruction.id, None, Some(VideoPlaybackIssue::custom(description, severity)), + None, + None, ); continue; } @@ -176,6 +303,14 @@ fn execute_video_stream_like( continue; } + let bit_depth = video + .read() + .video_renderer + .data_descr() + .encoding_details + .as_ref() + .and_then(|d| d.bit_depth); + let frame_output = { let video = video.read(); @@ -184,83 +319,158 @@ fn execute_video_stream_like( } let storage_engine = ctx.viewer_ctx.store_context.recording.storage_engine(); - let get_chunk_array = |id| { - let chunk = storage_engine.store().use_chunk_or_report_missing(&id); - - let Some(chunk) = chunk else { - output.set_missing_chunks(); // Make sure we show a view-wide loading indicator - return None; - }; - - let (_, buffer) = re_arrow_util::blob_arrays_offsets_and_buffer( - chunk.raw_component_array(sample_component)?, - )?; - - Some(buffer) - }; video.video_renderer.frame_at( ctx.viewer_ctx.render_ctx(), - video_stream_id(entity_path, sample_component, AT_TIME_CURSOR_SALT), + video_stream_id(entity_path, ctx.sample_component, AT_TIME_CURSOR_SALT), video_stream_time_from_query(&query_context.query), - &|id| { - let buffer = get_chunk_array(re_sdk_types::ChunkId::from_tuid(id)); - - buffer.map(|b| b.as_slice()).unwrap_or(&[]) + &VideoStoreSource { + store: storage_engine.store(), + sample_component: ctx.sample_component, + indicate: true, }, ) }; let depth_offset = depth_offsets .per_entity_and_visualizer - .get(&(visualizer_name, entity_path.hash())) + .get(&(ctx.visualizer_name, entity_path.hash())) .copied() .unwrap_or_default(); - let opacity = opacity.unwrap_or_else(|| { - typed_fallback_for( - &re_viewer_context::QueryContext { - view_ctx: ctx, - target_entity_path: entity_path, - instruction_id: Some(instruction.id), - archetype_name: Some(archetype_name), - query: latest_at.clone(), - }, - opacity_component, - ) - }); + let opacity = opacity.unwrap_or_else(|| Opacity(1.0.into())); #[expect(clippy::disallowed_methods)] // This is not a hard-coded color. let multiplicative_tint = egui::Rgba::from_white_alpha(opacity.0.clamp(0.0, 1.0)); - show_video_frame( - ctx, - data, - entity_path, - world_from_entity, - highlight, - video_resolution, - instruction.id, - frame_output.output.map(|texture| VideoFrameRenderInfo { - texture, - depth_offset, - multiplicative_tint, - }), - frame_output.error.map(VideoPlaybackIssue::from), - ); + let depth_config = ctx.depth_handler.as_ref().map(|depth_handler| { + (depth_handler.get_depth_config)( + ctx.view_context, + &latest_at, + data_result, + instruction, + &output, + ) + }); + + // In 3D views, depth images should render as point clouds when a pinhole camera is available. + let rendered_as_depth_cloud = if view_kind == SpaceKind::ThreeD + && let Some(depth_config) = &depth_config + && let Some(frame_texture) = frame_output + .output + .as_ref() + .and_then(|t| t.texture.as_ref()) + { + let tree_root_frame = transform_info.tree_root(); + if let Some(pinhole_tree_root_info) = transforms.pinhole_tree_root_info(tree_root_frame) + && let Some(world_from_view) = transforms.target_from_pinhole_root(tree_root_frame) + { + let colormapped = depth_config.to_colormapped_texture(frame_texture.clone()); + + let pinhole = &pinhole_tree_root_info.pinhole_projection; + let world_from_view = world_from_view.as_affine3a(); + let world_from_rdf = world_from_view + * glam::Affine3A::from_mat3(pinhole.view_coordinates.from_rdf()); + + let dimensions = glam::UVec2::from_array(colormapped.texture.width_height()); + + if dimensions.x == 0 || dimensions.y == 0 { + false + } else { + let world_depth_from_texture_depth = 1.0 / *depth_config.depth_meter.0; + + let fov_y = pinhole + .image_from_camera + .fov_y(pinhole.resolution.unwrap_or_else(|| [1.0, 1.0].into())); + let pixel_width_from_depth = (0.5 * fov_y).tan() / (0.5 * dimensions.y as f32); + let point_radius_from_world_depth = + *depth_config.fill_ratio.0 * pixel_width_from_depth; + + let cloud = re_renderer::renderer::DepthCloud { + world_from_rdf, + depth_camera_intrinsics: pinhole.image_from_camera.0.into(), + world_depth_from_texture_depth, + point_radius_from_world_depth, + min_max_depth_in_world: [ + world_depth_from_texture_depth * colormapped.range[0], + world_depth_from_texture_depth * colormapped.range[1], + ], + depth_dimensions: dimensions, + depth_texture: colormapped.texture.clone(), + colormap: depth_config.colormap, + outline_mask_id: highlight.overall, + picking_object_id: re_renderer::PickingLayerObjectId(entity_path.hash64()), + }; + + ctx.data.add_bounding_box_3d( + entity_path.hash(), + cloud.world_space_bbox(), + glam::Affine3A::IDENTITY, + ); + if let Some(depth_handler) = &mut ctx.depth_handler { + depth_handler.depth_cloud_entities.insert( + entity_path.hash(), + super::depth_images::DepthImageProcessResult { + image_info: None, + depth_meter: depth_config.depth_meter, + colormap: colormapped, + }, + ); + } + depth_clouds.push(cloud); + true + } + } else { + false + } + } else { + false + }; - if context_systems.view_class_identifier == SpatialView2D::identifier() { - let bounding_box = macaw::BoundingBox::from_min_size( - world_from_entity.transform_point3(glam::Vec3::ZERO), - video_resolution.extend(0.0), + if !rendered_as_depth_cloud { + show_video_frame( + ctx.view_context, + ctx.data, + entity_path, + world_from_entity, + highlight, + video_resolution, + instruction.id, + frame_output.output.map(|texture| VideoFrameRenderInfo { + texture, + depth_offset, + multiplicative_tint, + }), + frame_output.error.map(VideoPlaybackIssue::from), + depth_config.as_ref(), + bit_depth, ); - data.add_bounding_box(entity_path.hash(), bounding_box, world_from_entity); + + if ctx.context_systems.view_class_identifier == SpatialView2D::identifier() { + let bounding_box = macaw::BoundingBox::from_min_size( + world_from_entity.transform_point3(glam::Vec3::ZERO), + video_resolution.extend(0.0), + ); + ctx.data + .add_bounding_box_2d(entity_path.hash(), bounding_box, world_from_entity); + } } } - Ok(output - .with_draw_data([PickableTexturedRect::to_draw_data( - viewer_ctx.render_ctx(), - &data.pickable_rects, - )?]) - .with_visualizer_data(std::mem::take(data))) + + if depth_clouds.is_empty() { + Ok(output + .with_draw_data([PickableTexturedRect::to_draw_data( + viewer_ctx.render_ctx(), + &ctx.data.pickable_rects, + )?]) + .with_visualizer_data(std::mem::take(ctx.data))) + } else { + super::depth_images::populate_depth_visualizer_execution_result( + &ctx, + ctx.data, + depth_clouds, + output, + ) + .map(|output| output.with_visualizer_data(std::mem::take(ctx.data))) + } } pub const AT_TIME_CURSOR_SALT: u64 = 0x12356; @@ -315,19 +525,7 @@ impl From for VideoPlaybackIssue { message: error.to_string(), severity: error.severity(), should_request_more_frames: error.should_request_more_frames(), - show_frame: match error { - VideoPlayerError::NegativeTimestamp - | VideoPlayerError::InsufficientSampleData(_) => false, - - VideoPlayerError::EmptyBuffer - | VideoPlayerError::UnloadedSampleData(_) - | VideoPlayerError::CreateChunk(_) - | VideoPlayerError::DecodeChunk(_) - | VideoPlayerError::Decoding(_) - | VideoPlayerError::BadData - | VideoPlayerError::TextureUploadError(_) - | VideoPlayerError::DecoderUnexpectedlyExited => true, - }, + show_frame: error.severity() != VideoPlaybackIssueSeverity::Informational, } } } @@ -338,7 +536,6 @@ impl From for VideoPlaybackIssue { /// - Only `frame`: renders the frame texture. /// - Only `issue`: shows the error/loading overlay. /// - Both `Some`: renders the frame with the error overlaid on top. -#[expect(clippy::too_many_arguments)] fn show_video_frame( ctx: &ViewContext<'_>, visualizer_data: &mut SpatialViewVisualizerData, @@ -349,6 +546,8 @@ fn show_video_frame( visualizer_instruction: VisualizerInstructionId, frame: Option, issue: Option, + depth_config: Option<&DepthTextureConfig>, + bit_depth: Option, ) { let show_frame = issue.as_ref().map(|issue| issue.show_frame).unwrap_or(true); if !show_frame { @@ -386,6 +585,13 @@ fn show_video_frame( }; if let Some(reason) = loading_indicator_reason { + ctx.viewer_ctx.command_sender().send_system( + re_viewer_context::SystemCommand::TimeControlCommands { + store_id: ctx.store_id().clone(), + time_commands: vec![re_viewer_context::TimeControlCommand::Buffer], + }, + ); + visualizer_data.loading_indicators.push(LoadingIndicator { center: top_left_corner_position + 0.5 * (extent_u + extent_v), half_extent_u: 0.5 * extent_u, @@ -419,7 +625,11 @@ fn show_video_frame( top_left_corner_position, extent_u, extent_v, - colormapped_texture: renderer::ColormappedTexture::from_video_frame(texture), + colormapped_texture: if let Some(config) = depth_config { + config.to_colormapped_texture(texture) + } else { + renderer::ColormappedTexture::from_video_frame(texture, bit_depth) + }, options: renderer::RectangleOptions { texture_filter_magnification: renderer::TextureFilterMag::Nearest, texture_filter_minification: renderer::TextureFilterMin::Linear, @@ -435,9 +645,11 @@ fn show_video_frame( PickableTexturedRect { ent_path: entity_path.clone(), textured_rect, - source_data: PickableRectSourceData::Video, + source_data: PickableRectSourceData::Video { + depth_meter: depth_config.map(|c| c.depth_meter), + }, }, - ctx.view_class_identifier, + SpaceKind::TwoD, ); } } @@ -450,7 +662,6 @@ fn show_video_frame( visualizer_data, world_from_entity, video_size, - ctx.view_class_identifier, ); } @@ -512,24 +723,16 @@ fn show_video_frame( }; let video_error_rect_size = { - // Show the error icon with 2 texel per scene unit by default. - let mut rect_size = glam::vec2( + // Scale the icon and its label together so the whole error fits inside the + // video rect. The stale texture underneath keeps showing through on purpose. + // The label sits beside the icon and spans 3x the icon width total, so width + // has to account for that. + let icon_size = glam::vec2( video_error_texture.width() as f32, video_error_texture.height() as f32, - ) / 2.0; - - // But never larger than the area the video would take up. - // If we have to go smaller, preserve the aspect ratio. - if rect_size.x > video_size.x { - let scale = video_size.x / rect_size.x; - rect_size *= scale; - } - if rect_size.y > video_size.y { - let scale = video_size.y / rect_size.y; - rect_size *= scale; - } - - rect_size + ); + let scale = (video_size.x / (icon_size.x * 3.0)).min(video_size.y / icon_size.y); + icon_size * scale }; // Center the icon in the middle of the video rectangle. @@ -576,7 +779,7 @@ fn show_video_frame( textured_rect: error_rect, source_data: PickableRectSourceData::Placeholder, }, - ctx.view_class_identifier, + SpaceKind::TwoD, ); } @@ -585,19 +788,10 @@ fn register_video_bounds_with_bounding_box( visualizer_data: &mut SpatialViewVisualizerData, world_from_entity: glam::Affine3A, video_size: glam::Vec2, - class_identifier: ViewClassIdentifier, ) { - // Only update the bounding box if this is a 2D view. - // This is avoids a cyclic relationship where the image plane grows - // the bounds which in turn influence the size of the image plane. - // See: https://github.com/rerun-io/rerun/issues/3728 - if class_identifier != SpatialView2D::identifier() { - return; - } - let top_left = glam::Vec3::from(world_from_entity.translation); - visualizer_data.add_bounding_box( + visualizer_data.add_bounding_box_2d( entity_path, macaw::BoundingBox { min: top_left, diff --git a/crates/viewer/re_view_spatial/src/visualizers/video/video_frame_reference.rs b/crates/viewer/re_view_spatial/src/visualizers/video/video_frame_reference.rs index 1ab98d4f8d2e..f178cea2e7d4 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/video/video_frame_reference.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/video/video_frame_reference.rs @@ -1,11 +1,13 @@ use std::sync::Arc; +use re_chunk_store::ChunkTrackingMode; use re_log_types::EntityPath; use re_renderer::external::re_video::VideoLoadError; use re_renderer::video::Video; use re_sdk_types::Archetype as _; use re_sdk_types::archetypes::{AssetVideo, VideoFrameReference}; use re_sdk_types::components::{Blob, MediaType, Opacity, VideoTimestamp}; +use re_video::player::VideoSliceSource; use re_viewer_context::{ IdentifiedViewSystem, VideoAssetCache, ViewClass as _, ViewContext, ViewContextCollection, ViewQuery, ViewSystemExecutionError, ViewerContext, VisualizerExecutionOutput, @@ -26,7 +28,10 @@ pub struct VideoFrameReferenceVisualizer; impl IdentifiedViewSystem for VideoFrameReferenceVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "VideoFrameReference".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "VideoFrameReference" + ) } } @@ -176,6 +181,8 @@ impl VideoFrameReferenceVisualizer { format!("No video asset at {video_reference:?}"), VideoPlaybackIssueSeverity::Informational, )), + None, + None, ); } @@ -191,15 +198,23 @@ impl VideoFrameReferenceVisualizer { video.data_descr().timescale, ); - let frame_output = - video.frame_at(ctx.render_ctx(), player_stream_id, video_time, &|_| { - &video_buffer - }); + let frame_output = video.frame_at( + ctx.render_ctx(), + player_stream_id, + video_time, + &VideoSliceSource(&video_buffer), + ); #[expect(clippy::disallowed_methods)] // This is not a hard-coded color. let multiplicative_tint = re_renderer::Rgba::from_white_alpha(opacity.0.clamp(0.0, 1.0)); + let bit_depth = video + .data_descr() + .encoding_details + .as_ref() + .and_then(|d| d.bit_depth); + show_video_frame( ctx.view_ctx, data, @@ -214,6 +229,8 @@ impl VideoFrameReferenceVisualizer { multiplicative_tint, }), frame_output.error.map(VideoPlaybackIssue::from), + None, + bit_depth, ); } Err(err) => { @@ -230,6 +247,8 @@ impl VideoFrameReferenceVisualizer { err.to_string(), VideoPlaybackIssueSeverity::Error, )), + None, + None, ); } }, @@ -252,6 +271,7 @@ fn latest_at_query_video_from_datastore( let query = ctx.current_query(); let results = ctx.recording_engine().cache().latest_at( + ChunkTrackingMode::Report, &query, entity_path, AssetVideo::all_component_identifiers(), diff --git a/crates/viewer/re_view_spatial/src/visualizers/video/video_stream.rs b/crates/viewer/re_view_spatial/src/visualizers/video/video_stream.rs index ffe0880ddd2e..11d94b9cb3c1 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/video/video_stream.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/video/video_stream.rs @@ -9,14 +9,17 @@ use re_viewer_context::{ }; use crate::visualizers::SpatialViewVisualizerData; -use crate::visualizers::video::execute_video_stream_like; +use crate::visualizers::video::{VideoStreamCtx, execute_video_stream_like}; #[derive(Default)] pub struct VideoStreamVisualizer; impl IdentifiedViewSystem for VideoStreamVisualizer { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "VideoStream".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "VideoStream" + ) } } @@ -45,7 +48,7 @@ impl VisualizerSystem for VideoStreamVisualizer { let mut data = SpatialViewVisualizerData::default(); - execute_video_stream_like( + let ctx = VideoStreamCtx::new( ctx, view_query, context_systems, @@ -53,35 +56,26 @@ impl VisualizerSystem for VideoStreamVisualizer { Self::identifier(), VideoStream::name(), VideoStream::descriptor_sample().component, - VideoStream::descriptor_opacity().component, &|ctx, latest_at, data_result, instruction, output| { let codec_component = VideoStream::descriptor_codec().component; - let codec_result_wrapped = re_view::BlueprintResolvedResults::LatestAt( - latest_at.clone(), - data_result.latest_at_with_blueprint_resolved_data_for_component( - ctx, - latest_at, - codec_component, - Some(instruction), - ), - ); - let codec_result = re_view::VisualizerInstructionQueryResults::new( - instruction, - &codec_result_wrapped, - output, + let results = data_result.latest_at_with_blueprint_resolved_data_for_component( + ctx, + latest_at, + codec_component, + Some(instruction), ); + if results.any_missing_chunks() { + output.set_missing_chunks(); + } - let all_codecs = codec_result.iter_optional(codec_component); - let codec = all_codecs - .slice::() - .next() - .and_then(|((_time, _row_id), codec)| { - re_sdk_types::components::VideoCodec::try_from_u32(*codec.first()?) - }) + let codec = results + .get_mono::(codec_component) .ok_or(VideoStreamProcessingError::MissingCodec)?; - Ok(codec.into()) }, ) + .with_opacity_component(VideoStream::descriptor_opacity().component); + + execute_video_stream_like(ctx) } } diff --git a/crates/viewer/re_view_spatial/src/visualizers/voxel_grid_map.rs b/crates/viewer/re_view_spatial/src/visualizers/voxel_grid_map.rs new file mode 100644 index 000000000000..229d05c9c709 --- /dev/null +++ b/crates/viewer/re_view_spatial/src/visualizers/voxel_grid_map.rs @@ -0,0 +1,400 @@ +use std::sync::Arc; + +use re_log_types::Instance; +use re_renderer::Color32; +use re_renderer::renderer::{VoxelGridDrawData, VoxelGridInstance, VoxelGridOptions}; +use re_sdk_types::Archetype as _; +use re_sdk_types::archetypes::VoxelGridMap; +use re_sdk_types::components::{ + Colormap, Opacity, RotationAxisAngle, RotationQuat, Translation3D, VoxelSize, +}; +use re_sdk_types::reflection::Enum as _; +use re_view::clamped_or_nothing; +use re_viewer_context::{ + IdentifiedViewSystem, QueryContext, ViewClass as _, ViewContext, ViewContextCollection, + ViewQuery, ViewSystemExecutionError, VisualizerExecutionOutput, VisualizerQueryInfo, + VisualizerReportSeverity, VisualizerSystem, gpu_bridge, typed_fallback_for, +}; + +use super::SpatialViewVisualizerData; +use super::entity_iterator::process_archetype; +use crate::contexts::SpatialSceneVisualizerInstructionContext; + +const NUM_VOXEL_LIMIT_PER_BATCH: usize = 5_000_000; + +#[derive(Default)] +pub struct VoxelGridMapVisualizer; + +#[derive(Clone, Copy)] +struct VoxelGridMapComponentData<'a> { + indices: &'a [[i32; 3]], + voxel_size: VoxelSize, + values: &'a [f32], + colors: &'a [u32], + translation: Option, + rotation_axis_angle: Option, + quaternion: Option, + opacity: Option, + value_range: Option<[f64; 2]>, + colormap: Colormap, +} + +impl IdentifiedViewSystem for VoxelGridMapVisualizer { + fn identifier() -> re_viewer_context::ViewSystemIdentifier { + "VoxelGridMap".into() + } +} + +impl VisualizerSystem for VoxelGridMapVisualizer { + fn visualizer_query_info( + &self, + _app_options: &re_viewer_context::AppOptions, + ) -> VisualizerQueryInfo { + VisualizerQueryInfo::single_required_component::( + &VoxelGridMap::descriptor_voxel_indices(), + &VoxelGridMap::all_components(), + ) + } + + fn affinity(&self) -> Option { + Some(crate::SpatialView3D::identifier()) + } + + fn execute( + &self, + ctx: &ViewContext<'_>, + view_query: &ViewQuery<'_>, + context_systems: &ViewContextCollection, + ) -> Result { + re_tracing::profile_function!(); + + let mut data = SpatialViewVisualizerData::default(); + let mut draw_data = Vec::new(); + let output = VisualizerExecutionOutput::default(); + + process_archetype::( + ctx, + view_query, + context_systems, + &output, + self, + |ctx, spatial_ctx, results| { + let all_indices = + results.iter_required(VoxelGridMap::descriptor_voxel_indices().component); + if all_indices.is_empty() { + return Ok(()); + } + + let all_voxel_sizes = + results.iter_optional(VoxelGridMap::descriptor_voxel_size().component); + + let all_values = results.iter_optional(VoxelGridMap::descriptor_values().component); + let all_colors = results.iter_optional(VoxelGridMap::descriptor_colors().component); + let all_translations = + results.iter_optional(VoxelGridMap::descriptor_translation().component); + let all_rotations = + results.iter_optional(VoxelGridMap::descriptor_rotation_axis_angle().component); + let all_quaternions = + results.iter_optional(VoxelGridMap::descriptor_quaternion().component); + let all_opacities = + results.iter_optional(VoxelGridMap::descriptor_opacity().component); + let all_value_ranges = + results.iter_optional(VoxelGridMap::descriptor_value_range().component); + let all_colormaps = + results.iter_optional(VoxelGridMap::descriptor_colormap().component); + + let voxel_maps = re_query::range_zip_1x9( + all_indices.slice::<[i32; 3]>(), + all_voxel_sizes.slice::<[f32; 3]>(), + all_values.slice::(), + all_colors.slice::(), + all_translations.slice::<[f32; 3]>(), + all_rotations.component_slow::(), + all_quaternions.slice::<[f32; 4]>(), + all_opacities.slice::(), + all_value_ranges.slice::<[f64; 2]>(), + all_colormaps.slice::(), + ) + .map( + |( + _index, + indices, + voxel_sizes, + values, + colors, + translations, + rotations, + quaternions, + opacities, + value_ranges, + colormaps, + )| { + VoxelGridMapComponentData { + indices, + voxel_size: voxel_sizes + .and_then(|voxel_sizes| voxel_sizes.first().copied()) + .map(VoxelSize::from) + .unwrap_or_else(|| { + typed_fallback_for( + ctx, + VoxelGridMap::descriptor_voxel_size().component, + ) + }), + values: values.unwrap_or(&[]), + colors: colors.unwrap_or(&[]), + translation: translations + .and_then(|t| t.first().copied()) + .map(Translation3D::from), + rotation_axis_angle: rotations.and_then(|r| r.first().copied()), + quaternion: quaternions + .and_then(|q| q.first().copied()) + .map(RotationQuat::from), + opacity: opacities.and_then(|o| o.first().copied()).map(Into::into), + value_range: value_ranges.and_then(|r| r.first().copied()), + colormap: colormaps + .and_then(|c| c.first().copied()) + .and_then(Colormap::try_from_integer) + .unwrap_or_else(|| { + typed_fallback_for( + ctx, + VoxelGridMap::descriptor_colormap().component, + ) + }), + } + }, + ); + + for voxel_map in voxel_maps { + if let Some(voxel_draw_data) = Self::process_voxel_grid_map( + &mut data, + ctx, + results, + spatial_ctx, + &output, + voxel_map, + )? { + draw_data.push(voxel_draw_data.into()); + } + } + + Ok(()) + }, + )?; + + Ok(output.with_draw_data(draw_data).with_visualizer_data(data)) + } +} + +impl VoxelGridMapVisualizer { + fn process_voxel_grid_map( + data: &mut SpatialViewVisualizerData, + ctx: &QueryContext<'_>, + results: &re_view::VisualizerInstructionQueryResults<'_>, + spatial_ctx: &SpatialSceneVisualizerInstructionContext<'_>, + output: &VisualizerExecutionOutput, + component_data: VoxelGridMapComponentData<'_>, + ) -> Result, ViewSystemExecutionError> { + let entity_path = ctx.target_entity_path; + let VoxelGridMapComponentData { + indices, + voxel_size, + values, + colors, + translation, + rotation_axis_angle, + quaternion, + opacity, + value_range, + colormap, + } = component_data; + + if indices.is_empty() { + return Ok(None); + } + + let voxel_size = glam::Vec3::from_array(voxel_size.0.0); + if !voxel_size.is_finite() || !voxel_size.cmpgt(glam::Vec3::ZERO).all() { + results.report_for_component( + VoxelGridMap::descriptor_voxel_size().component, + VisualizerReportSeverity::Error, + "voxel_size must be finite and positive", + ); + return Ok(None); + } + + let world_from_entity = spatial_ctx + .transform_info + .single_transform_required_for_entity(entity_path, VoxelGridMap::name()) + .as_affine3a(); + + let Some(entity_from_grid) = super::entity_from_grid_transform( + results, + entity_path, + "VoxelGridMap", + translation, + rotation_axis_angle, + quaternion, + VoxelGridMap::descriptor_quaternion().component, + VoxelGridMap::descriptor_rotation_axis_angle().component, + ) else { + return Ok(None); + }; + let world_from_grid = world_from_entity * entity_from_grid; + + let max_voxels = if ctx.app_ctx().app_options.visualizer_limits_enabled + && indices.len() > NUM_VOXEL_LIMIT_PER_BATCH + { + if let Some(instruction_id) = ctx.instruction_id { + output.report_unspecified_source( + instruction_id, + VisualizerReportSeverity::Warning, + format!( + "Too many voxels ({}), capping to {}. This limit can be lifted in Settings.", + re_format::format_uint(indices.len()), + re_format::format_uint(NUM_VOXEL_LIMIT_PER_BATCH), + ), + ); + } + NUM_VOXEL_LIMIT_PER_BATCH + } else { + indices.len() + }; + + let opacity = opacity + .unwrap_or_else(|| { + typed_fallback_for(ctx, VoxelGridMap::descriptor_opacity().component) + }) + .0 + .clamp(0.0, 1.0); + + let colors = Self::resolve_colors( + ctx, + results, + max_voxels, + colors, + values, + value_range, + colormap, + ); + + // Outline masks for individually highlighted voxels (e.g. picked ones). + let highlighted_instances = &spatial_ctx.highlight.instances; + let mut additional_outline_mask_ids_instance_ranges = Vec::new(); + + let mut voxel_instances = Vec::with_capacity(max_voxels); + let mut min_index = glam::IVec3::splat(i32::MAX); + let mut max_index = glam::IVec3::splat(i32::MIN); + for (instance_index, (index, mut color)) in std::iter::zip(indices, colors).enumerate() { + if instance_index >= max_voxels { + break; + } + + if opacity < 1.0 { + color = color.gamma_multiply(opacity); + } + + let index = glam::IVec3::from_array(*index); + voxel_instances.push(VoxelGridInstance { index, color }); + + if let Some(mask) = highlighted_instances.get(&Instance::from(instance_index as u64)) { + let instance_index = instance_index as u32; + additional_outline_mask_ids_instance_ranges + .push((instance_index..instance_index + 1, *mask)); + } + + min_index = min_index.min(index); + max_index = max_index.max(index); + } + + let local_bbox = macaw::BoundingBox::from_min_max( + min_index.as_vec3() * voxel_size, + (max_index + glam::IVec3::ONE).as_vec3() * voxel_size, + ); + if local_bbox.is_nothing() { + return Ok(None); + } + + let world_bbox = local_bbox.transform_affine3(&world_from_grid); + data.add_bounding_box_3d(entity_path.hash(), world_bbox, glam::Affine3A::IDENTITY); + + let draw_data = VoxelGridDrawData::new( + ctx.viewer_ctx().render_ctx(), + &voxel_instances, + VoxelGridOptions { + world_from_grid, + draw_order_position: world_bbox.center().into(), + voxel_size, + picking_object_id: re_renderer::PickingLayerObjectId(entity_path.hash64()), + outline_mask_ids: spatial_ctx.highlight.overall, + additional_outline_mask_ids_instance_ranges, + depth_offset: spatial_ctx.depth_offset, + }, + ) + .map_err(|err| ViewSystemExecutionError::DrawDataCreationError(Arc::new(err)))?; + + Ok(Some(draw_data)) + } + + fn resolve_colors( + ctx: &QueryContext<'_>, + results: &re_view::VisualizerInstructionQueryResults<'_>, + num_voxels: usize, + colors: &[u32], + values: &[f32], + value_range: Option<[f64; 2]>, + colormap: Colormap, + ) -> Vec { + if !colors.is_empty() { + // Explicit per-voxel colors take precedence; a colormap only applies to scalar values. + // It's expected that the colormap has no effect here, so this is informational only. + results.report_for_component( + VoxelGridMap::descriptor_colormap().component, + VisualizerReportSeverity::Info, + "VoxelGridMap colormaps only apply to scalar values; ignoring the colormap because explicit per-voxel colors are present.", + ); + return clamped_or_nothing(colors, num_voxels) + .map(|&color| Color32::from(re_sdk_types::components::Color::from(color))) + .collect(); + } + + if !values.is_empty() { + let value_range = value_range + .map(|[min, max]| [min as f32, max as f32]) + .unwrap_or_else(|| { + let range: re_sdk_types::components::ValueRange = + typed_fallback_for(ctx, VoxelGridMap::descriptor_value_range().component); + [range.0.0[0] as f32, range.0.0[1] as f32] + }); + let colormap = gpu_bridge::colormap_to_re_renderer(colormap); + let value_span = value_range[1] - value_range[0]; + + return clamped_or_nothing(values, num_voxels) + .map(|&value| { + let t = if value_span.is_finite() && value_span > 0.0 { + (value - value_range[0]) / value_span + } else { + 0.5 + }; + let [r, g, b, a] = re_renderer::colormap_srgba(colormap, t); + #[expect(clippy::disallowed_methods)] + // This color comes from the selected data colormap, not from a hard-coded UI color. + { + Color32::from_rgba_unmultiplied(r, g, b, a) + } + }) + .collect(); + } + + // Neither explicit colors nor scalar values: the colormap can't apply. + // Expected, so informational only. + results.report_for_component( + VoxelGridMap::descriptor_colormap().component, + VisualizerReportSeverity::Info, + "VoxelGridMap colormaps require scalar values; showing the fallback color because no values are present.", + ); + + let fallback_color: re_sdk_types::components::Color = + typed_fallback_for(ctx, VoxelGridMap::descriptor_colors().component); + vec![Color32::from(fallback_color); num_voxels] + } +} diff --git a/crates/viewer/re_view_spatial/tests/annotation_context_update.rs b/crates/viewer/re_view_spatial/tests/annotation_context_update.rs index 7dcb05004650..e094c1195ffe 100644 --- a/crates/viewer/re_view_spatial/tests/annotation_context_update.rs +++ b/crates/viewer/re_view_spatial/tests/annotation_context_update.rs @@ -7,7 +7,7 @@ use re_sdk_types::components::Position3D; use re_test_context::TestContext; use re_test_context::external::egui_kittest::SnapshotResults; use re_test_viewport::TestContextExt as _; -use re_viewer_context::{BlueprintContext as _, TimeControlCommand, ViewClass as _, ViewId}; +use re_viewer_context::{TimeControlCommand, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; /// Log a point cloud with `class_ids`, and an annotation context that changes color between two frames. @@ -93,11 +93,7 @@ fn setup_blueprint(test_context: &mut TestContext) -> ViewId { blueprint.add_views(std::iter::once(view_blueprint), None, None); // Set eye position so both points are clearly visible. - let property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - view_id, - ); + let property = ViewProperty::from_archetype_for_view::(ctx, view_id); property.save_blueprint_component( ctx, &EyeControls3D::descriptor_position(), diff --git a/crates/viewer/re_view_spatial/tests/grid_map.rs b/crates/viewer/re_view_spatial/tests/grid_map.rs index 74ed62df62aa..9693b3d84fb2 100644 --- a/crates/viewer/re_view_spatial/tests/grid_map.rs +++ b/crates/viewer/re_view_spatial/tests/grid_map.rs @@ -9,7 +9,7 @@ use re_sdk_types::{ }; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; -use re_viewer_context::{BlueprintContext as _, ViewClass as _}; +use re_viewer_context::ViewClass as _; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; /// Verifies that grid map texels are rendered accurately using a tiny source image. @@ -41,7 +41,8 @@ fn test_grid_map_texel_accuracy() { pixels.clone(), ImageFormat::from_color_model([width, height], ColorModel::L, ChannelDatatype::U8), cell_size, - ), + ) + .with_colormap(Colormap::Grayscale), ) }); @@ -55,6 +56,7 @@ fn test_grid_map_texel_accuracy() { ImageFormat::from_color_model([width, height], ColorModel::L, ChannelDatatype::U8), cell_size, ) + .with_colormap(Colormap::Grayscale) .with_translation([5.0, 5.0, 0.0]) .with_rotation_axis_angle(RotationAxisAngle::new( glam::Vec3::Z, @@ -75,11 +77,7 @@ fn test_grid_map_texel_accuracy() { test_context.with_blueprint_ctx(|ctx, _| { // Configure world grid to match the cell size. - let grid_property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let grid_property = ViewProperty::from_archetype_for_view::(&ctx, view_id); grid_property.save_blueprint_component( &ctx, &LineGrid3D::descriptor_spacing(), @@ -87,11 +85,7 @@ fn test_grid_map_texel_accuracy() { ); // Configure top down eye view. - let eye_property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let eye_property = ViewProperty::from_archetype_for_view::(&ctx, view_id); eye_property.save_blueprint_component( &ctx, &EyeControls3D::descriptor_position(), @@ -104,11 +98,8 @@ fn test_grid_map_texel_accuracy() { ); // Show spatial origin. - let spatial_info_property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let spatial_info_property = + ViewProperty::from_archetype_for_view::(&ctx, view_id); spatial_info_property.save_blueprint_component( &ctx, &SpatialInformation::descriptor_show_axes(), @@ -174,11 +165,7 @@ fn run_grid_map_colormap_snapshot(name: &str, colormap: Colormap) { let eye_y = height as f32 * cell_size * 0.5; let eye_z = width as f32 * cell_size * 0.25; - let eye_property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let eye_property = ViewProperty::from_archetype_for_view::(&ctx, view_id); eye_property.save_blueprint_component( &ctx, &EyeControls3D::descriptor_position(), diff --git a/crates/viewer/re_view_spatial/tests/pinhole_camera.rs b/crates/viewer/re_view_spatial/tests/pinhole_camera.rs index b1b6126c82d1..268e0f01d8af 100644 --- a/crates/viewer/re_view_spatial/tests/pinhole_camera.rs +++ b/crates/viewer/re_view_spatial/tests/pinhole_camera.rs @@ -5,7 +5,7 @@ use re_sdk_types::blueprint::archetypes::EyeControls3D; use re_sdk_types::components::{Color, Position3D, Radius}; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; -use re_viewer_context::{BlueprintContext as _, ViewClass as _, ViewId}; +use re_viewer_context::{ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; #[test] @@ -39,16 +39,12 @@ fn run_view_ui_and_save_snapshot(test_context: &TestContext, view_id: ViewId, si }); test_context.with_blueprint_ctx(|ctx, _| { - ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ) - .save_blueprint_component( - &ctx, - &EyeControls3D::descriptor_position(), - &Position3D::new(1.0, 1.0, 1.0), - ); + ViewProperty::from_archetype_for_view::(&ctx, view_id) + .save_blueprint_component( + &ctx, + &EyeControls3D::descriptor_position(), + &Position3D::new(1.0, 1.0, 1.0), + ); }); harness.run_steps(10); diff --git a/crates/viewer/re_view_spatial/tests/pinhole_draw_order.rs b/crates/viewer/re_view_spatial/tests/pinhole_draw_order.rs index 6346144146dc..0ed914f8c2b0 100644 --- a/crates/viewer/re_view_spatial/tests/pinhole_draw_order.rs +++ b/crates/viewer/re_view_spatial/tests/pinhole_draw_order.rs @@ -8,7 +8,7 @@ use re_sdk_types::blueprint::archetypes::EyeControls3D; use re_sdk_types::components::Position3D; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; -use re_viewer_context::{BlueprintContext as _, Item, ViewClass as _, ViewId}; +use re_viewer_context::{Item, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; /// Helper struct to specify the properties of a pinhole image to be rendered in the test scene. @@ -255,11 +255,7 @@ fn run_view_ui_and_save_snapshot( }); test_context.with_blueprint_ctx(|ctx, _| { - let property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let property = ViewProperty::from_archetype_for_view::(&ctx, view_id); property.save_blueprint_component( &ctx, diff --git a/crates/viewer/re_view_spatial/tests/point_shading.rs b/crates/viewer/re_view_spatial/tests/point_shading.rs new file mode 100644 index 000000000000..398f02c1c032 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/point_shading.rs @@ -0,0 +1,68 @@ +use re_log_types::TimePoint; +use re_sdk_types::RowId; +use re_sdk_types::archetypes::Points3D; +use re_sdk_types::blueprint::archetypes::EyeControls3D; +use re_sdk_types::components::{PointShading, Position3D, Radius}; +use re_test_context::TestContext; +use re_test_viewport::TestContextExt as _; +use re_viewer_context::{RecommendedView, ViewClass as _}; +use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; + +/// Tests the different options for point shading. +#[test] +fn test_point_shading() { + let mut test_context = TestContext::new_with_view_class::(); + + let radius = Radius::new_scene_units(0.2); + + test_context.log_entity("world/gradient", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &Points3D::new([[-0.3, 0.0, 0.0]]) + .with_radii([radius]) + .with_colors([0x66CCFFFF]) + .with_point_shading(PointShading::Gradient), + ) + }); + + test_context.log_entity("world/flat", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &Points3D::new([[0.3, 0.0, 0.0]]) + .with_radii([radius]) + .with_colors([0x66CCFFFF]) + .with_point_shading(PointShading::Flat), + ) + }); + + let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint| { + let view_blueprint = ViewBlueprint::new( + re_view_spatial::SpatialView3D::identifier(), + RecommendedView::root(), + ); + let view_id = view_blueprint.id; + blueprint.add_views(std::iter::once(view_blueprint), None, None); + + let eye_property = ViewProperty::from_archetype_for_view::(ctx, view_id); + eye_property.save_blueprint_component( + ctx, + &EyeControls3D::descriptor_position(), + &Position3D::new(1.0, 1.0, 1.0), + ); + eye_property.save_blueprint_component( + ctx, + &EyeControls3D::descriptor_look_target(), + &Position3D::new(0.0, 0.0, 0.0), + ); + + view_id + }); + + let mut harness = test_context + .setup_kittest_for_rendering_3d(egui::vec2(300.0, 300.0)) + .build_ui(|ui| test_context.run_with_single_view(ui, view_id)); + + harness.snapshot("point_shading"); +} diff --git a/crates/viewer/re_view_spatial/tests/project_2d_and_3d.rs b/crates/viewer/re_view_spatial/tests/project_2d_and_3d.rs index 3ed069b358e9..9222111b5811 100644 --- a/crates/viewer/re_view_spatial/tests/project_2d_and_3d.rs +++ b/crates/viewer/re_view_spatial/tests/project_2d_and_3d.rs @@ -223,10 +223,8 @@ fn test_3d_in_2d(use_explicit_frames: bool) { // TODO(RR-3076): We don't correctly pick up the target frame from a pinhole origin without coordinate frame. // But we also want to remove origin in the future. Either way it's a matter of better target frame heuristic. if use_explicit_frames { - ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view.id, + ViewProperty::from_archetype_for_view::( + ctx, view.id, ) .save_blueprint_component( ctx, diff --git a/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_at_origin.png b/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_at_origin.png index 71a03d2e0f78..b7883d34204f 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_at_origin.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_at_origin.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2c8aafa891cf3d61aed65f8b4e3a2668491218e3084a9aab54bcedcd9536a37 -size 37482 +oid sha256:fc93a060a0a9169b5c27e6c673f4c9d2279466d8a015a3caf02d3d10e02e0977 +size 37205 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_at_root.png b/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_at_root.png index bafc618bae20..6baee1d1bee4 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_at_root.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_at_root.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5604ce718b0834d21aac5cd5a6063c2679f4ae15bf57fd1a65b2888cf0144598 -size 41438 +oid sha256:a6845338f2668ad89f2f61c65055ec6585aa4978ee33fab0e3a4c956fe489902 +size 41167 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_with_explicit_frames_at_origin.png b/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_with_explicit_frames_at_origin.png index a8c8e6ad8d76..eee69a1c5fbc 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_with_explicit_frames_at_origin.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_with_explicit_frames_at_origin.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34ddbe9c14a40a212188907a9de38819bfe45511e02393e0758f20adeb88e564 -size 37867 +oid sha256:58d9fcc9d29da192845d29d21acd0d40ba4f05a50b34bc53fc211cb7812e53d4 +size 37551 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_with_explicit_frames_at_root.png b/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_with_explicit_frames_at_root.png index 48390e56f323..bfe40cb756c3 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_with_explicit_frames_at_root.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/2d_in_3d_with_explicit_frames_at_root.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c918c8cdd2c7bc38c5a7376fa19e3437ca9a7966b96c79c76aca7ce9dfb6f45 -size 41616 +oid sha256:bf30f3f092b680af0b04091b1a23d4a1b85e259cb1c19cd0d03960e6d75c50fb +size 41336 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/annotation_context_update_frame1.png b/crates/viewer/re_view_spatial/tests/snapshots/annotation_context_update_frame1.png index 5f844bf9c915..facae47aecf3 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/annotation_context_update_frame1.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/annotation_context_update_frame1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2622e8c3b032d980d21eda01ede8e2fc8034bc4ad628d5f3bf3c1af82c2e6750 -size 16250 +oid sha256:396381245d0271a6b4393378a9388b418907b68f8d2af6dda4aa0fe17c48b7b4 +size 16252 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/annotation_context_update_frame2.png b/crates/viewer/re_view_spatial/tests/snapshots/annotation_context_update_frame2.png index 708319cc1df1..89b929568c34 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/annotation_context_update_frame2.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/annotation_context_update_frame2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eafcc62002284b8c6cabc31179f9e0ce94fd4b503bd0249c7eadf166cbe392bd -size 16655 +oid sha256:07b8ba3b79684cc91272ff2eae0b485c4d6c2fb493f8da269f1aeb02f6c1f6ee +size 16652 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_background.png b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_background.png index a94a22434133..57d617f2562c 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_background.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29092999acc7b4d4f30fe109d46addb952f2f3dd4e8c85e47b659d32e2d966ad -size 20733 +oid sha256:0409bef3ec8dd52190f919435f5a7e1744ec9bb14d33745da2f7433c47a40a35 +size 20642 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_rect_green.png b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_rect_green.png index 5abae63e9363..3f54ed1b7027 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_rect_green.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_rect_green.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35dd2960372580bf697d831d97034d9930cacd9b88d6134949524eb434ae45da -size 20966 +oid sha256:39f826464d892c1a1b5024535816e198e57125ee998cd2ca42501dde300546d6 +size 20875 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_rect_red.png b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_rect_red.png index 08f5e8249d13..ba5c9dbdd247 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_rect_red.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_rect_red.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:414b56b4f86713e4c30446e7a9b293ec1fd03485a8fd23fb4d975fcbbec9a55f -size 21318 +oid sha256:7b7bc374df9823c06fcdce96f3312ee69deabd94aa2823a8c66ed7f36f8c21c5 +size 21148 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_region_green.png b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_region_green.png index 8cf0909e402f..cfb883127e34 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_region_green.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_region_green.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b0e8bdc25d0d3cbeed8a237ae80fc1722c3800b629d6be3a72aba9ba6246b70 -size 34441 +oid sha256:1dcb4e5fceecbdd1d97a7dc5ba24fbe0dcbe0280d5609c029dc5b56237ffd8b4 +size 33304 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_region_red.png b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_region_red.png index decef9deac8e..25209f851939 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_region_red.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/annotations_hover_region_red.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50f3190611d196d9a7c74a37564c63420ca8395718c45669b8211ba218b6c016 -size 32473 +oid sha256:ce92607085d489efd4073fe3af0110e03536eda01d6a50a623704a5dbab10a33 +size 32024 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/annotations_overview.png b/crates/viewer/re_view_spatial/tests/snapshots/annotations_overview.png index e997c251508f..6055e33011c0 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/annotations_overview.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/annotations_overview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4943eda3784d0ce50f38322f2df908267c284053df7917c3f6803260ec274080 -size 10306 +oid sha256:5d9630180614251a7aeacece469ac0b2984f7120cd700d0256efac5b1bdfad87 +size 10279 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/blueprint_defaults_with_spatial_2d.png b/crates/viewer/re_view_spatial/tests/snapshots/blueprint_defaults_with_spatial_2d.png index 786f1e7ee3e0..fbafaedbbdd2 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/blueprint_defaults_with_spatial_2d.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/blueprint_defaults_with_spatial_2d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e5c9c9fe5e111d9023b508a8aaa732bd9151090cb51cc7bde59fe0d0850b744 -size 9336 +oid sha256:74276e5c8101176a967fa75c07e2b7ba97f401703ae13cf0543bef07609567aa +size 9168 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/blueprint_overrides_with_spatial_2d.png b/crates/viewer/re_view_spatial/tests/snapshots/blueprint_overrides_with_spatial_2d.png index 71b40841efdd..7748a73843cd 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/blueprint_overrides_with_spatial_2d.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/blueprint_overrides_with_spatial_2d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d49c596e6392955979353ce4537e3575affd9e63cd4833d43a5fa45ad3b48db0 -size 6937 +oid sha256:ff4acb635d6a78d53c15a7a793bbc76bb783011f1ff3dfed951e3efd5a322cac +size 6851 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/help_view_2d_view_mac.png b/crates/viewer/re_view_spatial/tests/snapshots/help_view_2d_view_mac.png index d89e4cb09dc1..cdcad71b7e19 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/help_view_2d_view_mac.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/help_view_2d_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c4fc28f958bf7b015bb7437cb1605f605ec287c2f59eb942f472df38ff0a668 -size 8032 +oid sha256:baf0fd973468422e67339d9964460de2849b50d3ecbe56b2f093cc23ae1664e4 +size 8011 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/help_view_2d_view_windows.png b/crates/viewer/re_view_spatial/tests/snapshots/help_view_2d_view_windows.png index ed1d12d53928..154119bca009 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/help_view_2d_view_windows.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/help_view_2d_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48ded7c933e1c9ad308dad46c3fd7c73f0aa0094414126d21a825e8aa47c3a5c -size 8072 +oid sha256:d131aa30f58f9eb64ec347faf8e1abcd283e959f5d4e1de145eeaa334cc4721d +size 8054 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/help_view_3d_view_mac.png b/crates/viewer/re_view_spatial/tests/snapshots/help_view_3d_view_mac.png index 006cec65c271..3335b4115c9f 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/help_view_3d_view_mac.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/help_view_3d_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66154932e846a938d5ae5fbf54121245df027e6a1b06911c30dca7bb4115fb60 -size 24139 +oid sha256:30bfdbbbb8f19af5ded82ca49b2c3c751a6b7c4e9cb5de16913e65784b463f52 +size 24142 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/help_view_3d_view_windows.png b/crates/viewer/re_view_spatial/tests/snapshots/help_view_3d_view_windows.png index d2c8e018c51b..432fb114f032 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/help_view_3d_view_windows.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/help_view_3d_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e20694b956094449494675fac5af71aca2bef087e15f4939a874a40c5ba09d6f -size 25153 +oid sha256:b29a09f7cf2e54c86fad69b6c6dbccd0bf7dd08d2cb5adbf011f45fc440f6d05 +size 25164 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_black_above_white.png b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_black_above_white.png index d26286417295..e759eb0b22b7 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_black_above_white.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_black_above_white.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a803c3f826d5325287cf797ba4000a64dd8235d460957e30ef0dc3e56014656 -size 63750 +oid sha256:62c4a396bbcd7ab4fb5fda6011967d90a4b0beed24915eff043d3b1f27b2e4c8 +size 63758 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_black_above_white_transparent.png b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_black_above_white_transparent.png index f351ef66f134..2ed562f63074 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_black_above_white_transparent.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_black_above_white_transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f5fca39d6a8c683f796dc2b7337a2bec9a9f49ca60a862b4cc5c7aa6c6f9f6e -size 65240 +oid sha256:008b439051cf0ca0d575065f24c072f6c7ae2ff7390574cb8b553366540d44a4 +size 65299 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_sandwiched_opaque_red.png b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_sandwiched_opaque_red.png index 4db6c0d9f01c..7048c712307e 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_sandwiched_opaque_red.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_sandwiched_opaque_red.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8837dca7988353102d20ea64028e98def9c1c0b38306e561021d941077ccaab2 -size 67445 +oid sha256:bf05af3f4967d33c3702030d3ee39374b4547012065b4d080401f1cf0a277649 +size 67480 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_selection_outline_single_entity.png b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_selection_outline_single_entity.png index 332236783315..d77a29fda241 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_selection_outline_single_entity.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_selection_outline_single_entity.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d9ea437e833af2524b4dbf856e2f69102ff7878e67059d7aa3d832cf9665133 -size 64673 +oid sha256:aeca94b575c63d438920bbeef991682fe3d8b7a134433115b72bfc9283f0014d +size 64720 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_white_above_black.png b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_white_above_black.png index 5c0b719d208d..01dc7eb6fc2c 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_white_above_black.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_white_above_black.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:906a713a78288013b693865f210c8c4994e6b3e16c5a8e6363f26ac5cbe0d856 -size 63714 +oid sha256:b42f3e939a725c4dc15a13c5839de986b6b18abe5f3e7c6c571233a6a976afc1 +size 63701 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_white_above_black_transparent.png b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_white_above_black_transparent.png index 0a9d50329b2a..7478aa63cb36 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_white_above_black_transparent.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/pinhole_draw_order_white_above_black_transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4dc9b33f5ffb0793baed604e7e022a36f1fd9b3e611bb8cfc8e46b4b2e5c796 -size 65557 +oid sha256:d9bf8f93f1b59a85722b67d4949f185fa8e076d8b4b5c241ef64a38bcfa6904a +size 65551 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/point_shading.png b/crates/viewer/re_view_spatial/tests/snapshots/point_shading.png new file mode 100644 index 000000000000..4165e80820e3 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/point_shading.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2c6307cb035cf3d8466c55df49a6a72f4faf8b94298f06abc02b3303bfe1d41 +size 45358 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/spatial_information_2d.png b/crates/viewer/re_view_spatial/tests/snapshots/spatial_information_2d.png new file mode 100644 index 000000000000..45373f9a8743 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/spatial_information_2d.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88604a4a28f7bf872d8ed50dd3e38984d2f3fa312627c9c576f90e4a3033628a +size 11184 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__2d_view_at_inner_pinhole_with_nested_pinhole.snap b/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__2d_view_at_inner_pinhole_with_nested_pinhole.snap new file mode 100644 index 000000000000..ac5da71ae1c3 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__2d_view_at_inner_pinhole_with_nested_pinhole.snap @@ -0,0 +1,5 @@ +--- +source: crates/viewer/re_view_spatial/tests/topology_errors.rs +expression: "snapshot_visualizer_errors(&test_context, view_id)" +--- +"Points2D": /points2d_entity: This 2D content has a pinhole transform frame ancestor ("outer_pinhole") that is different from the 2D view's pinhole root ("inner_pinhole"). diff --git a/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__2d_view_at_root.snap b/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__2d_view_at_root.snap index a65aa439abb7..d02df39e4a9d 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__2d_view_at_root.snap +++ b/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__2d_view_at_root.snap @@ -1,8 +1,8 @@ --- source: crates/viewer/re_view_spatial/tests/topology_errors.rs -expression: snapshot_content +expression: "snapshot_visualizer_errors(&test_context, view_id)" --- "Boxes3D": /misplaced_boxes3d_entity: Can't visualize 3D content that is under a pinhole projection. "Ellipsoids3D": /disconnected_entity: No transform path from "disconnected" to the view's target frame ("world"). -"Points2D": /points2d_entity: Can't visualize 2D content with a pinhole ancestor that's embedded within the 2D view. This applies a 3D → 2D projection to a space that's already regarded 2D. +"Points2D": /points2d_entity: This 2D content has a pinhole transform frame ancestor ("pinhole"), but the 2D view's target frame doesn't have a pinhole root. "Points3D": /points3d_entity: 3D visualizers require a pinhole at the origin of the 2D view. diff --git a/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__topology_error_empty_coordinate_frame_name.snap b/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__empty_coordinate_frame_name_falls_back_to_implicit_frame.snap similarity index 51% rename from crates/viewer/re_view_spatial/tests/snapshots/topology_errors__topology_error_empty_coordinate_frame_name.snap rename to crates/viewer/re_view_spatial/tests/snapshots/topology_errors__empty_coordinate_frame_name_falls_back_to_implicit_frame.snap index e9d36f98f503..3ef5a548b26c 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__topology_error_empty_coordinate_frame_name.snap +++ b/crates/viewer/re_view_spatial/tests/snapshots/topology_errors__empty_coordinate_frame_name_falls_back_to_implicit_frame.snap @@ -1,6 +1,5 @@ --- source: crates/viewer/re_view_spatial/tests/topology_errors.rs -assertion_line: 300 expression: "snapshot_visualizer_errors(&test_context, view_id)" --- -"Points3D": /points3d_entity: Transform relation can't be resolved due to empty coordinate frame name. +"Points3D": /points3d_entity: CoordinateFrame has an empty frame ID; falling back to the implicit frame "tf#/points3d_entity". diff --git a/crates/viewer/re_view_spatial/tests/snapshots/transform_axes_for_explicit_transforms.png b/crates/viewer/re_view_spatial/tests/snapshots/transform_axes_for_explicit_transforms.png index cedf40d51a02..62dd12b07e65 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/transform_axes_for_explicit_transforms.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/transform_axes_for_explicit_transforms.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d14b574d28a63a21062ed555f32e26715defe7e4829fabd567cf97323b8afd42 -size 19429 +oid sha256:3a554dac19fbcc53d59a1db182b38f88268ed97c204c911780ba6851c43e87e7 +size 19398 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/transform_many_child_parent_relations_on_single_time_and_entity.png b/crates/viewer/re_view_spatial/tests/snapshots/transform_many_child_parent_relations_on_single_time_and_entity.png index e3246368b41d..f03854e2ba94 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/transform_many_child_parent_relations_on_single_time_and_entity.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/transform_many_child_parent_relations_on_single_time_and_entity.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:243e79c92e8a5ca24e07fbeb74abda4c9f0bd7ef49eb6eff85c6f65fed8e7b42 -size 19022 +oid sha256:2861bd383b50ba5e7aa0f6e69064e119e831296052670d5d09a8509e538f98dd +size 19014 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/transform_many_child_parent_relations_on_single_time_and_entity_with_coordinate_frame_overrides.png b/crates/viewer/re_view_spatial/tests/snapshots/transform_many_child_parent_relations_on_single_time_and_entity_with_coordinate_frame_overrides.png index e3246368b41d..f03854e2ba94 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/transform_many_child_parent_relations_on_single_time_and_entity_with_coordinate_frame_overrides.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/transform_many_child_parent_relations_on_single_time_and_entity_with_coordinate_frame_overrides.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:243e79c92e8a5ca24e07fbeb74abda4c9f0bd7ef49eb6eff85c6f65fed8e7b42 -size 19022 +oid sha256:2861bd383b50ba5e7aa0f6e69064e119e831296052670d5d09a8509e538f98dd +size 19014 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/transform_tree_visualization.png b/crates/viewer/re_view_spatial/tests/snapshots/transform_tree_visualization.png index ce8010275059..ba96c7b5dd79 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/transform_tree_visualization.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/transform_tree_visualization.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e6f8edd6de16e39001ef6119fb5e3088524f64ee3f3a7e86bb41828515f09e5 -size 29470 +oid sha256:b76c391f052d5e42c91688d4649ad44deb2bae7e357629c09e87398376a517b5 +size 29207 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/transparent_points3d_0.png b/crates/viewer/re_view_spatial/tests/snapshots/transparent_points3d_0.png new file mode 100644 index 000000000000..b07b5f659e3e --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/transparent_points3d_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebd6d1d6a409b5ace4bf0e8c1154cd28d3ca637e759aee04fc2a962eb14c01e8 +size 37255 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/transparent_points3d_1.png b/crates/viewer/re_view_spatial/tests/snapshots/transparent_points3d_1.png new file mode 100644 index 000000000000..e9d3a3829495 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/transparent_points3d_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4be7cdb3d339209b7ec51ad4dea102471d5c48dd3bba99492a32b835371cbfda +size 38646 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_before_start.png b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_before_start.png new file mode 100644 index 000000000000..5e3a97f05307 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_before_start.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b498be2d084c4c2ff34265d9112cc0b6b23e6bb186b6b0c54f0eab1dbb3293d4 +size 1805 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_beyond_end.png b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_beyond_end.png new file mode 100644 index 000000000000..cda47a4f7afb --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_beyond_end.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bd584a849b638987b3376e6b8e67d8517b41917ed02379d618bab3a0daf4837 +size 115944 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_not_on_frame_boundary.png b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_not_on_frame_boundary.png new file mode 100644 index 000000000000..d6a4ad7d7ea3 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_not_on_frame_boundary.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc7b023b5bfca3e56741a010162d4e0a9e5512269d8ef4d18a53c896d7420d9e +size 120224 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_start.png b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_start.png new file mode 100644 index 000000000000..21523854675d --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP8_start.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fdea76689b9d26f5a18a5453abfc3f07d3b163698c799fdf6ec4e906c40124b +size 123321 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_beyond_end.png b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_beyond_end.png index d49352387116..602b663ab08d 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_beyond_end.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_beyond_end.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:935921586ef50c476fa3a68b10749e061687403e81db7f9611490e2c0937d612 -size 7632 +oid sha256:9efeaa51273f291811c62d5377205aa4f322903084b7f3eb29614fc2a5738e08 +size 127948 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_not_on_frame_boundary.png b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_not_on_frame_boundary.png index d49352387116..ac943ab92029 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_not_on_frame_boundary.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_not_on_frame_boundary.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:935921586ef50c476fa3a68b10749e061687403e81db7f9611490e2c0937d612 -size 7632 +oid sha256:baa8f76cf2667dfd4a3ece026d1fbf5a9db21e2213f79cfb3c01192da7a9ca12 +size 126378 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_start.png b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_start.png index d49352387116..4f5ede5d0345 100644 --- a/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_start.png +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_asset_VP9_start.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:935921586ef50c476fa3a68b10749e061687403e81db7f9611490e2c0937d612 -size 7632 +oid sha256:997d7f290e16208956d14b9d5616e32972bc70ece89712e6f598fde463bf4133 +size 127184 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_before_start.png b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_before_start.png new file mode 100644 index 000000000000..5e3a97f05307 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_before_start.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b498be2d084c4c2ff34265d9112cc0b6b23e6bb186b6b0c54f0eab1dbb3293d4 +size 1805 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_beyond_end.png b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_beyond_end.png new file mode 100644 index 000000000000..cda47a4f7afb --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_beyond_end.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bd584a849b638987b3376e6b8e67d8517b41917ed02379d618bab3a0daf4837 +size 115944 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_not_on_frame_boundary.png b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_not_on_frame_boundary.png new file mode 100644 index 000000000000..d6a4ad7d7ea3 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_not_on_frame_boundary.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc7b023b5bfca3e56741a010162d4e0a9e5512269d8ef4d18a53c896d7420d9e +size 120224 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_start.png b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_start.png new file mode 100644 index 000000000000..21523854675d --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP8_start.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fdea76689b9d26f5a18a5453abfc3f07d3b163698c799fdf6ec4e906c40124b +size 123321 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_before_start.png b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_before_start.png new file mode 100644 index 000000000000..5e3a97f05307 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_before_start.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b498be2d084c4c2ff34265d9112cc0b6b23e6bb186b6b0c54f0eab1dbb3293d4 +size 1805 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_beyond_end.png b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_beyond_end.png new file mode 100644 index 000000000000..602b663ab08d --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_beyond_end.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9efeaa51273f291811c62d5377205aa4f322903084b7f3eb29614fc2a5738e08 +size 127948 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_not_on_frame_boundary.png b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_not_on_frame_boundary.png new file mode 100644 index 000000000000..ac943ab92029 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_not_on_frame_boundary.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baa8f76cf2667dfd4a3ece026d1fbf5a9db21e2213f79cfb3c01192da7a9ca12 +size 126378 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_start.png b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_start.png new file mode 100644 index 000000000000..4f5ede5d0345 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/video_stream_VP9_start.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:997d7f290e16208956d14b9d5616e32972bc70ece89712e6f598fde463bf4133 +size 127184 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/voxel_grid_map.png b/crates/viewer/re_view_spatial/tests/snapshots/voxel_grid_map.png new file mode 100644 index 000000000000..55dead34c141 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/voxel_grid_map.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf4a0719bbe28691aa66ce645adeb2beeef2a90fe930966892cae10c946ef49c +size 72085 diff --git a/crates/viewer/re_view_spatial/tests/snapshots/voxel_grid_map_transparent_opacity.png b/crates/viewer/re_view_spatial/tests/snapshots/voxel_grid_map_transparent_opacity.png new file mode 100644 index 000000000000..8025524d9b41 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/snapshots/voxel_grid_map_transparent_opacity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be5f94273b9bb3ec2b7570ceadb3e4547de4373c39c08d7fb2842b68895e11da +size 24871 diff --git a/crates/viewer/re_view_spatial/tests/spatial_information_2d.rs b/crates/viewer/re_view_spatial/tests/spatial_information_2d.rs new file mode 100644 index 000000000000..9e26ad77df0c --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/spatial_information_2d.rs @@ -0,0 +1,104 @@ +use re_chunk_store::RowId; +use re_log_types::TimePoint; +use re_sdk_types::blueprint::archetypes::{SpatialInformation, VisualBounds2D}; +use re_sdk_types::blueprint::components::Enabled; +use re_sdk_types::{Archetype as _, archetypes::Points2D}; +use re_test_context::TestContext; +use re_test_viewport::TestContextExt as _; +use re_view_spatial::{SpatialView2D, SpatialViewState}; +use re_viewer_context::{BlueprintContext as _, ViewClass as _, ViewStateExt as _}; +use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; + +const SNAPSHOT_SIZE: egui::Vec2 = egui::vec2(400.0, 400.0); +const POINT_RADIUS: f32 = 0.25; + +/// Renders a 2D scene with [`SpatialInformation`] blueprint options enabled (axes, bounding boxes). +#[test] +fn test_spatial_information_2d() { + let mut test_context = TestContext::new_with_view_class::(); + + test_context.log_entity("cluster", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &Points2D::new([ + [2.0, 2.0], + [2.5, 3.0], + [3.0, 2.5], + [3.5, 3.5], + [4.0, 2.0], + [2.0, 4.0], + [3.0, 4.0], + [4.0, 4.0], + [3.5, 2.5], + [10.0, 10.0], + ]) + .with_colors([[0, 122, 255]]) + .with_radii([POINT_RADIUS]), + ) + }); + + test_context.log_entity("small_cluster", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &Points2D::new([[6.0, 2.0], [7.0, 1.5], [8.0, 2.0], [7.0, 3.5]]) + .with_colors([[255, 128, 0]]) + .with_radii([POINT_RADIUS]), + ) + }); + + let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint| { + let view = ViewBlueprint::new_with_root_wildcard(SpatialView2D::identifier()); + + let visual_bounds_path = re_viewport_blueprint::entity_path_for_view_property( + view.id, + ctx.store_context + .blueprint + .storage_engine() + .store() + .entity_tree(), + VisualBounds2D::name(), + ); + ctx.save_blueprint_archetype( + visual_bounds_path, + &VisualBounds2D::new(re_sdk_types::datatypes::Range2D { + x_range: [-1.0, 11.0].into(), + y_range: [-1.0, 11.0].into(), + }), + ); + + let spatial_information = + ViewProperty::from_archetype_for_view::(ctx, view.id); + spatial_information.save_blueprint_component( + ctx, + &SpatialInformation::descriptor_show_axes(), + &Enabled::from(true), + ); + spatial_information.save_blueprint_component( + ctx, + &SpatialInformation::descriptor_show_bounding_box(), + &Enabled::from(true), + ); + + blueprint.add_view_at_root(view) + }); + + { + let mut view_states = test_context.view_states.lock(); + let state = view_states.get_mut_or_create( + &test_context.recording_store_id, + view_id, + &SpatialView2D, + ); + let state = state + .downcast_mut::() + .expect("SpatialView2D should use SpatialViewState"); + state.show_smoothed_bbox = true; + state.show_per_entity_bbox = true; + } + + test_context + .run_view_ui_and_save_snapshot(view_id, "spatial_information_2d", SNAPSHOT_SIZE, None) + .unwrap(); +} diff --git a/crates/viewer/re_view_spatial/tests/spawn_heuristics.rs b/crates/viewer/re_view_spatial/tests/spawn_heuristics.rs index 495538b7c4c0..5979d1ddfb29 100644 --- a/crates/viewer/re_view_spatial/tests/spawn_heuristics.rs +++ b/crates/viewer/re_view_spatial/tests/spawn_heuristics.rs @@ -1,4 +1,3 @@ -#![expect(clippy::tuple_array_conversions)] #![expect(clippy::unwrap_used)] use ndarray::{Array, ShapeBuilder as _}; diff --git a/crates/viewer/re_view_spatial/tests/static_overwrite.rs b/crates/viewer/re_view_spatial/tests/static_overwrite.rs index 985c6515ed18..283952b00501 100644 --- a/crates/viewer/re_view_spatial/tests/static_overwrite.rs +++ b/crates/viewer/re_view_spatial/tests/static_overwrite.rs @@ -127,16 +127,12 @@ fn run_view_ui_and_save_snapshot( }); test_context.with_blueprint_ctx(|ctx, _| { - ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ) - .save_blueprint_component( - &ctx, - &EyeControls3D::descriptor_position(), - &Position3D::new(0.0, 5.0, 3.0), - ); + ViewProperty::from_archetype_for_view::(&ctx, view_id) + .save_blueprint_component( + &ctx, + &EyeControls3D::descriptor_position(), + &Position3D::new(0.0, 5.0, 3.0), + ); }); test_context.handle_system_commands(&harness.ctx); harness.run(); diff --git a/crates/viewer/re_view_spatial/tests/topology_errors.rs b/crates/viewer/re_view_spatial/tests/topology_errors.rs index 135c671993b9..06ebe407da5d 100644 --- a/crates/viewer/re_view_spatial/tests/topology_errors.rs +++ b/crates/viewer/re_view_spatial/tests/topology_errors.rs @@ -1,7 +1,7 @@ //! Ensures that 2D/3D visualizer report errors on incompatible topology. -use re_chunk_store::external::re_chunk::external::crossbeam::atomic::AtomicCell; -use re_log_types::TimePoint; +use re_chunk_store::{LatestAtQuery, external::re_chunk::external::crossbeam::atomic::AtomicCell}; +use re_log_types::{EntityPath, TimePoint, TimelineName}; use re_sdk_types::{ ViewClassIdentifier, archetypes, blueprint::archetypes as blueprint_archetypes, }; @@ -233,10 +233,8 @@ fn test_topology_errors() { let view_blueprint = ViewBlueprint::new(scenario.view_class, RecommendedView::root()); let view_id = view_blueprint.id; - ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view_id, + ViewProperty::from_archetype_for_view::( + ctx, view_id, ) .save_blueprint_component( ctx, @@ -256,18 +254,65 @@ fn test_topology_errors() { } #[test] -fn test_topology_error_for_empty_coordinate_frame_name() { +fn test_topology_errors_for_nested_pinholes() { let mut test_context = TestContext::new(); - test_context.register_view_class::(); + test_context.register_view_class::(); - test_context.log_entity("transforms", |builder| { + test_context.log_entity("outer_pinhole_entity", |builder| { builder.with_archetype_auto_row( TimePoint::STATIC, - &archetypes::Transform3D::new() - .with_child_frame("points3d") + &archetypes::Pinhole::from_focal_length_and_resolution([1.0, 1.0], [100.0, 100.0]) + .with_child_frame("outer_pinhole") .with_parent_frame("world"), ) }); + test_context.log_entity("inner_pinhole_entity", |builder| { + builder.with_archetype_auto_row( + TimePoint::STATIC, + &archetypes::Pinhole::from_focal_length_and_resolution([1.0, 1.0], [100.0, 100.0]) + .with_child_frame("inner_pinhole") + .with_parent_frame("outer_pinhole"), + ) + }); + test_context.log_entity("points2d_entity", |builder| { + builder + .with_archetype_auto_row(TimePoint::STATIC, &archetypes::Points2D::new([[1.0, 1.0]])) + .with_archetype_auto_row( + TimePoint::STATIC, + &archetypes::CoordinateFrame::new("outer_pinhole"), + ) + }); + + let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint| { + let view_blueprint = ViewBlueprint::new( + re_view_spatial::SpatialView2D::identifier(), + RecommendedView::root(), + ); + let view_id = view_blueprint.id; + + ViewProperty::from_archetype_for_view::( + ctx, view_id, + ) + .save_blueprint_component( + ctx, + &blueprint_archetypes::SpatialInformation::descriptor_target_frame(), + &re_tf::TransformFrameId::new("inner_pinhole"), + ); + + blueprint.add_views(std::iter::once(view_blueprint), None, None); + view_id + }); + + insta::assert_snapshot!( + "2d_view_at_inner_pinhole_with_nested_pinhole", + snapshot_visualizer_errors(&test_context, view_id) + ); +} + +#[test] +fn test_empty_coordinate_frame_name_falls_back_to_implicit_frame() { + let mut test_context = TestContext::new(); + test_context.register_view_class::(); test_context.log_entity("points3d_entity", |builder| { builder @@ -278,6 +323,19 @@ fn test_topology_error_for_empty_coordinate_frame_name() { .with_archetype_auto_row(TimePoint::STATIC, &archetypes::CoordinateFrame::new("")) }); + let stored_frame = test_context + .store_hub + .lock() + .entity_db(&test_context.recording_store_id) + .unwrap() + .latest_at_component::( + &EntityPath::from("points3d_entity"), + &LatestAtQuery::latest(TimelineName::log_tick()), + archetypes::CoordinateFrame::descriptor_frame().component, + ) + .map(|(_, frame)| frame); + assert_eq!(stored_frame, Some(re_tf::TransformFrameId::new(""))); + let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint| { let view_blueprint = ViewBlueprint::new( re_view_spatial::SpatialView3D::identifier(), @@ -285,15 +343,13 @@ fn test_topology_error_for_empty_coordinate_frame_name() { ); let view_id = view_blueprint.id; - ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view_id, + ViewProperty::from_archetype_for_view::( + ctx, view_id, ) .save_blueprint_component( ctx, &blueprint_archetypes::SpatialInformation::descriptor_target_frame(), - &re_tf::TransformFrameId::new("world"), + &re_tf::TransformFrameId::new("tf#/"), ); blueprint.add_views(std::iter::once(view_blueprint), None, None); @@ -301,7 +357,7 @@ fn test_topology_error_for_empty_coordinate_frame_name() { }); insta::assert_snapshot!( - "topology_error_empty_coordinate_frame_name", + "empty_coordinate_frame_name_falls_back_to_implicit_frame", snapshot_visualizer_errors(&test_context, view_id) ); } diff --git a/crates/viewer/re_view_spatial/tests/transform_child_parent_single_time.rs b/crates/viewer/re_view_spatial/tests/transform_child_parent_single_time.rs index 44c12eb5d4ca..3d392617989d 100644 --- a/crates/viewer/re_view_spatial/tests/transform_child_parent_single_time.rs +++ b/crates/viewer/re_view_spatial/tests/transform_child_parent_single_time.rs @@ -68,11 +68,7 @@ fn log_boxes(test_context: &mut TestContext, time: &TimePoint) { } fn setup_camera(ctx: &re_viewer_context::ViewerContext<'_>, view_id: ViewId) { - let property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - view_id, - ); + let property = ViewProperty::from_archetype_for_view::(ctx, view_id); property.save_blueprint_component( ctx, &EyeControls3D::descriptor_position(), diff --git a/crates/viewer/re_view_spatial/tests/transform_clamping.rs b/crates/viewer/re_view_spatial/tests/transform_clamping.rs index d67f78035646..1986815ec1a2 100644 --- a/crates/viewer/re_view_spatial/tests/transform_clamping.rs +++ b/crates/viewer/re_view_spatial/tests/transform_clamping.rs @@ -5,7 +5,7 @@ use re_sdk_types::components::Position3D; use re_test_context::TestContext; use re_test_context::external::egui_kittest::SnapshotResults; use re_test_viewport::TestContextExt as _; -use re_viewer_context::{BlueprintContext as _, RecommendedView, ViewClass as _, ViewId}; +use re_viewer_context::{RecommendedView, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; /// Whether everything is affected by a base-transform and how it is expressed. @@ -350,11 +350,8 @@ fn run_view_ui_and_save_snapshot( let name = format!("{name}_{target}"); test_context.with_blueprint_ctx(|ctx, _| { - let property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let property = + ViewProperty::from_archetype_for_view::(&ctx, view_id); property.save_blueprint_component( &ctx, &EyeControls3D::descriptor_position(), @@ -388,11 +385,8 @@ fn run_view_ui_and_save_snapshot( let name = format!("{name}_points"); test_context.with_blueprint_ctx(|ctx, _| { - let property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id_points, - ); + let property = + ViewProperty::from_archetype_for_view::(&ctx, view_id_points); property.save_blueprint_component( &ctx, &EyeControls3D::descriptor_position(), @@ -424,9 +418,8 @@ fn run_view_ui_and_save_snapshot( let name = format!("{name}_transform_axes"); test_context.with_blueprint_ctx(|ctx, _| { - let property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), + let property = ViewProperty::from_archetype_for_view::( + &ctx, view_id_transform_axes, ); property.save_blueprint_component( diff --git a/crates/viewer/re_view_spatial/tests/transform_hierarchy.rs b/crates/viewer/re_view_spatial/tests/transform_hierarchy.rs index 3d3bd98a020d..ee461931fdf8 100644 --- a/crates/viewer/re_view_spatial/tests/transform_hierarchy.rs +++ b/crates/viewer/re_view_spatial/tests/transform_hierarchy.rs @@ -4,7 +4,7 @@ use re_sdk_types::blueprint::archetypes::EyeControls3D; use re_sdk_types::components::Position3D; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; -use re_viewer_context::{BlueprintContext as _, TimeControlCommand, ViewClass as _, ViewId}; +use re_viewer_context::{TimeControlCommand, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; #[test] @@ -205,11 +205,7 @@ fn setup_blueprint(test_context: &mut TestContext) -> ViewId { let view_id = view_blueprint.id; blueprint.add_views(std::iter::once(view_blueprint), None, None); - let property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - view_id, - ); + let property = ViewProperty::from_archetype_for_view::(ctx, view_id); property.save_blueprint_component( ctx, &EyeControls3D::descriptor_position(), diff --git a/crates/viewer/re_view_spatial/tests/transform_tree_origins.rs b/crates/viewer/re_view_spatial/tests/transform_tree_origins.rs index daddd063d565..502792d8a955 100644 --- a/crates/viewer/re_view_spatial/tests/transform_tree_origins.rs +++ b/crates/viewer/re_view_spatial/tests/transform_tree_origins.rs @@ -176,16 +176,12 @@ fn setup_blueprint(test_context: &mut TestContext, origin: &str) -> ViewId { }, )); - ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view_id, - ) - .save_blueprint_component( - ctx, - &SpatialInformation::descriptor_show_axes(), - &Enabled::from(true), - ); + ViewProperty::from_archetype_for_view::(ctx, view_id) + .save_blueprint_component( + ctx, + &SpatialInformation::descriptor_show_axes(), + &Enabled::from(true), + ); view_id }) diff --git a/crates/viewer/re_view_spatial/tests/transform_tree_visualization.rs b/crates/viewer/re_view_spatial/tests/transform_tree_visualization.rs index 2d20a8606b86..f9c17edc3e47 100644 --- a/crates/viewer/re_view_spatial/tests/transform_tree_visualization.rs +++ b/crates/viewer/re_view_spatial/tests/transform_tree_visualization.rs @@ -9,7 +9,7 @@ use re_sdk_types::{archetypes, components}; use re_test_context::TestContext; use re_test_context::VisualizerBlueprintContext as _; use re_test_viewport::TestContextExt as _; -use re_viewer_context::{BlueprintContext as _, TimeControlCommand, ViewClass as _, ViewId}; +use re_viewer_context::{TimeControlCommand, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; fn log_transform_tree(test_context: &mut TestContext, time: &TimePoint) { @@ -44,11 +44,7 @@ fn log_transform_tree(test_context: &mut TestContext, time: &TimePoint) { } fn setup_camera(ctx: &re_viewer_context::ViewerContext<'_>, view_id: ViewId) { - let property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - view_id, - ); + let property = ViewProperty::from_archetype_for_view::(ctx, view_id); property.save_blueprint_component( ctx, &EyeControls3D::descriptor_position(), diff --git a/crates/viewer/re_view_spatial/tests/transparent_geometry.rs b/crates/viewer/re_view_spatial/tests/transparent_geometry.rs index 01e1628a079c..d6e12745df2c 100644 --- a/crates/viewer/re_view_spatial/tests/transparent_geometry.rs +++ b/crates/viewer/re_view_spatial/tests/transparent_geometry.rs @@ -10,33 +10,47 @@ use re_test_context::TestContext; use re_test_context::external::egui_kittest::OsThreshold; use re_test_viewport::TestContextExt as _; use re_view_spatial::SpatialView3D; -use re_viewer_context::{BlueprintContext as _, RecommendedView, ViewClass as _}; +use re_viewer_context::{RecommendedView, ViewClass as _}; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; fn test_transparent_geometry( name: &str, archetype_builder: impl Fn(f32, Color32) -> A, ) { + run_transparency_snapshot_test(name, |test_context| { + // Log a bunch of transparent geometry. + for (i, color) in [ + Color32::from_rgba_unmultiplied(255, 128, 128, 20), + Color32::from_rgba_unmultiplied(128, 255, 128, 20), + Color32::from_rgba_unmultiplied(128, 128, 255, 20), + ] + .into_iter() + .enumerate() + { + let y = i as f32 * 2.0 - 2.0; + test_context.log_entity(format!("geom_{i}"), |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::default(), + &archetype_builder(y, color), + ) + }); + } + }); +} + +/// Logs transparent geometry via `log`, then snapshots the scene from two opposite camera +/// orientations to make sure back-to-front sorting works regardless of view direction. +fn run_transparency_snapshot_test(name: &str, log: impl Fn(&mut TestContext)) { let mut test_context = TestContext::new_with_view_class::(); - // Log a bunch of transparent meshes. - for (i, color) in [ - Color32::from_rgba_unmultiplied(255, 128, 128, 20), - Color32::from_rgba_unmultiplied(128, 255, 128, 20), - Color32::from_rgba_unmultiplied(128, 128, 255, 20), - ] - .into_iter() - .enumerate() - { - let y = i as f32 * 2.0 - 2.0; - test_context.log_entity(format!("geom_{i}"), |builder| { - builder.with_archetype( - RowId::new(), - TimePoint::default(), - &archetype_builder(y, color), - ) - }); - } + // Point cloud transparency is opt-in; enable it so the points test exercises it. + test_context + .app_options + .experimental + .point_cloud_transparency = true; + + log(&mut test_context); let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint| { let view_blueprint = @@ -44,11 +58,7 @@ fn test_transparent_geometry( let view_id = view_blueprint.id; blueprint.add_views(std::iter::once(view_blueprint), None, None); - let eye_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view_id, - ); + let eye_property = ViewProperty::from_archetype_for_view::(ctx, view_id); eye_property.save_blueprint_component( ctx, @@ -81,11 +91,8 @@ fn test_transparent_geometry( // Flip the camera orientation to ensure sorting works as expected. test_context.with_blueprint_ctx(|ctx, _| { - let eye_property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let eye_property = + ViewProperty::from_archetype_for_view::(&ctx, view_id); let len = 3.5; let dir = Vec3::new(0.25, orientation_y, 0.25).normalize(); @@ -159,3 +166,36 @@ pub fn test_transparent_capsules3d() { .with_colors([color]) }); } + +#[test] +pub fn test_transparent_points3d() { + // A single point cloud (one batch) with several large, heavily-overlapping transparent points + // stacked along the axis the camera flips around. This exercises the per-cloud back-to-front + // sorting: which color ends up on top must depend on the view direction. + run_transparency_snapshot_test("points3d", |test_context| { + let positions = [ + [0.0, -1.0, 0.0], + [0.0, -0.5, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.5, 0.0], + [0.0, 1.0, 0.0], + ]; + let colors = [ + Color32::from_rgba_unmultiplied(255, 64, 64, 80), + Color32::from_rgba_unmultiplied(64, 255, 64, 80), + Color32::from_rgba_unmultiplied(64, 64, 255, 80), + Color32::from_rgba_unmultiplied(255, 255, 64, 80), + Color32::from_rgba_unmultiplied(64, 255, 255, 80), + ]; + + test_context.log_entity("points", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::default(), + &archetypes::Points3D::new(positions) + .with_colors(colors) + .with_radii([0.8]), + ) + }); + }); +} diff --git a/crates/viewer/re_view_spatial/tests/video.rs b/crates/viewer/re_view_spatial/tests/video.rs index f010cb68fdfe..1046d2227948 100644 --- a/crates/viewer/re_view_spatial/tests/video.rs +++ b/crates/viewer/re_view_spatial/tests/video.rs @@ -2,12 +2,13 @@ use re_chunk_store::RowId; use re_log_types::TimePoint; -use re_sdk_types::archetypes::{AssetVideo, VideoFrameReference, VideoStream}; +use re_sdk_types::archetypes::{AssetVideo, TextLog, VideoFrameReference, VideoStream}; use re_sdk_types::components::{self, MediaType, VideoTimestamp}; use re_sdk_types::datatypes; use re_test_context::TestContext; use re_test_context::external::egui_kittest::SnapshotOptions; use re_test_viewport::TestContextExt as _; +use re_video::player::VideoSliceSource; use re_video::{VideoCodec, VideoDataDescription}; use re_viewer_context::{TimeControlCommand, ViewClass as _}; use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; @@ -33,11 +34,9 @@ fn video_test_file_mp4(codec: &VideoCodec, need_dts_equal_pts: bool) -> std::pat let codec_str = match codec { VideoCodec::H264 => "h264", VideoCodec::H265 => "h265", - VideoCodec::VP9 => "vp9", - VideoCodec::VP8 => { - panic!("We don't have test data for vp8, because Mp4 doesn't support vp8.") - } VideoCodec::AV1 => "av1", + VideoCodec::VP8 => "vp8", + VideoCodec::VP9 => "vp9", VideoCodec::ImageSequence(_) => panic!("mp4 can't be an image sequence"), }; @@ -112,16 +111,19 @@ fn snapshot_options_for_codec(codec: &VideoCodec, viewport_size: egui::Vec2) -> match codec { // Despite version pinning, ffmpeg's results are quite different depending on the platform // and seemingly even between runs! - VideoCodec::H264 | VideoCodec::H265 => SnapshotOptions::new() - .threshold(2.2) - .failed_pixel_count_threshold(300), - + VideoCodec::H264 | VideoCodec::H265 | VideoCodec::VP8 | VideoCodec::VP9 => { + SnapshotOptions::new() + .threshold(2.2) + .failed_pixel_count_threshold(300) + } // AV1 has this problem as well but to a lesser extent. VideoCodec::AV1 => SnapshotOptions::new() .threshold(1.2) .failed_pixel_count_threshold(100), - _ => re_ui::testing::default_snapshot_options_for_3d(viewport_size), + VideoCodec::ImageSequence(_) => { + re_ui::testing::default_snapshot_options_for_3d(viewport_size) + } } } @@ -149,6 +151,16 @@ fn test_video(video_type: VideoType, codec: &VideoCodec) { .active_timeline() .expect("should have an active timeline"); + // Extend the timeline before the first frame so we can still test rendering + // before the video starts despite cursor clamping. + test_context.log_entity("marker", |builder| { + builder.with_archetype( + RowId::new(), + [(timeline, -1_i64)], + &TextLog::new("before video"), + ) + }); + match video_type { VideoType::AssetVideo => { test_context.log_entity("video", |builder| { @@ -172,12 +184,10 @@ fn test_video(video_type: VideoType, codec: &VideoCodec) { let blob_bytes = datatypes::Blob::serialized_blob_as_slice(video_asset.blob.as_ref().unwrap()) .unwrap(); - let tuid = re_log_types::external::re_tuid::Tuid::new(); let video_data_description = VideoDataDescription::load_from_bytes( blob_bytes, MediaType::mp4().as_str(), video_path.to_str().unwrap(), - tuid, ) .unwrap(); @@ -210,7 +220,7 @@ fn test_video(video_type: VideoType, codec: &VideoCodec) { &sample .sample() .unwrap() - .get(&|_| blob_bytes, sample_idx) + .get(&VideoSliceSource(blob_bytes), sample_idx) .unwrap(), &mut annexb_stream_state, ) @@ -237,7 +247,7 @@ fn test_video(video_type: VideoType, codec: &VideoCodec) { &sample .sample() .unwrap() - .get(&|_| blob_bytes, sample_idx) + .get(&VideoSliceSource(blob_bytes), sample_idx) .unwrap(), &mut annexb_stream_state, ) @@ -245,18 +255,20 @@ fn test_video(video_type: VideoType, codec: &VideoCodec) { (components::VideoCodec::H265, sample_bytes) } - VideoCodec::AV1 => { - // Extract raw sample bytes, under av1 they're OBUs already! - let sample_bytes = sample + VideoCodec::AV1 | VideoCodec::VP8 | VideoCodec::VP9 => { + let chunk = sample .sample() .unwrap() - .get(&|_| blob_bytes, sample_idx) - .unwrap() - .data; - (components::VideoCodec::AV1, sample_bytes) + .get(&VideoSliceSource(blob_bytes), sample_idx) + .unwrap(); + let sample_bytes = video_data_description + .sample_data_in_stream_format(&chunk) + .unwrap(); + let codec = + components::VideoCodec::try_from(video_data_description.codec.clone()) + .unwrap(); + (codec, sample_bytes) } - VideoCodec::VP9 => panic!("VP9 is not supported for video streams"), - VideoCodec::VP8 => panic!("VP8 is not supported for video streams"), VideoCodec::ImageSequence(_) => panic!("Won't be created from a video"), }; @@ -283,9 +295,9 @@ fn test_video(video_type: VideoType, codec: &VideoCodec) { )); // Set a background color other than black so we can see the effect of transparency on errors & lack thereof on the video. - let property = ViewProperty::from_archetype::< + let property = ViewProperty::from_archetype_for_view::< re_sdk_types::blueprint::archetypes::Background, - >(ctx.blueprint_db(), ctx.blueprint_query, view_id); + >(ctx, view_id); property.save_blueprint_component( ctx, &re_sdk_types::blueprint::archetypes::Background::descriptor_kind(), @@ -347,6 +359,11 @@ fn test_video_asset_codec_h265() { test_video(VideoType::AssetVideo, &VideoCodec::H265); } +#[test] +fn test_video_asset_codec_vp8() { + test_video(VideoType::AssetVideo, &VideoCodec::VP8); +} + #[test] fn test_video_asset_codec_vp9() { test_video(VideoType::AssetVideo, &VideoCodec::VP9); @@ -368,11 +385,15 @@ fn test_video_stream_codec_h265() { test_video(VideoType::VideoStream, &VideoCodec::H265); } -// TODO(#10186): Unsupported codec for VideoStream -// #[test] -// fn test_video_stream_codec_vp9() { -// test_video(VideoType::VideoStream, VideoCodec::VP9); -// } +#[test] +fn test_video_stream_codec_vp8() { + test_video(VideoType::VideoStream, &VideoCodec::VP8); +} + +#[test] +fn test_video_stream_codec_vp9() { + test_video(VideoType::VideoStream, &VideoCodec::VP9); +} #[cfg(feature = "nasm")] // Need nasm for Av1 decoding on some platforms otherwise we error. #[test] diff --git a/crates/viewer/re_view_spatial/tests/visible_time_range.rs b/crates/viewer/re_view_spatial/tests/visible_time_range.rs index ee445c08105f..d2fc76e453c0 100644 --- a/crates/viewer/re_view_spatial/tests/visible_time_range.rs +++ b/crates/viewer/re_view_spatial/tests/visible_time_range.rs @@ -540,11 +540,7 @@ fn run_test_3d( }); test_context.with_blueprint_ctx(|ctx, _| { - let eye_property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let eye_property = ViewProperty::from_archetype_for_view::(&ctx, view_id); eye_property.save_blueprint_component( &ctx, &EyeControls3D::descriptor_position(), @@ -594,11 +590,8 @@ fn test_sliding_window_3d() { let setup_eye = |test_context: &TestContext, view_id: ViewId| { test_context.with_blueprint_ctx(|ctx, _| { - let eye_property = ViewProperty::from_archetype::( - ctx.current_blueprint(), - ctx.blueprint_query(), - view_id, - ); + let eye_property = + ViewProperty::from_archetype_for_view::(&ctx, view_id); eye_property.save_blueprint_component( &ctx, &EyeControls3D::descriptor_position(), diff --git a/crates/viewer/re_view_spatial/tests/voxel_grid_map.rs b/crates/viewer/re_view_spatial/tests/voxel_grid_map.rs new file mode 100644 index 000000000000..4a2a1355b109 --- /dev/null +++ b/crates/viewer/re_view_spatial/tests/voxel_grid_map.rs @@ -0,0 +1,168 @@ +use re_log_types::TimePoint; +use re_sdk_types::{ + RowId, + archetypes::{TransformAxes3D, VoxelGridMap}, + blueprint::archetypes::{EyeControls3D, LineGrid3D, SpatialInformation}, + blueprint::components::{Enabled, GridSpacing}, + components::{Colormap, Position3D, RotationAxisAngle}, +}; +use re_test_context::TestContext; +use re_test_viewport::TestContextExt as _; +use re_viewer_context::{Item, ViewClass as _}; +use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; + +#[test] +fn test_voxel_grid_map_snapshot_and_instance_selection() { + let mut test_context = TestContext::new_with_view_class::(); + + test_context.log_entity("/", |builder| { + builder.with_archetype(RowId::new(), TimePoint::STATIC, &TransformAxes3D::new(1.0)) + }); + + test_context.log_entity("values", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &VoxelGridMap::new( + [ + (0, 0, 0), + (1, 0, 0), + (1, 1, 0), + (4, 0, 0), + (4, 0, 1), + (5, 0, 1), + ], + [0.5, 0.35, 0.65], + ) + .with_values([0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) + .with_value_range([0.0, 1.0]) + .with_colormap(Colormap::Turbo), + ) + }); + + test_context.log_entity("posed_colors", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &VoxelGridMap::new( + [(0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)], + [0.6, 0.45, 0.6], + ) + .with_translation([0.0, 2.2, 0.0]) + .with_rotation_axis_angle(RotationAxisAngle::new( + glam::Vec3::Z, + std::f32::consts::FRAC_PI_4, + )) + .with_colors([ + 0xFF0000FF, // red + 0x00FF00FF, // green + 0x0000FFFF, // blue + 0xFF00FF00, // alpha zero, should be skipped + ]), + ) + }); + + let view_id = test_context.setup_viewport_blueprint(|_ctx, blueprint| { + blueprint.add_view_at_root(ViewBlueprint::new_with_root_wildcard( + re_view_spatial::SpatialView3D::identifier(), + )) + }); + + let mut harness = test_context + .setup_kittest_for_rendering_3d(egui::vec2(360.0, 260.0)) + .build_ui(|ui| { + test_context.edit_selection(|selection_state| { + selection_state.set_selection(Item::InstancePath( + re_entity_db::InstancePath::instance("values", 1), + )); + }); + test_context.run_with_single_view(ui, view_id); + }); + + test_context.with_blueprint_ctx(|ctx, _| { + let grid_property = ViewProperty::from_archetype_for_view::(&ctx, view_id); + grid_property.save_blueprint_component( + &ctx, + &LineGrid3D::descriptor_spacing(), + &GridSpacing::from(0.5), + ); + + let eye_property = ViewProperty::from_archetype_for_view::(&ctx, view_id); + eye_property.save_blueprint_component( + &ctx, + &EyeControls3D::descriptor_position(), + &Position3D::new(3.0, -5.0, 5.0), + ); + eye_property.save_blueprint_component( + &ctx, + &EyeControls3D::descriptor_look_target(), + &Position3D::new(2.0, 0.8, 0.4), + ); + + let spatial_info_property = + ViewProperty::from_archetype_for_view::(&ctx, view_id); + spatial_info_property.save_blueprint_component( + &ctx, + &SpatialInformation::descriptor_show_axes(), + &Enabled::from(true), + ); + }); + harness.run_steps(10); + + harness.snapshot("voxel_grid_map"); +} + +#[test] +fn test_voxel_grid_map_transparent_opacity_snapshot() { + let mut test_context = TestContext::new_with_view_class::(); + + test_context.log_entity("opaque_back", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &VoxelGridMap::new([(0, 0, 0)], [1.0, 1.0, 1.0]) + .with_translation([0.0, 0.35, 0.0]) + .with_colors([0x00FF00FF]), + ) + }); + + test_context.log_entity("transparent_front", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &VoxelGridMap::new([(0, 0, 0)], [1.0, 1.0, 1.0]) + .with_translation([0.0, -0.35, 0.0]) + .with_colors([0xFF0000FF]) + .with_opacity(0.35), + ) + }); + + let view_id = test_context.setup_viewport_blueprint(|_ctx, blueprint| { + blueprint.add_view_at_root(ViewBlueprint::new_with_root_wildcard( + re_view_spatial::SpatialView3D::identifier(), + )) + }); + + let mut harness = test_context + .setup_kittest_for_rendering_3d(egui::vec2(260.0, 220.0)) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + test_context.with_blueprint_ctx(|ctx, _| { + let eye_property = ViewProperty::from_archetype_for_view::(&ctx, view_id); + eye_property.save_blueprint_component( + &ctx, + &EyeControls3D::descriptor_position(), + &Position3D::new(1.7, -4.0, 2.2), + ); + eye_property.save_blueprint_component( + &ctx, + &EyeControls3D::descriptor_look_target(), + &Position3D::new(0.5, 0.2, 0.5), + ); + }); + harness.run_steps(10); + + harness.snapshot("voxel_grid_map_transparent_opacity"); +} diff --git a/crates/viewer/re_view_status/Cargo.toml b/crates/viewer/re_view_state_timeline/Cargo.toml similarity index 69% rename from crates/viewer/re_view_status/Cargo.toml rename to crates/viewer/re_view_state_timeline/Cargo.toml index 6d8676bd6523..a64664c145e3 100644 --- a/crates/viewer/re_view_status/Cargo.toml +++ b/crates/viewer/re_view_state_timeline/Cargo.toml @@ -1,10 +1,10 @@ [package] authors.workspace = true -description = "A view that shows status transitions as horizontal lanes over time." +description = "A view that shows state transitions as horizontal lanes over time." edition.workspace = true homepage.workspace = true license.workspace = true -name = "re_view_status" +name = "re_view_state_timeline" publish = true readme = "README.md" repository.workspace = true @@ -19,17 +19,23 @@ workspace = true all-features = true [dependencies] +re_byte_size.workspace = true re_chunk_store.workspace = true +re_component_ui.workspace = true re_log_types.workspace = true re_sdk_types.workspace = true +re_selection_panel.workspace = true +re_time_ruler.workspace = true re_tracing.workspace = true re_ui.workspace = true re_view.workspace = true re_viewer_context.workspace = true +re_viewport_blueprint.workspace = true egui.workspace = true +nohash-hasher.workspace = true [dev-dependencies] re_test_context.workspace = true re_test_viewport.workspace = true -re_viewport_blueprint.workspace = true +re_viewport.workspace = true diff --git a/crates/viewer/re_view_state_timeline/README.md b/crates/viewer/re_view_state_timeline/README.md new file mode 100644 index 000000000000..0012fca3001a --- /dev/null +++ b/crates/viewer/re_view_state_timeline/README.md @@ -0,0 +1,10 @@ +# re_view_state_timeline + +Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. + +[![Latest version](https://img.shields.io/crates/v/re_view_state_timeline.svg)](https://crates.io/crates/re_view_state_timeline) +[![Documentation](https://docs.rs/re_view_state_timeline/badge.svg)](https://docs.rs/re_view_state_timeline) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +A view that shows state transitions as horizontal lanes over time. diff --git a/crates/viewer/re_view_state_timeline/src/data.rs b/crates/viewer/re_view_state_timeline/src/data.rs new file mode 100644 index 000000000000..d1ad56b60c3f --- /dev/null +++ b/crates/viewer/re_view_state_timeline/src/data.rs @@ -0,0 +1,65 @@ +/// Collection of state change lane groups produced by a visualizer. +#[derive(Clone, Debug, Default)] +pub struct StateLanesData { + pub groups: Vec, +} + +/// Canonical post-cast value type of a state lane. +/// +/// The polymorphic state cast collapses every accepted source type into one of these — strings +/// stay as strings, booleans stay as booleans, all numeric types collapse to `Scalar` (Float64). +/// The configuration editor branches on this to offer a type-appropriate UI. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StateValueKind { + String, + Scalar, + Bool, +} + +/// All state lanes of a single visualizer instruction, displayed under one shared label. +/// +/// A `StateChange` row can carry multiple instances (an array of states, e.g. the buttons of a +/// joystick). Each instance index becomes its own [`StateLane`]; they share the entity label, +/// value kind, and `StateConfiguration`. +#[derive(Clone, Debug)] +pub struct StateLaneGroup { + /// Display name for this group (typically the entity path). + pub label: String, + + /// The entity path this group belongs to. + pub entity_path: re_log_types::EntityPath, + + /// The canonical post-cast type of the values in this group. + pub value_kind: StateValueKind, + + /// One lane per instance index, in instance order. Never empty. + pub lanes: Vec, +} + +/// A single horizontal lane of state change phases. +#[derive(Clone, Debug)] +pub struct StateLane { + /// Ordered list of phases. Each phase starts at `start_time` and implicitly ends + /// where the next phase begins (or at the right edge of the visible range). + pub phases: Vec, +} + +/// One contiguous phase within a [`StateLane`]. +#[derive(Clone, Debug)] +pub struct StateLanePhase { + /// Start time in timeline units. + pub start_time: i64, + + /// `Some` for a drawn state; `None` for a gap region or invisible state. + pub content: Option, +} + +/// Visual style for a drawn state phase. +#[derive(Clone, Debug)] +pub struct StateLanePhaseContent { + /// Human-readable state label (e.g. "Idle", "Moving"). + pub label: String, + + /// Display color for this phase. + pub color: egui::Color32, +} diff --git a/crates/viewer/re_view_state_timeline/src/lib.rs b/crates/viewer/re_view_state_timeline/src/lib.rs new file mode 100644 index 000000000000..d52202912f61 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/src/lib.rs @@ -0,0 +1,15 @@ +//! Rerun state timeline View. +//! +//! A View that shows state transitions as horizontal lanes over time. + +mod data; +mod view_class; +mod visualizer; +mod visualizer_ui; + +pub use data::{ + StateLane, StateLaneGroup, StateLanePhase, StateLanePhaseContent, StateLanesData, + StateValueKind, +}; +pub use view_class::{StateTimelineView, StateTimelineViewState}; +pub use visualizer::StateVisualizer; diff --git a/crates/viewer/re_view_state_timeline/src/view_class.rs b/crates/viewer/re_view_state_timeline/src/view_class.rs new file mode 100644 index 000000000000..6b3079a801c1 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/src/view_class.rs @@ -0,0 +1,1424 @@ +use re_log_types::{ + AbsoluteTimeRange, ComponentPath, EntityPath, TimeCell, TimeInt, TimeReal, TimeType, + TimelineName, TimestampFormat, +}; +use re_sdk_types::blueprint::archetypes::TimeAxis; +use re_sdk_types::blueprint::components::LinkAxis; +use re_time_ruler::{MAX_ZIG_WIDTH, TimeRangesUi}; +use re_ui::{Help, IconText, MouseButtonText, UiExt as _, icons, list_item}; +use re_viewer_context::{ + DataQueryResult, DataResultInteractionAddress, DragAndDropFeedback, GLOBAL_VIEW_ID, + IdentifiedViewSystem as _, Item, TimeControlCommand, TimeView, ViewClass, ViewClassExt as _, + ViewClassLayoutPriority, ViewClassRegistryError, ViewContext, ViewId, ViewQuery, + ViewSpawnHeuristics, ViewState, ViewStateExt as _, ViewSystemExecutionError, ViewerContext, +}; +use re_viewport_blueprint::ViewProperty; + +use crate::data::{StateLaneGroup, StateLanePhase, StateLanesData}; + +// Layout constants (in screen pixels). +const LANE_BAND_HEIGHT: f32 = 22.0; +const LANE_LABEL_HEIGHT: f32 = 14.0; +const LANE_GAP: f32 = 4.0; + +/// Vertical gap between the stacked instance lanes of a multi-instance group. +const SUB_LANE_GAP: f32 = 1.0; + +const TIME_AXIS_HEIGHT: f32 = 20.0; +const TOP_MARGIN: f32 = 4.0; + +/// Phases narrower than this on screen get folded into a merged region with their +/// narrow neighbors. Wide phases always render with their own color. +const MERGE_PHASE_THRESHOLD_PIXEL: f32 = 4.0; + +/// One drawable item along a lane: either a single phase or a merged region. +#[derive(Debug)] +enum RenderItem<'a> { + /// A phase wide enough to render with its own color and label. + Single { + phase: &'a StateLanePhase, + x_start: f32, + x_end: f32, + + /// End time of the phase (start of the next phase). `None` for the last phase. + end_time: Option, + }, + + /// Two or more consecutive narrow visible phases collapsed into one region. + Merged { + x_start: f32, + x_end: f32, + start_time: i64, + + /// End time of the last phase in the group, if known. + end_time: Option, + count: usize, + }, +} + +impl RenderItem<'_> { + fn x_range(&self) -> (f32, f32) { + match self { + Self::Single { x_start, x_end, .. } | Self::Merged { x_start, x_end, .. } => { + (*x_start, *x_end) + } + } + } +} + +/// View state for pan/zoom. +#[derive(Default, re_byte_size::SizeBytes)] +pub struct StateTimelineViewState { + /// Pan/zoom window, stored per timeline (in the same representation as the timeline panel). + pub time_views: std::collections::BTreeMap, +} + +impl StateTimelineViewState { + /// The visible time range to query for `timeline`, derived from its pan/zoom. + /// + /// `None` until `timeline` has been auto-fit. + pub fn visible_time_range(&self, timeline: TimelineName) -> Option { + let time_view = self.time_views.get(&timeline)?; + let min = time_view.min; + let max = min + TimeReal::from(time_view.time_spanned); + Some(AbsoluteTimeRange::new(min.floor(), max.ceil())) + } +} + +impl ViewState for StateTimelineViewState { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } +} + +#[derive(Default)] +pub struct StateTimelineView; + +impl StateTimelineView { + /// Read the configured time-axis link mode for this view. + fn time_axis_link( + &self, + ctx: &ViewerContext<'_>, + state: &dyn ViewState, + view_id: ViewId, + space_origin: &EntityPath, + ) -> Result { + let view_ctx = self.view_context(ctx, view_id, state, space_origin); + let time_axis = ViewProperty::from_archetype::(&view_ctx); + Ok(time_axis + .component_or_fallback::(&view_ctx, TimeAxis::descriptor_link().component)?) + } +} + +impl ViewClass for StateTimelineView { + fn identifier() -> re_sdk_types::ViewClassIdentifier { + "StateTimeline".into() + } + + fn display_name(&self) -> &'static str { + "State timeline" + } + + fn icon(&self) -> &'static re_ui::Icon { + &icons::VIEW_STATE_TIMELINE + } + + fn new_state(&self) -> Box { + Box::::default() + } + + fn help(&self, os: egui::os::OperatingSystem) -> Help { + let egui::InputOptions { + zoom_modifier, + horizontal_scroll_modifier, + .. + } = egui::InputOptions::default(); // This is OK, since we don't allow the user to change these modifiers. + + Help::new("State timeline view") + .markdown("Shows state transitions as horizontal colored lanes over time.") + .control("Move time cursor", icons::RIGHT_MOUSE_CLICK) + .control( + "Pan", + (MouseButtonText(egui::PointerButton::Primary), "+", "drag"), + ) + .control( + "Pan", + IconText::from_modifiers_and(os, horizontal_scroll_modifier, icons::SCROLL), + ) + .control( + "Zoom", + IconText::from_modifiers_and(os, zoom_modifier, icons::SCROLL), + ) + .control("Reset view", ("double", icons::LEFT_MOUSE_CLICK)) + } + + fn on_register( + &self, + system_registry: &mut re_viewer_context::ViewSystemRegistrator<'_>, + ) -> Result<(), ViewClassRegistryError> { + system_registry.register_visualizer::() + } + + fn preferred_tile_aspect_ratio(&self, _state: &dyn ViewState) -> Option { + Some(2.5) + } + + fn layout_priority(&self) -> ViewClassLayoutPriority { + ViewClassLayoutPriority::Low + } + + fn spawn_heuristics( + &self, + ctx: &ViewerContext<'_>, + include_entity: &dyn Fn(&EntityPath) -> bool, + ) -> re_viewer_context::ViewSpawnHeuristics { + re_tracing::profile_function!(); + + // Show every state change stream in a single view by default. + if ctx + .indicated_entities_per_visualizer + .get(&crate::StateVisualizer::identifier()) + .is_some_and(|entities| entities.iter().any(include_entity)) + { + ViewSpawnHeuristics::root() + } else { + ViewSpawnHeuristics::empty() + } + } + + fn selection_ui( + &self, + viewer_ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + state: &mut dyn ViewState, + space_origin: &EntityPath, + view_id: ViewId, + ) -> Result<(), ViewSystemExecutionError> { + list_item::list_item_scope(ui, "state_timeline_selection_ui", |ui| { + let ctx = self.view_context(viewer_ctx, view_id, state, space_origin); + let time_axis = ViewProperty::from_archetype::(&ctx); + let link = time_axis + .component_or_fallback::(&ctx, TimeAxis::descriptor_link().component)?; + + // Only the link mode is editable per-view. The view range is driven by pan/zoom. + let query_ctx = time_axis.query_context(&ctx); + if let Some(field) = ctx + .viewer_ctx + .reflection() + .field_reflection(&TimeAxis::descriptor_link()) + { + re_view::view_property_component_ui( + &query_ctx, + ui, + &time_axis, + field.display_name, + field, + ); + } + + // When linked to global, expose the shared view range (stored on the global view). + if link == LinkAxis::LinkToGlobal + && let Some(field) = ctx + .viewer_ctx + .reflection() + .field_reflection(&TimeAxis::descriptor_view_range()) + { + let global_time_axis = ViewProperty::from_archetype_for_view::( + ctx.viewer_ctx, + GLOBAL_VIEW_ID, + ); + let global_ctx = ViewContext { + viewer_ctx, + view_id: GLOBAL_VIEW_ID, + view_class_identifier: Self::identifier(), + space_origin, + view_state: state, + query_result: &DataQueryResult::default(), + }; + let global_query_ctx = global_time_axis.query_context(&global_ctx); + re_view::view_property_component_ui( + &global_query_ctx, + ui, + &global_time_axis, + field.display_name, + field, + ); + } + + Ok::<(), ViewSystemExecutionError>(()) + }) + .inner + } + + /// Accept drops of components onto the state timeline view. For each dropped component, a new + /// `StateVisualizer` is added that remaps `StateChange.state` from it. + fn handle_component_drop( + &self, + ctx: &ViewerContext<'_>, + view_id: ViewId, + component_paths: &[ComponentPath], + released: bool, + ) -> DragAndDropFeedback { + match re_view::handle_component_drop( + ctx, + view_id, + component_paths, + released, + crate::StateVisualizer::identifier(), + re_sdk_types::archetypes::StateChange::descriptor_state().component, + ) { + re_view::ComponentDropResult::Accept => DragAndDropFeedback::Accept, + re_view::ComponentDropResult::CompatibleButAlreadyVisualized => { + DragAndDropFeedback::Reject(Some("Already visualized")) + } + re_view::ComponentDropResult::Incompatible => { + DragAndDropFeedback::Reject(Some("Not a state component")) + } + } + } + + fn ui( + &self, + ctx: &ViewerContext<'_>, + _missing_chunk_reporter: &re_viewer_context::MissingChunkReporter, + ui: &mut egui::Ui, + state: &mut dyn ViewState, + query: &ViewQuery<'_>, + system_output: re_viewer_context::SystemExecutionOutput, + ) -> Result<(), ViewSystemExecutionError> { + re_tracing::profile_function!(); + + let state = state.downcast_mut::()?; + + // Collect all lane groups from all visualizers. + let all_groups: Vec<&StateLaneGroup> = system_output + .iter_visualizer_data::() + .flat_map(|d| d.groups.iter()) + .collect(); + + if all_groups.is_empty() { + let (rect, _) = + ui.allocate_exact_size(ui.available_size(), egui::Sense::click_and_drag()); + ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| { + ui.centered_and_justified(|ui| { + ui.label( + "No state data. Drag a string component from the streams tree into this view or add a new visualizer.", + ); + }); + }); + return Ok(()); + } + + // Compute data time range. + let timeline_range = ctx.recording().time_range_for(&query.timeline); + let timeline_end: Option = timeline_range.map(|r| r.max.as_i64()); + let (data_min, data_max) = data_time_range(&all_groups, timeline_end); + + // How is the time (X) axis linked? When linked to global, the pan/zoom window is + // shared with all other plots (e.g. time series views) via the global blueprint view, + // rather than kept local to this view. + let link = self.time_axis_link(ctx, state, query.view_id, query.space_origin)?; + let global_time_axis = (link == LinkAxis::LinkToGlobal) + .then(|| ViewProperty::from_archetype_for_view::(ctx, GLOBAL_VIEW_ID)); + + let data_span = (data_max - data_min).max(1.0); + + // When linked to global, derive the pan/zoom window from the shared blueprint view + // range. Otherwise auto-fit the first time we render this timeline (stored per-view). + let mut time_view = if let Some(global_time_axis) = &global_time_axis { + resolve_linked_time_view( + global_time_axis, + timeline_range, + query.latest_at, + data_min, + data_max, + ) + } else { + *state.time_views.entry(query.timeline).or_insert_with(|| { + let min = data_min - data_span * 0.05; + let max = data_max + data_span * 0.05; + TimeView { + min: TimeReal::from(min), + time_spanned: max - min, + } + }) + }; + let original_time_view = time_view; + + // Allocate the full available rect. + let (rect, response) = + ui.allocate_exact_size(ui.available_size(), egui::Sense::click_and_drag()); + + if !ui.is_rect_visible(rect) { + return Ok(()); + } + + // Layout: ruler at the top, lanes below. + let time_axis_rect = egui::Rect::from_min_max( + rect.left_top(), + egui::pos2(rect.right(), rect.top() + TIME_AXIS_HEIGHT), + ); + let lanes_rect = egui::Rect::from_min_max( + egui::pos2(rect.left(), rect.top() + TIME_AXIS_HEIGHT), + rect.right_bottom(), + ); + + // Build the time↔screen map. A single contiguous segment matches today's + // state timeline view behavior (no gap collapsing). + let data_segment = AbsoluteTimeRange::new( + TimeInt::saturated_temporal_i64(data_min as i64), + TimeInt::saturated_temporal_i64(data_max.ceil() as i64), + ); + let time_ranges_ui = TimeRangesUi::new( + rect.x_range(), + time_view, + std::slice::from_ref(&data_segment), + ); + + // The last phase has no real end; it extends to the (slightly expanded) end of the + // segment, i.e. right up to the zig-zag "end of timeline" band (same as in the time + // panel). Using the timeline end itself would leave a bare strip the width of the + // segment expansion between the last phase and the band. + let open_end_time: Option = timeline_end + .and_then(|_| time_ranges_ui.segments.last()) + .map(|segment| segment.time.max.as_f64()); + + let current_time = TimeReal::from(query.latest_at.as_i64() as f64); + let cursor_x = time_ranges_ui.x_from_time_f32(current_time); + + // Time cursor interaction. + let cursor_response = cursor_x.filter(|x| rect.x_range().contains(*x)).map(|x| { + const HALF_WIDTH: f32 = 4.0; + let interact_rect = + egui::Rect::from_x_y_ranges((x - HALF_WIDTH)..=(x + HALF_WIDTH), rect.y_range()); + ui.interact( + interact_rect, + ui.id().with("state_timeline_cursor"), + egui::Sense::click_and_drag(), + ) + .on_hover_cursor(egui::CursorIcon::ResizeColumn) + }); + + // Background. + let painter = ui.painter_at(rect); + painter.rect_filled(rect, 0.0, ui.style().visuals.extreme_bg_color); + + // Draw the time ruler at the top. + let time_type = ctx + .time_ctrl + .timeline() + .map_or(TimeType::Sequence, |tl| tl.typ()); + let timestamp_format = ctx.app_options().timestamp_format; + re_time_ruler::paint_time_ranges_and_ticks( + &time_ranges_ui, + ui, + &painter.with_clip_rect(time_axis_rect), + time_axis_rect.y_range(), + time_type, + timestamp_format, + ); + + // Separator between ruler and lanes. + painter.line_segment( + [time_axis_rect.left_bottom(), time_axis_rect.right_bottom()], + egui::Stroke::new(1.0, ui.style().visuals.weak_text_color()), + ); + + // Paint a vertical band of the highlighted state phase, behind the lanes. + if let Some(highlight) = ctx.time_ctrl.highlighted_range() + && highlight.timeline == query.timeline + && highlight.kind == re_viewer_context::TimeRangeHighlightKind::StateTimeline + && let Some(color) = highlight.color + { + let x_start = time_ranges_ui + .x_from_time_f32(TimeReal::from(highlight.range.min.as_i64() as f64)) + .unwrap_or_else(|| rect.left()) + .max(rect.left()); + let x_end = time_ranges_ui + .x_from_time_f32(TimeReal::from(highlight.range.max.as_i64() as f64)) + .unwrap_or_else(|| rect.right()) + .min(rect.right()); + if x_end > x_start { + painter.rect_filled( + egui::Rect::from_min_max( + egui::pos2(x_start, rect.top()), + egui::pos2(x_end, rect.bottom()), + ), + 0.0, + color, + ); + } + } + + // Lane groups: each one is its own widget, stacked vertically inside a ScrollArea. + let label_color = ui.style().visuals.text_color(); + let mut hovered_entity: Option<&EntityPath> = None; + let mut hovered_phase: Option = None; + let mut group_label_anchors: Vec<(egui::Pos2, &StateLaneGroup)> = + Vec::with_capacity(all_groups.len()); + ui.scope_builder(egui::UiBuilder::new().max_rect(lanes_rect), |ui| { + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .scroll_source(egui::scroll_area::ScrollSource { + scroll_bar: true, + drag: egui::scroll_area::DragScroll::Never, + mouse_wheel: true, + }) + .show(ui, |ui: &mut egui::Ui| { + ui.add_space(TOP_MARGIN); + ui.spacing_mut().item_spacing.y = 0.0; + for group in &all_groups { + let result = show_group( + ui, + group, + &time_ranges_ui, + time_type, + timestamp_format, + open_end_time, + ); + group_label_anchors.push((result.label_pos, *group)); + if result.is_hovered { + hovered_entity = Some(&group.entity_path); + } + if result.hovered_phase.is_some() { + hovered_phase = result.hovered_phase; + } + } + + // Mark the limits of the timeline with the same zig-zag bands the time + // panel uses. Painted over the lanes: the open-ended last phase of each + // lane extends slightly under the band, so its teeth carve into the phase. + // Painted inside the scroll area so its scroll bar stays on top. + re_time_ruler::paint_time_ranges_gaps( + &time_ranges_ui, + ui, + &painter, + rect.y_range(), + ); + + // Group labels go on top of the zig-zag bands; their translucent + // background plates keep them readable over the teeth. + let lanes_painter = painter.with_clip_rect(lanes_rect); + for (label_pos, group) in &group_label_anchors { + paint_group_label(ui, group, label_color, &lanes_painter, *label_pos); + } + }); + }); + + // Dragging the time cursor. + if let Some(cursor_response) = &cursor_response + && ui.input(|i| { + i.pointer.primary_pressed() + || i.pointer.primary_down() + || i.pointer.primary_released() + }) + && let Some(pos) = cursor_response.interact_pointer_pos() + && let Some(time) = time_ranges_ui.time_from_x_f32(pos.x) + { + ctx.send_time_commands([ + TimeControlCommand::Pause, + TimeControlCommand::SetTimeClamped(time), + ]); + } + + // Secondary (right) click anywhere in the view jumps the time cursor. + if response.clicked_by(egui::PointerButton::Secondary) + && let Some(pos) = response.interact_pointer_pos() + && let Some(time) = time_ranges_ui.time_from_x_f32(pos.x) + { + ctx.send_time_commands([ + TimeControlCommand::Pause, + TimeControlCommand::SetTimeClamped(time), + ]); + } + + // Pan: primary- or middle-click drag, plus two-finger touchpad horizontal scroll. + // Cmd+scroll is routed to `zoom_delta` by egui, so it won't double-fire here. + let mut pan_dx = 0.0; + if response.dragged_by(egui::PointerButton::Primary) + || response.dragged_by(egui::PointerButton::Middle) + { + pan_dx += response.drag_delta().x; + ui.ctx().set_cursor_icon(egui::CursorIcon::AllScroll); + } + if response.contains_pointer() { + pan_dx += ui.input(|i| i.smooth_scroll_delta.x); + } + if pan_dx != 0.0 + && let Some(new_view) = time_ranges_ui.pan(-pan_dx) + { + time_view = new_view; + } + + // Ctrl/Cmd + scroll to zoom. + let zoom_delta = ui.input(|i| i.zoom_delta()); + if zoom_delta != 1.0 + && response.contains_pointer() + && let Some(pointer_pos) = ui.input(|i| i.pointer.hover_pos()) + && let Some(new_view) = time_ranges_ui.zoom_at(pointer_pos.x, zoom_delta) + { + time_view = new_view; + } + + // Double click anywhere in the view to reset zoom. + // Doesn't reset global time cursor. + if let Some(global_time_axis) = &global_time_axis { + // Linked to global: persist pan/zoom to the shared blueprint view range. + if response.double_clicked() { + global_time_axis.reset_blueprint_component(ctx, TimeAxis::descriptor_view_range()); + ui.request_repaint(); + } else if time_view != original_time_view { + save_linked_time_view(ctx, global_time_axis, time_view); + ui.request_repaint(); + } + + // Keep the query window in sync with what we drew: the visualizer derives its + // `RangeQuery` from `visible_time_range` (i.e. `time_views`). Without this, a view + // that rendered independently before being linked would keep querying its stale + // local window while drawing the global one, dropping transitions outside it. + // Re-query (repaint) whenever that window changes. + if state.time_views.insert(query.timeline, time_view) != Some(time_view) { + ui.request_repaint(); + } + } else if response.double_clicked() { + state.time_views.remove(&query.timeline); + ui.request_repaint(); + } else { + state.time_views.insert(query.timeline, time_view); + } + + // Publish the hovered phase so other views can highlight the same range. + if let Some(phase) = hovered_phase { + let [r, g, b, _] = phase.color.to_array(); + #[expect(clippy::disallowed_methods)] + let band_color = egui::Color32::from_rgba_unmultiplied(r, g, b, 30); + let range = AbsoluteTimeRange::new( + phase.start_time, + phase + .end_time + .map_or(TimeInt::MAX, TimeInt::saturated_temporal_i64), + ); + ctx.send_time_commands([TimeControlCommand::HighlightRange( + re_viewer_context::TimeRangeHighlight { + range, + timeline: query.timeline, + kind: re_viewer_context::TimeRangeHighlightKind::StateTimeline, + color: Some(band_color), + }, + )]); + } + + // Time cursor — uses the same triangle-headed style as the time panel. + // Painted last so it appears above the lanes. + if let Some(cursor_x) = cursor_x + && rect.x_range().contains(cursor_x) + { + ui.paint_time_cursor(&painter, cursor_response.as_ref(), cursor_x, rect.y_range()); + } + + // Selection: a hovered lane band selects its entity, anywhere else the view. + let interacted_item = if let Some(entity_path) = hovered_entity { + Item::DataResult(DataResultInteractionAddress::from_entity_path( + query.view_id, + entity_path.clone(), + )) + } else { + Item::View(query.view_id) + }; + ctx.handle_select_hover_drag_interactions(&response, interacted_item, false); + + Ok(()) + } +} + +/// Walk a lane's phases and produce the list of items to render at the current zoom level, +/// merging consecutive narrow visible phases into [`RenderItem::Merged`] regions. +/// +/// Invisible phases break the merge chain so that user-hidden states remain hidden +/// rather than being folded into a visible merged region. A run of narrow phases that +/// contains a single phase is emitted as a [`RenderItem::Single`] (no merge marker). +fn compute_render_items<'a>( + phases: &'a [StateLanePhase], + lanes_rect: egui::Rect, + time_ranges_ui: &TimeRangesUi, + open_end_time: Option, +) -> Vec> { + struct PendingNarrow<'a> { + phase: &'a StateLanePhase, + x_start: f32, + x_end: f32, + end_time: Option, + } + + /// Accumulator for consecutive narrow visible phases. Tracks only the first + /// pending phase and the current tail, since `flush` never needs anything + /// in between — emitting a `Single` (count == 1) or a `Merged` (count >= 2) + /// uses just the first start and the last end. + #[derive(Default)] + struct Pending<'a> { + first: Option>, + last_x_end: f32, + last_end_time: Option, + count: usize, + } + + impl<'a> Pending<'a> { + fn push(&mut self, p: PendingNarrow<'a>) { + self.last_x_end = p.x_end; + self.last_end_time = p.end_time; + self.count += 1; + if self.first.is_none() { + self.first = Some(p); + } + } + + fn flush(&mut self, items: &mut Vec>) { + let count = std::mem::take(&mut self.count); + let Some(first) = self.first.take() else { + return; + }; + if count == 1 { + items.push(RenderItem::Single { + phase: first.phase, + x_start: first.x_start, + x_end: first.x_end, + end_time: first.end_time, + }); + } else { + items.push(RenderItem::Merged { + x_start: first.x_start, + x_end: self.last_x_end, + start_time: first.phase.start_time, + end_time: self.last_end_time, + count, + }); + } + } + } + + let mut items: Vec> = Vec::new(); + let mut pending = Pending::default(); + + for (i, phase) in phases.iter().enumerate() { + // Gaps break the merge chain. + if phase.content.is_none() { + pending.flush(&mut items); + continue; + } + + let is_last = i + 1 == phases.len(); + let next_time: Option = phases + .get(i + 1) + .map(|p| p.start_time as f64) + .or(open_end_time); + let Some(x_start) = time_ranges_ui.x_from_time_f32(TimeReal::from(phase.start_time as f64)) + else { + continue; + }; + let x_end_unclipped = match next_time { + Some(t) => time_ranges_ui + .x_from_time_f32(TimeReal::from(t)) + .unwrap_or_else(|| lanes_rect.right()), + None => lanes_rect.right(), + }; + + // Off-screen to the right: nothing past this is visible either. + // The post-loop flush below will handle any remaining pending phases. + if x_start >= lanes_rect.right() { + break; + } + // Off-screen to the left: skip but keep the merge chain going so the next + // visible phase can still merge with later ones. + if x_end_unclipped <= lanes_rect.left() { + continue; + } + + let visible_x_start = x_start.max(lanes_rect.left()); + let visible_x_end = x_end_unclipped.min(lanes_rect.right()); + let width = visible_x_end - visible_x_start; + if width <= 0.0 { + continue; + } + + if is_last { + // The last phase is always its own item (never merged) and open-ended. It + // extends slightly under the zig-zag "end of timeline" band so the band's + // teeth carve into it instead of leaving background notches along its edge. + pending.flush(&mut items); + items.push(RenderItem::Single { + phase, + x_start: visible_x_start, + x_end: (visible_x_end + MAX_ZIG_WIDTH).min(lanes_rect.right()), + end_time: None, + }); + } else if width >= MERGE_PHASE_THRESHOLD_PIXEL { + pending.flush(&mut items); + items.push(RenderItem::Single { + phase, + x_start: visible_x_start, + x_end: visible_x_end, + end_time: next_time.map(|t| t as i64), + }); + } else { + pending.push(PendingNarrow { + phase, + x_start: visible_x_start, + x_end: visible_x_end, + end_time: next_time.map(|t| t as i64), + }); + } + } + pending.flush(&mut items); + + items +} + +/// Compute the (min, max) time range across all lane groups. +fn data_time_range(groups: &[&StateLaneGroup], timeline_end: Option) -> (f64, f64) { + let mut min = f64::MAX; + let mut max = f64::MIN; + for group in groups { + for phase in group.lanes.iter().flat_map(|lane| &lane.phases) { + let t = phase.start_time as f64; + min = min.min(t); + max = max.max(t); + } + } + if let Some(end) = timeline_end { + max = max.max(end as f64); + } + if min > max { + (0.0, 1.0) + } else if (max - min).abs() < f64::EPSILON { + (min - 0.5, max + 0.5) + } else { + (min, max) + } +} + +/// Resolve the pan/zoom window shared via the global blueprint view range. +/// +/// The range is read without a fallback (none is registered for it in this crate), so an unset or +/// infinite range resolves to the full timeline range — or the data range when the timeline range +/// is unknown. +fn resolve_linked_time_view( + global_time_axis: &ViewProperty, + timeline_range: Option, + latest_at: TimeInt, + data_min: f64, + data_max: f64, +) -> TimeView { + let view_range = global_time_axis + .component_or_empty::( + TimeAxis::descriptor_view_range().component, + ) + .ok() + .flatten() + .unwrap_or(re_sdk_types::blueprint::components::TimeRange( + re_sdk_types::datatypes::TimeRange::EVERYTHING, + )); + + let cursor = re_sdk_types::datatypes::TimeInt(latest_at.as_i64()); + let min = match view_range.start { + re_sdk_types::datatypes::TimeRangeBoundary::Infinite => { + timeline_range.map_or(data_min as i64, |r| r.min.as_i64()) + } + _ => view_range.start.start_boundary_time(cursor).0, + }; + let max = match view_range.end { + re_sdk_types::datatypes::TimeRangeBoundary::Infinite => { + timeline_range.map_or_else(|| data_max.ceil() as i64, |r| r.max.as_i64()) + } + _ => view_range.end.end_boundary_time(cursor).0, + }; + let span = ((max - min) as f64).max(1.0); + TimeView { + min: TimeReal::from(min as f64), + time_spanned: span, + } +} + +/// Persist the pan/zoom window to the shared global blueprint view range. +/// +/// Both endpoints are rounded (rather than floored/ceiled) so the width is preserved: asymmetric +/// rounding would inflate the range by up to a unit every frame during a continuous pan, feeding +/// back through [`resolve_linked_time_view`] on the next frame. +fn save_linked_time_view( + ctx: &ViewerContext<'_>, + global_time_axis: &ViewProperty, + time_view: TimeView, +) { + let start = time_view.min.round(); + let end = (time_view.min + TimeReal::from(time_view.time_spanned)).round(); + let new_range = + re_sdk_types::blueprint::components::TimeRange(re_sdk_types::datatypes::TimeRange { + start: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( + re_sdk_types::datatypes::TimeInt(start.as_i64()), + ), + end: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( + re_sdk_types::datatypes::TimeInt(end.as_i64()), + ), + }); + global_time_axis.save_blueprint_component(ctx, &TimeAxis::descriptor_view_range(), &new_range); +} + +/// What the lane-group widgets found under the pointer this frame. +struct HoveredPhase { + start_time: i64, + + /// `None` for an open-ended last phase. + end_time: Option, + + color: egui::Color32, +} + +/// What [`show_group`] rendered and found under the pointer. +struct ShowGroupResult { + /// Where the shared group label goes. + label_pos: egui::Pos2, + + /// Whether the pointer is over one of the group's bands, including gaps. + is_hovered: bool, + + /// The render item under the pointer, if any. For a merged region this is the + /// region's full span. + hovered_phase: Option, +} + +/// Render a lane group as a self-contained widget: label space at the top, then one +/// band per instance lane, stacked vertically. +fn show_group( + ui: &mut egui::Ui, + group: &StateLaneGroup, + time_ranges_ui: &TimeRangesUi, + time_type: TimeType, + timestamp_format: TimestampFormat, + open_end_time: Option, +) -> ShowGroupResult { + let num_lanes = group.lanes.len(); + let bands_height = + num_lanes as f32 * LANE_BAND_HEIGHT + num_lanes.saturating_sub(1) as f32 * SUB_LANE_GAP; + let (response, painter) = ui.allocate_painter( + egui::vec2( + ui.available_width(), + LANE_LABEL_HEIGHT + bands_height + LANE_GAP, + ), + egui::Sense::hover(), + ); + let rect = response.rect; + + let hover_pos = response.hover_pos(); + let merged_fill_inactive = ui.visuals().widgets.inactive.bg_fill; + let merged_fill_hovered = ui.visuals().widgets.hovered.bg_fill; + let merged_text_color = ui.visuals().text_color(); + + let mut is_hovered = false; + let mut hovered_phase = None; + + for (instance, lane) in group.lanes.iter().enumerate() { + let band_top = + rect.top() + LANE_LABEL_HEIGHT + instance as f32 * (LANE_BAND_HEIGHT + SUB_LANE_GAP); + let band_rect = egui::Rect::from_min_max( + egui::pos2(rect.left(), band_top), + egui::pos2(rect.right(), band_top + LANE_BAND_HEIGHT), + ); + + if hover_pos.is_some_and(|pos| band_rect.contains(pos)) { + is_hovered = true; + } + + // `compute_render_items` uses the rect's x bounds for clipping phases to the + // visible time range; y is unused. Passing the group's own rect gives the same + // bounds as the old whole-area lanes_rect since every group spans the full width. + let render_items = compute_render_items(&lane.phases, rect, time_ranges_ui, open_end_time); + + for item in &render_items { + let (x_start, x_end) = item.x_range(); + let item_rect = egui::Rect::from_min_max( + egui::pos2(x_start, band_rect.top()), + egui::pos2(x_end, band_rect.bottom()), + ); + let hovered = hover_pos.is_some_and(|pos| item_rect.contains(pos)); + + match item { + RenderItem::Single { + phase, end_time, .. + } => { + paint_single_phase(&painter, item_rect, phase, hovered); + if hovered && let Some(content) = &phase.content { + hovered_phase = Some(HoveredPhase { + start_time: phase.start_time, + end_time: *end_time, + color: content.color, + }); + } + } + RenderItem::Merged { + start_time, + end_time, + count, + .. + } => { + let fill = if hovered { + merged_fill_hovered + } else { + merged_fill_inactive + }; + paint_merged_phase(&painter, item_rect, *count, fill, merged_text_color); + if hovered { + hovered_phase = Some(HoveredPhase { + start_time: *start_time, + end_time: *end_time, + color: fill, + }); + } + } + } + + if hovered { + let instance = (num_lanes > 1).then_some(instance); + show_item_tooltip(ui, item, instance, time_type, timestamp_format); + } + } + } + + ShowGroupResult { + label_pos: egui::pos2(rect.left() + 4.0, rect.top()), + is_hovered, + hovered_phase, + } +} + +/// Paint a group's label (shared by all its instance lanes) on a translucent +/// background-colored plate, so it stays readable where it overlaps the zig-zag +/// "end of timeline" bands. +/// Over the plain background (the common case) the plate is invisible. +fn paint_group_label( + ui: &egui::Ui, + group: &StateLaneGroup, + label_color: egui::Color32, + painter: &egui::Painter, + label_pos: egui::Pos2, +) { + let label_galley = painter.layout_no_wrap( + group.label.clone(), + egui::FontId::proportional(11.0), + label_color, + ); + let bg_color = ui.visuals().extreme_bg_color; + #[expect(clippy::disallowed_methods)] // Data-driven visualization color, not a UI theme color. + let label_bg_color = + egui::Color32::from_rgba_unmultiplied(bg_color.r(), bg_color.g(), bg_color.b(), 160); + painter.rect_filled( + egui::Rect::from_min_size(label_pos, label_galley.size()).expand2(egui::vec2(3.0, 1.0)), + 2.0, + label_bg_color, + ); + painter.galley(label_pos, label_galley, label_color); +} + +/// Paint one normal phase: filled band (dimmed when not hovered) + clipped label. +fn paint_single_phase( + painter: &egui::Painter, + rect: egui::Rect, + phase: &StateLanePhase, + hovered: bool, +) { + let Some(style) = &phase.content else { + return; + }; + + #[expect(clippy::disallowed_methods)] // Data-driven visualization color, not a UI theme color. + let fill = if hovered { + style.color + } else { + let [r, g, b, _] = style.color.to_array(); + egui::Color32::from_rgba_unmultiplied(r, g, b, 200) + }; + + painter.add(egui::epaint::RectShape::new( + rect, + 0.0, + fill, + egui::Stroke::NONE, + egui::StrokeKind::Outside, + )); + + if rect.width() - 6.0 > 10.0 { + painter.with_clip_rect(rect).text( + egui::pos2(rect.left() + 4.0, rect.top() + 3.0), + egui::Align2::LEFT_TOP, + &style.label, + egui::FontId::proportional(12.0), + readable_text_color(style.color), + ); + } +} + +/// Paint a merged region: a flat band in a theme widget color signaling that many +/// narrow phases have been collapsed at the current zoom level. The caller picks the +/// fill from `widgets.inactive`/`widgets.hovered` so the hover state stays +/// token-driven rather than relying on an arbitrary multiplier. +fn paint_merged_phase( + painter: &egui::Painter, + rect: egui::Rect, + count: usize, + fill: egui::Color32, + text_color: egui::Color32, +) { + painter.add(egui::epaint::RectShape::new( + rect, + 0.0, + fill, + egui::Stroke::NONE, + egui::StrokeKind::Outside, + )); + + if rect.width() - 6.0 > 24.0 { + let label = format!("{count} states"); + painter.with_clip_rect(rect).text( + egui::pos2(rect.left() + 4.0, rect.top() + 3.0), + egui::Align2::LEFT_TOP, + label, + egui::FontId::proportional(12.0), + text_color, + ); + } +} + +fn show_item_tooltip( + ui: &egui::Ui, + item: &RenderItem<'_>, + instance: Option, + time_type: TimeType, + timestamp_format: TimestampFormat, +) { + egui::Tooltip::always_open( + ui.ctx().clone(), + ui.layer_id(), + egui::Id::new("state_tooltip"), + egui::PopupAnchor::Pointer, + ) + .show(|ui| { + let weak = ui.visuals().weak_text_color(); + let small = egui::FontId::proportional(11.0); + if let Some(instance) = instance { + ui.label( + egui::RichText::new(format!("Instance {instance}")) + .font(small.clone()) + .color(weak), + ); + } + match item { + RenderItem::Single { + phase, end_time, .. + } => { + ui.label(phase.content.as_ref().map_or("", |s| s.label.as_str())); + ui.add_space(4.0); + let start = TimeCell::new(time_type, phase.start_time).format(timestamp_format); + ui.label( + egui::RichText::new(format!("Start: {start}")) + .font(small.clone()) + .color(weak), + ); + if let Some(end) = end_time { + let end = TimeCell::new(time_type, *end).format(timestamp_format); + ui.label( + egui::RichText::new(format!("End: {end}")) + .font(small) + .color(weak), + ); + } else { + // No end time → open-ended last phase. + ui.label( + egui::RichText::new("End: ongoing (no later data)") + .font(small) + .color(weak), + ); + } + } + RenderItem::Merged { + start_time, + end_time, + count, + .. + } => { + ui.label(format!("{count} states (zoom in to see details)")); + ui.add_space(4.0); + let start = TimeCell::new(time_type, *start_time).format(timestamp_format); + ui.label( + egui::RichText::new(format!("Start: {start}")) + .font(small.clone()) + .color(weak), + ); + if let Some(end) = end_time { + let end = TimeCell::new(time_type, *end).format(timestamp_format); + ui.label( + egui::RichText::new(format!("End: {end}")) + .font(small) + .color(weak), + ); + } + } + } + }); +} + +/// Choose white or black text depending on background luminance. +fn readable_text_color(bg: egui::Color32) -> egui::Color32 { + if bg.intensity() > 0.6 { + egui::Color32::BLACK + } else { + egui::Color32::WHITE + } +} + +#[test] +fn test_help_view() { + re_test_context::TestContext::test_help_view(|ctx| StateTimelineView.help(ctx)); +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Construct a phase list from `(start_time, drawn)` pairs. `drawn = true` is + /// a visible state; `drawn = false` is a gap. Color/label are unused by + /// `compute_render_items`, so we leave them dummy. + fn lane(phases: &[(i64, bool)]) -> Vec { + phases + .iter() + .map(|&(t, drawn)| StateLanePhase { + start_time: t, + content: drawn.then(|| crate::data::StateLanePhaseContent { + label: String::new(), + color: egui::Color32::TRANSPARENT, + }), + }) + .collect() + } + + /// 100-pixel-wide lane rect; combined with a `TimeView` covering `[0, 100]` + /// this maps one time unit to one pixel, so phase widths in time equal pixel + /// widths. + fn unit_rect() -> egui::Rect { + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(100.0, 22.0)) + } + + fn ranges_ui(t_min: f64, t_max: f64) -> TimeRangesUi { + let time_view = TimeView { + min: TimeReal::from(t_min), + time_spanned: t_max - t_min, + }; + let segment = AbsoluteTimeRange::new( + TimeInt::saturated_temporal_i64(t_min as i64), + TimeInt::saturated_temporal_i64(t_max.ceil() as i64), + ); + TimeRangesUi::new( + unit_rect().x_range(), + time_view, + std::slice::from_ref(&segment), + ) + } + + fn is_single(item: &RenderItem<'_>, expected_start: i64) -> bool { + matches!(item, RenderItem::Single { phase, .. } if phase.start_time == expected_start) + } + + fn is_open_single(item: &RenderItem<'_>, expected_start: i64) -> bool { + matches!( + item, + RenderItem::Single { phase, end_time: None, .. } + if phase.start_time == expected_start + ) + } + + fn is_merged(item: &RenderItem<'_>, expected_start: i64, expected_count: usize) -> bool { + matches!( + item, + RenderItem::Merged { start_time, count, .. } + if *start_time == expected_start && *count == expected_count + ) + } + + #[test] + fn empty_lane_produces_no_items() { + let lane = lane(&[]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert!(items.is_empty(), "{items:?}"); + } + + #[test] + fn single_wide_phase_renders_as_single() { + // One phase covering x=0..100 — well above the merge threshold. + let lane = lane(&[(0, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 1, "{items:?}"); + assert!(is_single(&items[0], 0), "{items:?}"); + } + + #[test] + fn lone_narrow_phase_renders_as_single_not_merged() { + // Phase 0: x=0..2 (narrow). Phase 1: x=2..100 (wide). + // The narrow phase has no narrow neighbor to merge with, so it stays Single. + let lane = lane(&[(0, true), (2, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 2, "{items:?}"); + assert!(is_single(&items[0], 0), "{items:?}"); + assert!(is_single(&items[1], 2), "{items:?}"); + } + + #[test] + fn two_consecutive_narrow_phases_merge() { + // Two narrow (x=0..2, 2..4) + one wide (x=4..100). + let lane = lane(&[(0, true), (2, true), (4, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 2, "{items:?}"); + assert!(is_merged(&items[0], 0, 2), "{items:?}"); + assert!(is_single(&items[1], 4), "{items:?}"); + } + + #[test] + fn wide_phase_breaks_merge_chain() { + // Wide (0..10), narrow (10..12), wide (12..100) — the lone narrow stays Single. + let lane = lane(&[(0, true), (10, true), (12, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 3, "{items:?}"); + assert!(is_single(&items[0], 0), "{items:?}"); + assert!(is_single(&items[1], 10), "{items:?}"); + assert!(is_single(&items[2], 12), "{items:?}"); + } + + #[test] + fn invisible_phase_breaks_merge_chain() { + // narrow visible (0..2), narrow invisible (2..4), narrow visible (4..6), wide (6..100). + // The two visible narrow phases must NOT merge across the invisible gap. + let lane = lane(&[(0, true), (2, false), (4, true), (6, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 3, "{items:?}"); + assert!(is_single(&items[0], 0), "{items:?}"); + assert!(is_single(&items[1], 4), "{items:?}"); + assert!(is_single(&items[2], 6), "{items:?}"); + } + + #[test] + fn gap_phase_is_not_drawn_and_bounds_previous_state() { + // wide state (0..50), gap at 50, wide state (60..100). + // The gap should not produce a render item, but the first state must end at + // t=50 (not t=60). The gap also breaks any merge chain. + let lane = lane(&[(0, true), (50, false), (60, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 2, "{items:?}"); + match &items[0] { + RenderItem::Single { end_time, .. } => assert_eq!(*end_time, Some(50)), + item @ RenderItem::Merged { .. } => { + panic!("expected first item to be Single, got {item:?}") + } + } + assert!(is_single(&items[0], 0), "{items:?}"); + assert!(is_single(&items[1], 60), "{items:?}"); + } + + #[test] + fn trailing_gap_truncates_last_state() { + // wide state (0..70), gap at 70 — the lane ends with no active state. + // The state's end_time must be the gap's start, and the gap itself produces no item. + let lane = lane(&[(0, true), (70, false)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 1, "{items:?}"); + match &items[0] { + RenderItem::Single { end_time, .. } => assert_eq!(*end_time, Some(70)), + item @ RenderItem::Merged { .. } => panic!("expected Single, got {item:?}"), + } + } + + #[test] + fn off_screen_left_phases_dont_break_merge_chain() { + // Viewport t=[30, 130]: phases at 0 and 5 are entirely off-screen left; + // phases at 10 and 32 are narrow on-screen; phase at 34 is wide. + // The two on-screen narrow phases must merge — the off-screen phases + // shouldn't terminate the run. + let lane = lane(&[(0, true), (5, true), (10, true), (32, true), (34, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(30.0, 130.0), None); + assert_eq!(items.len(), 2, "{items:?}"); + assert!(is_merged(&items[0], 10, 2), "{items:?}"); + assert!(is_single(&items[1], 34), "{items:?}"); + } + + #[test] + fn off_screen_right_phase_stops_iteration() { + // Viewport t=[0, 100], two visible wide phases, then one off-screen right. + let lane = lane(&[(0, true), (10, true), (200, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 2, "{items:?}"); + assert!(is_single(&items[0], 0), "{items:?}"); + assert!(is_single(&items[1], 10), "{items:?}"); + } + + #[test] + fn trailing_narrow_run_merges_all_but_the_open_ended_last_phase() { + // 50 narrow phases spaced 2 apart. The chronologically last phase is always pulled + // out as its own open-ended item, so the first 49 merge and #50 stays separate. + let phases: Vec<(i64, bool)> = (0..50).map(|i| (i * 2, true)).collect(); + let lane = lane(&phases); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), Some(100.0)); + assert_eq!(items.len(), 2, "{items:?}"); + assert!(is_merged(&items[0], 0, 49), "{items:?}"); + assert!(is_open_single(&items[1], 98), "{items:?}"); + } + + #[test] + fn trailing_narrow_run_flushes_when_remaining_phases_are_off_screen_right() { + // Two narrow phases (50..52, 52..54), then a wide (54..100), then a phase + // at t=200 that's off-screen-right. The merge group must still be emitted, and the + // off-screen last phase yields no open-ended item. + let lane = lane(&[(50, true), (52, true), (54, true), (200, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), None); + assert_eq!(items.len(), 2, "{items:?}"); + assert!(is_merged(&items[0], 50, 2), "{items:?}"); + assert!(is_single(&items[1], 54), "{items:?}"); + } + + #[test] + fn last_phase_is_open_ended_and_extends_to_open_end_time() { + // Viewport t=[0, 100], one phase at t=0, open_end_time=50 (end of the timeline). + // The phase is open-ended and ends at x=50 plus the overshoot that tucks it + // under the zig-zag "end of timeline" band, rather than at the rect edge. + let lane = lane(&[(0, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 100.0), Some(50.0)); + assert_eq!(items.len(), 1, "{items:?}"); + let RenderItem::Single { + x_end, end_time, .. + } = &items[0] + else { + panic!("expected Single, got {items:?}"); + }; + assert_eq!( + *end_time, None, + "open-ended last phase has no end time: {items:?}" + ); + assert!( + (x_end - (50.0 + MAX_ZIG_WIDTH)).abs() < 0.5, + "x_end={x_end} items={items:?}" + ); + } + + #[test] + fn last_phase_starting_at_open_end_has_zero_width_and_is_not_drawn() { + // Phase starting exactly at open_end_time gets zero width and produces no item. + // (In the view this doesn't normally happen: open_end_time is the segment's + // *expanded* end, so a state logged at the last tick keeps a small width.) + let lane = lane(&[(0, true), (100, true)]); + let items = compute_render_items(&lane, unit_rect(), &ranges_ui(0.0, 120.0), Some(100.0)); + assert_eq!(items.len(), 1, "{items:?}"); + assert!(is_single(&items[0], 0), "{items:?}"); + } +} diff --git a/crates/viewer/re_view_state_timeline/src/visualizer.rs b/crates/viewer/re_view_state_timeline/src/visualizer.rs new file mode 100644 index 000000000000..c3993d5e46bc --- /dev/null +++ b/crates/viewer/re_view_state_timeline/src/visualizer.rs @@ -0,0 +1,760 @@ +use nohash_hasher::IntMap; +use re_chunk_store::external::arrow::datatypes::DataType; +use re_chunk_store::{AbsoluteTimeRange, RowId}; +use re_log_types::TimeInt; +use re_sdk_types::Archetype as _; +use re_sdk_types::ArrowString; +use re_sdk_types::archetypes::{StateChange, StateConfiguration}; +use re_sdk_types::components::Text; +use re_view::{ComponentCastRule, collect_recursive_clears}; +use re_viewer_context::{ + AppOptions, IdentifiedViewSystem, SingleRequiredComponentConstraint, ViewContext, + ViewContextCollection, ViewQuery, ViewSystemExecutionError, ViewSystemIdentifier, + VisualizerExecutionOutput, VisualizerQueryInfo, VisualizerReportSeverity, VisualizerSystem, +}; + +use crate::data::{ + StateLane, StateLaneGroup, StateLanePhase, StateLanePhaseContent, StateLanesData, + StateValueKind, +}; + +/// One logged row of the state component. +struct StateRow { + time: i64, + row_id: RowId, + + /// One formatted label per instance in the row's state array. + labels: Vec>, +} + +/// Maps each accepted source physical type to a type that the visualizer can handle. +static COMPONENT_CAST_MAP: std::sync::LazyLock> = + std::sync::LazyLock::new(|| { + [ + (DataType::Utf8, DataType::Utf8), + (DataType::LargeUtf8, DataType::LargeUtf8), + (DataType::Boolean, DataType::Boolean), + (DataType::Int8, DataType::Float64), + (DataType::Int16, DataType::Float64), + (DataType::Int32, DataType::Float64), + (DataType::Int64, DataType::Float64), + (DataType::UInt8, DataType::Float64), + (DataType::UInt16, DataType::Float64), + (DataType::UInt32, DataType::Float64), + (DataType::UInt64, DataType::Float64), + (DataType::Float16, DataType::Float64), + (DataType::Float32, DataType::Float64), + (DataType::Float64, DataType::Float64), + ] + .into_iter() + .collect() + }); + +/// Map a post-cast element datatype to its canonical lane kind. +pub fn state_value_kind_from_datatype(dt: &DataType) -> Option { + match dt { + DataType::Utf8 | DataType::LargeUtf8 => Some(StateValueKind::String), + DataType::Float64 => Some(StateValueKind::Scalar), + DataType::Boolean => Some(StateValueKind::Bool), + _ => None, + } +} + +/// Determine the canonical state value kind for the lane addressed by `instruction`. +pub fn current_state_value_kind( + ctx: &ViewContext<'_>, + data_result: &re_viewer_context::DataResult, + instruction: &re_viewer_context::VisualizerInstruction, +) -> Option { + let state_component = StateChange::descriptor_state().component; + let rules: IntMap<_, ComponentCastRule> = + std::iter::once((state_component, state_cast_rule as ComponentCastRule)).collect(); + let result = re_view::latest_at_with_blueprint_resolved_data_polymorphic( + ctx, + None, + &ctx.current_query(), + data_result, + [state_component], + Some(instruction), + &rules, + ); + let arr = result.get_raw_cell(state_component)?; + state_value_kind_from_datatype(arr.data_type()) +} + +/// Polymorphic cast rule for the state slot: a thin lookup into [`COMPONENT_CAST_MAP`]. +/// +/// Returning `None` for an unsupported source type causes the query layer to leave the chunk +/// unchanged (no cast applied). The visualizer then detects this and emits a per-instruction +/// error from `execute()`. +pub fn state_cast_rule(source: &DataType) -> Option { + COMPONENT_CAST_MAP.get(source).cloned() +} + +/// Color palette for state change phases. +#[expect(clippy::disallowed_methods)] // These are data-driven visualization colors, not UI theme colors. +const PALETTE: &[egui::Color32] = &[ + egui::Color32::from_rgb(76, 175, 80), // green + egui::Color32::from_rgb(255, 183, 77), // amber + egui::Color32::from_rgb(66, 165, 245), // blue + egui::Color32::from_rgb(239, 83, 80), // red + egui::Color32::from_rgb(171, 71, 188), // purple + egui::Color32::from_rgb(38, 198, 218), // teal + egui::Color32::from_rgb(255, 241, 118), // yellow + egui::Color32::from_rgb(141, 110, 99), // brown +]; + +/// Stable color derived from the raw state value. +/// +/// Hashing the value keeps the color fixed as the user adds, reorders, or +/// removes entries in the `StateConfiguration` — unlike an order-based index. +fn color_for_value(value: &str) -> egui::Color32 { + let hash = re_log_types::hash::Hash64::hash(value).hash64(); + PALETTE[(hash as usize) % PALETTE.len()] +} + +/// Resolved configuration for a single state value. +struct StateStyle { + label: String, + color: egui::Color32, + visible: bool, +} + +/// Parse a [`StateConfiguration`] from the query results, building a map from raw value to style. +fn resolve_state_config( + results: &re_view::VisualizerInstructionQueryResults<'_>, +) -> Vec<(String, StateStyle)> { + let mut config = Vec::new(); + + let values_component = StateConfiguration::descriptor_values().component; + let labels_component = StateConfiguration::descriptor_labels().component; + let colors_component = StateConfiguration::descriptor_colors().component; + let visible_component = StateConfiguration::descriptor_visible().component; + + let values: Vec = results + .iter_optional(values_component) + .slice::() + .flat_map(|(_, texts)| texts.into_iter().map(|t| t.to_string())) + .collect(); + + if values.is_empty() { + return config; + } + + let labels: Vec = results + .iter_optional(labels_component) + .slice::() + .flat_map(|(_, texts)| texts.into_iter().map(|t| t.to_string())) + .collect(); + + #[expect(clippy::disallowed_methods)] // Data-driven visualization color, not a UI theme color. + let colors: Vec = results + .iter_optional(colors_component) + .slice::() + .flat_map(|(_, rgba_values)| { + rgba_values.iter().map(|&rgba| { + let [r, g, b, a] = rgba.to_be_bytes(); + egui::Color32::from_rgba_unmultiplied(r, g, b, a) + }) + }) + .collect(); + + let visible: Vec = results + .iter_optional(visible_component) + .slice::() + .flat_map(|(_, bools)| bools.iter().collect::>()) + .collect(); + + for (i, value) in values.into_iter().enumerate() { + let label = labels + .get(i) + .filter(|l| !l.is_empty()) + .cloned() + .unwrap_or_else(|| value.clone()); + let color = colors + .get(i) + .copied() + .unwrap_or_else(|| color_for_value(&value)); + let is_visible = visible.get(i).copied().unwrap_or(true); + config.push(( + value, + StateStyle { + label, + color, + visible: is_visible, + }, + )); + } + + config +} + +/// A visualizer that queries [`StateChange`] archetypes and groups them into state change lanes. +/// +/// Each visualizer instruction (typically one per entity path) becomes one lane group, with one +/// lane per state instance. Each distinct state value within a lane gets a unique color. +#[derive(Default)] +pub struct StateVisualizer; + +impl IdentifiedViewSystem for StateVisualizer { + fn identifier() -> ViewSystemIdentifier { + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "StateVisualizer" + ) + } +} + +impl VisualizerSystem for StateVisualizer { + fn selection_ui( + &self, + ctx: &ViewContext<'_>, + ui: &mut egui::Ui, + data_result: &re_viewer_context::DataResult, + instruction: &re_viewer_context::VisualizerInstruction, + type_report: Option<&re_viewer_context::VisualizerTypeReport>, + ) -> bool { + // `StateConfiguration.values`/`colors`/`visible` are edited as a group by + // `state_config_editor` and aren't remappable, so we render source selectors + // only for the components that are: the primary `StateChange:state` and the + // optional `StateConfiguration:labels`. + let selectors = re_selection_panel::SourceSelectorContext::new( + ctx, + data_result, + instruction, + self, + type_report, + ); + // For state values, default and override options aren't meaningful. + selectors.render(ui, &StateChange::descriptor_state(), false); + selectors.render(ui, &StateConfiguration::descriptor_labels(), true); + + crate::visualizer_ui::state_config_editor(ui, ctx, data_result, instruction); + true + } + + fn visualizer_query_info(&self, _app_options: &AppOptions) -> VisualizerQueryInfo { + // Accept any of the physical types the polymorphic state cast rule can canonicalize. + // The source selector consults this set to decide which entity components are offered + // as candidates for the state slot. + let constraints = + SingleRequiredComponentConstraint::new::(&StateChange::descriptor_state()) + .with_additional_physical_types(COMPONENT_CAST_MAP.keys().cloned()) + .with_allow_static_data(false) + .into(); + + let queried = std::iter::chain( + StateChange::all_components().iter(), + StateConfiguration::all_components().iter(), + ) + .cloned() + .collect(); + + VisualizerQueryInfo { + relevant_archetype: StateChange::descriptor_state().archetype, + constraints, + queried, + } + } + + fn execute( + &self, + ctx: &ViewContext<'_>, + view_query: &ViewQuery<'_>, + _context_systems: &ViewContextCollection, + ) -> Result { + re_tracing::profile_function!(); + + let output = VisualizerExecutionOutput::default(); + + // Until the view has auto-fit on its first frame, `visible_time_range` is `None`; we + // query everything so the auto-fit (which runs in `ui`) has the full data to fit to. + let visible_range = ctx + .view_state + .as_any() + .downcast_ref::() + .and_then(|state| state.visible_time_range(view_query.timeline)) + .unwrap_or(AbsoluteTimeRange::EVERYTHING); + + // Including extended bounds means we also query the next state right after the visible range. + // Visually, it doesn't matter, but the hover tooltip needs to show when exactly the state ends. + let query = re_chunk_store::RangeQuery::new(view_query.timeline, visible_range) + .include_extended_bounds(true); + + // We get the state (and config) active at the left edge using a latest-at query. + // The `include_extended_bounds` above only considered visible chunks. + let window_start_query_time = query.range.min(); + + let mut groups: Vec = Vec::new(); + + // The state slot is polymorphic on the source datatype: numerics collapse to f64, + // strings/bools pass through. The post-cast chunks served by the query layer are + // therefore one of {Utf8, Float64, Boolean}. + let state_component = StateChange::descriptor_state().component; + let cast_rules: IntMap = + std::iter::once((state_component, state_cast_rule as ComponentCastRule)).collect(); + + for (data_result, instruction) in + view_query.iter_visualizer_instruction_for(Self::identifier()) + { + let all_component_ids: Vec<_> = std::iter::chain( + StateChange::all_component_identifiers(), + StateConfiguration::all_component_identifiers(), + ) + .collect(); + + // In-window data. + let range_results = re_view::BlueprintResolvedResults::from(( + query.clone(), + re_view::range_with_blueprint_resolved_data_polymorphic( + ctx, + None, + &query, + data_result, + all_component_ids.iter().copied(), + instruction, + &cast_rules, + ), + )); + let range_results = re_view::VisualizerInstructionQueryResults::new( + instruction, + &range_results, + &output, + ); + + // State + config active at the window start. + let latest_query = + re_chunk_store::LatestAtQuery::new(query.timeline, window_start_query_time); + let bootstrap_results = re_view::BlueprintResolvedResults::from(( + latest_query.clone(), + re_view::latest_at_with_blueprint_resolved_data_polymorphic( + ctx, + None, + &latest_query, + data_result, + all_component_ids.iter().copied(), + Some(instruction), + &cast_rules, + ), + )); + let bootstrap_results = re_view::VisualizerInstructionQueryResults::new( + instruction, + &bootstrap_results, + &output, + ); + + let range_values = range_results.iter_required(state_component); + let bootstrap_values = bootstrap_results.iter_required(state_component); + + // Dispatch on the post-cast element type, observed across both queries. The cast + // normally yields a single type; a mix means the column's physical type changed. + let mut element_types = state_chunk_element_types(&range_values); + element_types.extend(state_chunk_element_types(&bootstrap_values)); + if element_types.len() > 1 { + let kinds_list = element_types + .iter() + .map(|dt| format!("{dt:?}")) + .collect::>() + .join(", "); + range_results.report_for_component( + state_component, + VisualizerReportSeverity::Error, + format!( + "State component type changed over time ({kinds_list}). \ + The lane cannot be rendered until the column has a single type." + ), + ); + continue; + } + let element_type = element_types.into_iter().next().or_else(|| { + // The visible window is panned entirely before the first state change. Probe + // the entity's state type at the end of time so the lane still renders. + let latest_query = + re_chunk_store::LatestAtQuery::new(view_query.timeline, TimeInt::MAX); + let probe = re_view::BlueprintResolvedResults::from(( + latest_query.clone(), + re_view::latest_at_with_blueprint_resolved_data_polymorphic( + ctx, + None, + &latest_query, + data_result, + [state_component], + Some(instruction), + &cast_rules, + ), + )); + let probe = + re_view::VisualizerInstructionQueryResults::new(instruction, &probe, &output); + state_chunk_element_types(&probe.iter_required(state_component)) + .into_iter() + .next() + }); + let Some(element_type) = element_type else { + continue; + }; + let Some(value_kind) = state_value_kind_from_datatype(&element_type) else { + continue; + }; + + // Prefer the in-window `StateConfiguration`; fall back to the bootstrapped one so the + // colors/labels/visibility stay correct when the config was set before the window. + let mut state_config = resolve_state_config(&range_results); + if state_config.is_empty() { + state_config = resolve_state_config(&bootstrap_results); + } + + // The bootstrapped state-before-the-window comes first (it has the earliest time), + // followed by the in-window changes. + let mut rows = collect_state_rows(&bootstrap_values, &element_type); + rows.extend(collect_state_rows(&range_values, &element_type)); + + // `Clear` archetypes logged on this entity (or on an ancestor with + // `is_recursive = true`) end the current state regardless of value type. + let clear_events = collect_recursive_clears(ctx, &query, &data_result.entity_path); + + // One lane per instance index. + let instance_count = calculate_instance_count( + ctx, + &rows, + view_query.timeline, + &data_result.entity_path, + instruction, + state_component, + ); + + // Build the lane label: append the source component if remapped, and mark + // multi-instance groups with a `[]` suffix. + let lane_label = { + let mut label = data_result.entity_path.to_string(); + if let Some(re_viewer_context::VisualizerComponentSource::SourceComponent { + source_component, + .. + }) = instruction.component_mappings.get(&state_component) + && source_component != &state_component + { + label = format!("{label} ({source_component})"); + } + if instance_count > 1 { + label.push_str("[]"); + } + label + }; + + let lanes = (0..instance_count) + .map(|instance| { + let mut instance_events = Vec::with_capacity(rows.len()); + for row in &rows { + instance_events.push(( + row.time, + row.row_id, + row.labels.get(instance).cloned().flatten(), + )); + } + let phases = build_lane_phases(instance_events, &clear_events, &state_config); + StateLane { phases } + }) + .collect(); + + groups.push(StateLaneGroup { + label: lane_label, + entity_path: data_result.entity_path.clone(), + value_kind, + lanes, + }); + } + + Ok(output.with_visualizer_data(StateLanesData { groups })) + } +} + +/// Format a typed state value into its lane label string. +/// +/// One impl per type the polymorphic state cast can produce. +trait StateLabel { + fn to_lane_label(&self) -> String; +} + +impl StateLabel for ArrowString { + #[inline] + fn to_lane_label(&self) -> String { + self.as_str().to_owned() + } +} + +impl StateLabel for f64 { + #[inline] + fn to_lane_label(&self) -> String { + if self.is_finite() && self.fract() == 0.0 && self.abs() < 1e16 { + // Integer-valued floats: render without a trailing `.0` so config entries typed as + // `"1"`, `"42"` continue to match values that arrive as `Float64`. + format!("{}", *self as i64) + } else { + format!("{self}") + } + } +} + +impl StateLabel for bool { + #[inline] + fn to_lane_label(&self) -> String { + if *self { "true" } else { "false" }.to_owned() + } +} + +/// Format a typed iterator of rows into [`StateRow`]s. +fn collect_typed_rows(rows: ChunkIter) -> Vec +where + T: StateLabel, + ChunkIter: IntoIterator, + RowValues: IntoIterator>, +{ + rows.into_iter() + .map(|(data_time, row_id, row_values)| StateRow { + time: data_time.as_i64(), + row_id, + labels: row_values + .into_iter() + .map(|v| v.map(|v| v.to_lane_label())) + .collect(), + }) + .collect() +} + +/// Merge typed value events with `Clear`-derived gap events into a deduplicated phase list. +/// +/// Dedup rules: +/// - Same time: later row id wins (last logged event in this time bucket). +/// - Consecutive identical `Some(label)`s collapse to one. +/// - Consecutive `None`s (gaps) collapse to one. +/// - Leading `None`s (no preceding state) are dropped. +fn build_lane_phases( + value_events: Vec<(i64, RowId, Option)>, + clear_events: &[(TimeInt, RowId)], + state_config: &[(String, StateStyle)], +) -> Vec { + let mut events = value_events; + events.extend(clear_events.iter().map(|&(t, r)| (t.as_i64(), r, None))); + if events.is_empty() { + return Vec::new(); + } + events.sort_by_key(|(t, r, _)| (*t, *r)); + + let mut phases: Vec<(i64, Option)> = Vec::new(); + for (t, _r, event) in events { + if let Some(last) = phases.last_mut() + && last.0 == t + { + last.1 = event; + continue; + } + if event.is_none() && phases.last().is_none_or(|(_, last)| last.is_none()) { + // Leading gap (no preceding state) or gap-after-gap: skip. + continue; + } + if let (Some((_, Some(prev))), Some(next)) = (phases.last(), &event) + && prev == next + { + continue; + } + phases.push((t, event)); + } + if matches!(phases.first(), Some((_, None))) { + phases.remove(0); + } + + phases + .into_iter() + .map(|(t, event)| StateLanePhase { + start_time: t, + content: event.and_then(|label| build_phase_content(&label, state_config)), + }) + .collect() +} + +/// Resolve a formatted phase value against the user-authored `StateConfiguration`. +/// +/// Returns `None` (gap) when the matching config entry is hidden; otherwise builds the +/// drawn-phase style. Without a config match, falls back to a hash-derived color and the +/// raw label. +fn build_phase_content( + label: &str, + state_config: &[(String, StateStyle)], +) -> Option { + if let Some((_, style)) = state_config.iter().find(|(v, _)| v == label) { + style.visible.then(|| StateLanePhaseContent { + label: style.label.clone(), + color: style.color, + }) + } else { + Some(StateLanePhaseContent { + color: color_for_value(label), + label: label.to_owned(), + }) + } +} + +/// Collect typed state rows for one element type from a query result iterator. +/// Returns no rows for element types the polymorphic cast can't produce. +/// +/// Null values, empty strings, and empty arrays all reset (see [`StateRow`]); any other +/// value starts a new phase for its instance. +fn collect_state_rows( + values: &re_view::HybridResultsChunkIter<'_>, + element_type: &DataType, +) -> Vec { + match element_type { + DataType::Utf8 | DataType::LargeUtf8 => { + // Strings get their own path: unlike the typed collector, an empty string is + // also a reset for its instance. + values + .slice::>() + .map(|((data_time, row_id), texts)| StateRow { + time: data_time.as_i64(), + row_id, + labels: texts + .into_iter() + .map(|opt| opt.filter(|s| !s.is_empty()).map(|s| s.to_lane_label())) + .collect(), + }) + .collect() + } + DataType::Float64 => collect_typed_rows::( + values + .slice::>() + .map(|((data_time, row_id), values)| (data_time, row_id, values)), + ), + DataType::Boolean => collect_typed_rows::( + values + .slice::>() + .map(|((data_time, row_id), values)| (data_time, row_id, values)), + ), + _ => Vec::new(), + } +} + +/// Collect the set of post-cast element types observed across every chunk for the state slot. +/// +/// The cast normally produces a single type — one of {`Utf8`, `LargeUtf8`, `Float64`, +/// `Boolean`} — but if the underlying column's physical type changed over time, the chunks +/// come back with mixed element types. Returning the deduped set lets the caller treat +/// "empty", "uniform" and "mixed" by inspecting `len()`. +fn state_chunk_element_types( + all_values: &re_view::HybridResultsChunkIter<'_>, +) -> std::collections::BTreeSet { + let chunks = all_values.chunks(); + chunks + .chunks + .iter() + .filter_map(|chunk| chunk.components().get_array(chunks.component)) + .map(|arr| arr.value_type()) + .collect() +} + +/// The length (instance count) of the state arrays logged for `entity_path` on `timeline`. +fn calculate_instance_count( + ctx: &ViewContext<'_>, + rows: &[StateRow], + timeline: re_log_types::TimelineName, + entity_path: &re_log_types::EntityPath, + instruction: &re_viewer_context::VisualizerInstruction, + state_component: re_sdk_types::ComponentIdentifier, +) -> usize { + use re_chunk_store::external::arrow::array::Array as _; + + if let Some(max) = rows.iter().map(|row| row.labels.len()).max() { + return max; + } + + // No on-screen data, fallback to probing the store. + // Component remappings redirect the state slot to another source component. + let source_component = match instruction.component_mappings.get(&state_component) { + Some(re_viewer_context::VisualizerComponentSource::SourceComponent { + source_component, + .. + }) => *source_component, + _ => state_component, + }; + + let full_range_query = re_chunk_store::RangeQuery::new(timeline, AbsoluteTimeRange::EVERYTHING); + let engine = ctx.recording_engine(); + let results = engine.store().range_relevant_chunks( + re_chunk_store::ChunkTrackingMode::Ignore, + &full_range_query, + entity_path, + source_component, + ); + + // Best effort: check the first piece of data we find instead of scanning the entire timeline. + for chunk in &results.chunks { + if let Some(array) = chunk.components().get_array(source_component) { + for i in 0..array.len() { + if array.is_valid(i) { + // The length of the first valid array. + return (array.value_length(i) as usize).max(1); + } + } + } + } + + // No data found in store, display a single empty lane. + 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a string config so phase content resolves to a visible drawn phase. + fn visible_config(values: &[&str]) -> Vec<(String, StateStyle)> { + values + .iter() + .map(|v| { + ( + (*v).to_owned(), + StateStyle { + label: (*v).to_owned(), + color: egui::Color32::WHITE, + visible: true, + }, + ) + }) + .collect() + } + + #[test] + fn bootstrapped_state_becomes_leading_phase() { + // Reproduces RR-4294's pan regression at the data level: the only state change was logged + // before the visible window (here at its real time t=40, recovered via the bootstrap + // latest-at), and there are no changes inside the window. The lane must still produce a + // phase rather than vanishing; rendering clips its off-screen-left start to the edge. + let cfg = visible_config(&["Idle"]); + let events = vec![(40, RowId::new(), Some("Idle".to_owned()))]; + + let phases = build_lane_phases(events, &[], &cfg); + + assert_eq!(phases.len(), 1, "{phases:?}"); + assert_eq!(phases[0].start_time, 40, "{phases:?}"); + assert!(phases[0].content.is_some(), "{phases:?}"); + } + + #[test] + fn in_window_change_at_window_start_wins_over_bootstrap() { + // If a real change sits at the same time as the bootstrap row, the later row id wins, + // leaving a single phase with the in-window value. + let cfg = visible_config(&["Idle", "Moving"]); + let events = vec![ + (100, RowId::ZERO, Some("Idle".to_owned())), // bootstrap value + (100, RowId::new(), Some("Moving".to_owned())), // real change at the same time + ]; + + let phases = build_lane_phases(events, &[], &cfg); + + assert_eq!(phases.len(), 1, "{phases:?}"); + assert_eq!(phases[0].start_time, 100, "{phases:?}"); + assert_eq!( + phases[0].content.as_ref().map(|c| c.label.as_str()), + Some("Moving"), + "{phases:?}" + ); + } +} diff --git a/crates/viewer/re_view_state_timeline/src/visualizer_ui.rs b/crates/viewer/re_view_state_timeline/src/visualizer_ui.rs new file mode 100644 index 000000000000..6a721c4f8753 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/src/visualizer_ui.rs @@ -0,0 +1,385 @@ +//! Custom visualizer UI for the state timeline view selection panel. +//! +//! Shows an editable list of state value→label+color+visibility mappings per entity. + +use re_component_ui::color_swatch::ColorSwatch; +use re_sdk_types::archetypes::StateConfiguration; +use re_sdk_types::components::{Color, Text, Visible}; +use re_sdk_types::datatypes::{Bool, Rgba32}; +use re_sdk_types::{ComponentDescriptor, Loggable as _}; +use re_ui::UiExt as _; +use re_viewer_context::external::re_entity_db::InstancePath; +use re_viewer_context::{DataResultInteractionAddress, Item, MaybeMutRef}; + +use crate::data::StateValueKind; +use crate::visualizer::current_state_value_kind; + +/// One row in the state configuration editor. +struct StateMapping { + value: String, + label: String, + color: Rgba32, + visible: bool, +} + +/// Canonical value strings used to back boolean lanes — kept in sync with +/// `StateLabel for bool` in the visualizer so the editor's lookups match what the renderer +/// produces. +const BOOL_VALUES: [&str; 2] = ["true", "false"]; + +/// Which parts of the [`StateConfiguration`] the row UI has changed. +/// +/// Tracked so we only persist the components the user actually touched — avoids locking in +/// hash-derived default colors or empty labels when an unrelated field is edited. +#[derive(Default, Clone, Copy)] +struct ChangeFlags { + values: bool, + labels: bool, + colors: bool, + visible: bool, +} + +impl ChangeFlags { + fn merge(&mut self, other: Self) { + self.values |= other.values; + self.labels |= other.labels; + self.colors |= other.colors; + self.visible |= other.visible; + } +} + +/// Editable state configuration for a single visualizer instruction. +pub fn state_config_editor( + ui: &mut egui::Ui, + ctx: &re_viewer_context::ViewContext<'_>, + data_result: &re_viewer_context::DataResult, + instruction: &re_viewer_context::VisualizerInstruction, +) { + let entity_path = &data_result.entity_path; + + // Query current state configuration. + let query_result = re_view::latest_at_with_blueprint_resolved_data( + ctx, + None, + &ctx.current_query(), + data_result, + [ + StateConfiguration::descriptor_values().component, + StateConfiguration::descriptor_labels().component, + StateConfiguration::descriptor_colors().component, + StateConfiguration::descriptor_visible().component, + ], + Some(instruction), + ); + + let values = extract_texts(&query_result, &StateConfiguration::descriptor_values()); + let labels = extract_texts(&query_result, &StateConfiguration::descriptor_labels()); + let colors = extract_colors(&query_result, &StateConfiguration::descriptor_colors()); + let visible = extract_bools(&query_result, &StateConfiguration::descriptor_visible()); + + // Selection/hover item for this entity. + let item = Item::DataResult(DataResultInteractionAddress { + view_id: ctx.view_id, + instance_path: InstancePath::from(entity_path.clone()), + visualizer: Some(instruction.id), + }); + + let id = ui.make_persistent_id(("state_config", entity_path)); + + // For boolean lanes, the only meaningful values are `"true"` and `"false"`, so the editor + // renders a simplified two-row UI; everything else gets the freeform editor. + let kind = current_state_value_kind(ctx, data_result, instruction); + let is_bool_lane = kind == Some(StateValueKind::Bool); + + let (changes, mappings) = if is_bool_lane { + render_bool_mapping_rows(ui, entity_path, &values, &labels, &colors, &visible) + } else { + render_freeform_mapping_rows(ui, entity_path, &values, &labels, &colors, &visible) + }; + + let values_changed = changes.values; + let labels_changed = changes.labels; + let colors_changed = changes.colors; + let visible_changed = changes.visible; + + // Write only the components that actually changed. + if values_changed { + let new_values: Vec = mappings + .iter() + .map(|m| Text::from(m.value.as_str())) + .collect(); + instruction.save_override( + ctx.viewer_ctx, + &StateConfiguration::descriptor_values(), + &new_values, + ); + } + if labels_changed { + let new_labels: Vec = mappings + .iter() + .map(|m| Text::from(m.label.as_str())) + .collect(); + instruction.save_override( + ctx.viewer_ctx, + &StateConfiguration::descriptor_labels(), + &new_labels, + ); + } + if colors_changed { + let new_colors: Vec = mappings.iter().map(|m| Color::from(m.color)).collect(); + instruction.save_override( + ctx.viewer_ctx, + &StateConfiguration::descriptor_colors(), + &new_colors, + ); + } + if visible_changed { + let new_visible: Vec = mappings + .iter() + .map(|m| Visible::from(Bool(m.visible))) + .collect(); + instruction.save_override( + ctx.viewer_ctx, + &StateConfiguration::descriptor_visible(), + &new_visible, + ); + } + + // Handle hover/click selection. + let response = ui.interact(egui::Rect::NOTHING, id, egui::Sense::hover()); + if response.hovered() { + ctx.viewer_ctx.selection_state().set_hovered(item.clone()); + } +} + +/// A single row of the value mapping UI. +fn render_mapping_row_contents( + ui: &mut egui::Ui, + mapping: &mut StateMapping, + value_editable: bool, +) -> ChangeFlags { + let mut changes = ChangeFlags::default(); + + let mut value_edit = egui::TextEdit::singleline(&mut mapping.value) + .desired_width(60.0) + .interactive(value_editable) + .hint_text("value"); + if !value_editable { + // Tone down the locked-in value so it reads as informational rather than editable. + value_edit = value_edit.text_color(ui.tokens().text_subdued); + } + let value_response = ui.add(value_edit); + if value_editable && (value_response.lost_focus() || value_response.changed()) { + changes.values = true; + } + + ui.label("\u{2192}"); + + if ui.visibility_toggle_button(&mut mapping.visible).changed() { + changes.visible = true; + } + + let mut color_ref = MaybeMutRef::MutRef(&mut mapping.color); + if ui.add(ColorSwatch::new(&mut color_ref)).changed() { + changes.colors = true; + } + + let label_response = ui.add( + egui::TextEdit::singleline(&mut mapping.label) + .desired_width(f32::INFINITY) + .hint_text("label"), + ); + if label_response.lost_focus() || label_response.changed() { + changes.labels = true; + } + + changes +} + +/// Render the freeform mapping rows and a bottom "Add mapping" button. +fn render_freeform_mapping_rows( + ui: &mut egui::Ui, + entity_path: &re_log_types::EntityPath, + values: &[String], + labels: &[String], + colors: &[Rgba32], + visible: &[bool], +) -> (ChangeFlags, Vec) { + let mut mappings: Vec = (0..values.len()) + .map(|i| { + let value = values.get(i).cloned().unwrap_or_default(); + let color = colors + .get(i) + .copied() + .unwrap_or_else(|| default_color(&value)); + StateMapping { + value, + label: labels.get(i).cloned().unwrap_or_default(), + color, + visible: visible.get(i).copied().unwrap_or(true), + } + }) + .collect(); + + let mut changes = ChangeFlags::default(); + let mut remove_idx = None; + + for (i, mapping) in mappings.iter_mut().enumerate() { + let row_id = ui.make_persistent_id(("state_row", entity_path, i)); + ui.push_id(row_id, |ui| { + let (row_changes, remove_clicked) = egui::Sides::new().shrink_left().show( + ui, + |ui| render_mapping_row_contents(ui, mapping, true), + |ui| { + ui.small_icon_button(&re_ui::icons::REMOVE, "Remove mapping") + .clicked() + }, + ); + changes.merge(row_changes); + if remove_clicked { + remove_idx = Some(i); + } + ui.add_space(6.0); + }); + } + + if let Some(idx) = remove_idx { + mappings.remove(idx); + // Every array shrinks, so every array needs to be rewritten. + changes.merge(ChangeFlags { + values: true, + labels: true, + colors: true, + visible: true, + }); + } + + if ui + .small_icon_button(&re_ui::icons::ADD, "Add state mapping") + .clicked() + { + mappings.push(StateMapping { + value: String::new(), + label: String::new(), + color: default_color(""), + visible: true, + }); + // New row: values (and labels, as an aligned empty) need to grow. Colors/visible fall + // back to their defaults at render time until the user explicitly sets them. + changes.values = true; + changes.labels = true; + } + + (changes, mappings) +} + +/// Render the simplified two-row editor for boolean lanes. +fn render_bool_mapping_rows( + ui: &mut egui::Ui, + entity_path: &re_log_types::EntityPath, + values: &[String], + labels: &[String], + colors: &[Rgba32], + visible: &[bool], +) -> (ChangeFlags, Vec) { + let mut mappings: Vec = BOOL_VALUES + .iter() + .map(|v| { + let existing = values.iter().position(|stored| stored == *v); + let color = existing + .and_then(|i| colors.get(i).copied()) + .unwrap_or_else(|| default_color(v)); + StateMapping { + value: (*v).to_owned(), + label: existing + .and_then(|i| labels.get(i).cloned()) + .unwrap_or_default(), + color, + visible: existing + .and_then(|i| visible.get(i).copied()) + .unwrap_or(true), + } + }) + .collect(); + + // Force-write the canonical bool values list if the stored values don't already match. + let bool_values_need_seeding = !(values.len() == BOOL_VALUES.len() + && std::iter::zip(values, BOOL_VALUES).all(|(stored, canonical)| stored == canonical)); + + let mut changes = ChangeFlags::default(); + + for (i, mapping) in mappings.iter_mut().enumerate() { + let row_id = ui.make_persistent_id(("state_row", entity_path, i)); + ui.push_id(row_id, |ui| { + let (row_changes, ()) = egui::Sides::new().shrink_left().show( + ui, + |ui| render_mapping_row_contents(ui, mapping, false), + |_ui| {}, + ); + changes.merge(row_changes); + ui.add_space(6.0); + }); + } + + if bool_values_need_seeding && (changes.labels || changes.colors || changes.visible) { + changes.values = true; + } + + (changes, mappings) +} + +fn extract_texts( + query_result: &re_view::BlueprintResolvedLatestAtResults<'_>, + descr: &ComponentDescriptor, +) -> Vec { + let Some(raw) = query_result.get_raw_cell(descr.component) else { + return Vec::new(); + }; + Text::from_arrow(&raw) + .map(|texts| texts.iter().map(|t| t.to_string()).collect()) + .unwrap_or_default() +} + +fn extract_colors( + query_result: &re_view::BlueprintResolvedLatestAtResults<'_>, + descr: &ComponentDescriptor, +) -> Vec { + let Some(raw) = query_result.get_raw_cell(descr.component) else { + return Vec::new(); + }; + Color::from_arrow(&raw) + .map(|colors| colors.iter().map(|c| c.0).collect()) + .unwrap_or_default() +} + +fn extract_bools( + query_result: &re_view::BlueprintResolvedLatestAtResults<'_>, + descr: &ComponentDescriptor, +) -> Vec { + let Some(raw) = query_result.get_raw_cell(descr.component) else { + return Vec::new(); + }; + Visible::from_arrow(&raw) + .map(|rows| rows.iter().map(|v| v.0.0).collect()) + .unwrap_or_default() +} + +/// Stable default color for a value, matching the renderer's fallback. +/// +/// Using a hash of the value keeps the color fixed as the user adds or +/// reorders rows in the editor. +#[expect(clippy::disallowed_methods)] // Data-driven visualization color, not a UI theme color. +fn default_color(value: &str) -> Rgba32 { + const PALETTE: &[egui::Color32] = &[ + egui::Color32::from_rgb(76, 175, 80), + egui::Color32::from_rgb(255, 183, 77), + egui::Color32::from_rgb(66, 165, 245), + egui::Color32::from_rgb(239, 83, 80), + egui::Color32::from_rgb(171, 71, 188), + egui::Color32::from_rgb(38, 198, 218), + egui::Color32::from_rgb(255, 241, 118), + egui::Color32::from_rgb(141, 110, 99), + ]; + let hash = re_log_types::hash::Hash64::hash(value).hash64(); + Rgba32::from(PALETTE[(hash as usize) % PALETTE.len()]) +} diff --git a/crates/viewer/re_view_state_timeline/tests/basic.rs b/crates/viewer/re_view_state_timeline/tests/basic.rs new file mode 100644 index 000000000000..904d59dcf686 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/basic.rs @@ -0,0 +1,1060 @@ +use re_chunk_store::RowId; +use re_log_types::{TimePoint, Timeline}; +use re_test_context::TestContext; +use re_test_context::external::egui_kittest::SnapshotResults; +use re_test_viewport::TestContextExt as _; +use re_view_state_timeline::StateTimelineView; +use re_viewer_context::{GLOBAL_VIEW_ID, TimeControlCommand, ViewClass as _, ViewId}; +use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; + +fn setup_blueprint(test_context: &mut TestContext) -> ViewId { + test_context.setup_viewport_blueprint(|_ctx, blueprint| { + blueprint.add_view_at_root(ViewBlueprint::new_with_root_wildcard( + StateTimelineView::identifier(), + )) + }) +} + +/// Log a `StateChange` whose state array can contain nulls (per-instance resets). +fn log_multi_state( + test_context: &mut TestContext, + entity: &str, + timeline: Timeline, + tick: i64, + states: &[Option<&str>], +) { + let state_change = + re_sdk_types::archetypes::StateChange::new().with_state_opt(states.iter().copied()); + test_context.log_entity(entity, |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::from([(timeline, tick)]), + &state_change, + ) + }); +} + +/// A `StateChange` row can carry multiple instances (e.g. the buttons of a joystick). Each +/// instance gets its own lane, grouped under a single entity label. Every row is a full +/// assignment of the state array: a null instance resets its lane (gap), and so does being +/// omitted from a shorter row. +#[test] +fn test_state_timeline_multi_instance() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + let entity = "state/buttons"; + + // (tick, states per instance); `None` = reset (gap for that instance). The shorter row + // at tick 10 resets the omitted instance 2 the same way. + let multi_data: Vec<(i64, Vec>)> = vec![ + (0, vec![Some("Idle"), Some("Idle"), Some("Idle")]), + (10, vec![Some("Idle"), Some("Pressed")]), + (20, vec![Some("Pressed"), None, Some("Pressed")]), + (30, vec![Some("Idle"), Some("Idle"), Some("Idle")]), + ]; + for (tick, states) in &multi_data { + log_multi_state(&mut test_context, entity, timeline, *tick, states); + } + + // A single-instance lane next to the group, to contrast the two layouts. + for (tick, state) in [(0, "On"), (25, "Off")] { + test_context.log_entity("state/power", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::from([(timeline, tick)]), + &re_sdk_types::archetypes::StateChange::single(state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_timeline_multi_instance", + egui::vec2(500.0, 250.0), + None, + ) + .unwrap(); +} + +/// Bootstrapping a window whose left edge falls past a row that is narrower than the group: +/// every row is a full assignment, so the narrow row resets the instances it omits. The +/// single latest row before the window is always a sufficient bootstrap. +#[test] +fn test_state_timeline_multi_instance_bootstrap() { + use re_sdk_types::blueprint; + + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::new_sequence("tick"); + let entity = "state/buttons"; + + // The narrow row at tick 10 sets instance 0 and resets instances 1 and 2. With the + // window starting at tick 12, lane 0 must carry "A2" at the left edge while lanes 1 + // and 2 stay empty until the full row at tick 20. + let multi_data: Vec<(i64, Vec>)> = vec![ + (0, vec![Some("A"), Some("B"), Some("C")]), + (10, vec![Some("A2")]), + (20, vec![Some("A3"), Some("B3"), Some("C3")]), + ]; + for (tick, states) in &multi_data { + log_multi_state(&mut test_context, entity, timeline, *tick, states); + } + + test_context.set_active_timeline(*timeline.name()); + + // Pin the visible window to [12, 30] via the global time axis link — deterministic, + // unlike emulating pan/zoom scroll events. + let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint_ctx| { + let view = ViewBlueprint::new_with_root_wildcard(StateTimelineView::identifier()); + let time_axis = + ViewProperty::from_archetype_for_view::(ctx, view.id); + time_axis.save_blueprint_component( + ctx, + &blueprint::archetypes::TimeAxis::descriptor_link(), + &blueprint::components::LinkAxis::LinkToGlobal, + ); + let global_time_axis = ViewProperty::from_archetype_for_view::< + blueprint::archetypes::TimeAxis, + >(ctx, GLOBAL_VIEW_ID); + global_time_axis.save_blueprint_component( + ctx, + &blueprint::archetypes::TimeAxis::descriptor_view_range(), + &blueprint::components::TimeRange(re_sdk_types::datatypes::TimeRange { + start: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( + re_sdk_types::datatypes::TimeInt(12), + ), + end: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( + re_sdk_types::datatypes::TimeInt(30), + ), + }), + ); + blueprint_ctx.add_view_at_root(view) + }); + + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "state_timeline_multi_instance_bootstrap", + egui::vec2(500.0, 150.0), + None, + )); +} + +/// The lane count is derived from the whole timeline, not the visible window: with the +/// window panned entirely past a narrowing row and no wider row after it, the omitted +/// instances must keep their (empty) lanes instead of disappearing — otherwise the group's +/// height changes while panning. +#[test] +fn test_state_timeline_multi_instance_stable_lane_count() { + use re_sdk_types::blueprint; + + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::new_sequence("tick"); + let entity = "state/buttons"; + + // The row at tick 10 narrows the group to one instance, and no later row widens it + // again. With the window at [12, 30], only the narrow row is bootstrapped — the + // three-lane layout must come from the full-timeline width probe. + let multi_data: Vec<(i64, Vec>)> = vec![ + (0, vec![Some("A"), Some("B"), Some("C")]), + (10, vec![Some("A2")]), + ]; + for (tick, states) in &multi_data { + log_multi_state(&mut test_context, entity, timeline, *tick, states); + } + + // A second entity extends the timeline to tick 30, so the buttons' open-ended last + // phase stretches across the window. Its lane's vertical position also pins the + // buttons group's height in the snapshot. + for (tick, state) in [(0, "On"), (25, "Off")] { + test_context.log_entity("state/power", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::from([(timeline, tick)]), + &re_sdk_types::archetypes::StateChange::single(state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + // Pin the visible window to [12, 30] via the global time axis link — deterministic, + // unlike emulating pan/zoom scroll events. + let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint_ctx| { + let view = ViewBlueprint::new_with_root_wildcard(StateTimelineView::identifier()); + let time_axis = + ViewProperty::from_archetype_for_view::(ctx, view.id); + time_axis.save_blueprint_component( + ctx, + &blueprint::archetypes::TimeAxis::descriptor_link(), + &blueprint::components::LinkAxis::LinkToGlobal, + ); + let global_time_axis = ViewProperty::from_archetype_for_view::< + blueprint::archetypes::TimeAxis, + >(ctx, GLOBAL_VIEW_ID); + global_time_axis.save_blueprint_component( + ctx, + &blueprint::archetypes::TimeAxis::descriptor_view_range(), + &blueprint::components::TimeRange(re_sdk_types::datatypes::TimeRange { + start: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( + re_sdk_types::datatypes::TimeInt(12), + ), + end: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( + re_sdk_types::datatypes::TimeInt(30), + ), + }), + ); + blueprint_ctx.add_view_at_root(view) + }); + + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "state_timeline_multi_instance_stable_lane_count", + egui::vec2(500.0, 150.0), + None, + )); +} + +#[test] +fn test_state_timeline_basic() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + // Log state transitions for multiple entities using StateChange. + let state_data: Vec<(i64, &str, &str)> = vec![ + // (tick, entity, state_label) + (0, "state/robot_mode", "Idle"), + (10, "state/robot_mode", "Moving"), + (25, "state/robot_mode", "Working"), + (40, "state/robot_mode", "Idle"), + (0, "state/power", "On"), + (20, "state/power", "Low"), + (35, "state/power", "Critical"), + (45, "state/power", "On"), + (0, "state/connection", "Connected"), + (15, "state/connection", "Disconnected"), + (30, "state/connection", "Connected"), + ]; + + for (tick, entity, state) in &state_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity(*entity, |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + // Set time cursor to tick 20 (mid-range). + let store_id = test_context.active_store_id(); + test_context.send_time_commands( + store_id, + [TimeControlCommand::SetTime( + re_log_types::TimeInt::new_temporal(20).into(), + )], + ); + test_context.handle_system_commands(&egui::Context::default()); + + let view_id = setup_blueprint(&mut test_context); + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_timeline_basic", + egui::vec2(500.0, 250.0), + None, + ) + .unwrap(); +} + +#[test] +fn test_state_timeline_time_cursor() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + let state_data: Vec<(i64, &str, &str)> = vec![ + (0, "state/mode", "Idle"), + (20, "state/mode", "Active"), + (40, "state/mode", "Idle"), + ]; + + for (tick, entity, state) in &state_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity(*entity, |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + // Set time cursor to tick 30. + let store_id = test_context.active_store_id(); + test_context.send_time_commands( + store_id, + [TimeControlCommand::SetTime( + re_log_types::TimeInt::new_temporal(30).into(), + )], + ); + test_context.handle_system_commands(&egui::Context::default()); + + let view_id = setup_blueprint(&mut test_context); + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_timeline_time_cursor", + egui::vec2(400.0, 120.0), + None, + ) + .unwrap(); +} + +/// A null state is a reset: it must end the preceding phase and leave a gap until the +/// next non-null state. +#[test] +fn test_state_timeline_null_is_reset() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + // Log a state, then a null in the middle, then another state. + // The null should end the first phase, leaving a gap until the next state. + let timepoint_0 = TimePoint::from([(timeline, 0)]); + test_context.log_entity("state/mode", |builder| { + builder.with_archetype( + RowId::new(), + timepoint_0, + &re_sdk_types::archetypes::StateChange::single("Idle"), + ) + }); + + let timepoint_20 = TimePoint::from([(timeline, 20)]); + let null_state = re_sdk_types::archetypes::StateChange::new().with_state_opt([None::<&str>]); + test_context.log_entity("state/mode", |builder| { + builder.with_archetype(RowId::new(), timepoint_20, &null_state) + }); + + let timepoint_40 = TimePoint::from([(timeline, 40)]); + test_context.log_entity("state/mode", |builder| { + builder.with_archetype( + RowId::new(), + timepoint_40, + &re_sdk_types::archetypes::StateChange::single("Active"), + ) + }); + + test_context.set_active_timeline(*timeline.name()); + + // Place the cursor in the null region to confirm the gap left by the reset. + let store_id = test_context.active_store_id(); + test_context.send_time_commands( + store_id, + [TimeControlCommand::SetTime( + re_log_types::TimeInt::new_temporal(30).into(), + )], + ); + test_context.handle_system_commands(&egui::Context::default()); + + let view_id = setup_blueprint(&mut test_context); + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_timeline_null_is_reset", + egui::vec2(400.0, 120.0), + None, + ) + .unwrap(); +} + +/// An explicit empty-string `StateChange` should end the current state and leave the +/// lane empty until the next non-empty state is logged. A `Clear` archetype should +/// do the same. +#[test] +fn test_state_timeline_empty_and_clear() { + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + // Lane "/empty" — three states with empty-string resets in between. + let empty_data: Vec<(i64, &str)> = vec![(0, "Open"), (10, "Closed"), (20, ""), (30, "Open")]; + for (tick, state) in &empty_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity("empty", |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + // Lane "/cleared" — state, then a `Clear` to wipe it, then another state. + test_context.log_entity("cleared", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::from([(timeline, 0)]), + &re_sdk_types::archetypes::StateChange::single("Running"), + ) + }); + test_context.log_entity("cleared", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::from([(timeline, 15)]), + &re_sdk_types::archetypes::Clear::new(false), + ) + }); + test_context.log_entity("cleared", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::from([(timeline, 30)]), + &re_sdk_types::archetypes::StateChange::single("Running"), + ) + }); + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "state_timeline_empty_and_clear", + egui::vec2(500.0, 150.0), + None, + )); +} + +/// A recursive `Clear` logged on a parent path should end the state on all descendant +/// lanes, while a non-recursive `Clear` on the parent must not affect them. +#[test] +fn test_state_timeline_recursive_clear() { + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + for (tick, entity, state) in &[ + (0i64, "robots/r1", "Idle"), + (0, "robots/r2", "Idle"), + (40, "robots/r1", "Resuming"), + (40, "robots/r2", "Resuming"), + ] { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity(*entity, |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + // Recursive clear at the parent `/robots` should drop both descendant states. + test_context.log_entity("robots", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::from([(timeline, 20)]), + &re_sdk_types::archetypes::Clear::new(true), + ) + }); + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "state_timeline_recursive_clear", + egui::vec2(500.0, 150.0), + None, + )); +} + +/// Log data on both a sequence and a timestamp timeline, switch between them, +/// and verify the time axis labels update to match the active timeline. +#[test] +fn test_state_timeline_timeline_switch() { + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let seq_timeline = Timeline::log_tick(); + // Base timestamp: 2025-04-01 12:00:00 UTC (in nanoseconds since epoch) + let base_ns: i64 = 1_743_508_800_000_000_000; + let step_ns: i64 = 5_000_000_000; // 5 seconds + let ts_timeline = Timeline::new_timestamp("timestamp"); + + let state_data: Vec<(i64, &str, &str)> = vec![ + (0, "state/robot_mode", "Idle"), + (10, "state/robot_mode", "Moving"), + (25, "state/robot_mode", "Working"), + (40, "state/robot_mode", "Idle"), + (0, "state/power", "On"), + (20, "state/power", "Low"), + (35, "state/power", "Critical"), + (45, "state/power", "On"), + ]; + + for (tick, entity, state) in &state_data { + let timepoint = TimePoint::from([ + (seq_timeline, *tick), + (ts_timeline, base_ns + *tick * step_ns), + ]); + test_context.log_entity(*entity, |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + let view_id = setup_blueprint(&mut test_context); + let egui_ctx = egui::Context::default(); + + // Snapshot with the sequence timeline active. + test_context.set_active_timeline(*seq_timeline.name()); + let store_id = test_context.active_store_id(); + test_context.send_time_commands( + store_id.clone(), + [TimeControlCommand::SetTime( + re_log_types::TimeInt::new_temporal(20).into(), + )], + ); + test_context.handle_system_commands(&egui_ctx); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "state_timeline_timeline_switch_sequence", + egui::vec2(500.0, 200.0), + None, + )); + + // Switch to the timestamp timeline and snapshot again. + test_context.set_active_timeline(*ts_timeline.name()); + test_context.send_time_commands( + store_id, + [TimeControlCommand::SetTime( + re_log_types::TimeInt::new_temporal(base_ns + 20 * step_ns).into(), + )], + ); + test_context.handle_system_commands(&egui_ctx); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "state_timeline_timeline_switch_timestamp", + egui::vec2(500.0, 200.0), + None, + )); +} + +/// `StateConfiguration` overrides the label, color, and visibility per raw state value. +/// +/// This test logs three raw values, then logs a `StateConfiguration` that renames two of them, +/// recolors one, and hides another. The snapshot verifies the overrides apply end-to-end. +#[test] +fn test_state_configuration() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + let state_data: Vec<(i64, &str)> = + vec![(0, "Idle"), (10, "Moving"), (25, "Hidden"), (40, "Idle")]; + for (tick, state) in &state_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity("state/robot_mode", |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + // Configure labels/colors/visibility. `Hidden` is marked not visible and + // should not be drawn; `Moving` is relabeled and recolored. + test_context.log_entity("state/robot_mode", |builder| { + builder.with_archetype( + RowId::new(), + TimePoint::STATIC, + &re_sdk_types::archetypes::StateConfiguration::new() + .with_values(["Idle", "Moving", "Hidden"]) + .with_labels(["At rest", "In motion", "Hidden"]) + .with_colors([0x4CAF50FFu32, 0x42A5F5FFu32, 0xAB47BCFFu32]) + .with_visible([true, true, false]), + ) + }); + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_configuration", + egui::vec2(500.0, 120.0), + None, + ) + .unwrap(); +} + +/// When phases are too narrow to render individually, consecutive narrow phases +/// should be merged into a flat gray region. Wide phases on a separate lane +/// remain rendered with their own colors. +#[test] +fn test_state_timeline_merge_small_phases() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + // Lane 1: many tightly-packed phases that should collapse into a merged region. + let dense_values = ["A", "B", "C"]; + for tick in 0..200i64 { + let timepoint = TimePoint::from([(timeline, tick)]); + let state = dense_values[(tick as usize) % dense_values.len()]; + test_context.log_entity("state/dense", |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(state), + ) + }); + } + + // Lane 2: a few wide phases that should render normally. + let sparse_data: Vec<(i64, &str)> = vec![(0, "Idle"), (60, "Active"), (130, "Idle")]; + for (tick, state) in &sparse_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity("state/sparse", |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_timeline_merge_small_phases", + egui::vec2(400.0, 150.0), + None, + ) + .unwrap(); +} + +/// Cmd+scroll over the state timeline view should zoom in around the pointer. +#[test] +fn test_state_timeline_zoom() { + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::new_sequence("tick"); + + let state_data: Vec<(i64, &str, &str)> = vec![ + (0, "state/robot_mode", "Idle"), + (10, "state/robot_mode", "Moving"), + (25, "state/robot_mode", "Working"), + (40, "state/robot_mode", "Idle"), + (0, "state/power", "On"), + (20, "state/power", "Low"), + (35, "state/power", "Critical"), + (45, "state/power", "On"), + (0, "state/connection", "Connected"), + (15, "state/connection", "Disconnected"), + (30, "state/connection", "Connected"), + ]; + + for (tick, entity, state) in &state_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity(*entity, |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + + let size = egui::vec2(800.0, 400.0); + let mut harness = test_context + .setup_kittest_for_rendering_3d(size) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + // Let the view auto-fit and settle. + harness.run(); + snapshot_results.add(harness.try_snapshot("state_timeline_zoom_before")); + + // Cmd+scroll over the center of the view to zoom in. `handle_pan_zoom` + // only zooms when the pointer is hovering over the view, so we hover first. + let center = egui::pos2(size.x * 0.5, size.y * 0.5); + harness.hover_at(center); + for _ in 0..5 { + harness.event(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Line, + delta: egui::vec2(0.0, 1.0), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::COMMAND, + }); + harness.run(); + } + + snapshot_results.add(harness.try_snapshot("state_timeline_zoom_after")); +} + +/// Regression test for RR-4294: after panning so that every logged state change lies to the +/// *left* of the visible window, the lane must still render — showing the state active at the +/// window start (the last change before the range) — rather than disappearing entirely. +#[test] +fn test_state_timeline_pan_past_data() { + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::new_sequence("tick"); + + // All state changes happen early (ticks 0..=40); we then pan the window far past them. + // "Idle" at tick 40 is the last state, so it must fill the whole panned-past window. + let state_data: Vec<(i64, &str, &str)> = vec![ + (0, "state/robot_mode", "Idle"), + (10, "state/robot_mode", "Moving"), + (25, "state/robot_mode", "Working"), + (40, "state/robot_mode", "Idle"), + ]; + for (tick, entity, state) in &state_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity(*entity, |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + + let size = egui::vec2(800.0, 150.0); + let mut harness = test_context + .setup_kittest_for_rendering_3d(size) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + // Let the view auto-fit to the data. + harness.run(); + + // Pan far to the right so the whole [0, 40] data range scrolls off the left edge. + // The view pans on horizontal scroll while the pointer hovers over it. + // Smooth scrolling keeps the ui repainting, so step a fixed number of frames rather than + // `run()` (which bails out once it sees continuous repaints). + let center = egui::pos2(size.x * 0.5, size.y * 0.5); + harness.hover_at(center); + for _ in 0..8 { + harness.event(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(-2000.0, 0.0), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::NONE, + }); + harness.step(); + } + + snapshot_results.add(harness.try_snapshot("state_timeline_pan_past_data")); +} + +/// Panning the view entirely *before* the first state change must keep the lane visible (as an +/// empty row) rather than letting it vanish. Even with no data in the window — and nothing before +/// it to bootstrap — the visualizer probes the entity's state type so the lane keeps its identity. +#[test] +fn test_state_timeline_pan_before_data() { + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::new_sequence("tick"); + + // All state changes happen at ticks 0..=40; we then pan the window far to the left of them. + let state_data: Vec<(i64, &str, &str)> = vec![ + (0, "state/robot_mode", "Idle"), + (10, "state/robot_mode", "Moving"), + (25, "state/robot_mode", "Working"), + (40, "state/robot_mode", "Idle"), + ]; + for (tick, entity, state) in &state_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity(*entity, |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + + let size = egui::vec2(800.0, 150.0); + let mut harness = test_context + .setup_kittest_for_rendering_3d(size) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + // Let the view auto-fit to the data. + harness.run(); + + // Pan far to the left so the whole [0, 40] data range scrolls off the right edge, leaving the + // window entirely before the data. Positive horizontal scroll moves the window left. + let center = egui::pos2(size.x * 0.5, size.y * 0.5); + harness.hover_at(center); + for _ in 0..8 { + harness.event(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(2000.0, 0.0), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::NONE, + }); + harness.step(); + } + + snapshot_results.add(harness.try_snapshot("state_timeline_pan_before_data")); +} + +/// Exercises every awkward shape the state slot can take in one lane: +/// +/// ```text +/// tick | StateChange:state | scalars +/// -----+-------------------+------------- +/// 1 | ["hi!"] | (not logged) +/// 2 | (not logged) | [1] // no state update at this tick +/// 3 | [] | [2] // empty list (clear_fields) — reset → gap +/// 5 | [""] | [1, 2, 3] // explicit empty → gap (collapses into the open one) +/// 6 | ["bye!"] | (not logged) +/// 7 | [null] | [4] // null *inside* the list — reset → gap +/// 12 | ["end"] | (not logged) // trailing state so the gaps are clearly visible +/// ``` +/// +/// Expected lane: `hi!` (1..3), gap (3..6), `bye!` (6..7), gap (7..12), `end` (12..). The +/// degenerate input at tick 2 must not break the lane. +#[test] +fn test_state_timeline_edge_cases() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + let entity = "state/edge_cases"; + + // tick 1: state only. + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 1)]), + &re_sdk_types::archetypes::StateChange::single("hi!"), + ) + }); + + // tick 2: scalars only — no state update. + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 2)]), + &re_sdk_types::archetypes::Scalars::single(1.0), + ) + }); + + // tick 3: `clear_fields` serializes state as an empty list — a reset; scalars co-logged. + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 3)]), + &re_sdk_types::archetypes::StateChange::clear_fields(), + ) + }); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 3)]), + &re_sdk_types::archetypes::Scalars::single(2.0), + ) + }); + + // tick 5: explicit empty string in the state slot — should produce a gap. + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 5)]), + &re_sdk_types::archetypes::StateChange::single(""), + ) + }); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 5)]), + &re_sdk_types::archetypes::Scalars::new([1.0, 2.0, 3.0]), + ) + }); + + // tick 6: state only. + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 6)]), + &re_sdk_types::archetypes::StateChange::single("bye!"), + ) + }); + + // tick 7: single null `Text` inside the list — a reset, opening a gap until tick 12. + let null_state = re_sdk_types::archetypes::StateChange::new().with_state_opt([None::<&str>]); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row(TimePoint::from([(timeline, 7)]), &null_state) + }); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 7)]), + &re_sdk_types::archetypes::Scalars::single(4.0), + ) + }); + + // Log a trailing state well past the interesting ticks so auto-fit leaves room to the + // right — making the gap after the empty-string reset clearly visible. + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + TimePoint::from([(timeline, 12)]), + &re_sdk_types::archetypes::StateChange::single("end"), + ) + }); + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_timeline_edge_cases", + egui::vec2(700.0, 150.0), + None, + ) + .unwrap(); +} + +/// When the time axis is linked to global, the state timeline view must drive its pan/zoom +/// window from the shared global blueprint view range — and write pan/zoom back to it, +/// so it stays in sync with other plots (e.g. time series views) linked to the same range. +#[test] +fn test_state_timeline_link_to_global() { + use re_sdk_types::blueprint; + + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::new_sequence("tick"); + + let state_data: Vec<(i64, &str, &str)> = vec![ + (0, "state/robot_mode", "Idle"), + (10, "state/robot_mode", "Moving"), + (25, "state/robot_mode", "Working"), + (40, "state/robot_mode", "Idle"), + ]; + for (tick, entity, state) in &state_data { + let timepoint = TimePoint::from([(timeline, *tick)]); + test_context.log_entity(*entity, |builder| { + builder.with_archetype( + RowId::new(), + timepoint, + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + // Create the view and link its time axis to global. + let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint_ctx| { + let view = ViewBlueprint::new_with_root_wildcard(StateTimelineView::identifier()); + let time_axis = + ViewProperty::from_archetype_for_view::(ctx, view.id); + time_axis.save_blueprint_component( + ctx, + &blueprint::archetypes::TimeAxis::descriptor_link(), + &blueprint::components::LinkAxis::LinkToGlobal, + ); + blueprint_ctx.add_view_at_root(view) + }); + + let read_global_range = |test_context: &TestContext| { + test_context.with_blueprint_ctx(|ctx, _store_hub| { + ViewProperty::from_archetype_for_view::( + &ctx, + GLOBAL_VIEW_ID, + ) + .component_or_empty::( + blueprint::archetypes::TimeAxis::descriptor_view_range().component, + ) + .expect("failed to read global time range") + }) + }; + + // The global range starts out unset (the view falls back to the full timeline range). + assert!( + read_global_range(&test_context).is_none(), + "global view range should be unset before any interaction" + ); + + let size = egui::vec2(800.0, 150.0); + let mut harness = test_context + .setup_kittest_for_rendering_3d(size) + .build_ui(|ui| { + test_context.run_with_single_view(ui, view_id); + }); + + // Let the linked view settle (reads the global range, falling back to the full range). + harness.run(); + + // Pan the view; in linked mode this must persist the new window to the global range. + // Smooth scrolling keeps the ui repainting, so step a fixed number of frames rather than + // `run()` (which bails out once it sees continuous repaints). + let center = egui::pos2(size.x * 0.5, size.y * 0.5); + harness.hover_at(center); + for _ in 0..4 { + harness.event(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(-200.0, 0.0), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::NONE, + }); + harness.step(); + } + // Let the scroll momentum decay so the view (and the written range) settles. + for _ in 0..10 { + harness.step(); + } + + // The pan should have written an explicit, finite range to the global view. + let global_range = read_global_range(&test_context) + .expect("panning a linked view must write the global view range"); + assert!( + matches!( + global_range.start, + re_sdk_types::datatypes::TimeRangeBoundary::Absolute(_) + ) && matches!( + global_range.end, + re_sdk_types::datatypes::TimeRangeBoundary::Absolute(_) + ), + "linked pan should write an absolute range, got {global_range:?}" + ); +} diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/help_view_state_timeline_view_mac.png b/crates/viewer/re_view_state_timeline/tests/snapshots/help_view_state_timeline_view_mac.png new file mode 100644 index 000000000000..b7a35a6df4bb --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/help_view_state_timeline_view_mac.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55d2ca355fd7148c0981ee4b957c572a6de52eedc74507e6d35a11a17c881ec6 +size 20167 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/help_view_state_timeline_view_windows.png b/crates/viewer/re_view_state_timeline/tests/snapshots/help_view_state_timeline_view_windows.png new file mode 100644 index 000000000000..540573ad3161 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/help_view_state_timeline_view_windows.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a23860639784615552099c21d74ae35acf46426ab9a37b31d668fea2e7bafe6 +size 20746 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_bool.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_bool.png new file mode 100644 index 000000000000..fea2e3e45dc3 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_bool.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f1658fa2edf12b2227661d9f1100b4ceac96052ab8d03c2a1b831bb13d6760c +size 7307 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_float64.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_float64.png new file mode 100644 index 000000000000..31f5bfb7af71 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_float64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c05ab7885a55a2f9b89a7fa9ab0b13c23d4e12a19a97f4e7476eb1b4491ea432 +size 6749 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_int32.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_int32.png new file mode 100644 index 000000000000..ea3c09530ff5 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_int32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abd1d664a6752bb7cd980beb062a7f121fe1a19600a23d9585a33126d0636c01 +size 5946 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_multi_different_types.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_multi_different_types.png new file mode 100644 index 000000000000..0e2d67e5ec56 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_multi_different_types.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6a392958e32d215126b634303ce0235526b7e9541a503e4fc87836750afd56a +size 19695 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_multi_same_type.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_multi_same_type.png new file mode 100644 index 000000000000..3f928ea394dd --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_multi_same_type.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60ed9fa4cf520f5bbaaf48b8d319fd541889258865381007e693e90381f02c49 +size 14543 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_string.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_string.png new file mode 100644 index 000000000000..6c4fa6291208 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_string.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4939945206fdc5c18c30b59a3e95104e95bc746d147fe24defcf98e6913a0e4f +size 7836 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_textlog.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_textlog.png new file mode 100644 index 000000000000..7de0b511fe45 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_cast_textlog.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fffc3345d417f4746f0f486262b21ea8c5b94975dfce4ff29575f68e9975c527 +size 7400 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_configuration.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_configuration.png new file mode 100644 index 000000000000..0a201bbb1403 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_configuration.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c297fd56813cc22635bb581766e60923a31c33ed424748b39066abdc3b0df468 +size 10116 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_basic.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_basic.png new file mode 100644 index 000000000000..55c0f8bc5c75 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_basic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdebffcbf3e09f938dbf787e7fbdbfee3d6da659e173a643975f4b12aa4621d9 +size 26584 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_edge_cases.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_edge_cases.png new file mode 100644 index 000000000000..648036ea727a --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_edge_cases.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9ef25f674145611e9a965750b313afe237f420e346dd40717cf3e414e07fe87 +size 10462 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_empty_and_clear.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_empty_and_clear.png new file mode 100644 index 000000000000..2f51a2bce02f --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_empty_and_clear.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:290a1be49b0c986a6bcbe01ebaacd11813fb116c8c050601b294762b1aa5c931 +size 11804 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_merge_small_phases.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_merge_small_phases.png new file mode 100644 index 000000000000..e7a0717d985e --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_merge_small_phases.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c0e126dbfe5d601ca95c6631df272123d477ab9e8fafc78be12d1f57d36842a +size 12391 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance.png new file mode 100644 index 000000000000..9e184a165247 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:826cd95e0281de4df87a257298357faf431ff8584ad90651cf7732e7952c34a0 +size 20541 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance_bootstrap.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance_bootstrap.png new file mode 100644 index 000000000000..5a1a0731baf1 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance_bootstrap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f433dc1ce93a3017858e4ef2f85bd7fbd35d9daaeee87845b1be994f2d52dcab +size 6209 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance_stable_lane_count.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance_stable_lane_count.png new file mode 100644 index 000000000000..961ef076d495 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_multi_instance_stable_lane_count.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3a895319727ceba609f265f79191c58dab980e27c103a74c0bbebcdf3c40187 +size 7978 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_null_is_reset.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_null_is_reset.png new file mode 100644 index 000000000000..e22b10ed348c --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_null_is_reset.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b799d74aaf62875a89b2bdab1c92154836a3f33631b1109a15ee42d2a1701d94 +size 6943 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_pan_before_data.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_pan_before_data.png new file mode 100644 index 000000000000..0deacdf12c1d --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_pan_before_data.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:221d5d7f9641dce9a40ac86c4b33a8b2bd0ec3259bd9d62824b4e97ebb258d21 +size 5542 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_pan_past_data.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_pan_past_data.png new file mode 100644 index 000000000000..0deacdf12c1d --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_pan_past_data.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:221d5d7f9641dce9a40ac86c4b33a8b2bd0ec3259bd9d62824b4e97ebb258d21 +size 5542 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_recursive_clear.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_recursive_clear.png new file mode 100644 index 000000000000..4134e1d2a3cd --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_recursive_clear.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:157555e4b2350b44a667324173b44e97fea84a7c0a24e37f15b5859bfcf69964 +size 10708 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_time_cursor.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_time_cursor.png new file mode 100644 index 000000000000..407a99bc728b --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_time_cursor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24a466613550c922c8f5387716d9c1c56bcaa9f35c54fd78688de5039c148265 +size 7812 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_timeline_switch_sequence.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_timeline_switch_sequence.png new file mode 100644 index 000000000000..41ba5663ffd0 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_timeline_switch_sequence.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a51b8afdb6bfd6da4af54d6fba8b7cec38c8257af452b1f47b16bc9dbbc73ca +size 17835 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_timeline_switch_timestamp.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_timeline_switch_timestamp.png new file mode 100644 index 000000000000..c586d164cdad --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_timeline_switch_timestamp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fbabc779c0fda47e0d498ba73933db99b6a8233824d36731b07fb81ceb1732d +size 18139 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_zoom_after.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_zoom_after.png new file mode 100644 index 000000000000..383c61c93c16 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_zoom_after.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d81b814421e8746543927d5c5f66bcd590e12525a33c13f3dcc371551ed3362b +size 22209 diff --git a/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_zoom_before.png b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_zoom_before.png new file mode 100644 index 000000000000..aee2dde7273a --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/snapshots/state_timeline_zoom_before.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9cbb4fac84c76b3a87d458505ead4b7b201742ba51a1c9733f1e20b85c97c87 +size 34485 diff --git a/crates/viewer/re_view_state_timeline/tests/state_casting.rs b/crates/viewer/re_view_state_timeline/tests/state_casting.rs new file mode 100644 index 000000000000..a49bf091b8b4 --- /dev/null +++ b/crates/viewer/re_view_state_timeline/tests/state_casting.rs @@ -0,0 +1,882 @@ +//! Tests for the polymorphic state cast: how different physical types arriving at +//! the `StateChange:state` slot are canonicalized into a [`StateValueKind`] and +//! formatted into phase labels. +//! +//! Coverage: +//! - Casting various physical types via `DynamicArchetype` (Int, Float, Bool, String). +//! - A `DynamicArchetype` with multiple state-like components — same type and mixed types. +//! - Using a real `TextLog` archetype as the source for the state slot. + +use std::sync::Arc; + +use re_log_types::external::arrow::array::{ + BooleanArray, Float64Array, Int32Array, LargeStringArray, StringArray, +}; +use re_log_types::{EntityPath, Timeline}; +use re_sdk_types::archetypes::TextLog; +use re_sdk_types::blueprint::datatypes::{ComponentSourceKind, VisualizerComponentMapping}; +use re_sdk_types::{ArchetypeName, ComponentIdentifier, DynamicArchetype, Visualizer}; +use re_test_context::TestContext; +use re_test_context::VisualizerBlueprintContext as _; +use re_test_viewport::TestContextExt as _; +use re_view_state_timeline::{ + StateLanesData, StateTimelineView, StateTimelineViewState, StateValueKind, StateVisualizer, +}; +use re_viewer_context::{IdentifiedViewSystem as _, ViewClass as _, ViewId}; +use re_viewport::execute_systems_for_view; +use re_viewport_blueprint::{ViewBlueprint, ViewportBlueprint}; + +const STATE_TARGET: &str = "StateChange:state"; + +/// Map a custom source component onto the `StateChange:state` slot of a `StateVisualizer`. +/// +/// `save_visualizers` bypasses the default auto-spawn heuristics, which only fire when the +/// entity is indicated for the `StateChange` archetype. Custom archetypes (`DynamicArchetype`, +/// `TextLog`) are not indicated, so the visualizer instruction has to be installed explicitly. +fn map_source_to_state(source_component: impl Into) -> Visualizer { + map_source_to_state_with_selector(source_component, None) +} + +/// Like [`map_source_to_state`] but with a jq-like selector into the source component, +/// e.g. `.u8` to pick a field nested in a struct. +fn map_source_to_state_with_selector( + source_component: impl Into, + selector: Option<&str>, +) -> Visualizer { + let source_component = source_component.into(); + Visualizer::new(StateVisualizer::identifier().as_str()).with_mappings([ + VisualizerComponentMapping { + target: STATE_TARGET.into(), + source_kind: ComponentSourceKind::SourceComponent, + source_component: Some(source_component.as_str().into()), + selector: selector.map(Into::into), + } + .into(), + ]) +} + +/// Lock in the [`Timeline::log_tick`] timeline as active and set up a viewport blueprint +/// with a single view that maps the given visualizers onto `entity`. +/// +/// Must be called *after* data has been logged: `set_active_timeline` reads the entity DB +/// when it runs, so the timeline only resolves to a concrete [`Timeline`] (rather than +/// staying [`re_viewer_context::ActiveTimeline::Pending`]) once some data exists on it. +/// After this, [`TestContext::active_timeline`] returns `Some(Timeline::log_tick())`. +fn build_view( + test_context: &mut TestContext, + entity: &str, + visualizers: impl IntoIterator, +) -> ViewId { + test_context.set_active_timeline(*Timeline::log_tick().name()); + + let visualizers: Vec<_> = visualizers.into_iter().collect(); + test_context.setup_viewport_blueprint(|ctx, blueprint| { + let view = ViewBlueprint::new_with_root_wildcard(StateTimelineView::identifier()); + ctx.save_visualizers(&EntityPath::from(entity), view.id, visualizers); + blueprint.add_view_at_root(view) + }) +} + +/// Run the state visualizer and collect every emitted [`StateLanesData`]. +/// +/// Reconstructs the per-frame execution that the viewport normally performs, then peeks at +/// the visualizer's typed output rather than rendering it. +fn run_visualizer(test_context: &TestContext, view_id: ViewId) -> Vec { + run_visualizer_impl(test_context, view_id, None) +} + +/// Like [`run_visualizer`] but with the visible window constrained to +/// `[min, min + time_spanned]`, as if the user had panned/zoomed there. +fn run_visualizer_with_window( + test_context: &TestContext, + view_id: ViewId, + min: f64, + time_spanned: f64, +) -> Vec { + run_visualizer_impl(test_context, view_id, Some((min, time_spanned))) +} + +fn run_visualizer_impl( + test_context: &TestContext, + view_id: ViewId, + window: Option<(f64, f64)>, +) -> Vec { + test_context.run_once_in_egui_central_panel(|ctx, _ui| { + let viewport_blueprint = + ViewportBlueprint::from_db(ctx.store_context.blueprint, &test_context.blueprint_query); + let view_blueprint = viewport_blueprint + .view(&view_id) + .expect("view should exist in blueprint"); + + let class_registry = ctx.view_class_registry(); + let view_class = class_registry.get_class_or_log_error(view_blueprint.class_identifier()); + let mut view_state = view_class.new_state(); + + if let Some((min, time_spanned)) = window { + view_state + .as_any_mut() + .downcast_mut::() + .expect("state timeline view state") + .time_views + .insert( + *Timeline::log_tick().name(), + re_viewer_context::TimeView { + min: min.into(), + time_spanned, + }, + ); + } + + let once_per_frame = class_registry.run_once_per_frame_context_systems( + ctx, + std::iter::once(view_blueprint.class_identifier()), + ); + + let (_view_query, system_output) = + execute_systems_for_view(ctx, view_blueprint, view_state.as_ref(), &once_per_frame); + + system_output + .iter_visualizer_data::() + .cloned() + .collect() + }) +} + +fn phase_labels(lanes_data: &StateLanesData, entity: &str) -> Vec { + let group = lanes_data + .groups + .iter() + .find(|g| g.entity_path == EntityPath::from(entity)) + .unwrap_or_else(|| panic!("no lane group for entity {entity}")); + assert_eq!( + group.lanes.len(), + 1, + "expected a single-instance lane group for entity {entity}" + ); + group.lanes[0] + .phases + .iter() + .map(|p| { + p.content + .as_ref() + .map_or_else(String::new, |s| s.label.clone()) + }) + .collect() +} + +/// Like [`phase_labels`] but keeps each phase's start time, so tests can assert *when* a +/// reset (gap, rendered as an empty label) begins. +fn timed_phase_labels(lanes_data: &StateLanesData, entity: &str) -> Vec<(i64, String)> { + let group = lanes_data + .groups + .iter() + .find(|g| g.entity_path == EntityPath::from(entity)) + .unwrap_or_else(|| panic!("no lane group for entity {entity}")); + assert_eq!( + group.lanes.len(), + 1, + "expected a single-instance lane group for entity {entity}" + ); + group.lanes[0] + .phases + .iter() + .map(|p| { + ( + p.start_time, + p.content + .as_ref() + .map_or_else(String::new, |s| s.label.clone()), + ) + }) + .collect() +} + +fn value_kind(lanes_data: &StateLanesData, entity: &str) -> StateValueKind { + let group = lanes_data + .groups + .iter() + .find(|g| g.entity_path == EntityPath::from(entity)) + .unwrap_or_else(|| panic!("no lane group for entity {entity}")); + group.value_kind +} + +/// Log a `DynamicArchetype` with one field at three ticks, then install an explicit visualizer +/// mapping from that field to `StateChange:state`. Returns the view id. +fn setup_single_field( + test_context: &mut TestContext, + entity: &str, + archetype_name: impl Into, + field_name: &str, + arrays: [F; 3], +) -> ViewId +where + F: Into, +{ + let archetype = archetype_name.into(); + let source_component = ComponentIdentifier::from_archetype_field(archetype, field_name); + + for (tick, array) in std::iter::zip(0..3i64, arrays) { + let dyn_archetype = DynamicArchetype::new(archetype).with_component_from_data( + ComponentIdentifier::try_new(field_name).expect("valid component"), + array.into(), + ); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row([(Timeline::log_tick(), tick)], &dyn_archetype) + }); + } + + build_view( + test_context, + entity, + [map_source_to_state(source_component)], + ) +} + +#[test] +fn test_cast_int32_via_dynamic_archetype() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/int", + "ints", + "value", + [ + Arc::new(Int32Array::from(vec![1])) as Arc<_>, + Arc::new(Int32Array::from(vec![2])) as Arc<_>, + Arc::new(Int32Array::from(vec![1])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1, "expected one StateLanesData output"); + + // Int32 collapses to Float64; integer-valued floats render without a trailing `.0`, + // and consecutive identical phases merge. + assert_eq!( + value_kind(&outputs[0], "/state/int"), + StateValueKind::Scalar + ); + assert_eq!(phase_labels(&outputs[0], "/state/int"), vec!["1", "2", "1"]); + + test_context + .run_view_ui_and_save_snapshot(view_id, "state_cast_int32", egui::vec2(400.0, 80.0), None) + .unwrap(); +} + +#[test] +fn test_cast_float64_via_dynamic_archetype() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/float", + "floats", + "value", + [ + Arc::new(Float64Array::from(vec![1.5])) as Arc<_>, + Arc::new(Float64Array::from(vec![2.0])) as Arc<_>, + Arc::new(Float64Array::from(vec![2.0])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + value_kind(&outputs[0], "/state/float"), + StateValueKind::Scalar + ); + // Non-integer floats keep their fractional part; integer-valued floats drop the `.0`. + // The trailing duplicate `2.0` merges with the previous phase. + assert_eq!(phase_labels(&outputs[0], "/state/float"), vec!["1.5", "2"]); + + test_context + .run_view_ui_and_save_snapshot(view_id, "state_cast_float64", egui::vec2(400.0, 80.0), None) + .unwrap(); +} + +#[test] +fn test_cast_bool_via_dynamic_archetype() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/bool", + "bools", + "value", + [ + Arc::new(BooleanArray::from(vec![false])) as Arc<_>, + Arc::new(BooleanArray::from(vec![true])) as Arc<_>, + Arc::new(BooleanArray::from(vec![false])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!(value_kind(&outputs[0], "/state/bool"), StateValueKind::Bool); + assert_eq!( + phase_labels(&outputs[0], "/state/bool"), + vec!["false", "true", "false"] + ); + + test_context + .run_view_ui_and_save_snapshot(view_id, "state_cast_bool", egui::vec2(400.0, 80.0), None) + .unwrap(); +} + +#[test] +fn test_cast_string_via_dynamic_archetype() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/string", + "strings", + "value", + [ + Arc::new(StringArray::from(vec!["idle"])) as Arc<_>, + Arc::new(StringArray::from(vec!["active"])) as Arc<_>, + Arc::new(StringArray::from(vec!["idle"])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + value_kind(&outputs[0], "/state/string"), + StateValueKind::String + ); + assert_eq!( + phase_labels(&outputs[0], "/state/string"), + vec!["idle", "active", "idle"] + ); + + test_context + .run_view_ui_and_save_snapshot(view_id, "state_cast_string", egui::vec2(400.0, 80.0), None) + .unwrap(); +} + +/// A null value resets a scalar lane, ending the current phase and leaving a gap +/// (rendered here as an empty label) until the next non-null value. +#[test] +fn test_null_resets_float_lane() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/float_null", + "floats", + "value", + [ + Arc::new(Float64Array::from(vec![Some(1.5)])) as Arc<_>, + Arc::new(Float64Array::from(vec![None])) as Arc<_>, + Arc::new(Float64Array::from(vec![Some(2.5)])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + timed_phase_labels(&outputs[0], "/state/float_null"), + vec![ + (0, "1.5".to_owned()), + (1, String::new()), + (2, "2.5".to_owned()) + ] + ); +} + +/// The Int32 → Float64 state cast must preserve nulls, so a null integer also +/// resets the lane. +#[test] +fn test_null_resets_int_lane_via_cast() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/int_null", + "ints", + "value", + [ + Arc::new(Int32Array::from(vec![Some(1)])) as Arc<_>, + Arc::new(Int32Array::from(vec![None])) as Arc<_>, + Arc::new(Int32Array::from(vec![Some(2)])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + timed_phase_labels(&outputs[0], "/state/int_null"), + vec![(0, "1".to_owned()), (1, String::new()), (2, "2".to_owned())] + ); +} + +/// A null value resets a bool lane. +#[test] +fn test_null_resets_bool_lane() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/bool_null", + "bools", + "value", + [ + Arc::new(BooleanArray::from(vec![Some(true)])) as Arc<_>, + Arc::new(BooleanArray::from(vec![None])) as Arc<_>, + Arc::new(BooleanArray::from(vec![Some(false)])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + timed_phase_labels(&outputs[0], "/state/bool_null"), + vec![ + (0, "true".to_owned()), + (1, String::new()), + (2, "false".to_owned()) + ] + ); +} + +/// A null value resets a string lane, just like an explicitly-empty string. +#[test] +fn test_null_resets_string_lane() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/string_null", + "strings", + "value", + [ + Arc::new(StringArray::from(vec![Some("idle")])) as Arc<_>, + Arc::new(StringArray::from(vec![None::<&str>])) as Arc<_>, + Arc::new(StringArray::from(vec![Some("active")])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + timed_phase_labels(&outputs[0], "/state/string_null"), + vec![ + (0, "idle".to_owned()), + (1, String::new()), + (2, "active".to_owned()) + ] + ); +} + +/// `LargeUtf8` source data behaves like `Utf8`: values render and nulls reset. +#[test] +fn test_null_resets_large_string_lane() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/large_string_null", + "large_strings", + "value", + [ + Arc::new(LargeStringArray::from(vec![Some("idle")])) as Arc<_>, + Arc::new(LargeStringArray::from(vec![None::<&str>])) as Arc<_>, + Arc::new(LargeStringArray::from(vec![Some("active")])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + value_kind(&outputs[0], "/state/large_string_null"), + StateValueKind::String + ); + assert_eq!( + timed_phase_labels(&outputs[0], "/state/large_string_null"), + vec![ + (0, "idle".to_owned()), + (1, String::new()), + (2, "active".to_owned()) + ] + ); +} + +/// An empty state batch (a row with zero values, e.g. from `clear_fields`) resets a +/// scalar lane, matching `Clear` and latest-at clear semantics. +#[test] +fn test_empty_batch_resets_float_lane() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/float_empty", + "floats", + "value", + [ + Arc::new(Float64Array::from(vec![1.5])) as Arc<_>, + Arc::new(Float64Array::from(Vec::::new())) as Arc<_>, + Arc::new(Float64Array::from(vec![2.5])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + timed_phase_labels(&outputs[0], "/state/float_empty"), + vec![ + (0, "1.5".to_owned()), + (1, String::new()), + (2, "2.5".to_owned()) + ] + ); +} + +/// An empty state batch resets a string lane. +#[test] +fn test_empty_batch_resets_string_lane() { + let mut test_context = TestContext::new_with_view_class::(); + let view_id = setup_single_field( + &mut test_context, + "/state/string_empty", + "strings", + "value", + [ + Arc::new(StringArray::from(vec!["idle"])) as Arc<_>, + Arc::new(StringArray::from(Vec::<&str>::new())) as Arc<_>, + Arc::new(StringArray::from(vec!["active"])) as Arc<_>, + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!( + timed_phase_labels(&outputs[0], "/state/string_empty"), + vec![ + (0, "idle".to_owned()), + (1, String::new()), + (2, "active".to_owned()) + ] + ); +} + +/// A null row before the visible window is a reset. With the window panned to +/// `[25, 35]` and data `Idle@0`, `[null]@20`, `Active@40`, the lane shows a gap at the +/// window's left edge: the single latest-at bootstrap row (the null) fully describes the +/// state there — no further look-back is needed. +#[test] +fn test_null_before_window_resets_lane() { + let mut test_context = TestContext::new_with_view_class::(); + let entity = "/state/null_before_window"; + + for (tick, array) in [ + (0i64, StringArray::from(vec![Some("Idle")])), + (20, StringArray::from(vec![None::<&str>])), + (40, StringArray::from(vec![Some("Active")])), + ] { + let archetype = DynamicArchetype::new("strings") + .with_component_from_data("value", Arc::new(array) as Arc<_>); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row([(Timeline::log_tick(), tick)], &archetype) + }); + } + + let view_id = build_view( + &mut test_context, + entity, + [map_source_to_state("strings:value")], + ); + + let outputs = run_visualizer_with_window(&test_context, view_id, 25.0, 10.0); + assert_eq!(outputs.len(), 1); + // The bootstrap yields the gap event from the null@20; being a leading gap it is + // dropped, leaving the lane empty until `Active`@40 (just past the window). + assert_eq!( + timed_phase_labels(&outputs[0], entity), + vec![(40, "Active".to_owned())] + ); +} + +/// Same-timestamp sibling rows — the later row id wins, both in-window and at the +/// window-edge bootstrap: `Idle`@20 then `[null]`@20 means the state at t=20 is reset, so a +/// window starting after 20 shows a gap until the next state. +#[test] +fn test_null_wins_over_same_time_sibling_at_bootstrap() { + let mut test_context = TestContext::new_with_view_class::(); + let entity = "/state/null_same_time"; + + for (tick, array) in [ + (20i64, StringArray::from(vec![Some("Idle")])), + (20, StringArray::from(vec![None::<&str>])), + (40, StringArray::from(vec![Some("Active")])), + ] { + let archetype = DynamicArchetype::new("strings") + .with_component_from_data("value", Arc::new(array) as Arc<_>); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row([(Timeline::log_tick(), tick)], &archetype) + }); + } + + let view_id = build_view( + &mut test_context, + entity, + [map_source_to_state("strings:value")], + ); + + let outputs = run_visualizer_with_window(&test_context, view_id, 25.0, 10.0); + assert_eq!(outputs.len(), 1); + assert_eq!( + timed_phase_labels(&outputs[0], entity), + vec![(40, "Active".to_owned())] + ); +} + +/// A `DynamicArchetype` carrying two fields of the same physical type. Mapping each as a +/// separate state source yields two lanes on the same entity. +#[test] +fn test_dynamic_archetype_multiple_same_type() { + let mut test_context = TestContext::new_with_view_class::(); + let entity = "/state/multi_same"; + + for (tick, (a, b)) in + std::iter::zip(0..3i64, [("Idle", "Off"), ("Active", "On"), ("Idle", "On")]) + { + let archetype = DynamicArchetype::new("multi_str") + .with_component_from_data("mode", Arc::new(StringArray::from(vec![a]))) + .with_component_from_data("power", Arc::new(StringArray::from(vec![b]))); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row([(Timeline::log_tick(), tick)], &archetype) + }); + } + + let view_id = build_view( + &mut test_context, + entity, + [ + map_source_to_state("multi_str:mode"), + map_source_to_state("multi_str:power"), + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + + // One lane group per visualizer instruction; both groups share the same entity path. + let groups_on_entity: Vec<_> = outputs[0] + .groups + .iter() + .filter(|g| g.entity_path == EntityPath::from(entity)) + .collect(); + assert_eq!(groups_on_entity.len(), 2); + + for group in &groups_on_entity { + assert_eq!(group.value_kind, StateValueKind::String); + assert_eq!(group.lanes.len(), 1); + } + + // The group label disambiguates which source field is feeding this group. + let mode_group = groups_on_entity + .iter() + .find(|g| g.label.contains("multi_str:mode")) + .expect("expected a lane group sourced from multi_str:mode"); + let power_group = groups_on_entity + .iter() + .find(|g| g.label.contains("multi_str:power")) + .expect("expected a lane group sourced from multi_str:power"); + + let phase_label = |p: &re_view_state_timeline::StateLanePhase| { + p.content + .as_ref() + .map_or_else(String::new, |s| s.label.clone()) + }; + let mode_labels: Vec<_> = mode_group.lanes[0].phases.iter().map(phase_label).collect(); + let power_labels: Vec<_> = power_group.lanes[0] + .phases + .iter() + .map(phase_label) + .collect(); + assert_eq!(mode_labels, vec!["Idle", "Active", "Idle"]); + // "On" at ticks 1 and 2 merge into a single phase. + assert_eq!(power_labels, vec!["Off", "On"]); + + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_cast_multi_same_type", + egui::vec2(400.0, 150.0), + None, + ) + .unwrap(); +} + +/// A `DynamicArchetype` carrying three fields of different physical types. Each mapping +/// produces a lane whose `value_kind` matches the post-cast type. +#[test] +fn test_dynamic_archetype_multiple_different_types() { + let mut test_context = TestContext::new_with_view_class::(); + let entity = "/state/multi_mixed"; + + let frames = [ + ("idle", 0.0_f64, false), + ("running", 1.0_f64, true), + ("idle", 0.0_f64, false), + ]; + + for (tick, (s, f, b)) in std::iter::zip(0..3i64, frames) { + let archetype = DynamicArchetype::new("multi_mix") + .with_component_from_data("label", Arc::new(StringArray::from(vec![s]))) + .with_component_from_data("speed", Arc::new(Float64Array::from(vec![f]))) + .with_component_from_data("on", Arc::new(BooleanArray::from(vec![b]))); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row([(Timeline::log_tick(), tick)], &archetype) + }); + } + + let view_id = build_view( + &mut test_context, + entity, + [ + map_source_to_state("multi_mix:label"), + map_source_to_state("multi_mix:speed"), + map_source_to_state("multi_mix:on"), + ], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + let groups = &outputs[0].groups; + assert_eq!(groups.len(), 3); + + let kind_of = |source: &str| { + groups + .iter() + .find(|g| g.label.contains(source)) + .unwrap_or_else(|| panic!("no lane group labelled with {source}")) + .value_kind + }; + assert_eq!(kind_of("multi_mix:label"), StateValueKind::String); + assert_eq!(kind_of("multi_mix:speed"), StateValueKind::Scalar); + assert_eq!(kind_of("multi_mix:on"), StateValueKind::Bool); + + test_context + .run_view_ui_and_save_snapshot( + view_id, + "state_cast_multi_different_types", + egui::vec2(400.0, 200.0), + None, + ) + .unwrap(); +} + +/// `TextLog` is a real Rerun archetype with a `text` string field. Mapping that field as +/// the state source should produce a string-kind lane carrying the logged messages. +#[test] +fn test_textlog_archetype_visualized_as_string() { + let mut test_context = TestContext::new_with_view_class::(); + let entity = "/log"; + + for (tick, message) in [(0_i64, "starting"), (1, "ready"), (2, "stopping")] { + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row([(Timeline::log_tick(), tick)], &TextLog::new(message)) + }); + } + + let view_id = build_view( + &mut test_context, + entity, + [map_source_to_state( + TextLog::descriptor_text().component.as_str(), + )], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!(value_kind(&outputs[0], entity), StateValueKind::String); + assert_eq!( + phase_labels(&outputs[0], entity), + vec!["starting", "ready", "stopping"] + ); + + test_context + .run_view_ui_and_save_snapshot(view_id, "state_cast_textlog", egui::vec2(400.0, 80.0), None) + .unwrap(); +} + +/// A `u8` field nested in a struct, picked via a selector (RR-5038): the polymorphic cast +/// rule must judge the post-selector element type (`UInt8` → `Float64`), not the struct's +/// datatype — otherwise the cast is skipped and the lane silently vanishes. +#[test] +fn test_cast_nested_struct_u8_field_via_selector() { + use re_log_types::external::arrow::array::{ArrayRef, StructArray, UInt8Array}; + use re_log_types::external::arrow::datatypes::{DataType, Field}; + + let mut test_context = TestContext::new_with_view_class::(); + let entity = "/state/nested_u8"; + + for (tick, value) in [(0_i64, 1_u8), (1, 2), (2, 1)] { + let struct_array = StructArray::from(vec![ + ( + Arc::new(Field::new("u8", DataType::UInt8, false)), + Arc::new(UInt8Array::from(vec![value])) as ArrayRef, + ), + ( + Arc::new(Field::new("text", DataType::Utf8, false)), + Arc::new(StringArray::from(vec!["ignored"])) as ArrayRef, + ), + ]); + let archetype = DynamicArchetype::new("nested") + .with_component_from_data("value", Arc::new(struct_array)); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row([(Timeline::log_tick(), tick)], &archetype) + }); + } + + let view_id = build_view( + &mut test_context, + entity, + [map_source_to_state_with_selector( + "nested:value", + Some(".u8"), + )], + ); + + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert_eq!(value_kind(&outputs[0], entity), StateValueKind::Scalar); + assert_eq!(phase_labels(&outputs[0], entity), vec!["1", "2", "1"]); +} + +/// When the underlying column's physical type changes over time, the polymorphic cast hands +/// back chunks with mixed element types and slicing them as a single type would +/// `debug_panic!` in `re_chunk::iter`. The visualizer must detect this and skip the lane +/// rather than panic. +#[test] +fn test_mixed_chunk_types_do_not_panic() { + let mut test_context = TestContext::new_with_view_class::(); + let entity = "/state/mixed"; + + // First a Utf8 chunk, then a Boolean chunk under the same component identifier. + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + [(Timeline::log_tick(), 0_i64)], + &DynamicArchetype::new("mixed") + .with_component_from_data("value", Arc::new(StringArray::from(vec!["ponies"]))), + ) + }); + test_context.log_entity(entity, |builder| { + builder.with_archetype_auto_row( + [(Timeline::log_tick(), 1_i64)], + &DynamicArchetype::new("mixed") + .with_component_from_data("value", Arc::new(BooleanArray::from(vec![true]))), + ) + }); + + let view_id = build_view( + &mut test_context, + entity, + [map_source_to_state("mixed:value")], + ); + + // The visualizer must run to completion (no panic) and emit no lane for this entity. + let outputs = run_visualizer(&test_context, view_id); + assert_eq!(outputs.len(), 1); + assert!( + outputs[0] + .groups + .iter() + .all(|g| g.entity_path != EntityPath::from(entity)), + "expected no lane group for the mixed-type entity, got: {:?}", + outputs[0].groups + ); +} diff --git a/crates/viewer/re_view_status/src/data.rs b/crates/viewer/re_view_status/src/data.rs deleted file mode 100644 index 87ba30457c3d..000000000000 --- a/crates/viewer/re_view_status/src/data.rs +++ /dev/null @@ -1,29 +0,0 @@ -/// Collection of status lanes produced by a visualizer. -#[derive(Clone, Debug, Default)] -pub struct StatusLanesData { - pub lanes: Vec, -} - -/// A single horizontal lane of status phases. -#[derive(Clone, Debug)] -pub struct StatusLane { - /// Display name for this lane (typically the entity path). - pub label: String, - - /// Ordered list of phases. Each phase starts at `start_time` and implicitly ends - /// where the next phase begins (or at the right edge of the visible range). - pub phases: Vec, -} - -/// One contiguous phase within a [`StatusLane`]. -#[derive(Clone, Debug)] -pub struct StatusLanePhase { - /// Start time in timeline units. - pub start_time: i64, - - /// Human-readable status label (e.g. "Idle", "Moving"). - pub label: String, - - /// Display color for this phase. - pub color: egui::Color32, -} diff --git a/crates/viewer/re_view_status/src/lib.rs b/crates/viewer/re_view_status/src/lib.rs deleted file mode 100644 index f7832677a669..000000000000 --- a/crates/viewer/re_view_status/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Rerun Status View. -//! -//! A View that shows status transitions as horizontal lanes over time. - -mod data; -mod view_class; -mod visualizer; - -pub use data::{StatusLane, StatusLanePhase, StatusLanesData}; -pub use view_class::StatusView; -pub use visualizer::StatusVisualizer; diff --git a/crates/viewer/re_view_status/src/view_class.rs b/crates/viewer/re_view_status/src/view_class.rs deleted file mode 100644 index b76726eed636..000000000000 --- a/crates/viewer/re_view_status/src/view_class.rs +++ /dev/null @@ -1,553 +0,0 @@ -use re_log_types::{EntityPath, TimeCell, TimeReal, TimeType, TimelineName, TimestampFormat}; -use re_ui::{Help, icons}; -use re_viewer_context::{ - IdentifiedViewSystem as _, TimeControlCommand, ViewClass, ViewClassLayoutPriority, - ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, ViewStateExt as _, - ViewSystemExecutionError, ViewerContext, -}; - -use crate::data::{StatusLane, StatusLanesData}; - -// Layout constants (in screen pixels). -const LANE_BAND_HEIGHT: f32 = 22.0; -const LANE_LABEL_HEIGHT: f32 = 14.0; -const LANE_GAP: f32 = 4.0; -const LANE_TOTAL_HEIGHT: f32 = LANE_BAND_HEIGHT + LANE_LABEL_HEIGHT + LANE_GAP; - -const TIME_AXIS_HEIGHT: f32 = 20.0; -const TOP_MARGIN: f32 = 4.0; - -/// View state for pan/zoom. -#[derive(Default)] -struct StatusViewState { - /// Visible time range: (min, max) in timeline units. - /// `None` means "fit all data". - time_range: Option<(f64, f64)>, - - /// The timeline we last rendered. When the active timeline changes, - /// we reset `time_range` so the view auto-fits to the new data. - active_timeline: Option, - - /// `true` while the user is dragging the time cursor. - dragging_cursor: bool, -} - -impl ViewState for StatusViewState { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } -} - -#[derive(Default)] -pub struct StatusView; - -impl ViewClass for StatusView { - fn identifier() -> re_sdk_types::ViewClassIdentifier { - "Status".into() - } - - fn display_name(&self) -> &'static str { - "Status" - } - - fn icon(&self) -> &'static re_ui::Icon { - // TODO(RR-4264): Add a proper icon. - &icons::VIEW_GENERIC - } - - fn new_state(&self) -> Box { - Box::::default() - } - - fn help(&self, _os: egui::os::OperatingSystem) -> Help { - Help::new("Status view") - .markdown("Shows status transitions as horizontal colored lanes over time.") - } - - fn on_register( - &self, - system_registry: &mut re_viewer_context::ViewSystemRegistrator<'_>, - ) -> Result<(), ViewClassRegistryError> { - system_registry.register_visualizer::() - } - - fn preferred_tile_aspect_ratio(&self, _state: &dyn ViewState) -> Option { - Some(2.5) - } - - fn layout_priority(&self) -> ViewClassLayoutPriority { - ViewClassLayoutPriority::Low - } - - fn spawn_heuristics( - &self, - ctx: &ViewerContext<'_>, - include_entity: &dyn Fn(&EntityPath) -> bool, - ) -> re_viewer_context::ViewSpawnHeuristics { - re_tracing::profile_function!(); - - // Show every status in a single view by default. - if ctx - .indicated_entities_per_visualizer - .get(&crate::StatusVisualizer::identifier()) - .is_some_and(|entities| entities.iter().any(include_entity)) - { - ViewSpawnHeuristics::root() - } else { - ViewSpawnHeuristics::empty() - } - } - - fn selection_ui( - &self, - _ctx: &ViewerContext<'_>, - _ui: &mut egui::Ui, - _state: &mut dyn ViewState, - _space_origin: &EntityPath, - _view_id: ViewId, - ) -> Result<(), ViewSystemExecutionError> { - Ok(()) - } - - fn ui( - &self, - ctx: &ViewerContext<'_>, - _missing_chunk_reporter: &re_viewer_context::MissingChunkReporter, - ui: &mut egui::Ui, - state: &mut dyn ViewState, - query: &ViewQuery<'_>, - system_output: re_viewer_context::SystemExecutionOutput, - ) -> Result<(), ViewSystemExecutionError> { - re_tracing::profile_function!(); - - let state = state.downcast_mut::()?; - - // Reset time range when the active timeline changes. - if state.active_timeline.as_ref() != Some(&query.timeline) { - state.active_timeline = Some(query.timeline); - state.time_range = None; - } - - // Collect all lanes from all visualizers. - let all_lanes: Vec<&StatusLane> = system_output - .iter_visualizer_data::() - .flat_map(|d| d.lanes.iter()) - .collect(); - - if all_lanes.is_empty() { - ui.centered_and_justified(|ui| { - ui.label("No status data. Add a visualizer that produces StatusLanesData."); - }); - return Ok(()); - } - - // Compute data time range. - let (data_min, data_max) = data_time_range(&all_lanes); - - // Auto-fit on first frame. - // TODO(aedm): The calculation of the end time is incorrect since status transitions don't have an end time. - // We should use an estimation so that the latest state is still somewhat visible. Maybe also consider - // the density of states? An idea is to keep as much space for the last state as the average state - // duration on the screen. - if state.time_range.is_none() { - let padding = (data_max - data_min).max(1.0) * 0.05; - state.time_range = Some((data_min - padding, data_max + padding)); - } - - let Some((mut t_min, mut t_max)) = state.time_range else { - return Ok(()); - }; - - // Allocate the full available rect. - let (rect, response) = - ui.allocate_exact_size(ui.available_size(), egui::Sense::click_and_drag()); - - if !ui.is_rect_visible(rect) { - return Ok(()); - } - - // Handle select / hover on the view itself. - ctx.handle_select_hover_drag_interactions( - &response, - re_viewer_context::Item::View(query.view_id), - false, - ); - - // Lane drawing area (above the time axis). - let lanes_rect = egui::Rect::from_min_max( - rect.left_top(), - egui::pos2(rect.right(), rect.bottom() - TIME_AXIS_HEIGHT), - ); - let time_axis_rect = egui::Rect::from_min_max( - egui::pos2(rect.left(), rect.bottom() - TIME_AXIS_HEIGHT), - rect.right_bottom(), - ); - - // Detect cursor drag vs pan: if drag started near the cursor, drag the cursor. - // TODO(RR-4433): the implementation is incomplete and inconsistent with the time series view. - let current_time = query.latest_at.as_i64() as f64; - let cursor_x = time_to_x(current_time, rect, t_min, t_max); - const CURSOR_GRAB_RADIUS: f32 = 6.0; - - if response.drag_started() { - state.dragging_cursor = ui - .input(|i| i.pointer.press_origin()) - .is_some_and(|pos| (pos.x - cursor_x).abs() <= CURSOR_GRAB_RADIUS); - } - if !response.dragged() { - state.dragging_cursor = false; - } - - if state.dragging_cursor { - // Drag the time cursor. - if let Some(pos) = response.interact_pointer_pos() { - let frac = ((pos.x - rect.left()) / rect.width()) as f64; - let drag_time = t_min + frac * (t_max - t_min); - ctx.send_time_commands([ - TimeControlCommand::Pause, - TimeControlCommand::SetTime(TimeReal::from(drag_time)), - ]); - } - } else { - // Pan & zoom. - handle_pan_zoom(ui, &response, rect, &mut t_min, &mut t_max); - } - state.time_range = Some((t_min, t_max)); - - // Background. - let painter = ui.painter_at(rect); - painter.rect_filled(rect, 0.0, ui.style().visuals.extreme_bg_color); - - // Draw lanes. - let label_color = ui.style().visuals.text_color(); - let weak_color = ui.style().visuals.weak_text_color(); - let time_type = ctx - .time_ctrl - .timeline() - .map_or(TimeType::Sequence, |tl| tl.typ()); - let timestamp_format = ctx.app_options().timestamp_format; - for (lane_idx, lane) in all_lanes.iter().enumerate() { - paint_lane( - ui, - &painter, - lanes_rect, - lane_idx, - lane, - t_min, - t_max, - time_type, - timestamp_format, - label_color, - ); - } - - // Draw time axis. - paint_time_axis( - &painter, - time_axis_rect, - t_min, - t_max, - time_type, - timestamp_format, - label_color, - weak_color, - ); - - // Draw time cursor as a full-height vertical line. - if current_time >= t_min && current_time <= t_max { - let cursor_hovered = ui - .input(|i| i.pointer.hover_pos()) - .is_some_and(|pos| (pos.x - cursor_x).abs() <= CURSOR_GRAB_RADIUS); - let stroke = ui.visuals().widgets.inactive.fg_stroke; - if cursor_hovered || state.dragging_cursor { - ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal); - } - let width_multiplier = if cursor_hovered || state.dragging_cursor { - 3.0 - } else { - 1.5 - }; - painter.vline( - cursor_x, - rect.top()..=rect.bottom(), - egui::Stroke::new(width_multiplier * stroke.width, stroke.color), - ); - } - - // Click to set time cursor. - if response.clicked() - && let Some(pos) = response.interact_pointer_pos() - { - let frac = ((pos.x - rect.left()) / rect.width()) as f64; - let click_time = t_min + frac * (t_max - t_min); - ctx.send_time_commands([ - TimeControlCommand::Pause, - TimeControlCommand::SetTime(TimeReal::from(click_time)), - ]); - } - - Ok(()) - } -} - -/// Compute the (min, max) time range across all lanes. -fn data_time_range(lanes: &[&StatusLane]) -> (f64, f64) { - let mut min = f64::MAX; - let mut max = f64::MIN; - for lane in lanes { - for phase in &lane.phases { - let t = phase.start_time as f64; - min = min.min(t); - max = max.max(t); - } - } - if min > max { - (0.0, 1.0) - } else if (max - min).abs() < f64::EPSILON { - (min - 0.5, max + 0.5) - } else { - (min, max) - } -} - -/// Map a time value to screen x within the given rect. -fn time_to_x(t: f64, rect: egui::Rect, t_min: f64, t_max: f64) -> f32 { - let frac = ((t - t_min) / (t_max - t_min)) as f32; - egui::lerp(rect.left()..=rect.right(), frac) -} - -/// Handle drag-to-pan and scroll-to-zoom interactions. -fn handle_pan_zoom( - ui: &egui::Ui, - response: &egui::Response, - rect: egui::Rect, - t_min: &mut f64, - t_max: &mut f64, -) { - let range = *t_max - *t_min; - - // Drag to pan. - if response.dragged() { - let dx = response.drag_delta().x; - let dt = -(dx as f64 / rect.width() as f64) * range; - *t_min += dt; - *t_max += dt; - } - - // Cmd/Ctrl + scroll to zoom (egui routes Cmd+scroll to zoom_delta). - let zoom_delta = ui.input(|i| i.zoom_delta()); - if zoom_delta != 1.0 && response.hovered() { - let zoom_factor = zoom_delta as f64; - // Zoom centered on the mouse position. - let mouse_x = ui - .input(|i| i.pointer.hover_pos()) - .map(|p| p.x) - .unwrap_or_else(|| rect.center().x); - let frac = ((mouse_x - rect.left()) / rect.width()) as f64; - let pivot = *t_min + frac * range; - - *t_min = pivot - (pivot - *t_min) / zoom_factor; - *t_max = pivot + (*t_max - pivot) / zoom_factor; - } -} - -/// Paint a single lane (label + colored band of phases) and show tooltips on hover. -#[expect(clippy::too_many_arguments)] -fn paint_lane( - ui: &egui::Ui, - painter: &egui::Painter, - lanes_rect: egui::Rect, - lane_idx: usize, - lane: &StatusLane, - t_min: f64, - t_max: f64, - time_type: TimeType, - timestamp_format: TimestampFormat, - label_color: egui::Color32, -) { - let y_top = lanes_rect.top() + TOP_MARGIN + lane_idx as f32 * LANE_TOTAL_HEIGHT; - let label_rect = egui::Rect::from_min_size( - egui::pos2(lanes_rect.left() + 4.0, y_top), - egui::vec2(lanes_rect.width() - 8.0, LANE_LABEL_HEIGHT), - ); - let band_y_top = y_top + LANE_LABEL_HEIGHT; - let band_y_bottom = band_y_top + LANE_BAND_HEIGHT; - - // Lane label. - painter.text( - label_rect.left_top(), - egui::Align2::LEFT_TOP, - &lane.label, - egui::FontId::proportional(11.0), - label_color, - ); - - let hover_pos = ui.input(|i| i.pointer.hover_pos()); - - // Phases. - for (i, phase) in lane.phases.iter().enumerate() { - let x_start = time_to_x(phase.start_time as f64, lanes_rect, t_min, t_max); - let next_phase = lane.phases.get(i + 1); - let x_end = if let Some(next) = next_phase { - time_to_x(next.start_time as f64, lanes_rect, t_min, t_max) - } else { - lanes_rect.right() - }; - - // Clip to visible area. - let x_start = x_start.max(lanes_rect.left()); - let x_end = x_end.min(lanes_rect.right()); - - if x_end <= x_start { - continue; - } - - let phase_rect = egui::Rect::from_min_max( - egui::pos2(x_start, band_y_top), - egui::pos2(x_end, band_y_bottom), - ); - - // Filled band. Dim when not hovered. - let hovered = hover_pos.is_some_and(|pos| phase_rect.contains(pos)); - #[expect(clippy::disallowed_methods)] - // Data-driven visualization color, not a UI theme color. - let fill = if hovered { - phase.color - } else { - let [r, g, b, _] = phase.color.to_array(); - egui::Color32::from_rgba_unmultiplied(r, g, b, 200) - }; - painter.add(egui::epaint::RectShape::new( - phase_rect, - 0.0, - fill, - egui::Stroke::NONE, - egui::StrokeKind::Outside, - )); - - // Phase label (clipped to band width). - let text_width = x_end - x_start - 6.0; - if text_width > 10.0 { - painter.with_clip_rect(phase_rect).text( - egui::pos2(x_start + 4.0, band_y_top + 3.0), - egui::Align2::LEFT_TOP, - &phase.label, - egui::FontId::proportional(12.0), - readable_text_color(phase.color), - ); - } - - // Tooltip on hover. - if let Some(pos) = hover_pos - && phase_rect.contains(pos) - { - let start = TimeCell::new(time_type, phase.start_time).format(timestamp_format); - egui::Tooltip::always_open( - ui.ctx().clone(), - ui.layer_id(), - egui::Id::new("state_tooltip"), - egui::PopupAnchor::Pointer, - ) - .show(|ui| { - ui.label(&phase.label); - ui.add_space(4.0); - let weak = ui.visuals().weak_text_color(); - let small = egui::FontId::proportional(11.0); - ui.label( - egui::RichText::new(format!("Start: {start}")) - .font(small.clone()) - .color(weak), - ); - if let Some(next) = next_phase { - let end = TimeCell::new(time_type, next.start_time).format(timestamp_format); - ui.label( - egui::RichText::new(format!("End: {end}")) - .font(small) - .color(weak), - ); - } - }); - } - } -} - -/// Choose white or black text depending on background luminance. -fn readable_text_color(bg: egui::Color32) -> egui::Color32 { - if bg.intensity() > 0.6 { - egui::Color32::BLACK - } else { - egui::Color32::WHITE - } -} - -/// Paint the time axis with tick marks and labels. -#[expect(clippy::too_many_arguments)] -fn paint_time_axis( - painter: &egui::Painter, - rect: egui::Rect, - t_min: f64, - t_max: f64, - time_type: TimeType, - timestamp_format: TimestampFormat, - text_color: egui::Color32, - weak_color: egui::Color32, -) { - let range = t_max - t_min; - if range <= 0.0 { - return; - } - - // Separator line. - painter.line_segment( - [rect.left_top(), rect.right_top()], - egui::Stroke::new(1.0, weak_color), - ); - - // Compute a nice tick spacing. - let approx_num_ticks = (rect.width() / 80.0).max(2.0) as usize; - let raw_step = range / approx_num_ticks as f64; - let magnitude = 10.0_f64.powf(raw_step.log10().floor()); - let residual = raw_step / magnitude; - let step = if residual <= 1.5 { - magnitude - } else if residual <= 3.5 { - 2.0 * magnitude - } else if residual <= 7.5 { - 5.0 * magnitude - } else { - 10.0 * magnitude - }; - - let first_tick = (t_min / step).ceil() * step; - let mut t = first_tick; - while t <= t_max { - let x = time_to_x(t, rect, t_min, t_max); - - // Tick mark. - painter.line_segment( - [egui::pos2(x, rect.top()), egui::pos2(x, rect.top() + 4.0)], - egui::Stroke::new(1.0, weak_color), - ); - - // Label. - let label = TimeCell::new(time_type, t as i64).format_compact(timestamp_format); - painter.text( - egui::pos2(x, rect.top() + 5.0), - egui::Align2::CENTER_TOP, - label, - egui::FontId::proportional(10.0), - text_color, - ); - - t += step; - } -} - -#[test] -fn test_help_view() { - re_test_context::TestContext::test_help_view(|ctx| StatusView.help(ctx)); -} diff --git a/crates/viewer/re_view_status/src/visualizer.rs b/crates/viewer/re_view_status/src/visualizer.rs deleted file mode 100644 index 1079ffba1ff1..000000000000 --- a/crates/viewer/re_view_status/src/visualizer.rs +++ /dev/null @@ -1,133 +0,0 @@ -use re_chunk_store::AbsoluteTimeRange; -use re_sdk_types::Archetype as _; -use re_sdk_types::archetypes::Status; -use re_sdk_types::components::Text; -use re_viewer_context::{ - AppOptions, IdentifiedViewSystem, ViewContext, ViewContextCollection, ViewQuery, - ViewSystemExecutionError, ViewSystemIdentifier, VisualizerExecutionOutput, VisualizerQueryInfo, - VisualizerSystem, -}; - -use crate::data::{StatusLane, StatusLanePhase, StatusLanesData}; - -/// Color palette for status phases. -#[expect(clippy::disallowed_methods)] // These are data-driven visualization colors, not UI theme colors. -const PALETTE: &[egui::Color32] = &[ - egui::Color32::from_rgb(76, 175, 80), // green - egui::Color32::from_rgb(255, 183, 77), // amber - egui::Color32::from_rgb(66, 165, 245), // blue - egui::Color32::from_rgb(239, 83, 80), // red - egui::Color32::from_rgb(171, 71, 188), // purple - egui::Color32::from_rgb(38, 198, 218), // teal - egui::Color32::from_rgb(255, 241, 118), // yellow - egui::Color32::from_rgb(141, 110, 99), // brown -]; - -fn color_for_index(idx: usize) -> egui::Color32 { - PALETTE[idx % PALETTE.len()] -} - -/// A visualizer that queries [`Status`] archetypes and groups them into status lanes per entity. -/// -/// Each entity path becomes one lane. Each distinct status value within a lane gets a unique color. -#[derive(Default)] -pub struct StatusVisualizer; - -impl IdentifiedViewSystem for StatusVisualizer { - fn identifier() -> ViewSystemIdentifier { - "StatusVisualizer".into() - } -} - -impl VisualizerSystem for StatusVisualizer { - fn visualizer_query_info(&self, _app_options: &AppOptions) -> VisualizerQueryInfo { - VisualizerQueryInfo::single_required_component::( - &Status::descriptor_status(), - &Status::all_components(), - ) - } - - fn execute( - &self, - ctx: &ViewContext<'_>, - view_query: &ViewQuery<'_>, - _context_systems: &ViewContextCollection, - ) -> Result { - re_tracing::profile_function!(); - - let output = VisualizerExecutionOutput::default(); - let query = - re_chunk_store::RangeQuery::new(view_query.timeline, AbsoluteTimeRange::EVERYTHING); - - let mut lanes: Vec = Vec::new(); - - for (data_result, instruction) in - view_query.iter_visualizer_instruction_for(Self::identifier()) - { - let range_results = re_view::range_with_blueprint_resolved_data( - ctx, - None, - &query, - data_result, - Status::all_component_identifiers(), - instruction, - ); - - let results = re_view::BlueprintResolvedResults::from((query.clone(), range_results)); - let results = - re_view::VisualizerInstructionQueryResults::new(instruction, &results, &output); - - let all_texts = results.iter_required(Status::descriptor_status().component); - if all_texts.is_empty() { - continue; - } - - // Collect (time, text) pairs. - // A null status is a fallthrough, not a phase change: the preceding phase - // must continue across it. `slice::` represents null entries as - // zero-length slices, so we skip empty texts here. - let mut phases: Vec<(i64, String)> = Vec::new(); - for ((data_time, _row_id), texts) in all_texts.slice::() { - let time_value = data_time.as_i64(); - for text in texts { - if text.is_empty() { - continue; - } - phases.push((time_value, text.to_string())); - } - } - - if phases.is_empty() { - continue; - } - - phases.sort_by_key(|(t, _)| *t); - - // Collect unique labels for deterministic color assignment. - let mut unique_labels: Vec = Vec::new(); - for (_, label) in &phases { - if !unique_labels.contains(label) { - unique_labels.push(label.clone()); - } - } - - let lane = StatusLane { - label: data_result.entity_path.to_string(), - phases: phases - .into_iter() - .map(|(t, label)| { - let color_idx = unique_labels.iter().position(|l| l == &label).unwrap_or(0); - StatusLanePhase { - start_time: t, - label, - color: color_for_index(color_idx), - } - }) - .collect(), - }; - lanes.push(lane); - } - - Ok(output.with_visualizer_data(StatusLanesData { lanes })) - } -} diff --git a/crates/viewer/re_view_status/tests/basic.rs b/crates/viewer/re_view_status/tests/basic.rs deleted file mode 100644 index 897f225a4f2f..000000000000 --- a/crates/viewer/re_view_status/tests/basic.rs +++ /dev/null @@ -1,325 +0,0 @@ -use re_chunk_store::RowId; -use re_log_types::{TimePoint, Timeline}; -use re_test_context::TestContext; -use re_test_context::external::egui_kittest::SnapshotResults; -use re_test_viewport::TestContextExt as _; -use re_view_status::StatusView; -use re_viewer_context::{TimeControlCommand, ViewClass as _, ViewId}; -use re_viewport_blueprint::ViewBlueprint; - -fn setup_blueprint(test_context: &mut TestContext) -> ViewId { - test_context.setup_viewport_blueprint(|_ctx, blueprint| { - blueprint.add_view_at_root(ViewBlueprint::new_with_root_wildcard( - StatusView::identifier(), - )) - }) -} - -// TODO(RR-4254): Add a test for multiple status instances. - -#[test] -fn test_status_basic() { - let mut snapshot_results = SnapshotResults::new(); - let mut test_context = TestContext::new_with_view_class::(); - - let timeline = Timeline::log_tick(); - - // Log state transitions for multiple entities using Status. - let state_data: Vec<(i64, &str, &str)> = vec![ - // (tick, entity, state_label) - (0, "state/robot_mode", "Idle"), - (10, "state/robot_mode", "Moving"), - (25, "state/robot_mode", "Working"), - (40, "state/robot_mode", "Idle"), - (0, "state/power", "On"), - (20, "state/power", "Low"), - (35, "state/power", "Critical"), - (45, "state/power", "On"), - (0, "state/connection", "Connected"), - (15, "state/connection", "Disconnected"), - (30, "state/connection", "Connected"), - ]; - - for (tick, entity, status) in &state_data { - let timepoint = TimePoint::from([(timeline, *tick)]); - test_context.log_entity(*entity, |builder| { - builder.with_archetype( - RowId::new(), - timepoint, - &re_sdk_types::archetypes::Status::new().with_status(*status), - ) - }); - } - - test_context.set_active_timeline(*timeline.name()); - - // Set time cursor to tick 20 (mid-range). - let store_id = test_context.active_store_id(); - test_context.send_time_commands( - store_id, - [TimeControlCommand::SetTime( - re_log_types::TimeInt::new_temporal(20).into(), - )], - ); - test_context.handle_system_commands(&egui::Context::default()); - - let view_id = setup_blueprint(&mut test_context); - snapshot_results.add(test_context.run_view_ui_and_save_snapshot( - view_id, - "status_basic", - egui::vec2(500.0, 250.0), - None, - )); -} - -#[test] -fn test_status_time_cursor() { - let mut snapshot_results = SnapshotResults::new(); - let mut test_context = TestContext::new_with_view_class::(); - - let timeline = Timeline::log_tick(); - - let state_data: Vec<(i64, &str, &str)> = vec![ - (0, "state/mode", "Idle"), - (20, "state/mode", "Active"), - (40, "state/mode", "Idle"), - ]; - - for (tick, entity, status) in &state_data { - let timepoint = TimePoint::from([(timeline, *tick)]); - test_context.log_entity(*entity, |builder| { - builder.with_archetype( - RowId::new(), - timepoint, - &re_sdk_types::archetypes::Status::new().with_status(*status), - ) - }); - } - - test_context.set_active_timeline(*timeline.name()); - - // Set time cursor to tick 30. - let store_id = test_context.active_store_id(); - test_context.send_time_commands( - store_id, - [TimeControlCommand::SetTime( - re_log_types::TimeInt::new_temporal(30).into(), - )], - ); - test_context.handle_system_commands(&egui::Context::default()); - - let view_id = setup_blueprint(&mut test_context); - snapshot_results.add(test_context.run_view_ui_and_save_snapshot( - view_id, - "status_time_cursor", - egui::vec2(400.0, 120.0), - None, - )); -} - -/// A null status is a fallthrough: it must not terminate the preceding phase. -#[test] -fn test_status_null_is_fallthrough() { - let mut snapshot_results = SnapshotResults::new(); - let mut test_context = TestContext::new_with_view_class::(); - - let timeline = Timeline::log_tick(); - - // Log a status, then a null in the middle, then another status. - // The null should be ignored so that the first phase extends all the way - // until the next non-null status. - let timepoint_0 = TimePoint::from([(timeline, 0)]); - test_context.log_entity("state/mode", |builder| { - builder.with_archetype( - RowId::new(), - timepoint_0, - &re_sdk_types::archetypes::Status::new().with_status("Idle"), - ) - }); - - let timepoint_20 = TimePoint::from([(timeline, 20)]); - let null_status_array = - ::to_arrow_opt( - [None::], - ) - .expect("serializing a single null text should not fail"); - let null_status = re_sdk_types::archetypes::Status { - status: Some(re_sdk_types::SerializedComponentBatch::new( - null_status_array, - re_sdk_types::archetypes::Status::descriptor_status(), - )), - }; - test_context.log_entity("state/mode", |builder| { - builder.with_archetype(RowId::new(), timepoint_20, &null_status) - }); - - let timepoint_40 = TimePoint::from([(timeline, 40)]); - test_context.log_entity("state/mode", |builder| { - builder.with_archetype( - RowId::new(), - timepoint_40, - &re_sdk_types::archetypes::Status::new().with_status("Active"), - ) - }); - - test_context.set_active_timeline(*timeline.name()); - - // Place the cursor in the null region to confirm the previous phase - // visibly extends through the null. - let store_id = test_context.active_store_id(); - test_context.send_time_commands( - store_id, - [TimeControlCommand::SetTime( - re_log_types::TimeInt::new_temporal(30).into(), - )], - ); - test_context.handle_system_commands(&egui::Context::default()); - - let view_id = setup_blueprint(&mut test_context); - snapshot_results.add(test_context.run_view_ui_and_save_snapshot( - view_id, - "status_null_is_fallthrough", - egui::vec2(400.0, 120.0), - None, - )); -} - -/// Log data on both a sequence and a timestamp timeline, switch between them, -/// and verify the time axis labels update to match the active timeline. -#[test] -fn test_status_timeline_switch() { - let mut snapshot_results = SnapshotResults::new(); - let mut test_context = TestContext::new_with_view_class::(); - - let seq_timeline = Timeline::log_tick(); - // Base timestamp: 2025-04-01 12:00:00 UTC (in nanoseconds since epoch) - let base_ns: i64 = 1_743_508_800_000_000_000; - let step_ns: i64 = 5_000_000_000; // 5 seconds - let ts_timeline = Timeline::new_timestamp("timestamp"); - - let state_data: Vec<(i64, &str, &str)> = vec![ - (0, "state/robot_mode", "Idle"), - (10, "state/robot_mode", "Moving"), - (25, "state/robot_mode", "Working"), - (40, "state/robot_mode", "Idle"), - (0, "state/power", "On"), - (20, "state/power", "Low"), - (35, "state/power", "Critical"), - (45, "state/power", "On"), - ]; - - for (tick, entity, status) in &state_data { - let timepoint = TimePoint::from([ - (seq_timeline, *tick), - (ts_timeline, base_ns + *tick * step_ns), - ]); - test_context.log_entity(*entity, |builder| { - builder.with_archetype( - RowId::new(), - timepoint, - &re_sdk_types::archetypes::Status::new().with_status(*status), - ) - }); - } - - let view_id = setup_blueprint(&mut test_context); - let egui_ctx = egui::Context::default(); - - // Snapshot with the sequence timeline active. - test_context.set_active_timeline(*seq_timeline.name()); - let store_id = test_context.active_store_id(); - test_context.send_time_commands( - store_id.clone(), - [TimeControlCommand::SetTime( - re_log_types::TimeInt::new_temporal(20).into(), - )], - ); - test_context.handle_system_commands(&egui_ctx); - snapshot_results.add(test_context.run_view_ui_and_save_snapshot( - view_id, - "status_timeline_switch_sequence", - egui::vec2(500.0, 200.0), - None, - )); - - // Switch to the timestamp timeline and snapshot again. - test_context.set_active_timeline(*ts_timeline.name()); - test_context.send_time_commands( - store_id, - [TimeControlCommand::SetTime( - re_log_types::TimeInt::new_temporal(base_ns + 20 * step_ns).into(), - )], - ); - test_context.handle_system_commands(&egui_ctx); - snapshot_results.add(test_context.run_view_ui_and_save_snapshot( - view_id, - "status_timeline_switch_timestamp", - egui::vec2(500.0, 200.0), - None, - )); -} - -/// Cmd+scroll over the Status view should zoom in around the pointer. -#[test] -fn test_status_zoom() { - let mut snapshot_results = SnapshotResults::new(); - let mut test_context = TestContext::new_with_view_class::(); - - let timeline = Timeline::new_sequence("tick"); - - let state_data: Vec<(i64, &str, &str)> = vec![ - (0, "state/robot_mode", "Idle"), - (10, "state/robot_mode", "Moving"), - (25, "state/robot_mode", "Working"), - (40, "state/robot_mode", "Idle"), - (0, "state/power", "On"), - (20, "state/power", "Low"), - (35, "state/power", "Critical"), - (45, "state/power", "On"), - (0, "state/connection", "Connected"), - (15, "state/connection", "Disconnected"), - (30, "state/connection", "Connected"), - ]; - - for (tick, entity, status) in &state_data { - let timepoint = TimePoint::from([(timeline, *tick)]); - test_context.log_entity(*entity, |builder| { - builder.with_archetype( - RowId::new(), - timepoint, - &re_sdk_types::archetypes::Status::new().with_status(*status), - ) - }); - } - - test_context.set_active_timeline(*timeline.name()); - - let view_id = setup_blueprint(&mut test_context); - - let size = egui::vec2(800.0, 400.0); - let mut harness = test_context - .setup_kittest_for_rendering_3d(size) - .build_ui(|ui| { - test_context.run_with_single_view(ui, view_id); - }); - - // Let the view auto-fit and settle. - harness.run(); - snapshot_results.add(harness.try_snapshot("status_zoom_before")); - - // Cmd+scroll over the center of the view to zoom in. `handle_pan_zoom` - // only zooms when the pointer is hovering over the view, so we hover first. - let center = egui::pos2(size.x * 0.5, size.y * 0.5); - harness.hover_at(center); - for _ in 0..5 { - harness.event(egui::Event::MouseWheel { - unit: egui::MouseWheelUnit::Line, - delta: egui::vec2(0.0, 1.0), - phase: egui::TouchPhase::Move, - modifiers: egui::Modifiers::COMMAND, - }); - harness.run(); - } - - snapshot_results.add(harness.try_snapshot("status_zoom_after")); -} diff --git a/crates/viewer/re_view_status/tests/snapshots/help_view_status_view_mac.png b/crates/viewer/re_view_status/tests/snapshots/help_view_status_view_mac.png deleted file mode 100644 index 4e62928e7a57..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/help_view_status_view_mac.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de66576e9c7638c4e1ff76c94b51395d9b155513bfc22ed41d6d39fb1f5efd24 -size 9719 diff --git a/crates/viewer/re_view_status/tests/snapshots/help_view_status_view_windows.png b/crates/viewer/re_view_status/tests/snapshots/help_view_status_view_windows.png deleted file mode 100644 index 4e62928e7a57..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/help_view_status_view_windows.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de66576e9c7638c4e1ff76c94b51395d9b155513bfc22ed41d6d39fb1f5efd24 -size 9719 diff --git a/crates/viewer/re_view_status/tests/snapshots/status_basic.png b/crates/viewer/re_view_status/tests/snapshots/status_basic.png deleted file mode 100644 index 45a2f6b0b31c..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/status_basic.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a76159b3bb2eff0d3c0dc72d2789a6b699182201ed4655159e1ad50d601e9c56 -size 21496 diff --git a/crates/viewer/re_view_status/tests/snapshots/status_null_is_fallthrough.png b/crates/viewer/re_view_status/tests/snapshots/status_null_is_fallthrough.png deleted file mode 100644 index 77bcb4d4f88b..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/status_null_is_fallthrough.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20de7318f5cc7704a95540fe1ae72550ebdcb28e44af8ac1b26b538190bea5d1 -size 6257 diff --git a/crates/viewer/re_view_status/tests/snapshots/status_time_cursor.png b/crates/viewer/re_view_status/tests/snapshots/status_time_cursor.png deleted file mode 100644 index 508b187147c5..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/status_time_cursor.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5f42c2decb4bff89440e5c89c45985ccbd2b51fe9ebcfe59e855054d40d28206 -size 7034 diff --git a/crates/viewer/re_view_status/tests/snapshots/status_timeline_switch_sequence.png b/crates/viewer/re_view_status/tests/snapshots/status_timeline_switch_sequence.png deleted file mode 100644 index bfacc663237e..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/status_timeline_switch_sequence.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d529fefcb7639d351b58ab845b527c224c6c429de723dcb6918fae1826dc3fc4 -size 14011 diff --git a/crates/viewer/re_view_status/tests/snapshots/status_timeline_switch_timestamp.png b/crates/viewer/re_view_status/tests/snapshots/status_timeline_switch_timestamp.png deleted file mode 100644 index 1cb078d7c84f..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/status_timeline_switch_timestamp.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1604a5debb58bca36ba259914169f589ad61eb218d279c7ad30bb360a1acaa2a -size 16471 diff --git a/crates/viewer/re_view_status/tests/snapshots/status_zoom_after.png b/crates/viewer/re_view_status/tests/snapshots/status_zoom_after.png deleted file mode 100644 index 17a61b7ba623..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/status_zoom_after.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9c1db2c5121d551ed166701bb1c30d5a2ef265f5a4099309448d685f772f8d91 -size 25697 diff --git a/crates/viewer/re_view_status/tests/snapshots/status_zoom_before.png b/crates/viewer/re_view_status/tests/snapshots/status_zoom_before.png deleted file mode 100644 index 7f9d2fa8ae6d..000000000000 --- a/crates/viewer/re_view_status/tests/snapshots/status_zoom_before.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:136c055361c1481a8e2025f15311704800316a521fc73173c934287807433f40 -size 28268 diff --git a/crates/viewer/re_view_tensor/Cargo.toml b/crates/viewer/re_view_tensor/Cargo.toml index 959ad2e33c5c..7358ff6813b5 100644 --- a/crates/viewer/re_view_tensor/Cargo.toml +++ b/crates/viewer/re_view_tensor/Cargo.toml @@ -19,6 +19,7 @@ workspace = true all-features = true [dependencies] +re_byte_size.workspace = true re_chunk_store.workspace = true re_data_ui.workspace = true re_log_types.workspace = true diff --git a/crates/viewer/re_view_tensor/src/view_class.rs b/crates/viewer/re_view_tensor/src/view_class.rs index e91896016535..fc68d644a966 100644 --- a/crates/viewer/re_view_tensor/src/view_class.rs +++ b/crates/viewer/re_view_tensor/src/view_class.rs @@ -32,7 +32,7 @@ pub struct TensorView; type ViewType = re_sdk_types::blueprint::views::TensorView; -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] pub struct ViewTensorState { /// Last viewed tensor, copied each frame. /// Used for the selection view. @@ -47,6 +47,10 @@ impl ViewState for ViewTensorState { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } } impl ViewClass for TensorView { @@ -151,9 +155,9 @@ Set the displayed dimensions in a selection panel.", // TODO(#6075): Listitemify if let Some(TensorVisualization { tensor, .. }) = &state.tensor { - let slice_property = ViewProperty::from_archetype::< + let slice_property = ViewProperty::from_archetype_for_view::< re_sdk_types::blueprint::archetypes::TensorSliceSelection, - >(ctx.blueprint_db(), ctx.blueprint_query, view_id); + >(ctx, view_id); let slice_selection = TensorSliceSelection::load_and_make_valid( &slice_property, &TensorDimension::from_tensor_data(tensor), @@ -220,8 +224,8 @@ Set the displayed dimensions in a selection panel.", let state = state.downcast_mut::()?; state.tensor = None; - let tensors = - system_output.visualizer_data::>(TensorSystem::identifier())?; + let tensors = system_output + .visualizer_data_or_default::>(TensorSystem::identifier())?; let response = { let mut ui = ui.new_child(egui::UiBuilder::new().sense(egui::Sense::click())); @@ -280,9 +284,9 @@ impl TensorView { ) -> Result<(), ViewSystemExecutionError> { re_tracing::profile_function!(); - let slice_property = ViewProperty::from_archetype::< + let slice_property = ViewProperty::from_archetype_for_view::< re_sdk_types::blueprint::archetypes::TensorSliceSelection, - >(ctx.blueprint_db(), ctx.blueprint_query, view_id); + >(ctx, view_id); let slice_selection = TensorSliceSelection::load_and_make_valid( &slice_property, &TensorDimension::from_tensor_data(tensor), @@ -373,11 +377,7 @@ impl TensorView { data_range, } = &tensor_view; - let scalar_mapping = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - ctx.view_id, - ); + let scalar_mapping = ViewProperty::from_archetype::(ctx); let colormap: Colormap = scalar_mapping .component_or_fallback(ctx, TensorScalarMapping::descriptor_colormap().component)?; let gamma: GammaCorrection = scalar_mapping @@ -399,12 +399,8 @@ impl TensorView { )?; let [width, height] = colormapped_texture.width_height(); - let view_fit: ViewFit = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - ctx.view_id, - ) - .component_or_fallback(ctx, TensorViewFit::descriptor_scaling().component)?; + let view_fit: ViewFit = ViewProperty::from_archetype::(ctx) + .component_or_fallback(ctx, TensorViewFit::descriptor_scaling().component)?; let img_size = egui::vec2(width as _, height as _); let img_size = Vec2::max(Vec2::splat(1.0), img_size); // better safe than sorry @@ -438,6 +434,7 @@ impl TensorView { image_rect, colormapped_texture, texture_options, + ctx.view_id.render_view_id(), re_renderer::Label::from("tensor_slice"), )?; @@ -482,10 +479,11 @@ pub fn selected_tensor_slice<'a, T: Copy>( tensor.view() }; - let axis = [dheight as usize, dwidth as usize] - .into_iter() - .chain(indices.iter().map(|s| s.dimension as usize)) - .collect::>(); + let axis = std::iter::chain( + [dheight as usize, dwidth as usize], + indices.iter().map(|s| s.dimension as usize), + ) + .collect::>(); let mut slice = view.permuted_axes(axis); for index_selection in indices { diff --git a/crates/viewer/re_view_tensor/src/visualizer_system.rs b/crates/viewer/re_view_tensor/src/visualizer_system.rs index 4946a75be38d..fec864512767 100644 --- a/crates/viewer/re_view_tensor/src/visualizer_system.rs +++ b/crates/viewer/re_view_tensor/src/visualizer_system.rs @@ -8,9 +8,11 @@ use re_viewer_context::{ VisualizerExecutionOutput, VisualizerQueryInfo, VisualizerSystem, typed_fallback_for, }; -#[derive(Clone)] +#[derive(Clone, re_byte_size::SizeBytes)] pub struct TensorVisualization { pub tensor_row_id: RowId, + // Tensor is already counted as part of the store. + #[size_bytes(ignore)] pub tensor: TensorData, pub data_range: ValueRange, } @@ -20,7 +22,10 @@ pub struct TensorSystem; impl IdentifiedViewSystem for TensorSystem { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "Tensor".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "Tensor" + ) } } @@ -70,9 +75,10 @@ impl VisualizerSystem for TensorSystem { } let all_tensors_indexed = all_tensor_chunks.chunks().iter().flat_map(move |chunk| { - chunk - .iter_component_indices(query.timeline) - .zip(chunk.iter_component::()) + std::iter::zip( + chunk.iter_component_indices(query.timeline), + chunk.iter_component::(), + ) }); let all_ranges = results.iter_optional(Tensor::descriptor_value_range().component); diff --git a/crates/viewer/re_view_tensor/tests/snapshots/help_view_tensor_view_mac.png b/crates/viewer/re_view_tensor/tests/snapshots/help_view_tensor_view_mac.png index 22729265d6e9..f25d60ea32a2 100644 --- a/crates/viewer/re_view_tensor/tests/snapshots/help_view_tensor_view_mac.png +++ b/crates/viewer/re_view_tensor/tests/snapshots/help_view_tensor_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de5d837695953543ba190b17d8e94068db3a2db6a3385531a01a3557c6cc5238 -size 18127 +oid sha256:c6b3363a8b2a5f8ea3ee65ceb8de34ee62a2a1f892a8688823d783b4fad38b23 +size 18221 diff --git a/crates/viewer/re_view_tensor/tests/snapshots/help_view_tensor_view_windows.png b/crates/viewer/re_view_tensor/tests/snapshots/help_view_tensor_view_windows.png index 22729265d6e9..f25d60ea32a2 100644 --- a/crates/viewer/re_view_tensor/tests/snapshots/help_view_tensor_view_windows.png +++ b/crates/viewer/re_view_tensor/tests/snapshots/help_view_tensor_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de5d837695953543ba190b17d8e94068db3a2db6a3385531a01a3557c6cc5238 -size 18127 +oid sha256:c6b3363a8b2a5f8ea3ee65ceb8de34ee62a2a1f892a8688823d783b4fad38b23 +size 18221 diff --git a/crates/viewer/re_view_tensor/tests/snapshots/tensor_1d.png b/crates/viewer/re_view_tensor/tests/snapshots/tensor_1d.png index 0834ff5a4b38..2f433112877e 100644 --- a/crates/viewer/re_view_tensor/tests/snapshots/tensor_1d.png +++ b/crates/viewer/re_view_tensor/tests/snapshots/tensor_1d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72fe375ec907115eb3695d483ff1ab131e3ab723a096a1b8ff390e727e738649 -size 4299 +oid sha256:b3d4e70ccfe20d4696b3ae2f80058d6eeedbe19c455c49de4c56493c142369c3 +size 4324 diff --git a/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_both.png b/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_both.png index 0d9bd9c52bcd..e1088a272a50 100644 --- a/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_both.png +++ b/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95a96fce24563968ceb5ed9af3626852d934483ab7808c4d08c7105539be43c6 -size 25132 +oid sha256:ac28f5db3aed496f89e367d78d3713ef98220777c3f2135ef65b2df0c21e3925 +size 25128 diff --git a/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_root.png b/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_root.png index 0d9bd9c52bcd..e1088a272a50 100644 --- a/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_root.png +++ b/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_root.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95a96fce24563968ceb5ed9af3626852d934483ab7808c4d08c7105539be43c6 -size 25132 +oid sha256:ac28f5db3aed496f89e367d78d3713ef98220777c3f2135ef65b2df0c21e3925 +size 25128 diff --git a/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_t1.png b/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_t1.png index e4f48ae05ec2..eab1e46e47c5 100644 --- a/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_t1.png +++ b/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_t1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87280a48df25affca76407a8e1d69c8bb69a106de2fe0583a83dc21684c47d61 -size 9537 +oid sha256:c6650d61d8d75200277f7df9617d2949678277e7425c43e68bc15d854db8bb82 +size 9560 diff --git a/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_t2.png b/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_t2.png index e167d57cb210..d16c1117a254 100644 --- a/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_t2.png +++ b/crates/viewer/re_view_tensor/tests/snapshots/tensor_2d_t2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:972470e3ecf49d1aeddcd5849a79cbae514a6e234aee976ee2828cc134c063fb -size 8517 +oid sha256:39e30f2a4e9c3135e30c59c1be156debc33e37b5eb8caf340fae4422f67f6e01 +size 8544 diff --git a/crates/viewer/re_view_text_document/Cargo.toml b/crates/viewer/re_view_text_document/Cargo.toml index b0ef1ac2f619..5e2d90d7db92 100644 --- a/crates/viewer/re_view_text_document/Cargo.toml +++ b/crates/viewer/re_view_text_document/Cargo.toml @@ -22,12 +22,14 @@ all-features = true default = [] [dependencies] +re_byte_size.workspace = true re_chunk_store.workspace = true re_view.workspace = true re_tracing.workspace = true re_sdk_types.workspace = true re_ui.workspace = true re_viewer_context.workspace = true +re_viewport_blueprint.workspace = true egui.workspace = true egui_commonmark.workspace = true @@ -36,4 +38,3 @@ egui_commonmark.workspace = true re_log_types.workspace = true re_test_context.workspace = true re_test_viewport.workspace = true -re_viewport_blueprint.workspace = true diff --git a/crates/viewer/re_view_text_document/src/view_class.rs b/crates/viewer/re_view_text_document/src/view_class.rs index 6eb0c4d612f1..e8d5dc1d36e9 100644 --- a/crates/viewer/re_view_text_document/src/view_class.rs +++ b/crates/viewer/re_view_text_document/src/view_class.rs @@ -1,30 +1,32 @@ use egui::{Label, Sense}; +use re_sdk_types::blueprint::archetypes::TextDocumentFormat; +use re_sdk_types::blueprint::components::Enabled; use re_sdk_types::{View as _, ViewClassIdentifier}; use re_ui::{Help, UiExt as _}; use re_viewer_context::external::re_log_types::EntityPath; use re_viewer_context::{ IdentifiedViewSystem as _, Item, SystemCommand, SystemCommandSender as _, ViewClass, - ViewClassRegistryError, ViewId, ViewQuery, ViewState, ViewStateExt as _, + ViewClassExt as _, ViewClassRegistryError, ViewId, ViewQuery, ViewState, ViewStateExt as _, ViewSystemExecutionError, ViewerContext, suggest_view_for_each_entity, }; +use re_viewport_blueprint::ViewProperty; use crate::visualizer_system::{TextDocumentEntry, TextDocumentSystem}; -// TODO(andreas): This should be a blueprint component. - +#[derive(Default)] pub struct TextDocumentViewState { - monospace: bool, - word_wrap: bool, commonmark_cache: egui_commonmark::CommonMarkCache, + only_showing_markdown: bool, } -impl Default for TextDocumentViewState { - fn default() -> Self { - Self { - monospace: false, - word_wrap: true, - commonmark_cache: Default::default(), - } +impl re_byte_size::SizeBytes for TextDocumentViewState { + fn heap_size_bytes(&self) -> u64 { + let Self { + commonmark_cache, + only_showing_markdown: _, + } = self; + // Most of the memory not tracked unfortunately. + commonmark_cache.link_hooks().heap_size_bytes() } } @@ -36,6 +38,10 @@ impl ViewState for TextDocumentViewState { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } } #[derive(Default)] @@ -66,6 +72,11 @@ impl ViewClass for TextDocumentView { &self, system_registry: &mut re_viewer_context::ViewSystemRegistrator<'_>, ) -> Result<(), ViewClassRegistryError> { + system_registry.register_fallback_provider( + TextDocumentFormat::descriptor_word_wrap().component, + |_ctx| Enabled::from(true), + ); + system_registry.register_visualizer::() } @@ -79,23 +90,20 @@ impl ViewClass for TextDocumentView { fn selection_ui( &self, - _ctx: &ViewerContext<'_>, + ctx: &ViewerContext<'_>, ui: &mut egui::Ui, state: &mut dyn ViewState, - _space_origin: &EntityPath, - _view_id: ViewId, + space_origin: &EntityPath, + view_id: ViewId, ) -> Result<(), ViewSystemExecutionError> { - let state = state.downcast_mut::()?; + let state = state.downcast_ref::()?; - ui.selection_grid("text_config").show(ui, |ui| { - ui.grid_left_hand_label("Text style"); - ui.vertical(|ui| { - ui.re_radio_value(&mut state.monospace, false, "Proportional"); - ui.re_radio_value(&mut state.monospace, true, "Monospace"); - ui.re_checkbox(&mut state.word_wrap, "Word Wrap"); + if !state.only_showing_markdown { + ui.list_item_scope("text_document_selection_ui", |ui| { + let ctx = self.view_context(ctx, view_id, state, space_origin); + re_view::view_property_ui::(&ctx, ui); }); - ui.end_row(); - }); + } Ok(()) } @@ -121,25 +129,34 @@ impl ViewClass for TextDocumentView { ) -> Result<(), ViewSystemExecutionError> { let tokens = ui.tokens(); let state = state.downcast_mut::()?; - let text_entries = system_output - .visualizer_data::>(TextDocumentSystem::identifier())?; + let text_entries = system_output.visualizer_data_or_default::>( + TextDocumentSystem::identifier(), + )?; + state.only_showing_markdown = !text_entries.is_empty() + && text_entries + .iter() + .all(|entry| entry.media_type == re_sdk_types::components::MediaType::markdown()); let frame = egui::Frame::new().inner_margin(tokens.view_padding()); - let response = frame + let (response, text_document_result) = frame .show(ui, |ui| { let inner_ui_builder = egui::UiBuilder::new() .layout(egui::Layout::top_down(egui::Align::LEFT)) .sense(Sense::click()); ui.scope_builder(inner_ui_builder, |ui| { - egui::ScrollArea::both() + let text_document_result = egui::ScrollArea::both() .auto_shrink([false, false]) - .show(ui, |ui| text_document_ui(ui, state, text_entries)); + .show(ui, |ui| { + text_document_ui(ctx, query, ui, state, &text_entries) + }) + .inner; - ui.response() + (ui.response(), text_document_result) }) .inner }) .inner; + text_document_result?; // Since we want the view to be hoverable / clickable when the pointer is over a label // (and we want selectable labels), we need to work around egui's interactions here. @@ -162,10 +179,23 @@ impl ViewClass for TextDocumentView { } fn text_document_ui( + ctx: &ViewerContext<'_>, + query: &ViewQuery<'_>, ui: &mut egui::Ui, state: &mut TextDocumentViewState, text_entries: &[TextDocumentEntry], -) { +) -> Result<(), ViewSystemExecutionError> { + let view_ctx = TextDocumentView.view_context(ctx, query.view_id, state, query.space_origin); + let format_property = ViewProperty::from_archetype::(&view_ctx); + let monospace = format_property.component_or_fallback::( + &view_ctx, + TextDocumentFormat::descriptor_monospace().component, + )?; + let word_wrap = format_property.component_or_fallback::( + &view_ctx, + TextDocumentFormat::descriptor_word_wrap().component, + )?; + if text_entries.is_empty() { // We get here if we scroll back time to before the first text document was logged. ui.weak("(empty)"); @@ -188,11 +218,11 @@ fn text_document_ui( } else { let mut text = egui::RichText::new(body.as_str()); - if state.monospace { + if **monospace { text = text.monospace(); } - ui.add(Label::new(text).wrap_mode(if state.word_wrap { + ui.add(Label::new(text).wrap_mode(if **word_wrap { egui::TextWrapMode::Wrap } else { egui::TextWrapMode::Extend @@ -207,6 +237,8 @@ fn text_document_ui( text_entries.len() )); } + + Ok(()) } #[test] diff --git a/crates/viewer/re_view_text_document/src/visualizer_system.rs b/crates/viewer/re_view_text_document/src/visualizer_system.rs index 947daf65bcc1..da0c41b95b85 100644 --- a/crates/viewer/re_view_text_document/src/visualizer_system.rs +++ b/crates/viewer/re_view_text_document/src/visualizer_system.rs @@ -22,7 +22,10 @@ pub struct TextDocumentSystem; impl IdentifiedViewSystem for TextDocumentSystem { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "TextDocument".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "TextDocument" + ) } } diff --git a/crates/viewer/re_view_text_document/tests/snapshots/help_view_text_document_view_mac.png b/crates/viewer/re_view_text_document/tests/snapshots/help_view_text_document_view_mac.png index 11d958758806..0cd5625f0e7a 100644 --- a/crates/viewer/re_view_text_document/tests/snapshots/help_view_text_document_view_mac.png +++ b/crates/viewer/re_view_text_document/tests/snapshots/help_view_text_document_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da71a3f7c55f87b4967048b0846b312e251af6d401d3c06f90ceeef6f5aeb8df -size 7579 +oid sha256:23c181e9990d262a6fad233afe9680eb2005e712da3dfd90e1c04f281313b456 +size 7603 diff --git a/crates/viewer/re_view_text_document/tests/snapshots/help_view_text_document_view_windows.png b/crates/viewer/re_view_text_document/tests/snapshots/help_view_text_document_view_windows.png index 11d958758806..0cd5625f0e7a 100644 --- a/crates/viewer/re_view_text_document/tests/snapshots/help_view_text_document_view_windows.png +++ b/crates/viewer/re_view_text_document/tests/snapshots/help_view_text_document_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da71a3f7c55f87b4967048b0846b312e251af6d401d3c06f90ceeef6f5aeb8df -size 7579 +oid sha256:23c181e9990d262a6fad233afe9680eb2005e712da3dfd90e1c04f281313b456 +size 7603 diff --git a/crates/viewer/re_view_text_document/tests/snapshots/text_view_both.png b/crates/viewer/re_view_text_document/tests/snapshots/text_view_both.png index 4b49bf735d5a..a2c99d37cf3e 100644 --- a/crates/viewer/re_view_text_document/tests/snapshots/text_view_both.png +++ b/crates/viewer/re_view_text_document/tests/snapshots/text_view_both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c23bfd8b43925f588ab925ac8512676990050469715e43405a7ef7227e18abd -size 22380 +oid sha256:adf546e50c1f2c78e55bc2b6bea67ede32f8e171cc0b483385e0dd5932e714a8 +size 22274 diff --git a/crates/viewer/re_view_text_document/tests/snapshots/text_view_one.png b/crates/viewer/re_view_text_document/tests/snapshots/text_view_one.png index a57c0cea1d43..f7ec7ff9f34a 100644 --- a/crates/viewer/re_view_text_document/tests/snapshots/text_view_one.png +++ b/crates/viewer/re_view_text_document/tests/snapshots/text_view_one.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:211c3f52480f8b0c55921d86cd9a70d0ea2eadad129e996694dff3d71090eebd -size 3121 +oid sha256:c44d544970a794aeba08da50882761ae9b5c845da5d3b44ad2d8e8763e901fe6 +size 3123 diff --git a/crates/viewer/re_view_text_document/tests/snapshots/text_view_root.png b/crates/viewer/re_view_text_document/tests/snapshots/text_view_root.png index 4b49bf735d5a..a2c99d37cf3e 100644 --- a/crates/viewer/re_view_text_document/tests/snapshots/text_view_root.png +++ b/crates/viewer/re_view_text_document/tests/snapshots/text_view_root.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c23bfd8b43925f588ab925ac8512676990050469715e43405a7ef7227e18abd -size 22380 +oid sha256:adf546e50c1f2c78e55bc2b6bea67ede32f8e171cc0b483385e0dd5932e714a8 +size 22274 diff --git a/crates/viewer/re_view_text_document/tests/snapshots/text_view_two.png b/crates/viewer/re_view_text_document/tests/snapshots/text_view_two.png index 99de60b3a31b..f9d26100c1ec 100644 --- a/crates/viewer/re_view_text_document/tests/snapshots/text_view_two.png +++ b/crates/viewer/re_view_text_document/tests/snapshots/text_view_two.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d62f1dbc1639a903b4c9d4ba284b09a8f9dfd0d168d899a5efc341aaf923fe3d -size 3145 +oid sha256:9163df8fd99b1a04f2c1eaac86cefa6ef098d414b64422cda45e6ac56fec853a +size 3147 diff --git a/crates/viewer/re_view_text_log/Cargo.toml b/crates/viewer/re_view_text_log/Cargo.toml index 61937cc712de..25831649d2b7 100644 --- a/crates/viewer/re_view_text_log/Cargo.toml +++ b/crates/viewer/re_view_text_log/Cargo.toml @@ -19,6 +19,7 @@ workspace = true all-features = true [dependencies] +re_byte_size.workspace = true re_chunk_store.workspace = true re_data_ui.workspace = true re_entity_db.workspace = true @@ -37,4 +38,6 @@ egui.workspace = true itertools.workspace = true [dev-dependencies] +re_chunk.workspace = true re_test_context.workspace = true +re_test_viewport.workspace = true diff --git a/crates/viewer/re_view_text_log/src/view_class.rs b/crates/viewer/re_view_text_log/src/view_class.rs index f47f35097129..24cefe1543a1 100644 --- a/crates/viewer/re_view_text_log/src/view_class.rs +++ b/crates/viewer/re_view_text_log/src/view_class.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use re_data_ui::item_ui::{self, timeline_button}; +use re_log::ResultExt as _; use re_log_types::{EntityPath, TimelineName}; use re_sdk_types::blueprint::archetypes::{TextLogColumns, TextLogFormat, TextLogRows}; use re_sdk_types::blueprint::components::{Enabled, TextLogColumn, TimelineColumn}; @@ -19,7 +20,7 @@ use re_viewport_blueprint::ViewProperty; use super::visualizer_system::{Entry, TextLogSystem}; // TODO(andreas): This should be a blueprint component. -#[derive(Clone, PartialEq, Eq, Default)] +#[derive(Clone, PartialEq, Eq, Default, re_byte_size::SizeBytes)] pub struct TextViewState { /// Keeps track of the latest time selection made by the user. /// @@ -27,6 +28,15 @@ pub struct TextViewState { /// text entry window however they please when the time cursor isn't moving. latest_time: i64, + /// Time of the latest entry at or before the cursor on the previous render. + /// + /// We auto-scroll whenever this changes so the view tracks the latest-at + /// row as new (possibly out-of-order) data streams in. This handles both + /// the initial catch-up to a programmatic `SetTime` (e.g. a `#when` URL + /// anchor pointing past the data loaded so far) and any later arrival + /// that lands closer to the cursor. + last_anchor_time: Option, + seen_levels: BTreeSet, last_columns_min_sizes: Vec, @@ -40,6 +50,10 @@ impl ViewState for TextViewState { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } } #[derive(Default)] @@ -187,25 +201,13 @@ Filter message types and toggle column visibility in a selection panel.", let tokens = ui.tokens(); let state = state.downcast_mut::()?; - let text = system_output.visualizer_data::>(TextLogSystem::identifier())?; - - let columns_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); - let rows_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); - let format_property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - query.view_id, - ); + let text = + system_output.visualizer_data_or_default::>(TextLogSystem::identifier())?; let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); + let columns_property = ViewProperty::from_archetype::(&view_ctx); + let rows_property = ViewProperty::from_archetype::(&view_ctx); + let format_property = ViewProperty::from_archetype::(&view_ctx); let monospace_body = format_property.component_or_fallback::( &view_ctx, @@ -226,7 +228,7 @@ Filter message types and toggle column visibility in a selection panel.", TextLogRows::descriptor_filter_by_log_level().component, )?; - for te in text { + for te in text.iter() { if let Some(lvl) = &te.level { state.seen_levels.insert(lvl.to_string()); } @@ -249,14 +251,20 @@ Filter message types and toggle column visibility in a selection panel.", ..egui::Frame::default() } .show(ui, |ui| { - // Did the time cursor move since last time? - // - If it did, autoscroll to the text log to reveal the current time. - // - Otherwise, let the user scroll around freely! + // Auto-scroll when the time cursor moves, or whenever the + // latest-at row shifts because new (possibly out-of-order) data + // landed closer to the cursor. + let anchor_time = entries + .partition_point(|te| te.time.as_i64() <= time) + .checked_sub(1) + .map(|i| entries[i].time.as_i64()); + let anchor_moved = anchor_time != state.last_anchor_time; let time_cursor_moved = state.latest_time != time; - let scroll_to_row = time_cursor_moved.then(|| { + let scroll_to_row = (time_cursor_moved || anchor_moved).then(|| { re_tracing::profile_scope!("search scroll time"); entries.partition_point(|te| te.time.as_i64() < time) }); + state.last_anchor_time = anchor_time; ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| { egui::ScrollArea::horizontal().show(ui, |ui| { @@ -285,7 +293,6 @@ Filter message types and toggle column visibility in a selection panel.", /// `scroll_to_row` indicates how far down we want to scroll in terms of logical rows, /// as opposed to `scroll_to_offset` (computed below) which is how far down we want to /// scroll in terms of actual points. -#[expect(clippy::too_many_arguments)] fn table_ui( ctx: &ViewerContext<'_>, ui: &mut egui::Ui, @@ -365,7 +372,11 @@ fn table_ui( } header.col(|ui| { - timeline_button(&ctx.app_ctx, ui, &TimelineName::new(&col.timeline)); + if let Some(timeline) = + TimelineName::try_new(col.timeline.as_str()).ok_or_log_error_once() + { + timeline_button(&ctx.app_ctx, ui, &timeline); + } }); } for col in columns { @@ -393,7 +404,11 @@ fn table_ui( continue; } - let timeline = TimelineName::new(&col.timeline); + let Some(timeline) = + TimelineName::try_new(col.timeline.as_str()).ok_or_log_error_once() + else { + continue; + }; row.col(|ui| { let row_time = entry @@ -478,11 +493,7 @@ fn column_name_ui(ui: &mut egui::Ui, column: &bp_datatypes::TextLogColumnKind) - /// /// This could potentially be avoided if we could add component ui's from this crate. fn view_property_ui_rows(ctx: &ViewContext<'_>, ui: &mut egui::Ui) { - let property = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - ctx.view_id, - ); + let property = ViewProperty::from_archetype::(ctx); let reflection = ctx.viewer_ctx.reflection(); let Some(reflection) = reflection.archetypes.get(&property.archetype_name) else { @@ -523,20 +534,17 @@ fn view_property_ui_rows(ctx: &ViewContext<'_>, ui: &mut egui::Ui) { return; }; - let mut new_levels = state - .seen_levels - .iter() - .map(|s| { + let mut new_levels = std::iter::chain( + state.seen_levels.iter().map(|s| { let level_active = levels.iter().any(|l| l.as_str() == s); (s.clone(), level_active) - }) - .chain( - levels - .iter() - .filter(|lvl| !state.seen_levels.contains(lvl.as_str())) - .map(|lvl| (lvl.as_str().to_owned(), true)), - ) - .collect::>(); + }), + levels + .iter() + .filter(|lvl| !state.seen_levels.contains(lvl.as_str())) + .map(|lvl| (lvl.as_str().to_owned(), true)), + ) + .collect::>(); let mut any_change = false; for (lvl, active) in &mut new_levels { diff --git a/crates/viewer/re_view_text_log/src/visualizer_system.rs b/crates/viewer/re_view_text_log/src/visualizer_system.rs index 0c4c387619cd..7491e727c89b 100644 --- a/crates/viewer/re_view_text_log/src/visualizer_system.rs +++ b/crates/viewer/re_view_text_log/src/visualizer_system.rs @@ -28,7 +28,10 @@ pub struct TextLogSystem; impl IdentifiedViewSystem for TextLogSystem { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "TextLog".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "TextLog" + ) } } diff --git a/crates/viewer/re_view_text_log/tests/basic.rs b/crates/viewer/re_view_text_log/tests/basic.rs new file mode 100644 index 000000000000..d33c512fb97d --- /dev/null +++ b/crates/viewer/re_view_text_log/tests/basic.rs @@ -0,0 +1,67 @@ +use re_chunk::Chunk; +use re_log_types::{TimeInt, Timeline}; +use re_sdk_types::archetypes::TextLog; +use re_test_context::TestContext; +use re_test_context::external::egui_kittest::SnapshotResults; +use re_test_viewport::TestContextExt as _; +use re_view_text_log::TextView; +use re_viewer_context::{TimeControlCommand, ViewClass as _, ViewId}; +use re_viewport_blueprint::ViewBlueprint; + +fn setup_blueprint(test_context: &mut TestContext) -> ViewId { + test_context.setup_viewport_blueprint(|_ctx, blueprint| { + blueprint.add_view_at_root(ViewBlueprint::new_with_root_wildcard(TextView::identifier())) + }) +} + +#[test] +fn temporal_anchor_between_sequence_steps() { + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + let chunks = &mut text_log_chunks(timeline); + + // Only add the first one (tick = 0) + test_context.add_chunks(chunks.take(1)); + test_context.set_active_timeline(*timeline.name()); + + // The temporal anchor is intentionally between two sequence steps. + test_context.send_time_commands( + test_context.active_store_id(), + [TimeControlCommand::SetTime( + TimeInt::new_temporal(100).into(), + )], + ); + test_context.handle_system_commands(&egui::Context::default()); + + let view_id = setup_blueprint(&mut test_context); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "text_log_temporal_anchor_between_steps_first_chunk", + egui::vec2(500.0, 180.0), + None, + )); + + // Add the rest + test_context.add_chunks(chunks); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "text_log_temporal_anchor_between_steps_rest", + egui::vec2(500.0, 180.0), + None, + )); +} + +fn text_log_chunks(timeline: Timeline) -> impl Iterator { + (0_i64..=200).step_by(10).map(move |tick| { + Chunk::builder("logs") + .with_archetype_auto_row( + [(timeline, tick)], + &TextLog::new(format!("Log at tick {tick}")), + ) + .build() + .expect("failed to build chunk") + }) +} diff --git a/crates/viewer/re_view_text_log/tests/snapshots/help_view_text_log_view_mac.png b/crates/viewer/re_view_text_log/tests/snapshots/help_view_text_log_view_mac.png index c51ce9d77f7f..537ccd266a66 100644 --- a/crates/viewer/re_view_text_log/tests/snapshots/help_view_text_log_view_mac.png +++ b/crates/viewer/re_view_text_log/tests/snapshots/help_view_text_log_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:713fe47ede572eb333ced410f3c0ec65878005b5aa684ee9a2dd0a6aa9629f7f -size 14750 +oid sha256:eb6e53d602fb86a1c8600ac73a8a000775ce54175cb8832b192ac2f4df8dbafc +size 14897 diff --git a/crates/viewer/re_view_text_log/tests/snapshots/help_view_text_log_view_windows.png b/crates/viewer/re_view_text_log/tests/snapshots/help_view_text_log_view_windows.png index c51ce9d77f7f..537ccd266a66 100644 --- a/crates/viewer/re_view_text_log/tests/snapshots/help_view_text_log_view_windows.png +++ b/crates/viewer/re_view_text_log/tests/snapshots/help_view_text_log_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:713fe47ede572eb333ced410f3c0ec65878005b5aa684ee9a2dd0a6aa9629f7f -size 14750 +oid sha256:eb6e53d602fb86a1c8600ac73a8a000775ce54175cb8832b192ac2f4df8dbafc +size 14897 diff --git a/crates/viewer/re_view_text_log/tests/snapshots/text_log_temporal_anchor_between_steps_first_chunk.png b/crates/viewer/re_view_text_log/tests/snapshots/text_log_temporal_anchor_between_steps_first_chunk.png new file mode 100644 index 000000000000..1740649f1594 --- /dev/null +++ b/crates/viewer/re_view_text_log/tests/snapshots/text_log_temporal_anchor_between_steps_first_chunk.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:078e6d2ed16db1ba3203c5a8723208c404d674dc504ca7ce3d9063d311d2f03a +size 9725 diff --git a/crates/viewer/re_view_text_log/tests/snapshots/text_log_temporal_anchor_between_steps_rest.png b/crates/viewer/re_view_text_log/tests/snapshots/text_log_temporal_anchor_between_steps_rest.png new file mode 100644 index 000000000000..4db611101f3f --- /dev/null +++ b/crates/viewer/re_view_text_log/tests/snapshots/text_log_temporal_anchor_between_steps_rest.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4d0f155338c94880fc4e57e6f5bc2b9846d44ea50af6a637714856075d50df7 +size 27848 diff --git a/crates/viewer/re_view_time_series/Cargo.toml b/crates/viewer/re_view_time_series/Cargo.toml index 42f56bae9597..ab95d66fefb8 100644 --- a/crates/viewer/re_view_time_series/Cargo.toml +++ b/crates/viewer/re_view_time_series/Cargo.toml @@ -19,7 +19,7 @@ workspace = true all-features = true [dependencies] -re_byte_size.workspace = true +re_byte_size = { workspace = true, features = ["egui"] } re_chunk_store.workspace = true re_component_ui.workspace = true re_format.workspace = true @@ -48,6 +48,7 @@ smallvec.workspace = true vec1.workspace = true [dev-dependencies] +re_chunk.workspace = true re_chunk_store.workspace = true re_test_context.workspace = true re_test_viewport.workspace = true diff --git a/crates/viewer/re_view_time_series/src/aggregation.rs b/crates/viewer/re_view_time_series/src/aggregation.rs index 9462ac7e4c5f..a05e121084c9 100644 --- a/crates/viewer/re_view_time_series/src/aggregation.rs +++ b/crates/viewer/re_view_time_series/src/aggregation.rs @@ -14,6 +14,8 @@ //! The first and last output points are time-aligned to the input's time bounds //! to prevent visual glitches at the edges. +use egui::emath::fast_midpoint; + use crate::{PlotPoint, PlotPointAttrs}; /// Implements aggregation behaviors for `Average`. @@ -193,9 +195,9 @@ impl MinMaxAggregator { Self::MinMaxAverage => { // Don't average a single point with itself. if j > 1 { - acc_min.value = (acc_min.value + acc_max.value) * 0.5; + acc_min.value = fast_midpoint(acc_min.value, acc_max.value); acc_min.attrs.radius_ui = - (acc_min.attrs.radius_ui + acc_max.attrs.radius_ui) * 0.5; + fast_midpoint(acc_min.attrs.radius_ui, acc_max.attrs.radius_ui); } aggregated.push(acc_min); } diff --git a/crates/viewer/re_view_time_series/src/fallbacks.rs b/crates/viewer/re_view_time_series/src/fallbacks.rs index 0d756821fd1e..dc1457d29a33 100644 --- a/crates/viewer/re_view_time_series/src/fallbacks.rs +++ b/crates/viewer/re_view_time_series/src/fallbacks.rs @@ -1,10 +1,11 @@ +use re_sdk_types::archetypes::Scalars; use re_sdk_types::blueprint::archetypes::{PlotLegend, ScalarAxis, TimeAxis}; use re_sdk_types::datatypes::TimeRange; use re_sdk_types::{ archetypes::{SeriesLines, SeriesPoints}, datatypes::TimeRangeBoundary, }; -use re_viewer_context::ViewStateExt as _; +use re_viewer_context::{ViewStateExt as _, VisualizerComponentSource}; use crate::view_class::{TimeSeriesViewState, add_margin_to_range, make_range_sane}; @@ -84,10 +85,40 @@ pub fn register_fallbacks(system_registry: &mut re_viewer_context::ViewSystemReg .and_then(|id| state.num_time_series_last_frame_per_instruction.get(&id)) .map_or(1, |set| set.len()); + // There can be several visualizer instructions on the same entity + // and for the same component so we additionally look at the + // `(source_component, selector)` pair. + let source_selector = ctx.instruction_id.and_then(|id| { + let data_result = ctx + .view_ctx + .query_result + .tree + .lookup_result_by_visualizer_instruction(id)?; + let instruction = data_result + .visualizer_instructions + .iter() + .find(|instr| instr.id == id)?; + match instruction + .component_mappings + .get(&Scalars::descriptor_scalars().component)? + { + VisualizerComponentSource::SourceComponent { + source_component, + selector, + } => Some((*source_component, selector.as_str())), + VisualizerComponentSource::Override + | VisualizerComponentSource::Default => None, + } + }); + (0..num_series) .map(|i| { - let hash = re_log_types::hash::Hash64::hash((ctx.instruction_id, i)) - .hash64() + let hash = re_log_types::hash::Hash64::hash(( + ctx.target_entity_path, + source_selector, + i, + )) + .hash64() % u16::MAX as u64; re_viewer_context::auto_color_egui(hash as u16).into() }) diff --git a/crates/viewer/re_view_time_series/src/line_visualizer_system.rs b/crates/viewer/re_view_time_series/src/line_visualizer_system.rs index 7f8dc4cce78a..5457a9b0a7ed 100644 --- a/crates/viewer/re_view_time_series/src/line_visualizer_system.rs +++ b/crates/viewer/re_view_time_series/src/line_visualizer_system.rs @@ -1,11 +1,10 @@ use itertools::Itertools as _; use rayon::prelude::*; -use re_chunk_store::{LatestAtQuery, RangeQuery, RowId}; -use re_log_types::{EntityPath, TimeInt}; +use re_log_types::TimeInt; use re_sdk_types::components::{self, AggregationPolicy, InterpolationMode, StrokeWidth}; use re_sdk_types::reflection::Enum as _; use re_sdk_types::{Archetype as _, archetypes}; -use re_view::{ChunksWithComponent, range_with_blueprint_resolved_data}; +use re_view::{ChunksWithComponent, collect_recursive_clears, range_with_blueprint_resolved_data}; use re_viewer_context::external::re_entity_db::InstancePath; use re_viewer_context::{ IdentifiedViewSystem, SingleRequiredComponentConstraint, ViewContext, ViewQuery, @@ -20,6 +19,7 @@ use crate::series_query::{ use crate::{PlotPoint, PlotPointAttrs, PlotSeries, PlotSeriesKind, util}; /// Output data from [`SeriesLinesSystem`]. +#[derive(Default, Clone)] pub struct SeriesLinesOutput { pub all_series: Vec, } @@ -30,7 +30,10 @@ pub struct SeriesLinesSystem; impl IdentifiedViewSystem for SeriesLinesSystem { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "SeriesLines".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "SeriesLines" + ) } } @@ -48,11 +51,12 @@ impl VisualizerSystem for SeriesLinesSystem { .with_allow_static_data(false) .into(), - queried: archetypes::Scalars::all_components() - .iter() - .chain(archetypes::SeriesLines::all_components().iter()) - .cloned() - .collect(), + queried: std::iter::chain( + archetypes::Scalars::all_components().iter(), + archetypes::SeriesLines::all_components().iter(), + ) + .cloned() + .collect(), } } @@ -362,8 +366,10 @@ impl SeriesLinesSystem { None, &query, data_result, - archetypes::Scalars::all_component_identifiers() - .chain(archetypes::SeriesLines::all_component_identifiers()), + std::iter::chain( + archetypes::Scalars::all_component_identifiers(), + archetypes::SeriesLines::all_component_identifiers(), + ), instruction, ); @@ -571,76 +577,3 @@ impl SeriesLinesSystem { series } } - -fn collect_recursive_clears( - ctx: &ViewContext<'_>, - query: &RangeQuery, - entity_path: &EntityPath, -) -> Vec<(TimeInt, RowId)> { - re_tracing::profile_function!(); - - let mut cleared_indices = Vec::new(); - - let mut clear_entity_path = entity_path.clone(); - let clear_descriptor = archetypes::Clear::descriptor_is_recursive(); - - // Bootstrap in case there's a pending clear out of the visible time range. - { - let results = ctx.recording_engine().cache().latest_at( - &LatestAtQuery::new(query.timeline, query.range.min()), - &clear_entity_path, - [clear_descriptor.component], - ); - - cleared_indices.extend( - results - .get(clear_descriptor.component) - .iter() - .flat_map(|chunk| { - itertools::izip!( - chunk.iter_component_indices(*query.timeline(), clear_descriptor.component), - chunk.iter_slices::(clear_descriptor.component) - ) - }) - .filter_map(|(index, is_recursive_buffer)| { - let is_recursive = - !is_recursive_buffer.is_empty() && is_recursive_buffer.value(0); - (is_recursive || clear_entity_path == *entity_path).then_some(index) - }), - ); - } - - loop { - let results = ctx.recording_engine().cache().range( - query, - &clear_entity_path, - [clear_descriptor.component], - ); - - cleared_indices.extend( - results - .get(clear_descriptor.component) - .unwrap_or_default() - .iter() - .flat_map(|chunk| { - itertools::izip!( - chunk.iter_component_indices(*query.timeline(), clear_descriptor.component), - chunk.iter_slices::(clear_descriptor.component) - ) - }) - .filter_map(|(index, is_recursive_buffer)| { - let is_recursive = - !is_recursive_buffer.is_empty() && is_recursive_buffer.value(0); - (is_recursive || clear_entity_path == *entity_path).then_some(index) - }), - ); - - let Some(parent_entity_path) = clear_entity_path.parent() else { - break; - }; - - clear_entity_path = parent_entity_path; - } - - cleared_indices -} diff --git a/crates/viewer/re_view_time_series/src/markers.rs b/crates/viewer/re_view_time_series/src/markers.rs index 0983be83a6a0..ead8847055dc 100644 --- a/crates/viewer/re_view_time_series/src/markers.rs +++ b/crates/viewer/re_view_time_series/src/markers.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use glam::{Vec2, Vec3, vec2, vec3}; +use itertools::Itertools as _; use re_renderer::{ Color32, OutlineMaskPreference, PickingLayerId, RenderContext, ShapeBuilder, mesh::{CpuMesh, GpuMesh, MeshError}, @@ -43,7 +44,7 @@ impl MarkerMeshes { let cpu_mesh = build_marker_cpu_mesh(shape, render_ctx); GpuMesh::new(render_ctx, &cpu_mesh).map(Arc::new) }) - .collect::, _>>()?; + .try_collect()?; Ok(Self { meshes }) } diff --git a/crates/viewer/re_view_time_series/src/point_visualizer_system.rs b/crates/viewer/re_view_time_series/src/point_visualizer_system.rs index add489b5093a..1326a2320455 100644 --- a/crates/viewer/re_view_time_series/src/point_visualizer_system.rs +++ b/crates/viewer/re_view_time_series/src/point_visualizer_system.rs @@ -18,6 +18,7 @@ use crate::series_query::{ use crate::{PlotPoint, PlotPointAttrs, PlotSeries, PlotSeriesKind, ScatterAttrs, util}; /// Output data from [`SeriesPointsSystem`]. +#[derive(Default, Clone)] pub struct SeriesPointsOutput { pub all_series: Vec, } @@ -28,7 +29,10 @@ pub struct SeriesPointsSystem; impl IdentifiedViewSystem for SeriesPointsSystem { fn identifier() -> re_viewer_context::ViewSystemIdentifier { - "SeriesPoints".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "SeriesPoints" + ) } } @@ -45,11 +49,12 @@ impl VisualizerSystem for SeriesPointsSystem { .with_additional_physical_types(util::series_supported_datatypes()) .with_allow_static_data(false) .into(), - queried: archetypes::Scalars::all_components() - .iter() - .chain(archetypes::SeriesPoints::all_components().iter()) - .cloned() - .collect(), + queried: std::iter::chain( + archetypes::Scalars::all_components().iter(), + archetypes::SeriesPoints::all_components().iter(), + ) + .cloned() + .collect(), } } @@ -213,8 +218,10 @@ impl SeriesPointsSystem { None, &query, data_result, - archetypes::Scalars::all_component_identifiers() - .chain(archetypes::SeriesPoints::all_component_identifiers()), + std::iter::chain( + archetypes::Scalars::all_component_identifiers(), + archetypes::SeriesPoints::all_component_identifiers(), + ), instruction, ); @@ -317,7 +324,7 @@ impl SeriesPointsSystem { let all_marker_shapes_chunks = marker_iter.chunks().iter().collect_vec(); if all_marker_shapes_chunks.len() == 1 - && all_marker_shapes_chunks[0].chunk.is_static() + && all_marker_shapes_chunks[0].chunk.num_rows() == 1 { re_tracing::profile_scope!("override/default fast path"); @@ -325,10 +332,10 @@ impl SeriesPointsSystem { .iter_component::() .next() { - for (points, marker_shape) in points_per_series - .iter_mut() - .zip(clamped_or_nothing(marker_shapes.as_slice(), num_series)) - { + for (points, marker_shape) in std::iter::zip( + points_per_series.iter_mut(), + clamped_or_nothing(marker_shapes.as_slice(), num_series), + ) { for point in points { point.attrs.kind = PlotSeriesKind::Scatter(ScatterAttrs { marker: *marker_shape, @@ -344,10 +351,10 @@ impl SeriesPointsSystem { .component_fallback_registry() .fallback_for(&SeriesPoints::descriptor_markers(), &query_ctx); if let Ok(marker_array) = MarkerShape::from_arrow(&fallback_array) { - for (points, marker) in points_per_series - .iter_mut() - .zip(clamped_or_nothing(&marker_array, num_series)) - { + for (points, marker) in std::iter::zip( + points_per_series.iter_mut(), + clamped_or_nothing(&marker_array, num_series), + ) { for p in points { p.attrs.kind = PlotSeriesKind::Scatter(ScatterAttrs { marker: *marker }); @@ -391,10 +398,10 @@ impl SeriesPointsSystem { } else { all_frames.for_each(|(i, (_index, _scalars, marker_shapes))| { if let Some(marker_shapes) = marker_shapes { - for (points, marker) in points_per_series - .iter_mut() - .zip(clamped_or_nothing(&marker_shapes, num_series)) - { + for (points, marker) in std::iter::zip( + points_per_series.iter_mut(), + clamped_or_nothing(&marker_shapes, num_series), + ) { points[i].attrs.kind = PlotSeriesKind::Scatter(ScatterAttrs { marker: *marker }); } @@ -424,12 +431,8 @@ impl SeriesPointsSystem { series_names.len(), points_per_series.len() ); - for (instance, (points, label, visible)) in itertools::izip!( - points_per_series.into_iter(), - series_names.into_iter(), - series_visibility.into_iter() - ) - .enumerate() + for (instance, (points, label, visible)) in + itertools::izip!(points_per_series, series_names, series_visibility).enumerate() { let instance_path = if num_series == 1 { InstancePath::entity_all(data_result.entity_path.clone()) diff --git a/crates/viewer/re_view_time_series/src/series_query.rs b/crates/viewer/re_view_time_series/src/series_query.rs index 3c8f0c11eaea..cf77dfc5d59d 100644 --- a/crates/viewer/re_view_time_series/src/series_query.rs +++ b/crates/viewer/re_view_time_series/src/series_query.rs @@ -158,7 +158,7 @@ pub fn collect_scalars( .flat_map(|chunk| chunk.iter_slices::()) .enumerate() { - for (points, value) in points_per_series.iter_mut().zip(values) { + for (points, value) in std::iter::zip(&mut *points_per_series, values) { points[i].value = *value; } for points in points_per_series.iter_mut().skip(values.len()) { @@ -192,14 +192,14 @@ pub fn collect_colors( let color_iter = query_results.iter_optional(color_descriptor.component); let all_color_chunks = color_iter.chunks().iter().collect_vec(); - if all_color_chunks.len() == 1 && all_color_chunks[0].chunk.is_static() { + if all_color_chunks.len() == 1 && all_color_chunks[0].chunk.num_rows() == 1 { re_tracing::profile_scope!("override/default fast path"); if let Some(colors) = all_color_chunks[0].iter_slices::().next() { - for (points, color) in points_per_series - .iter_mut() - .zip(clamped_or_nothing(colors, num_series)) - { + for (points, color) in std::iter::zip( + points_per_series.iter_mut(), + clamped_or_nothing(colors, num_series), + ) { let color = map_raw_color(color); for point in points { point.attrs.color = color; @@ -217,10 +217,10 @@ pub fn collect_colors( if let Some(color_array) = fallback_array.as_primitive_opt::() { let fallback_colors = color_array.values(); - for (points, color) in points_per_series - .iter_mut() - .zip(clamped_or_nothing(fallback_colors.as_ref(), num_series)) - { + for (points, color) in std::iter::zip( + points_per_series.iter_mut(), + clamped_or_nothing(fallback_colors.as_ref(), num_series), + ) { let color = map_raw_color(color); for point in points { point.attrs.color = color; @@ -254,10 +254,10 @@ pub fn collect_colors( } else { all_frames.for_each(|(i, (_index, _scalars, colors))| { if let Some(colors) = colors { - for (points, color) in points_per_series - .iter_mut() - .zip(clamped_or_nothing(colors, num_series)) - { + for (points, color) in std::iter::zip( + points_per_series.iter_mut(), + clamped_or_nothing(colors, num_series), + ) { points[i].attrs.color = map_raw_color(color); } } @@ -270,8 +270,7 @@ pub fn collect_colors( /// For selectors like `data[]`, strips the `[]` suffix before adding indices. fn expand_series_names(names: &[String], num_series: usize) -> Vec { let name_count = names.len(); - (0..num_series) - .zip(clamped_or_nothing(names, num_series)) + std::iter::zip(0..num_series, clamped_or_nothing(names, num_series)) .map(|(i, name)| { if i < name_count { name.clone() @@ -350,21 +349,21 @@ pub fn collect_radius_ui( let radius_iter = query_results.iter_optional(radius_descriptor.component); let all_radius_chunks = radius_iter.chunks().iter().collect_vec(); - if all_radius_chunks.len() == 1 && all_radius_chunks[0].chunk.is_static() { + if all_radius_chunks.len() == 1 && all_radius_chunks[0].chunk.num_rows() == 1 { re_tracing::profile_scope!("override/default fast path"); if let Some(radius) = all_radius_chunks[0].iter_slices::().next() { - for (points, radius) in points_per_series - .iter_mut() - .zip(clamped_or_nothing(radius, num_series)) - { + for (points, radius) in std::iter::zip( + points_per_series.iter_mut(), + clamped_or_nothing(radius, num_series), + ) { let radius = radius * radius_multiplier; for point in points { point.attrs.radius_ui = radius; } } } - } else { + } else if !all_radius_chunks.is_empty() { re_tracing::profile_scope!("standard path"); let all_radii = all_radius_chunks.iter().flat_map(|chunk| { @@ -389,10 +388,10 @@ pub fn collect_radius_ui( } else { all_frames.for_each(|(i, (_index, _scalars, radius))| { if let Some(radii) = radius { - for (points, stroke_width) in points_per_series - .iter_mut() - .zip(clamped_or_nothing(radii, num_series)) - { + for (points, stroke_width) in std::iter::zip( + points_per_series.iter_mut(), + clamped_or_nothing(radii, num_series), + ) { points[i].attrs.radius_ui = stroke_width * radius_multiplier; } } diff --git a/crates/viewer/re_view_time_series/src/util.rs b/crates/viewer/re_view_time_series/src/util.rs index 2e800028dde7..9aeeec0c2eea 100644 --- a/crates/viewer/re_view_time_series/src/util.rs +++ b/crates/viewer/re_view_time_series/src/util.rs @@ -9,7 +9,7 @@ use re_viewer_context::{ViewContext, ViewQuery, ViewerContext}; use re_viewport_blueprint::{ViewProperty, ViewPropertyQueryError}; use crate::aggregation::{AverageAggregator, MinMaxAggregator}; -use crate::{PlotPoint, PlotSeries, PlotSeriesKind, ScatterAttrs}; +use crate::{PlotPoint, PlotSeries, PlotSeriesKind}; pub fn series_supported_datatypes() -> impl IntoIterator { [ @@ -75,20 +75,15 @@ pub fn determine_query_range( .time_int() .unwrap_or(re_log_types::TimeInt::ZERO); - let time_axis = ViewProperty::from_archetype::( - ctx.viewer_ctx.blueprint_db(), - ctx.viewer_ctx.blueprint_query, - ctx.view_id, - ); + let time_axis = ViewProperty::from_archetype::(ctx); let link_x_axis = time_axis.component_or_fallback::(ctx, TimeAxis::descriptor_link().component)?; let time_range_property = match link_x_axis { LinkAxis::Independent => &time_axis, - LinkAxis::LinkToGlobal => &ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), + LinkAxis::LinkToGlobal => &ViewProperty::from_archetype_for_view::( + ctx.viewer_ctx, re_viewer_context::GLOBAL_VIEW_ID, ), }; @@ -110,7 +105,6 @@ pub fn determine_query_range( // We have a bunch of raw points, and now we need to group them into individual series. // A series is a continuous run of points with identical attributes: each time // we notice a change in attributes, we need a new series. -#[expect(clippy::too_many_arguments)] pub fn points_to_series( instance_path: InstancePath, time_per_pixel: f64, @@ -138,45 +132,17 @@ pub fn points_to_series( |time| time.as_i64(), ); - if points.len() == 1 { - // Can't draw a single point as a continuous line, so fall back on scatter - let mut kind = points[0].attrs.kind; - if matches!( - kind, - PlotSeriesKind::Continuous | PlotSeriesKind::Stepped(_) - ) { - kind = PlotSeriesKind::Scatter(ScatterAttrs::default()); - } - - let mut series = PlotSeries { - instance_path, - visible, - label: series_label, - color: points[0].attrs.color, - radius_ui: points[0].attrs.radius_ui, - kind, - points: Vec::with_capacity(1), - value_range: None, - aggregator, - aggregation_factor, - min_time, - visualizer_instruction_id, - }; - series.push_point(points[0].time, points[0].value); - all_series.push(series); - } else { - add_series_runs( - instance_path, - visible, - series_label, - points, - aggregator, - aggregation_factor, - min_time, - all_series, - visualizer_instruction_id, - ); - } + add_series_runs( + instance_path, + visible, + series_label, + points, + aggregator, + aggregation_factor, + min_time, + all_series, + visualizer_instruction_id, + ); } /// Apply the given aggregation to the provided points. @@ -244,7 +210,6 @@ pub fn apply_aggregation( (actual_aggregation_factor, points) } -#[expect(clippy::too_many_arguments)] #[expect(clippy::needless_pass_by_value)] #[inline(never)] // Better callstacks on crashes fn add_series_runs( diff --git a/crates/viewer/re_view_time_series/src/view_class.rs b/crates/viewer/re_view_time_series/src/view_class.rs index 383755398ec5..694af12a3be3 100644 --- a/crates/viewer/re_view_time_series/src/view_class.rs +++ b/crates/viewer/re_view_time_series/src/view_class.rs @@ -1,30 +1,31 @@ use ahash::HashMap; -use egui::{NumExt as _, Vec2, Vec2b}; +use egui::{NumExt as _, Vec2, Vec2b, emath::fast_midpoint}; use egui_plot::{Plot, PlotPoint}; -use itertools::{Either, Itertools as _}; +use itertools::{Either, Itertools as _, chain}; use nohash_hasher::{IntMap, IntSet}; use re_chunk_store::TimeType; use re_format::time::next_grid_tick_magnitude_nanos; use re_log_types::external::arrow::datatypes::DataType; -use re_log_types::{AbsoluteTimeRange, EntityPath}; +use re_log_types::{AbsoluteTimeRange, ComponentPath, EntityPath}; use re_sdk_types::archetypes::{Scalars, SeriesLines, SeriesPoints}; use re_sdk_types::blueprint::archetypes::{PlotBackground, PlotLegend, ScalarAxis, TimeAxis}; use re_sdk_types::blueprint::components::{ Corner2D, Enabled, LinkAxis, LockRangeDuringZoom, VisualizerInstructionId, }; use re_sdk_types::components::{AggregationPolicy, Color, Range1D, Visible}; -use re_sdk_types::datatypes::TimeRange; +use re_sdk_types::datatypes::{TimeRange, TimeRangeBoundary}; use re_sdk_types::{ComponentBatch as _, ComponentIdentifier, View as _, ViewClassIdentifier}; use re_ui::{Help, IconText, MouseButtonText, UiExt as _, icons, list_item}; use re_view::controls::{MOVE_TIME_CURSOR_BUTTON, SELECTION_RECT_ZOOM_BUTTON}; use re_view::view_property_ui; use re_viewer_context::{ - BlueprintContext as _, DataResultInteractionAddress, DatatypeMatch, IdentifiedViewSystem as _, - IndicatedEntities, PerVisualizerType, QueryRange, RecommendedMappings, RecommendedView, - RecommendedVisualizers, SingleRequiredComponentMatch, SystemExecutionOutput, - TimeControlCommand, ViewClass, ViewClassExt as _, ViewClassRegistryError, ViewId, ViewQuery, - ViewSpawnHeuristics, ViewState, ViewStateExt as _, ViewSystemExecutionError, - ViewSystemIdentifier, ViewerContext, VisualizableReason, VisualizerComponentSource, + BlueprintContext as _, DataResultInteractionAddress, DatatypeMatch, DragAndDropFeedback, + IdentifiedViewSystem as _, IndicatedEntities, PerVisualizerType, QueryRange, + RecommendedMappings, RecommendedView, RecommendedVisualizers, SingleRequiredComponentMatch, + SystemExecutionOutput, TimeControlCommand, ViewClass, ViewClassExt as _, + ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, ViewStateExt as _, + ViewSystemExecutionError, ViewSystemIdentifier, ViewerContext, VisualizableReason, + VisualizerComponentSource, }; use re_viewport_blueprint::ViewProperty; use smallvec::SmallVec; @@ -38,7 +39,7 @@ use crate::{MAX_NUM_NON_INDICATED_RECOMMENDED_VISUALIZERS_PER_ENTITY, PlotSeries // --- -#[derive(Clone)] +#[derive(Clone, re_byte_size::SizeBytes)] pub struct TimeSeriesViewState { /// The range of the scalar values currently on screen. /// @@ -72,6 +73,8 @@ pub struct TimeSeriesViewState { /// data-space points to screen-space for `re_renderer` primitives. /// /// `None` on the first frame (before `plot.show()` has run). + // `egui_plot::PlotTransform` doesn't impl `SizeBytes`; it's POD with no heap. + #[size_bytes(ignore)] pub plot_transform: Option, /// How many time units correspond to a single physical pixel on the plot. @@ -105,6 +108,10 @@ impl ViewState for TimeSeriesViewState { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn heap_size_bytes(&self) -> u64 { + re_byte_size::SizeBytes::heap_size_bytes(self) + } } #[derive(Default)] @@ -223,12 +230,8 @@ impl ViewClass for TimeSeriesView { view_property_ui::(&ctx, ui); view_property_ui::(&ctx, ui); - let link_x_axis = ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query(), - view_id, - ) - .component_or_fallback::(&ctx, TimeAxis::descriptor_link().component)?; + let link_x_axis = ViewProperty::from_archetype::(&ctx) + .component_or_fallback::(&ctx, TimeAxis::descriptor_link().component)?; match link_x_axis { LinkAxis::Independent => { @@ -275,9 +278,7 @@ impl ViewClass for TimeSeriesView { if !include_entity(entity_path) { return None; } - reason - .full_native_match(Scalars::descriptor_scalars().component) - .then_some(entity_path) + should_auto_spawn_time_series(reason).then_some(entity_path) }); ViewSpawnHeuristics::new_with_order_preserved( @@ -409,6 +410,33 @@ impl ViewClass for TimeSeriesView { }) } + /// Accept drops of scalar components onto the time series view. For each dropped component, a + /// new `SeriesLines` visualizer is added that remaps `Scalars.scalars` from it. + fn handle_component_drop( + &self, + ctx: &ViewerContext<'_>, + view_id: ViewId, + component_paths: &[ComponentPath], + released: bool, + ) -> DragAndDropFeedback { + match re_view::handle_component_drop( + ctx, + view_id, + component_paths, + released, + SeriesLinesSystem::identifier(), + Scalars::descriptor_scalars().component, + ) { + re_view::ComponentDropResult::Accept => DragAndDropFeedback::Accept, + re_view::ComponentDropResult::CompatibleButAlreadyVisualized => { + DragAndDropFeedback::Reject(Some("Already visualized")) + } + re_view::ComponentDropResult::Incompatible => { + DragAndDropFeedback::Reject(Some("Not a scalar component")) + } + } + } + fn ui( &self, ctx: &ViewerContext<'_>, @@ -426,15 +454,13 @@ impl ViewClass for TimeSeriesView { // borrow conflict with view_systems which is borrowed via all_plot_series). let re_renderer_draw_data: Vec<_> = system_output.drain_draw_data().collect(); - let line_series = - system_output.visualizer_data::(SeriesLinesSystem::identifier())?; - let point_series = - system_output.visualizer_data::(SeriesPointsSystem::identifier())?; + let line_series = system_output + .visualizer_data_or_default::(SeriesLinesSystem::identifier())?; + let point_series = system_output + .visualizer_data_or_default::(SeriesPointsSystem::identifier())?; - let all_plot_series: Vec<_> = std::iter::empty() - .chain(line_series.all_series.iter()) - .chain(point_series.all_series.iter()) - .collect(); + let all_plot_series: Vec<_> = + chain!(&line_series.all_series, &point_series.all_series).collect(); state.num_time_series_last_frame_per_instruction.clear(); @@ -552,15 +578,10 @@ impl ViewClass for TimeSeriesView { } } - let blueprint_db = ctx.blueprint_db(); let view_id = query.view_id; let view_ctx = self.view_context(ctx, view_id, state, query.space_origin); - let background = ViewProperty::from_archetype::( - blueprint_db, - ctx.blueprint_query, - view_id, - ); + let background = ViewProperty::from_archetype::(&view_ctx); let background_color = background.component_or_fallback::( &view_ctx, PlotBackground::descriptor_color().component, @@ -570,8 +591,7 @@ impl ViewClass for TimeSeriesView { PlotBackground::descriptor_show_grid().component, )?; - let plot_legend = - ViewProperty::from_archetype::(blueprint_db, ctx.blueprint_query, view_id); + let plot_legend = ViewProperty::from_archetype::(&view_ctx); let legend_visible = plot_legend.component_or_fallback::( &view_ctx, PlotLegend::descriptor_visible().component, @@ -581,8 +601,7 @@ impl ViewClass for TimeSeriesView { PlotLegend::descriptor_corner().component, )?; - let time_axis = - ViewProperty::from_archetype::(blueprint_db, ctx.blueprint_query, view_id); + let time_axis = ViewProperty::from_archetype::(&view_ctx); let link_x_axis = time_axis .component_or_fallback::(&view_ctx, TimeAxis::descriptor_link().component)?; @@ -602,9 +621,8 @@ impl ViewClass for TimeSeriesView { query_result = re_viewer_context::DataQueryResult::default(); ( - &ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, + &ViewProperty::from_archetype_for_view::( + ctx, re_viewer_context::GLOBAL_VIEW_ID, ), &re_viewer_context::ViewContext { @@ -652,8 +670,7 @@ impl ViewClass for TimeSeriesView { let x_range = resolve_time_range(&view_time_range); - let scalar_axis = - ViewProperty::from_archetype::(blueprint_db, ctx.blueprint_query, view_id); + let scalar_axis = ViewProperty::from_archetype::(&view_ctx); let y_range = scalar_axis.component_or_fallback::( &view_ctx, ScalarAxis::descriptor_range().component, @@ -734,6 +751,7 @@ impl ViewClass for TimeSeriesView { } let mut plot_double_clicked = false; + let mut new_view_time_range = None; let egui_plot::PlotResponse { inner: (), response, @@ -744,10 +762,14 @@ impl ViewClass for TimeSeriesView { && let Some(pointer) = plot_ui.pointer_coordinate() { let time = re_log_types::TimeReal::from(pointer.x as i64 + time_offset); - ctx.send_time_commands([ - TimeControlCommand::SetTime(time), - TimeControlCommand::Pause, - ]); + + set_time( + ctx, + current_time, + &view_time_range, + &mut new_view_time_range, + time, + ); } plot_double_clicked = plot_ui.response().double_clicked(); @@ -789,8 +811,31 @@ impl ViewClass for TimeSeriesView { state.time_per_pixel = 1.0 / pixels_per_time.max(f64::EPSILON); } + // Cross-view time-range highlight (e.g. hovered state phase). Only paint + // StateTimeline-kind highlights on the current timeline that carry a color. + if let Some(highlight) = ctx.time_ctrl.highlighted_range() + && highlight.timeline == *timeline.name() + && highlight.kind == re_viewer_context::TimeRangeHighlightKind::StateTimeline + && let Some(color) = highlight.color + { + paint_time_range_highlight( + ui, + &response, + &transform, + time_offset, + highlight.range, + color, + ); + } + // Render re_renderer draw data (already in screen space) via ViewBuilder. - render_re_renderer_draw_data(ctx, ui, &response, re_renderer_draw_data); + render_re_renderer_draw_data( + ctx, + ui, + &response, + query.view_id.render_view_id(), + re_renderer_draw_data, + ); // Custom hover detection: find nearest actual data point and show tooltip. let hovered_data_result = (!legend_hovered) @@ -818,18 +863,16 @@ impl ViewClass for TimeSeriesView { ctx.handle_select_hover_drag_interactions(&response, hovered, false); } - // Decide if the time cursor should be displayed, and if so where: - let time_x = current_time - .map(|current_time| (current_time.saturating_sub(time_offset)) as f64) - .filter(|&x| { - // only display the time cursor when it's actually above the plot area - transform.bounds().min()[0] <= x && x <= transform.bounds().max()[0] - }) - .map(|x| transform.position_from_point(&PlotPoint::new(x, 0.0)).x); - - if let Some(time_x) = time_x { - paint_time_cursor(ctx, ui, &response, &transform, time_offset, time_x); - } + paint_time_cursor( + ctx, + ui, + &response, + &transform, + time_offset, + current_time, + &view_time_range, + &mut new_view_time_range, + ); // Can determine whether we're resetting only now since we need to know whether there's a plot item hovered. let is_resetting = plot_double_clicked && hovered_data_result.is_none(); @@ -844,27 +887,40 @@ impl ViewClass for TimeSeriesView { [x_range.end(), y_range.end()], ); - if unchanged_bounds != *transform.bounds() { - let new_x_range = transform_axis_range(transform, 0); - let new_x_range_rounded = - Range1D::new(new_x_range.start().round(), new_x_range.end().round()); - - let new_view_time_range = - re_sdk_types::blueprint::components::TimeRange(TimeRange { - start: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( - re_sdk_types::datatypes::TimeInt( - (new_x_range_rounded.start() as i64) - .saturating_add(time_offset), + if unchanged_bounds != *transform.bounds() || new_view_time_range.is_some() { + if let Some(new_view_time_range) = new_view_time_range + .or_else(|| { + let new_x_range = transform_axis_range(transform, 0); + + if new_x_range == x_range { + return None; + } + + let new_x_range_rounded = Range1D::new( + new_x_range.start().round(), + new_x_range.end().round(), + ); + + let new_view_time_range = TimeRange { + start: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( + re_sdk_types::datatypes::TimeInt( + (new_x_range_rounded.start() as i64) + .saturating_add(time_offset), + ), ), - ), - end: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( - re_sdk_types::datatypes::TimeInt( - (new_x_range_rounded.end() as i64).saturating_add(time_offset), + end: re_sdk_types::datatypes::TimeRangeBoundary::Absolute( + re_sdk_types::datatypes::TimeInt( + (new_x_range_rounded.end() as i64) + .saturating_add(time_offset), + ), ), - ), - }); + }; - if new_x_range != x_range && view_time_range != new_view_time_range { + Some(new_view_time_range) + }) + .map(re_sdk_types::blueprint::components::TimeRange) + && view_time_range != new_view_time_range + { time_range_property.save_blueprint_component( ctx, &TimeAxis::descriptor_view_range(), @@ -961,6 +1017,42 @@ impl ViewClass for TimeSeriesView { } } +fn set_time( + ctx: &ViewerContext<'_>, + current_time: Option, + view_time_range: &re_sdk_types::blueprint::components::TimeRange, + new_view_time_range: &mut Option, + time: re_log_types::TimeReal, +) { + if let Some(current_time) = current_time { + let current_time = re_log_types::TimeInt::new_temporal(current_time); + let time = time.floor(); + + let time_diff = current_time.as_i64() - time.as_i64(); + + let mut either_relative = false; + let mut map_time_range_boundary = |boundary| { + if let TimeRangeBoundary::CursorRelative(offset) = boundary { + either_relative = true; + TimeRangeBoundary::CursorRelative((offset.0 + time_diff).into()) + } else { + boundary + } + }; + + *new_view_time_range = Some(TimeRange { + start: map_time_range_boundary(view_time_range.start), + end: map_time_range_boundary(view_time_range.end), + }) + .filter(|_| either_relative); + } + + ctx.send_time_commands([ + TimeControlCommand::SetTimeClamped(time), + TimeControlCommand::Pause, + ]); +} + fn all_scalar_mappings_for( matches: &IntMap, ) -> Vec { @@ -1061,6 +1153,30 @@ fn scalar_datatype_priority(datatype: &re_log_types::external::arrow::datatypes: const RECOMMENDED_DATATYPES: &[DataType] = &[DataType::Float64, DataType::Float32, DataType::Float16]; +fn should_auto_spawn_time_series(reason: &VisualizableReason) -> bool { + has_native_scalar_semantics(reason) && all_scalar_mappings(reason).next().is_some() +} + +fn has_native_scalar_semantics(reason: &VisualizableReason) -> bool { + // This is always going to be `Some`, but nicer than writing `expect`. + let Some(scalar_type) = Scalars::descriptor_scalars().component_type else { + return false; + }; + + let VisualizableReason::SingleRequiredComponentMatch(m) = reason else { + return reason.full_native_match(Scalars::descriptor_scalars().component); + }; + + m.matches.values().any(|match_info| { + matches!( + match_info, + DatatypeMatch::NativeSemantics { component_type, .. } + | DatatypeMatch::PhysicalDatatypeOnly { component_type, .. } + if component_type.as_ref() == Some(&scalar_type) + ) + }) +} + fn all_scalar_mappings( reason: &VisualizableReason, ) -> impl Iterator { @@ -1116,7 +1232,9 @@ fn all_scalar_mappings( .. } => { if selectors.is_empty() { - if RECOMMENDED_DATATYPES.contains(match_info.arrow_datatype()) { + if is_rerun_native_type + || RECOMMENDED_DATATYPES.contains(match_info.arrow_datatype()) + { Either::Left(Either::Left(std::iter::once(( primary_match_order, is_rerun_native_type, @@ -1132,14 +1250,15 @@ fn all_scalar_mappings( // Nested field access: selector_index preserves field definition order. Either::Right(selectors.iter().enumerate().filter_map( move |(selector_index, (selector, datatype))| { - RECOMMENDED_DATATYPES.contains(datatype).then_some(( - primary_match_order, - is_rerun_native_type, - scalar_datatype_priority(datatype), - *source_component, - selector_index, - selector.to_string(), - )) + (is_rerun_native_type || RECOMMENDED_DATATYPES.contains(datatype)) + .then_some(( + primary_match_order, + is_rerun_native_type, + scalar_datatype_priority(datatype), + *source_component, + selector_index, + selector.to_string(), + )) }, )) } @@ -1183,7 +1302,6 @@ fn all_scalar_mappings( /// /// Returns the hovered data result item for selection/highlighting, or `None` if /// no data point is close enough. -#[expect(clippy::too_many_arguments)] fn find_nearest_data_point_and_show_tooltip( ui: &egui::Ui, response: &egui::Response, @@ -1295,14 +1413,63 @@ fn find_nearest_data_point_and_show_tooltip( .map(|address| re_viewer_context::Item::DataResult(address.clone())) } +fn paint_time_range_highlight( + ui: &egui::Ui, + response: &egui::Response, + transform: &egui_plot::PlotTransform, + time_offset: i64, + range: re_log_types::AbsoluteTimeRange, + color: egui::Color32, +) { + let plot_rect = response.rect; + let bounds = transform.bounds(); + let start_plot_x = (range.min.as_i64().saturating_sub(time_offset)) as f64; + let end_plot_x = (range.max.as_i64().saturating_sub(time_offset)) as f64; + + // Clip to the visible plot bounds before converting to screen. + let start_plot_x = start_plot_x.max(bounds.min()[0]); + let end_plot_x = end_plot_x.min(bounds.max()[0]); + if end_plot_x <= start_plot_x { + return; + } + + let x_start = transform + .position_from_point(&PlotPoint::new(start_plot_x, 0.0)) + .x; + let x_end = transform + .position_from_point(&PlotPoint::new(end_plot_x, 0.0)) + .x; + + ui.painter().with_clip_rect(plot_rect).rect_filled( + egui::Rect::from_x_y_ranges(x_start..=x_end, plot_rect.y_range()), + 0.0, + color, + ); +} + fn paint_time_cursor( ctx: &ViewerContext<'_>, ui: &egui::Ui, response: &egui::Response, transform: &egui_plot::PlotTransform, time_offset: i64, - mut time_x: f32, + current_time: Option, + view_time_range: &re_sdk_types::blueprint::components::TimeRange, + new_view_time_range: &mut Option, ) { + // Decide if the time cursor should be displayed, and if so where: + let time_x = current_time + .map(|current_time| (current_time.saturating_sub(time_offset)) as f64) + .filter(|&x| { + // only display the time cursor when it's actually above the plot area + transform.bounds().min()[0] <= x && x <= transform.bounds().max()[0] + }) + .map(|x| transform.position_from_point(&PlotPoint::new(x, 0.0)).x); + + let Some(mut time_x) = time_x else { + return; + }; + let interact_radius = ui.style().interaction.resize_grab_radius_side; let line_rect = egui::Rect::from_x_y_ranges(time_x..=time_x, response.rect.y_range()) .expand(interact_radius); @@ -1325,6 +1492,7 @@ fn paint_time_cursor( if is_being_dragged && let Some(pointer_pos) = pointer_pos { let aim_radius = ui.input(|i| i.aim_radius()); + let new_offset_time = egui::emath::smart_aim::best_in_range_f64( transform .value_from_position(pointer_pos - aim_radius * Vec2::X) @@ -1338,10 +1506,13 @@ fn paint_time_cursor( // Avoid frame-delay: time_x = pointer_pos.x; - ctx.send_time_commands([ - TimeControlCommand::SetTime(new_time.into()), - TimeControlCommand::Pause, - ]); + set_time( + ctx, + current_time, + view_time_range, + new_view_time_range, + new_time.into(), + ); } let highlighted = is_near || is_being_dragged; @@ -1485,7 +1656,7 @@ pub(crate) fn to_stepped_points(points: &[[f64; 2]], mode: crate::StepMode) -> V } crate::StepMode::Mid => { for pair in points.windows(2) { - let mid_t = (pair[0][0] + pair[1][0]) * 0.5; + let mid_t = fast_midpoint(pair[0][0], pair[1][0]); stepped.push(pair[0]); stepped.push([mid_t, pair[0][1]]); stepped.push([mid_t, pair[1][1]]); @@ -1591,8 +1762,9 @@ pub fn make_range_sane(y_range: Range1D) -> Range1D { } if end <= start { - let center = f64::midpoint(start, end); - Range1D::new(center - 1.0, center + 1.0) + let center = fast_midpoint(start, end); + let margin = f64::max(1.0, center.abs() * 0.01); + Range1D::new(center - margin, center + margin) } else { Range1D::new(start, end) } @@ -1606,6 +1778,7 @@ fn render_re_renderer_draw_data( ctx: &ViewerContext<'_>, ui: &egui::Ui, response: &egui::Response, + view_id: re_renderer::ViewBuilderId, draw_data: Vec, ) { if draw_data.is_empty() { @@ -1650,7 +1823,8 @@ fn render_re_renderer_draw_data( ..Default::default() }; - let Ok(mut view_builder) = re_renderer::ViewBuilder::new(render_ctx, target_config) else { + let Ok(mut view_builder) = re_renderer::ViewBuilder::new(render_ctx, target_config, view_id) + else { return; }; diff --git a/crates/viewer/re_view_time_series/tests/automatic_mapping.rs b/crates/viewer/re_view_time_series/tests/automatic_mapping.rs index b3d7c67246d5..83ce20dcaf1d 100644 --- a/crates/viewer/re_view_time_series/tests/automatic_mapping.rs +++ b/crates/viewer/re_view_time_series/tests/automatic_mapping.rs @@ -6,12 +6,15 @@ use std::sync::Arc; use re_log_types::external::arrow::array::{ - Array, Float64Array, Int16Array, Int32Array, StructArray, UInt32Array, + Array, Float32Array, Float64Array, Int16Array, Int32Array, StringArray, StructArray, + UInt32Array, }; use re_log_types::external::arrow::datatypes::{DataType, Field}; use re_log_types::{EntityPath, TimePoint, Timeline}; -use re_sdk_types::components; -use re_sdk_types::{DynamicArchetype, archetypes}; +use re_sdk_types::{ + Component as _, ComponentDescriptor, DynamicArchetype, RowId, SerializedComponentBatch, + archetypes, components, +}; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; use re_view_time_series::TimeSeriesView; @@ -373,6 +376,80 @@ fn setup_store(test_context: &mut TestContext) { }); } + // Scenario 15: Entity with a Scalar component type under a custom component identifier + // Expected: Should be treated like native scalar semantics and map that custom component + for i in 0..10 { + test_context.log_entity("entity_custom_scalar_identifier", |builder| { + builder.with_archetype_auto_row( + [(timeline, i)], + &DynamicArchetype::new("custom") + .with_component::("custom_scalar", [i as f64 * 9.0]), + ) + }); + } + + // Scenario 16: Entity with a Scalar component type stored as a struct array + // Expected: Should map the Float64 fields and ignore non-scalar fields + for i in 0..10 { + let struct_array = StructArray::from(vec![ + ( + Arc::new(Field::new("a", DataType::Float64, false)), + Arc::new(Float64Array::from(vec![(i as f64 / 5.0).sin()])) as Arc, + ), + ( + Arc::new(Field::new("b", DataType::Float32, false)), + Arc::new(Float32Array::from(vec![(i as f32 / 5.0).cos()])) as Arc, + ), + ( + Arc::new(Field::new("c", DataType::Utf8, false)), + Arc::new(StringArray::from(vec!["not plottable"])) as Arc, + ), + ]); + + test_context.log_entity("entity_custom_struct_scalar", |builder| { + builder.with_serialized_batch( + RowId::new(), + [(timeline, i)], + SerializedComponentBatch { + descriptor: ComponentDescriptor::partial("custom_struct") + .with_component_type(components::Scalar::name()), + array: Arc::new(struct_array), + }, + ) + }); + } + + // Scenario 17: Entity with Scalar semantics stored as non-recommended Int32 data + // Expected: Should map both semantic components despite the non-recommended physical datatype + for i in 0..10 { + let struct_array = StructArray::from(vec![( + Arc::new(Field::new("value", DataType::Int32, false)), + Arc::new(Int32Array::from(vec![i as i32 * 2])) as Arc, + )]); + + test_context.log_entity("entity_scalar_semantics_int32", |builder| { + builder + .with_serialized_batch( + RowId::new(), + [(timeline, i)], + SerializedComponentBatch { + descriptor: ComponentDescriptor::partial("semantic_int32") + .with_component_type(components::Scalar::name()), + array: Arc::new(Int32Array::from(vec![i as i32])), + }, + ) + .with_serialized_batch( + RowId::new(), + [(timeline, i)], + SerializedComponentBatch { + descriptor: ComponentDescriptor::partial("nested_semantic_int32") + .with_component_type(components::Scalar::name()), + array: Arc::new(struct_array), + }, + ) + }); + } + test_context.set_active_timeline(*timeline.name()); } @@ -666,4 +743,51 @@ fn check_visualizer_instructions(test_context: &TestContext, view_id: ViewId) { ); } } + + // Scenario 15: Entity with a Scalar component type under a custom component identifier + // Expected: Should map the custom identifier as native scalar semantics. + { + let instructions = visualizers_for(data_result_tree, "entity_custom_scalar_identifier"); + assert_eq!(instructions.len(), 1); + + let (component, selector) = source_component_for(&instructions[0]); + assert_eq!(component, "custom:custom_scalar"); + assert!( + selector.is_empty(), + "Expected empty selector for direct component mapping" + ); + } + + // Scenario 16: Entity with a Scalar component type stored as a struct array + // Expected: Should map both Float64 fields and ignore the Utf8 field. + { + let instructions = visualizers_for(data_result_tree, "entity_custom_struct_scalar"); + assert_eq!(instructions.len(), 2); + + let (component, selector) = source_component_for(&instructions[0]); + assert_eq!(component, "custom_struct"); + assert_eq!(selector, ".a"); + + let (component, selector) = source_component_for(&instructions[1]); + assert_eq!(component, "custom_struct"); + assert_eq!(selector, ".b"); + } + + // Scenario 17: Entity with Scalar semantics stored as non-recommended Int32 data + // Expected: Should map both semantic components despite the non-recommended physical datatype. + { + let instructions = visualizers_for(data_result_tree, "entity_scalar_semantics_int32"); + assert_eq!(instructions.len(), 2); + + let (component, selector) = source_component_for(&instructions[0]); + assert_eq!(component, "semantic_int32"); + assert!( + selector.is_empty(), + "Expected empty selector for direct component mapping" + ); + + let (component, selector) = source_component_for(&instructions[1]); + assert_eq!(component, "nested_semantic_int32"); + assert_eq!(selector, ".value"); + } } diff --git a/crates/viewer/re_view_time_series/tests/basic.rs b/crates/viewer/re_view_time_series/tests/basic.rs index d02b3307f1e9..38248f55e12b 100644 --- a/crates/viewer/re_view_time_series/tests/basic.rs +++ b/crates/viewer/re_view_time_series/tests/basic.rs @@ -1,11 +1,18 @@ -use re_chunk_store::external::re_chunk::ChunkBuilder; -use re_log_types::{EntityPath, TimePoint, Timeline}; +use std::sync::Arc; + +use re_chunk::{Chunk, ChunkBuilder}; +use re_log_types::external::arrow::array::{Array, Float64Array, StringArray, StructArray}; +use re_log_types::external::arrow::datatypes::{DataType, Field}; +use re_log_types::{EntityPath, TimeInt, TimePoint, Timeline}; use re_sdk_types::blueprint::{archetypes::PlotLegend, components::Corner2D}; +use re_sdk_types::{ + Component as _, ComponentDescriptor, DynamicArchetype, RowId, SerializedComponentBatch, +}; use re_test_context::TestContext; use re_test_context::external::egui_kittest::SnapshotResults; use re_test_viewport::TestContextExt as _; use re_view_time_series::TimeSeriesView; -use re_viewer_context::{BlueprintContext as _, ViewClass as _, ViewId}; +use re_viewer_context::{BlueprintContext as _, TimeControlCommand, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewContents, ViewProperty}; fn color_gradient0(step: i64) -> re_sdk_types::components::Color { @@ -107,6 +114,101 @@ fn test_clear_series_points_and_line_impl( )); } +/// A single entity carrying multiple `Float64` channels (`a`, `b`, `c`) is auto-spawned as +/// one visualizer instruction per channel, and the channels should render with distinct colors. +#[test] +fn test_multi_channel_scalars_per_entity() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + for i in 0..32 { + let t = i as f64 / 5.0; + test_context.log_entity("channels", |builder| { + builder.with_archetype_auto_row( + [(timeline, i)], + &DynamicArchetype::new("channels") + .with_component_from_data("a", Arc::new(Float64Array::from(vec![t.sin()]))) + .with_component_from_data( + "b", + Arc::new(Float64Array::from(vec![t.cos() + 2.0])), + ) + .with_component_from_data( + "c", + Arc::new(Float64Array::from(vec![(t * 0.5).sin() - 2.0])), + ), + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + let mut snapshot_results = SnapshotResults::new(); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "multi_channel_scalars_per_entity", + egui::vec2(400.0, 300.0), + None, + )); +} + +#[test] +fn test_custom_scalar_component_identifier() { + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + for i in 0..32 { + let t = i as f64 / 5.0; + test_context.log_entity("custom_scalar", |builder| { + builder.with_archetype_auto_row( + [(timeline, i)], + &DynamicArchetype::new("custom") + .with_component::("custom_scalar", [t.sin()]), + ) + }); + + let struct_array = StructArray::from(vec![ + ( + Arc::new(Field::new("sin", DataType::Float64, false)), + Arc::new(Float64Array::from(vec![t.sin() - 2.0])) as Arc, + ), + ( + Arc::new(Field::new("cos", DataType::Float64, false)), + Arc::new(Float64Array::from(vec![t.cos() - 4.0])) as Arc, + ), + ( + Arc::new(Field::new("label", DataType::Utf8, false)), + Arc::new(StringArray::from(vec!["not plottable"])) as Arc, + ), + ]); + test_context.log_entity("custom_struct_scalar", |builder| { + builder.with_serialized_batch( + RowId::new(), + [(timeline, i)], + SerializedComponentBatch { + descriptor: ComponentDescriptor::partial("custom_struct") + .with_archetype("CustomArchetype".into()) + .with_component_type(re_sdk_types::components::Scalar::name()), + array: Arc::new(struct_array), + }, + ) + }); + } + + test_context.set_active_timeline(*timeline.name()); + + let view_id = setup_blueprint(&mut test_context); + let mut snapshot_results = SnapshotResults::new(); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "custom_scalar_component_identifier", + egui::vec2(300.0, 300.0), + None, + )); +} + fn scalars_for_properties_test( step: i64, multiple_scalars: bool, @@ -425,12 +527,7 @@ fn test_non_finite_islands() { // Move the legend to the top right corner to avoid covering any of the data. let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint| { let view = ViewBlueprint::new_with_root_wildcard(TimeSeriesView::identifier()); - ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view.id, - ) - .save_blueprint_component( + ViewProperty::from_archetype_for_view::(ctx, view.id).save_blueprint_component( ctx, &PlotLegend::descriptor_corner(), &[Corner2D::RightTop], @@ -447,6 +544,53 @@ fn test_non_finite_islands() { )); } +#[test] +fn test_series_lines_single_logged_point() { + let mut test_context = TestContext::new_with_view_class::(); + let timeline = Timeline::log_tick(); + + test_context.log_entity("plots/line", |builder| { + builder.with_archetype_auto_row( + TimePoint::default(), + &re_sdk_types::archetypes::SeriesLines::new().with_widths([8.0]), + ) + }); + test_context.log_entity("plots/line", |builder| { + builder.with_archetype_auto_row( + [(timeline, 10)], + &re_sdk_types::archetypes::Scalars::single(1.0), + ) + }); + + test_context.set_active_timeline(*timeline.name()); + + let view_id = test_context.setup_viewport_blueprint(|ctx, blueprint| { + let view = ViewBlueprint::new_with_root_wildcard(TimeSeriesView::identifier()); + + ViewProperty::from_archetype_for_view::( + ctx, view.id, + ) + .save_blueprint_component( + ctx, + &re_sdk_types::blueprint::archetypes::TimeAxis::descriptor_view_range(), + &re_sdk_types::datatypes::TimeRange { + start: re_sdk_types::datatypes::TimeRangeBoundary::Absolute(0.into()), + end: re_sdk_types::datatypes::TimeRangeBoundary::Absolute(20.into()), + }, + ); + + blueprint.add_view_at_root(view) + }); + + let mut snapshot_results = SnapshotResults::new(); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "series_lines_single_logged_point", + egui::vec2(300.0, 300.0), + None, + )); +} + /// Series containing only `±inf` values (no finite data). /// /// The viewer must not crash and should fall back to a sane y-range. @@ -600,3 +744,56 @@ fn test_bootstrapped_secondaries_impl(partial_range: bool, snapshot_results: &mu None, )); } + +#[test] +fn temporal_anchor_between_sequence_steps() { + let mut snapshot_results = SnapshotResults::new(); + let mut test_context = TestContext::new_with_view_class::(); + + let timeline = Timeline::log_tick(); + + let chunks = &mut time_series_chunks(timeline); + + // Add the first two ticks so the initial snapshot has a visible segment before the + // anchored cursor. + test_context.add_chunks(chunks.take(2)); + test_context.set_active_timeline(*timeline.name()); + + // Pin the cursor at 100 — like a `#when` URL anchor would. + test_context.send_time_commands( + test_context.active_store_id(), + [TimeControlCommand::SetTime( + TimeInt::new_temporal(100).into(), + )], + ); + test_context.handle_system_commands(&egui::Context::default()); + + let view_id = setup_blueprint(&mut test_context); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "time_series_temporal_anchor_between_steps_first_chunk", + egui::vec2(300.0, 300.0), + None, + )); + + // Add the rest + test_context.add_chunks(chunks); + snapshot_results.add(test_context.run_view_ui_and_save_snapshot( + view_id, + "time_series_temporal_anchor_between_steps_rest", + egui::vec2(300.0, 300.0), + None, + )); +} + +fn time_series_chunks(timeline: Timeline) -> impl Iterator { + (0_i64..=200).step_by(10).map(move |tick| { + Chunk::builder("scalars") + .with_archetype_auto_row( + [(timeline, tick)], + &re_sdk_types::archetypes::Scalars::single((tick as f64 / 30.0).sin()), + ) + .build() + .expect("failed to build chunk") + }) +} diff --git a/crates/viewer/re_view_time_series/tests/blueprint.rs b/crates/viewer/re_view_time_series/tests/blueprint.rs index 5a0abc4fa7d5..0a105ad09f85 100644 --- a/crates/viewer/re_view_time_series/tests/blueprint.rs +++ b/crates/viewer/re_view_time_series/tests/blueprint.rs @@ -218,9 +218,9 @@ fn setup_blueprint( ); if let Some(time_axis_view) = time_axis_view { - let time_axis = re_viewport_blueprint::ViewProperty::from_archetype::< + let time_axis = re_viewport_blueprint::ViewProperty::from_archetype_for_view::< blueprint::archetypes::TimeAxis, - >(ctx.blueprint_db(), ctx.blueprint_query, view.id); + >(ctx, view.id); time_axis.save_blueprint_component( ctx, @@ -230,11 +230,9 @@ fn setup_blueprint( } if let Some(visible_time_range) = visible_time_range { - let property = re_viewport_blueprint::ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, - view.id, - ); + let property = re_viewport_blueprint::ViewProperty::from_archetype_for_view::< + VisibleTimeRanges, + >(ctx, view.id); property.save_blueprint_component( ctx, diff --git a/crates/viewer/re_view_time_series/tests/component_drop.rs b/crates/viewer/re_view_time_series/tests/component_drop.rs new file mode 100644 index 000000000000..7b78fda76dfd --- /dev/null +++ b/crates/viewer/re_view_time_series/tests/component_drop.rs @@ -0,0 +1,240 @@ +//! Integration tests for dropping components from the streams tree onto a time series view. +//! +//! These exercise [`re_viewer_context::ViewClass::handle_component_drop`] end-to-end: the +//! drop-feedback gating (which components are accepted) and the resulting blueprint mutation. + +use std::sync::Arc; + +use re_log_types::external::arrow::array::{ + ArrayRef, BooleanArray, Int64Array, ListArray, StringArray, +}; +use re_log_types::external::arrow::buffer::OffsetBuffer; +use re_log_types::external::arrow::datatypes::{DataType, Field}; +use re_log_types::{ComponentPath, EntityPath}; +use re_sdk_types::DynamicArchetype; +use re_sdk_types::archetypes::Scalars; +use re_test_context::TestContext; +use re_test_viewport::TestContextExt as _; +use re_view_time_series::TimeSeriesView; +use re_viewer_context::{ + DragAndDropFeedback, RecommendedView, ViewClass as _, ViewId, ViewerContext, +}; +use re_viewport_blueprint::ViewBlueprint; + +/// An entity the view doesn't contain, so dropped components are always "new". +const OTHER_ENTITY: &str = "plots/other"; + +/// A list-typed component column holding a single `Int64` row — a plottable but non-float scalar. +fn int64_component(value: i64) -> ArrayRef { + let values = Arc::new(Int64Array::from(vec![value])) as ArrayRef; + let offsets = OffsetBuffer::from_lengths([1usize]); + Arc::new(ListArray::new( + Arc::new(Field::new("item", DataType::Int64, false)), + offsets, + values, + None, + )) +} + +/// A list-typed component column holding a single `Boolean` row — a plottable scalar. +fn bool_component(value: bool) -> ArrayRef { + let values = Arc::new(BooleanArray::from(vec![value])) as ArrayRef; + let offsets = OffsetBuffer::from_lengths([1usize]); + Arc::new(ListArray::new( + Arc::new(Field::new("item", DataType::Boolean, false)), + offsets, + values, + None, + )) +} + +/// A list-typed component column holding a single `Utf8` row — not plottable. +fn utf8_component(value: &str) -> ArrayRef { + let values = Arc::new(StringArray::from(vec![value])) as ArrayRef; + let offsets = OffsetBuffer::from_lengths([1usize]); + Arc::new(ListArray::new( + Arc::new(Field::new("item", DataType::Utf8, false)), + offsets, + values, + None, + )) +} + +/// The single component logged on `entity`, as a [`ComponentPath`] suitable for a drop. +fn sole_component(ctx: &ViewerContext<'_>, entity: &EntityPath) -> ComponentPath { + let engine = ctx.recording().storage_engine(); + let components = engine + .store() + .schema() + .all_components_for_entity(entity) + .expect("entity should have logged data") + .clone(); + assert_eq!( + components.len(), + 1, + "test entity {entity} should have exactly one component, got {components:?}" + ); + let component = *components.iter().next().expect("exactly one component"); + ComponentPath::new(entity.clone(), component) +} + +/// A view that only contains [`OTHER_ENTITY`], so every other entity is a fresh drop target. +fn setup_view_excluding_dropped_entities(test_context: &mut TestContext) -> ViewId { + test_context.setup_viewport_blueprint(|_ctx, blueprint| { + blueprint.add_view_at_root(ViewBlueprint::new( + TimeSeriesView::identifier(), + RecommendedView::new_single_entity(EntityPath::from(OTHER_ENTITY)), + )) + }) +} + +/// Dropping a fresh scalar component is accepted and adds a visualizer to the view. +#[test] +fn test_drop_scalar_component_adds_visualizer() { + let mut test_context = TestContext::new_with_view_class::(); + let timeline = test_context.active_timeline().expect("active timeline"); + + let entity = EntityPath::from("plots/sin"); + for i in 0..5 { + let t = i as f64 / 8.0; + test_context.log_entity(entity.clone(), |builder| { + builder.with_archetype_auto_row([(timeline, i)], &Scalars::single(t.sin())) + }); + } + + let view_id = setup_view_excluding_dropped_entities(&mut test_context); + + // Precondition: the entity is not part of the view yet. + assert!( + test_context + .query_results + .get(&view_id) + .and_then(|qr| qr.tree.lookup_result_by_path(entity.hash())) + .is_none(), + "entity should not be visualized before the drop" + ); + + test_context.run_once_in_egui_central_panel(|ctx, _ui| { + let component_path = sole_component(ctx, &entity); + let feedback = TimeSeriesView.handle_component_drop( + ctx, + view_id, + &[component_path], + /* released */ true, + ); + assert_eq!(feedback, DragAndDropFeedback::Accept); + }); + test_context.handle_system_commands(&egui::Context::default()); + + // Recompute query results against the mutated blueprint. + test_context.setup_viewport_blueprint(|_ctx, _blueprint| {}); + + let data_result = test_context + .query_results + .get(&view_id) + .expect("view has query results") + .tree + .lookup_result_by_path(entity.hash()) + .cloned(); + assert!( + data_result.is_some_and(|r| !r.visualizer_instructions.is_empty()), + "the dropped entity should now have a visualizer in the view" + ); +} + +/// Dropping a non-scalar component (e.g. a string) is rejected as incompatible. +#[test] +fn test_drop_non_scalar_component_is_rejected() { + let mut test_context = TestContext::new_with_view_class::(); + let timeline = test_context.active_timeline().expect("active timeline"); + + let entity = EntityPath::from("plots/text"); + for i in 0..5 { + test_context.log_entity(entity.clone(), |builder| { + builder.with_archetype_auto_row( + [(timeline, i)], + &DynamicArchetype::new("data") + .with_component_from_data("value", utf8_component("hi")), + ) + }); + } + + let view_id = setup_view_excluding_dropped_entities(&mut test_context); + + test_context.run_once_in_egui_central_panel(|ctx, _ui| { + let component_path = sole_component(ctx, &entity); + let feedback = TimeSeriesView.handle_component_drop( + ctx, + view_id, + &[component_path], + /* released */ false, + ); + assert_eq!( + feedback, + DragAndDropFeedback::Reject(Some("Not a scalar component")) + ); + }); +} + +/// Regression test: integer (non-float) scalars are plottable and must be accepted, even though +/// they aren't among the *recommended* (float) datatypes used for spawn heuristics. +#[test] +fn test_drop_integer_scalar_component_is_accepted() { + let mut test_context = TestContext::new_with_view_class::(); + let timeline = test_context.active_timeline().expect("active timeline"); + + let entity = EntityPath::from("plots/ints"); + for i in 0..5 { + test_context.log_entity(entity.clone(), |builder| { + builder.with_archetype_auto_row( + [(timeline, i)], + &DynamicArchetype::new("data") + .with_component_from_data("value", int64_component(i)), + ) + }); + } + + let view_id = setup_view_excluding_dropped_entities(&mut test_context); + + test_context.run_once_in_egui_central_panel(|ctx, _ui| { + let component_path = sole_component(ctx, &entity); + let feedback = TimeSeriesView.handle_component_drop( + ctx, + view_id, + &[component_path], + /* released */ false, + ); + assert_eq!(feedback, DragAndDropFeedback::Accept); + }); +} + +/// Boolean scalars are plottable (0/1) and must be accepted, like integers. +#[test] +fn test_drop_bool_scalar_component_is_accepted() { + let mut test_context = TestContext::new_with_view_class::(); + let timeline = test_context.active_timeline().expect("active timeline"); + + let entity = EntityPath::from("plots/flag"); + for i in 0..5 { + test_context.log_entity(entity.clone(), |builder| { + builder.with_archetype_auto_row( + [(timeline, i)], + &DynamicArchetype::new("data") + .with_component_from_data("value", bool_component(i % 2 == 0)), + ) + }); + } + + let view_id = setup_view_excluding_dropped_entities(&mut test_context); + + test_context.run_once_in_egui_central_panel(|ctx, _ui| { + let component_path = sole_component(ctx, &entity); + let feedback = TimeSeriesView.handle_component_drop( + ctx, + view_id, + &[component_path], + /* released */ false, + ); + assert_eq!(feedback, DragAndDropFeedback::Accept); + }); +} diff --git a/crates/viewer/re_view_time_series/tests/snapshots/all_neg_inf.png b/crates/viewer/re_view_time_series/tests/snapshots/all_neg_inf.png index 6b9bf818caef..4938f144df96 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/all_neg_inf.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/all_neg_inf.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b265a1635f3faa082be85f27a857d36ad834213adf477a70a5c94b7851307aa -size 7129 +oid sha256:b402e82bd30c35150c12fe6a9ee623a3c5ca789d80d7078cec6dcfd5091ac59d +size 7113 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/all_pos_inf.png b/crates/viewer/re_view_time_series/tests/snapshots/all_pos_inf.png index 6b9bf818caef..4938f144df96 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/all_pos_inf.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/all_pos_inf.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b265a1635f3faa082be85f27a857d36ad834213adf477a70a5c94b7851307aa -size 7129 +oid sha256:b402e82bd30c35150c12fe6a9ee623a3c5ca789d80d7078cec6dcfd5091ac59d +size 7113 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series.png b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series.png index f458cbf8d640..a44747778e9f 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1edc1f97e1a6797902ca975b146515e97c2be72eb27b6c38873356134b49873 -size 20237 +oid sha256:0025502ed7f47fc0b8375e21c82aad848c351f1062a6823fda796723da7dd813 +size 20243 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_absolute.png b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_absolute.png index c22c60a22d8d..bd974a4a0146 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_absolute.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_absolute.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e46ce951423b0747399ba697e953dcc788e00cc8bc1aae2fcee469a6788da07 -size 10224 +oid sha256:10b90c85b4b555a647769f837dae96bcc5824b1a89a4339277f91f6f152afe23 +size 10241 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_absolute_until_end.png b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_absolute_until_end.png index a8f38c4fa1ff..f0bb45364fb5 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_absolute_until_end.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_absolute_until_end.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e3c6974830272bcdbe5177eba9d3c936255cf03d45c56358007a7522e444a82 -size 15172 +oid sha256:10dfbeed5ead88c4349eff9754566236d97de3eda733a46c0476f44d7e945612 +size 14982 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_around_cursor.png b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_around_cursor.png index f03fb3471a6c..afb1d520f356 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_around_cursor.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_around_cursor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:650bfaa069714fe437e8b786ff543e419d760d792a0210729bc1938a5868c4e4 -size 15831 +oid sha256:efba69c1e3edc32a9b10de48f986b011e8ed27cb47ade6b861ddb30b92e60f00 +size 15905 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_at_cursor.png b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_at_cursor.png index a40ae3dfd00c..a4a44e2ef9fa 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_at_cursor.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_at_cursor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f034e7e49faa44065ed31f1615f084e0cdd9081e7c6986003f4437771cdfd60 -size 14119 +oid sha256:1fc15a8665a827f1b47aff3e14a55dbdfad8036d1cf7973c866ab86be38e7a7b +size 14192 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_everything.png b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_everything.png index f458cbf8d640..a44747778e9f 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_everything.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_everything.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1edc1f97e1a6797902ca975b146515e97c2be72eb27b6c38873356134b49873 -size 20237 +oid sha256:0025502ed7f47fc0b8375e21c82aad848c351f1062a6823fda796723da7dd813 +size 20243 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_start_until_absolute.png b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_start_until_absolute.png index 4e6431cc6a25..89cd870c9883 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_start_until_absolute.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/blueprint_overrides_and_defaults_with_time_series_start_until_absolute.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e4838826c28cb34cbcab5d079b8793469d9ff60334ffbdf8cac11ec90519f5a -size 11455 +oid sha256:da50418d2dc8020ac7971ca6dfe4f4259842f413c0003ec1eddc6725f1458ef2 +size 11462 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/bootstrapped_secondaries_full.png b/crates/viewer/re_view_time_series/tests/snapshots/bootstrapped_secondaries_full.png index 8cc7e90e90ee..1c33aa46da5e 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/bootstrapped_secondaries_full.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/bootstrapped_secondaries_full.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0553a3d7f2f7f7a5cb30d62785f7616c5e870953d1d7ad236e01fc9f45b77def -size 16258 +oid sha256:ff79ec12d3a2d2daf86978e912ab4b99a5333f3a4ce606b7d251fa92c58e789d +size 15743 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/bootstrapped_secondaries_partial.png b/crates/viewer/re_view_time_series/tests/snapshots/bootstrapped_secondaries_partial.png index 8cc7e90e90ee..1c33aa46da5e 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/bootstrapped_secondaries_partial.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/bootstrapped_secondaries_partial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0553a3d7f2f7f7a5cb30d62785f7616c5e870953d1d7ad236e01fc9f45b77def -size 16258 +oid sha256:ff79ec12d3a2d2daf86978e912ab4b99a5333f3a4ce606b7d251fa92c58e789d +size 15743 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/clear_series_points_and_line.png b/crates/viewer/re_view_time_series/tests/snapshots/clear_series_points_and_line.png index 0b08a6d3e0b7..c475945650ab 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/clear_series_points_and_line.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/clear_series_points_and_line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab6f6ea72aea0c8b914d027e7d0f1e3a21f9c4e1573ecca5694521ee93dcfef8 -size 21850 +oid sha256:42ffe8ca2de7cab17df564a6a7895364f9d2fbbe06d77577547ab0506e820d50 +size 21538 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/clear_series_points_and_line_two_series_per_entity.png b/crates/viewer/re_view_time_series/tests/snapshots/clear_series_points_and_line_two_series_per_entity.png index 696bd7681837..7042bb5a741b 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/clear_series_points_and_line_two_series_per_entity.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/clear_series_points_and_line_two_series_per_entity.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64f9fda87be33c359848271b81f979875dffefb0c2f9465cd3c9b1f08c48798d -size 29114 +oid sha256:df2aa593acd4c03c5c2a247ddf4e3cd17978a6c82ec995ef6fcf978d133105c5 +size 29006 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/custom_scalar_component_identifier.png b/crates/viewer/re_view_time_series/tests/snapshots/custom_scalar_component_identifier.png new file mode 100644 index 000000000000..d669771158bb --- /dev/null +++ b/crates/viewer/re_view_time_series/tests/snapshots/custom_scalar_component_identifier.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb76f30898c9f9a27a4de3fbaa182c2f1caa9bf9d0e91f0733adebd9f32d6a9d +size 26354 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/explicit_component_mapping.png b/crates/viewer/re_view_time_series/tests/snapshots/explicit_component_mapping.png index 6075c6e8977a..397f600ed56f 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/explicit_component_mapping.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/explicit_component_mapping.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51bc0f383936f751a0ac1207dfee612949c27d9090bfbdb426b7c04972d24585 -size 24285 +oid sha256:a27f2fbdc0ba008b45e91f99103c2e698676ae5cee40ba1bc7240d66bb1d7ccb +size 24383 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/explicit_component_mapping_nested.png b/crates/viewer/re_view_time_series/tests/snapshots/explicit_component_mapping_nested.png index d8f54f3218f6..a534b866dcec 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/explicit_component_mapping_nested.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/explicit_component_mapping_nested.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d9f8910c345ac26d618c6cdf52358f51d55a416489af0504635f09840f74632 -size 38280 +oid sha256:5fa8e25fe1d877ffa32433a63d47f2cd7fd25cf02a113b23abe6a1f8ac993846 +size 38585 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/help_view_time_series_view_mac.png b/crates/viewer/re_view_time_series/tests/snapshots/help_view_time_series_view_mac.png index 0ec40dd952ed..9d3c2cb29a37 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/help_view_time_series_view_mac.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/help_view_time_series_view_mac.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc68ec5be9bd9a2f299fb4ba9c43295b186d9ef107ea1874eac9c5fccc0e662e -size 29689 +oid sha256:053839436405a47bb807f1d922d513aa7bf3821a1a48ba2b4e8de881fc0ab8c0 +size 29637 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/help_view_time_series_view_windows.png b/crates/viewer/re_view_time_series/tests/snapshots/help_view_time_series_view_windows.png index d0752d96cfe1..ad1298c27172 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/help_view_time_series_view_windows.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/help_view_time_series_view_windows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cd5a8fc71c29fd43731952ada7e965d0f13443c195892f8f68264e45d0ccc59 -size 31111 +oid sha256:aff9e7a242dda1d08da0fa8d24297f4b257300d949db8a744a781c4bcc626fce +size 31017 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_Linear.png b/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_Linear.png index 98faf4f28df4..97d070887266 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_Linear.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_Linear.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe464953c77315b5cf972db776fd0450c834fedc65721896af8026e7adfac8a6 -size 20922 +oid sha256:527c2ed7df0145d4e98a152d6000bcebc438a6ba2390427d60a901e11374a01a +size 20914 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepAfter.png b/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepAfter.png index e8090cbf5370..17df475e9800 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepAfter.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepAfter.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55125021e54e13fd7907021296a2acb72ef2147fd230b839e44a44fdf4d754ae -size 19439 +oid sha256:9a4fd803125fbb9b839532736fee9d04fe104d5c52c657e27431a86ef28667fa +size 19400 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepBefore.png b/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepBefore.png index d82e3af0882a..e9941e3a3f72 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepBefore.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepBefore.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06bc75f6937ee5ed7bfefa83fa58296aa6705a4799064736551542e0ee9ad4fe -size 19665 +oid sha256:ee31be253c40b941d655ada63ec377cf334dbc1cbe097201c5a6c25de55a4418 +size 19686 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepMid.png b/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepMid.png index de6f260dc77d..466e7bfc782d 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepMid.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/interpolation_mode_StepMid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cd3afbb19211925717d0884a16426efcc974b6d892a2e91c322d46619c49880 -size 20167 +oid sha256:fe94a837736a1a94ca858ef273770b25db589a1eb3dce1864eb58a393d847bf2 +size 20172 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/line_properties_multiple_properties_two_series_per_entity.png b/crates/viewer/re_view_time_series/tests/snapshots/line_properties_multiple_properties_two_series_per_entity.png index a5e01e48330f..a6d228ff81eb 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/line_properties_multiple_properties_two_series_per_entity.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/line_properties_multiple_properties_two_series_per_entity.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54df84f233479c3108cde2679507fd0a59c87de31b776bc2e2f61d1cdf53ba1d -size 38946 +oid sha256:c7a8d1fc1d5262345487ea9cd91ebca02600888b24d50faf493beb812524510f +size 39195 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/line_properties_two_series_per_entity.png b/crates/viewer/re_view_time_series/tests/snapshots/line_properties_two_series_per_entity.png index 52be02f8e4a8..3c27d1bed0a3 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/line_properties_two_series_per_entity.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/line_properties_two_series_per_entity.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0740f2da2d38ffce8eb4593e37ddc33beb0e954f7b0b9e202feafc5215396fee -size 40012 +oid sha256:779944894ac528d62e79c582e73274444a0ca9d22862cb049cd5559600029f0e +size 40498 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/multi_channel_scalars_per_entity.png b/crates/viewer/re_view_time_series/tests/snapshots/multi_channel_scalars_per_entity.png new file mode 100644 index 000000000000..9d0ea42af67a --- /dev/null +++ b/crates/viewer/re_view_time_series/tests/snapshots/multi_channel_scalars_per_entity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c07501a172be98714bcc3151a36004ee98f368aa796a9f11b23f6a429624765 +size 23985 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/non_finite_islands.png b/crates/viewer/re_view_time_series/tests/snapshots/non_finite_islands.png index 066291213edc..46365a09f41d 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/non_finite_islands.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/non_finite_islands.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d37482b4e9341463e3277df2f2eaeb4ec5f7f6cb8cabcf9d2d4e6e3ab6dbd74 -size 13719 +oid sha256:1a3e277e043ff7196ffd5ec343189ceba1bff6acf9a481619574a9b2d3c0e0d7 +size 13832 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_show_second_only.png b/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_show_second_only.png index ed09587458f4..79241355fb51 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_show_second_only.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_show_second_only.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c1930f5356ebe7d4261d9d3e76f32312d709e747857da9108f0dee1397308ae -size 22901 +oid sha256:715d8a9c5dcb371140f5d74123abd0ce5cc48eba3b07552218ceb4137f7e08f4 +size 22748 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_splat_false.png b/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_splat_false.png index 8da23b0de491..400d4a7a0875 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_splat_false.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_splat_false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4e8ab544ad4a73570a8b3823ba1376d4a84c778dc3bf4df11a50b8eee47b543 -size 16623 +oid sha256:737ac1c6a59f68afb4807431440bc8e9fe97f97dae8b1c108b19132749f9460c +size 16640 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_splat_true.png b/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_splat_true.png index d4a866d61f55..b0c8efc64528 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_splat_true.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/per_series_visibility_splat_true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61fb34db734038abc10348d37d2fb651acb88d63f6f17a12b336c7d8c94b93aa -size 28661 +oid sha256:00f4b2a3ff6f49c9b08da8537e00102d2f80447051c672c263753e2380b05e4f +size 28325 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/point_properties_multiple_properties_two_series_per_entity.png b/crates/viewer/re_view_time_series/tests/snapshots/point_properties_multiple_properties_two_series_per_entity.png index 1c132e7d8387..eab91f792101 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/point_properties_multiple_properties_two_series_per_entity.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/point_properties_multiple_properties_two_series_per_entity.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd822872b40919b45f0d65d5d17eb0ea69fb430d6b435877bbf5e701274d5146 -size 36695 +oid sha256:1edba54f90b3c466a3aac3696ffffd0aaae5fa47f15b45323e0881c11c78c247 +size 37006 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/point_properties_two_series_per_entity.png b/crates/viewer/re_view_time_series/tests/snapshots/point_properties_two_series_per_entity.png index 0571fbf2460e..96502272fc42 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/point_properties_two_series_per_entity.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/point_properties_two_series_per_entity.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1145cdd50d811ec87784cf655dcef651ffffbd6ef0b3c3861b9dd9e4c34d2b3 -size 36987 +oid sha256:72cf30f254f78ee1a0f2e94c2aba24f386633cc741a51932157d854c60f67df8 +size 37198 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/series_lines_single_logged_point.png b/crates/viewer/re_view_time_series/tests/snapshots/series_lines_single_logged_point.png new file mode 100644 index 000000000000..04bc0ddbd262 --- /dev/null +++ b/crates/viewer/re_view_time_series/tests/snapshots/series_lines_single_logged_point.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:121c04268164fb50aeaf0957cb9afe4093b6e71aa59142d7aeffc946cbf041ea +size 10943 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/special_characters_in_entity_path.png b/crates/viewer/re_view_time_series/tests/snapshots/special_characters_in_entity_path.png index 02fdb3930d70..91705afb62f3 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/special_characters_in_entity_path.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/special_characters_in_entity_path.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6df6c6a1e137b199c65bc0af10e2eca086d7488e6aeabfa3918b2b5c8fe2110a -size 21472 +oid sha256:a637da7d3ec29a1e54510c28b823d23141cbbf44c23165fb5a3ec6280727a3ed +size 21664 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/time_series_temporal_anchor_between_steps_first_chunk.png b/crates/viewer/re_view_time_series/tests/snapshots/time_series_temporal_anchor_between_steps_first_chunk.png new file mode 100644 index 000000000000..7dfb6e0006f4 --- /dev/null +++ b/crates/viewer/re_view_time_series/tests/snapshots/time_series_temporal_anchor_between_steps_first_chunk.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bcdff4f75410bdccfe381bbf401e6563674e3b29bc6a916fcfce0bc86d78335 +size 11284 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/time_series_temporal_anchor_between_steps_rest.png b/crates/viewer/re_view_time_series/tests/snapshots/time_series_temporal_anchor_between_steps_rest.png new file mode 100644 index 000000000000..5a659111aadc --- /dev/null +++ b/crates/viewer/re_view_time_series/tests/snapshots/time_series_temporal_anchor_between_steps_rest.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6de52015f16fa52b6d82a0adbb908694dc14e4d9b6700bf71eb3a25295904516 +size 17552 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/transform3d_time_series.png b/crates/viewer/re_view_time_series/tests/snapshots/transform3d_time_series.png index 9f1b37218ce0..972b45aadb37 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/transform3d_time_series.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/transform3d_time_series.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ee74e020c24c0dd08d69ad5d20a7d19b61d8dc6356df95d7dbee4a1d3243bd4 -size 46450 +oid sha256:d41e5f33b648fe376a642a6f4dab41f5885d20539e07fd08a3f17ded2b4dad4a +size 46888 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_until_end_view_data.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_until_end_view_data.png index a8f38c4fa1ff..f0bb45364fb5 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_until_end_view_data.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_until_end_view_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e3c6974830272bcdbe5177eba9d3c936255cf03d45c56358007a7522e444a82 -size 15172 +oid sha256:10dfbeed5ead88c4349eff9754566236d97de3eda733a46c0476f44d7e945612 +size 14982 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_until_end_view_timeline.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_until_end_view_timeline.png index 8708cccbe334..bfa27010da0f 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_until_end_view_timeline.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_until_end_view_timeline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0ae9af35d6098c53a401a0b1ed5f22454bace2d57a1f699afb5ca3e2ac676f3 -size 18812 +oid sha256:baa4069c097b474ef8070773bae2450a65aaabcc8c64856d676bdfeea6fc9686 +size 18816 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_view_data.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_view_data.png index c22c60a22d8d..bd974a4a0146 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_view_data.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_view_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e46ce951423b0747399ba697e953dcc788e00cc8bc1aae2fcee469a6788da07 -size 10224 +oid sha256:10b90c85b4b555a647769f837dae96bcc5824b1a89a4339277f91f6f152afe23 +size 10241 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_view_timeline.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_view_timeline.png index c672f2ae37f6..84ded781feab 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_view_timeline.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_absolute_view_timeline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eab63d7206b7f9a086263bbf0d3d59a0150c26e7c1f1d80a79adb5a9a13269a7 -size 16444 +oid sha256:7acb9cac169c99d314fa18af8ba9b28e4d9309999e48c4e77dd1a27de929dbf7 +size 16583 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_around_cursor_view_data.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_around_cursor_view_data.png index 8fe001872e7c..faa66117c52b 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_around_cursor_view_data.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_around_cursor_view_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84a67b4585aff0811c3b58c7fe5e92f8ac514cbe689f7bac1ddd8b4d55e55f72 -size 13588 +oid sha256:dec49f3b727489f3083b99e7ad4c9bd3a5fee000c7b8ad569c56cb78b4774cfa +size 13661 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_around_cursor_view_timeline.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_around_cursor_view_timeline.png index 8586afdfb1e1..4fff787595ed 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_around_cursor_view_timeline.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_around_cursor_view_timeline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a6b4ed0a9180226a81c84c0527878feb9dd69e69b18254d39d3044900d0402a -size 17566 +oid sha256:b3ec92d67e533600bed6139ee13926a145e7f6c8281281a7613b57ca18c7e76b +size 17677 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_everything_view_data.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_everything_view_data.png index f458cbf8d640..a44747778e9f 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_everything_view_data.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_everything_view_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1edc1f97e1a6797902ca975b146515e97c2be72eb27b6c38873356134b49873 -size 20237 +oid sha256:0025502ed7f47fc0b8375e21c82aad848c351f1062a6823fda796723da7dd813 +size 20243 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_everything_view_timeline.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_everything_view_timeline.png index f458cbf8d640..a44747778e9f 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_everything_view_timeline.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_everything_view_timeline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1edc1f97e1a6797902ca975b146515e97c2be72eb27b6c38873356134b49873 -size 20237 +oid sha256:0025502ed7f47fc0b8375e21c82aad848c351f1062a6823fda796723da7dd813 +size 20243 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_start_until_absolute_view_data.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_start_until_absolute_view_data.png index 4e6431cc6a25..89cd870c9883 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_start_until_absolute_view_data.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_start_until_absolute_view_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e4838826c28cb34cbcab5d079b8793469d9ff60334ffbdf8cac11ec90519f5a -size 11455 +oid sha256:da50418d2dc8020ac7971ca6dfe4f4259842f413c0003ec1eddc6725f1458ef2 +size 11462 diff --git a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_start_until_absolute_view_timeline.png b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_start_until_absolute_view_timeline.png index 73eb7e948162..729933363681 100644 --- a/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_start_until_absolute_view_timeline.png +++ b/crates/viewer/re_view_time_series/tests/snapshots/visible_time_range_start_until_absolute_view_timeline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:383a58a92cca202f1aa9d31916f4b0c56b680181160f89ee654f7c02baac6e4c -size 15414 +oid sha256:917129d95b81273eafbe579ee87e36b566a834ba3a8c5a3b8181d93002dc1888 +size 15396 diff --git a/crates/viewer/re_viewer/Cargo.toml b/crates/viewer/re_viewer/Cargo.toml index ca70511c5b83..a5b2ef5fc113 100644 --- a/crates/viewer/re_viewer/Cargo.toml +++ b/crates/viewer/re_viewer/Cargo.toml @@ -53,8 +53,7 @@ perf_telemetry = ["dep:re_perf_telemetry", "re_redap_client/perf_telemetry"] ## This only works on native. perf_telemetry_tracy = ["perf_telemetry", "re_perf_telemetry/tracy"] - -testing = ["dep:egui_kittest", "dep:tokio", "re_ui/testing", "re_analytics/testing"] +testing = ["dep:tokio", "re_ui/testing", "re_analytics/testing"] [dependencies] @@ -83,6 +82,7 @@ re_log_types.workspace = true re_memory_view.workspace = true re_memory.workspace = true re_mutex.workspace = true +re_protos.workspace = true re_query.workspace = true re_recording_panel.workspace = true re_redap_browser.workspace = true @@ -90,6 +90,7 @@ re_redap_client.workspace = true re_renderer = { workspace = true, default-features = false } re_sdk_types.workspace = true re_selection_panel.workspace = true +re_server.workspace = true re_sorbet.workspace = true re_string_interner.workspace = true re_time_panel.workspace = true @@ -98,12 +99,13 @@ re_types_core.workspace = true re_ui.workspace = true re_uri.workspace = true re_video.workspace = true +re_gamepad.workspace = true re_view.workspace = true re_view_bar_chart.workspace = true re_view_dataframe.workspace = true re_view_graph.workspace = true re_view_spatial.workspace = true -re_view_status.workspace = true +re_view_state_timeline.workspace = true re_view_tensor.workspace = true re_view_text_document.workspace = true re_view_text_log.workspace = true @@ -117,7 +119,6 @@ re_analytics = { workspace = true, optional = true } re_view_map = { workspace = true, optional = true } # Test dependencies (optional): -egui_kittest = { workspace = true, optional = true } tokio = { workspace = true, optional = true } @@ -126,7 +127,7 @@ ahash.workspace = true anyhow.workspace = true arrow.workspace = true bytemuck.workspace = true -cfg-if.workspace = true +camino.workspace = true crossbeam.workspace = true eframe = { workspace = true, default-features = false, features = [ "default_fonts", @@ -136,8 +137,10 @@ eframe = { workspace = true, default-features = false, features = [ egui_plot.workspace = true egui-wgpu.workspace = true egui.workspace = true +egui_inspection.workspace = true ehttp.workspace = true emath.workspace = true +futures.workspace = true glam.workspace = true image = { workspace = true, default-features = false, features = ["png"] } itertools.workspace = true @@ -161,9 +164,14 @@ wgpu.workspace = true # Native dependencies: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +egui_kittest.workspace = true poll-promise = { workspace = true, features = ["tokio"] } re_perf_telemetry = { workspace = true, optional = true } tokio.workspace = true +tokio-util = { workspace = true, features = ["compat"] } + +[target.'cfg(target_os = "windows")'.dependencies] +winit.workspace = true # web dependencies: [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -192,7 +200,6 @@ web-sys = { workspace = true, features = [ [dev-dependencies] -egui_kittest.workspace = true re_test_context.workspace = true re_test_viewport.workspace = true tempfile.workspace = true diff --git a/crates/viewer/re_viewer/data/app_icon.png b/crates/viewer/re_viewer/data/app_icon.png new file mode 100644 index 000000000000..a0697cbc00e5 Binary files /dev/null and b/crates/viewer/re_viewer/data/app_icon.png differ diff --git a/crates/viewer/re_viewer/data/app_icon_mac.png b/crates/viewer/re_viewer/data/app_icon_mac.png index 80df0b007d75..c7481f8016b3 100644 Binary files a/crates/viewer/re_viewer/data/app_icon_mac.png and b/crates/viewer/re_viewer/data/app_icon_mac.png differ diff --git a/crates/viewer/re_viewer/data/app_icon_windows.png b/crates/viewer/re_viewer/data/app_icon_windows.png deleted file mode 100644 index fd3787d6dce8..000000000000 Binary files a/crates/viewer/re_viewer/data/app_icon_windows.png and /dev/null differ diff --git a/crates/viewer/re_viewer/data/quick_start_guides/rust_connect.md b/crates/viewer/re_viewer/data/quick_start_guides/rust_connect.md index a1d95e9f2d8d..caef2d52c1f6 100644 --- a/crates/viewer/re_viewer/data/quick_start_guides/rust_connect.md +++ b/crates/viewer/re_viewer/data/quick_start_guides/rust_connect.md @@ -10,7 +10,7 @@ Let's try it out in a brand-new Rust project: cargo init cube && cd cube && cargo add rerun --features native_viewer ``` -Note that the Rerun SDK requires a working installation of Rust 1.92+. +Note that the Rerun SDK requires a working installation of Rust 1.95+. ## Logging your own data diff --git a/crates/viewer/re_viewer/data/quick_start_guides/rust_spawn.md b/crates/viewer/re_viewer/data/quick_start_guides/rust_spawn.md index edcbdda7f0b1..1abd5a0ee0ba 100644 --- a/crates/viewer/re_viewer/data/quick_start_guides/rust_spawn.md +++ b/crates/viewer/re_viewer/data/quick_start_guides/rust_spawn.md @@ -10,7 +10,7 @@ Let's try it out in a brand-new Rust project: cargo init cube && cd cube && cargo add rerun ``` -Note that the Rerun SDK requires a working installation of Rust 1.92+. +Note that the Rerun SDK requires a working installation of Rust 1.95+. ## Logging your own data diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs deleted file mode 100644 index 5267815fd104..000000000000 --- a/crates/viewer/re_viewer/src/app.rs +++ /dev/null @@ -1,4492 +0,0 @@ -use std::str::FromStr as _; -use std::sync::Arc; - -use ahash::HashMap; -use anyhow::Context as _; -use egui::{FocusDirection, Key}; -use itertools::Itertools as _; -use re_auth::credentials::CredentialsProvider as _; -use re_build_info::CrateVersion; -use re_byte_size::{MemUsageTree, MemUsageTreeCapture, NamedMemUsageTree}; -use re_capabilities::MainThreadToken; -use re_chunk::TimelineName; -use re_data_source::{AuthErrorHandler, FileContents, LogDataSource}; -use re_entity_db::InstancePath; -use re_entity_db::entity_db::EntityDb; -use re_log::debug_assert; -use re_log_channel::{ - DataSourceMessage, DataSourceUiCommand, LogReceiver, LogReceiverSet, LogSource, - RecordingOpenBehavior, -}; -use re_log_types::{ApplicationId, FileSource, LogMsg, RecordingId, StoreId, StoreKind, TableMsg}; -use re_redap_client::ConnectionRegistryHandle; -use re_renderer::WgpuResourcePoolStatistics; -use re_sdk_types::blueprint::components::{LoopMode, PlayState}; -use re_sdk_types::external::uuid; -use re_ui::{ContextExt as _, UICommand, UICommandSender as _, UiExt as _, notifications}; -use re_viewer_context::open_url::{OpenUrlOptions, ViewerOpenUrl, combine_with_base_url}; -use re_viewer_context::store_hub::{BlueprintPersistence, StoreHub, StoreHubStats}; -use re_viewer_context::{ - ActiveStoreContext, AppBlueprintCtx, AppOptions, AsyncRuntimeHandle, AuthContext, - CommandReceiver, CommandSender, ComponentUiRegistry, EditRedapServerModalCommand, - FallbackProviderRegistry, Item, MoveDirection, MoveSpeed, NeedsRepaint, RecordingOrTable, - Route, StorageContext, SystemCommand, SystemCommandSender as _, TableStore, TimeControlCommand, - ViewClass, ViewClassRegistry, ViewClassRegistryError, command_channel, sanitize_file_name, -}; - -use crate::AppState; -use crate::app_blueprint::{AppBlueprint, PanelStateOverrides}; -use crate::app_state::WelcomeScreenState; -use crate::background_tasks::BackgroundTasks; -use crate::event::ViewerEventDispatcher; -use crate::latency_tracker::ServerLatencyTrackers; -use crate::startup_options::StartupOptions; - -// ---------------------------------------------------------------------------- - -/// Storage key used to store the last run Rerun version. -/// -/// This is then used to detect if the user has recently upgraded Rerun. -const RERUN_VERSION_KEY: &str = "rerun.version"; - -const REDAP_TOKEN_KEY: &str = "rerun.redap_token"; - -#[cfg(not(target_arch = "wasm32"))] -const MIN_ZOOM_FACTOR: f32 = 0.2; -#[cfg(not(target_arch = "wasm32"))] -const MAX_ZOOM_FACTOR: f32 = 5.0; - -#[cfg(target_arch = "wasm32")] -struct PendingFilePromise { - recommended_store_id: Option, - force_store_info: bool, - promise: poll_promise::Promise>, -} - -/// The Rerun Viewer as an [`eframe`] application. -pub struct App { - #[allow(clippy::allow_attributes, dead_code)] // Unused on wasm32 - main_thread_token: MainThreadToken, - build_info: re_build_info::BuildInfo, - - app_env: crate::AppEnvironment, - - startup_options: StartupOptions, - start_time: web_time::Instant, - ram_limit_warner: re_memory::RamLimitWarner, - pub(crate) egui_ctx: egui::Context, - screenshotter: crate::screenshotter::Screenshotter, - texture_readback: crate::texture_readback::TextureReadbacks, - - #[cfg(target_arch = "wasm32")] - pub(crate) popstate_listener: Option, - - #[cfg(not(target_arch = "wasm32"))] - profiler: re_tracing::Profiler, - - /// Active in-memory profile capture, if any. - #[cfg(not(target_arch = "wasm32"))] - profile_capture: Option, - - /// Listens to the local text log stream - text_log_rx: crossbeam::channel::Receiver, - - component_ui_registry: ComponentUiRegistry, - component_fallback_registry: FallbackProviderRegistry, - - rx_log: LogReceiverSet, - - #[cfg(target_arch = "wasm32")] - open_files_promise: Option, - - /// What is serialized - pub(crate) state: AppState, - - /// Pending background tasks, e.g. files being saved. - pub(crate) background_tasks: BackgroundTasks, - - /// Interface for all recordings and blueprints - pub(crate) store_hub: Option, - - /// Notification panel. - pub(crate) notifications: notifications::NotificationUi, - - memory_panel: crate::memory_panel::MemoryPanel, - memory_panel_open: bool, - - /// Cached app overhead: total memory use minus sum of all recording chunk sizes. - /// Updated during GC when we have a fresh memory snapshot. - cached_app_overhead_bytes: Option, - - egui_debug_panel_open: bool, - - /// Last time the latency was deemed interesting. - /// - /// Note that initializing with an "old" `Instant` won't work reliably cross platform - /// since `Instant`'s counter may start at program start. - pub(crate) latest_latency_interest: Option, - - /// Measures how long a frame takes to paint - pub(crate) frame_time_history: egui::util::History, - - /// Commands to run at the end of the frame. - pub command_sender: CommandSender, - command_receiver: CommandReceiver, - cmd_palette: re_ui::CommandPalette, - - /// All known view types. - view_class_registry: ViewClassRegistry, - - pub(crate) panel_state_overrides_active: bool, - pub(crate) panel_state_overrides: PanelStateOverrides, - - reflection: re_types_core::reflection::Reflection, - - /// External interactions with the Viewer host (JS, custom egui app, notebook, etc.). - pub event_dispatcher: Option, - - connection_registry: ConnectionRegistryHandle, - - pub(crate) server_latency_trackers: ServerLatencyTrackers, - - /// The async runtime that should be used for all asynchronous operations. - /// - /// Using the global tokio runtime should be avoided since: - /// * we don't have a tokio runtime on web - /// * we want the user to have full control over the runtime, - /// and not expect that a global runtime exists. - async_runtime: AsyncRuntimeHandle, -} - -impl App { - pub fn new( - main_thread_token: MainThreadToken, - build_info: re_build_info::BuildInfo, - app_env: crate::AppEnvironment, - startup_options: StartupOptions, - creation_context: &eframe::CreationContext<'_>, - connection_registry: Option, - tokio_runtime: AsyncRuntimeHandle, - ) -> Self { - Self::with_commands( - main_thread_token, - build_info, - app_env, - startup_options, - creation_context, - connection_registry, - tokio_runtime, - crate::register_text_log_receiver(), - command_channel(), - ) - } - - /// Create a viewer that receives new log messages over time - #[expect(clippy::too_many_arguments)] - pub fn with_commands( - main_thread_token: MainThreadToken, - build_info: re_build_info::BuildInfo, - app_env: crate::AppEnvironment, - startup_options: StartupOptions, - creation_context: &eframe::CreationContext<'_>, - connection_registry: Option, - tokio_runtime: AsyncRuntimeHandle, - text_log_rx: crossbeam::channel::Receiver, - command_channel: (CommandSender, CommandReceiver), - ) -> Self { - re_tracing::profile_function!(); - - let connection_registry = connection_registry - .unwrap_or_else(re_redap_client::ConnectionRegistry::new_with_stored_credentials); - - // Only subscribe to auth changes and load credentials if we're supposed to use stored credentials. - // This prevents tests from being affected by stored credentials on the developer's machine. - if connection_registry.should_use_stored_credentials() { - let command_sender = command_channel.0.clone(); - re_auth::credentials::subscribe_auth_changes(move |user| { - command_sender.send_system(SystemCommand::OnAuthChanged(user.map(|user| { - AuthContext { - email: user.email, - org_name: user.org_name, - } - }))); - }); - - // Call get_token once so the auth state is initialized. - tokio_runtime.spawn_future(async move { - re_auth::credentials::CliCredentialsProvider::new() - .get_token() - .await - .ok(); - }); - } - - if connection_registry.should_use_stored_credentials() - && let Some(storage) = creation_context.storage - && let Some(tokens) = eframe::get_value(storage, REDAP_TOKEN_KEY) - { - connection_registry.load_tokens(tokens); - } - - let mut state: AppState = if startup_options.persist_state { - creation_context.storage - .and_then(|storage| { - // This re-implements: `eframe::get_value` so we can customize the warning message. - // TODO(#2849): More thorough error-handling. - let value = storage.get_string(eframe::APP_KEY)?; - match ron::from_str(&value) { - Ok(value) => Some(value), - Err(err) => { - re_log::warn!("Failed to restore application state. This is expected if you have just upgraded Rerun versions."); - re_log::debug!("Failed to decode RON for app state: {err}"); - None - } - } - }) - .unwrap_or_default() - } else { - AppState::default() - }; - - if startup_options.persist_state { - // Check if the user has recently upgraded Rerun. - if let Some(storage) = creation_context.storage { - let current_version = build_info.version; - let previous_version: Option = - storage.get_string(RERUN_VERSION_KEY).and_then(|version| { - // `CrateVersion::try_parse` is `const` (for good reasons), and needs a `&'static str`. - // In order to accomplish this, we need to leak the string here. - let version = Box::leak(version.into_boxed_str()); - CrateVersion::try_parse(version).ok() - }); - - if previous_version - .is_none_or(|previous_version| previous_version < CrateVersion::new(0, 24, 0)) - { - re_log::debug!( - "Upgrading from {} to {}.", - previous_version.map_or_else(|| "".to_owned(), |v| v.to_string()), - current_version - ); - // We used to have Dark as the hard-coded theme preference. Let's change that! - creation_context - .egui_ctx - .options_mut(|o| o.theme_preference = egui::ThemePreference::System); - } - } - } - - if let Some(video_decoder_hw_acceleration) = startup_options.video_decoder_hw_acceleration { - state.app_options.video.hw_acceleration = video_decoder_hw_acceleration; - } - - if app_env.is_test() { - state.app_options = AppOptions::test(); - } - - if startup_options.enable_experimental_status_view { - state.app_options.experimental.enable_status_view = true; - } - - let reflection = re_sdk_types::reflection::generate_reflection().unwrap_or_else(|err| { - re_log::error!( - "Failed to create list of serialized default values for components: {err}" - ); - Default::default() - }); - - let mut component_fallback_registry = - re_component_fallbacks::create_component_fallback_registry(); - - let view_class_registry = crate::default_views::create_view_class_registry( - &reflection, - &state.app_options, - &mut component_fallback_registry, - ) - .unwrap_or_else(|err| { - re_log::error!("Failed to create view class registry: {err}"); - Default::default() - }); - - #[allow(clippy::allow_attributes, unused_mut, clippy::needless_update)] - // false positive on web - let mut screenshotter = crate::screenshotter::Screenshotter::default(); - - #[cfg(not(target_arch = "wasm32"))] - if let Some(screenshot_path) = startup_options.screenshot_to_path_then_quit.clone() { - screenshotter.screenshot_to_path_then_quit(&creation_context.egui_ctx, screenshot_path); - } - - let (command_sender, command_receiver) = command_channel; - - let mut component_ui_registry = re_component_ui::create_component_ui_registry(); - re_data_ui::register_component_uis(&mut component_ui_registry); - - let (_adapter_backend, _device_tier) = creation_context.wgpu_render_state.as_ref().map_or( - ( - wgpu::Backend::Noop, - re_renderer::device_caps::DeviceCapabilityTier::Limited, - ), - |render_state| { - let egui_renderer = render_state.renderer.read(); - let render_ctx = egui_renderer - .callback_resources - .get::(); - - ( - render_state.adapter.get_info().backend, - render_ctx.map_or( - re_renderer::device_caps::DeviceCapabilityTier::Limited, - |ctx| ctx.device_caps().tier, - ), - ) - }, - ); - - #[cfg(feature = "analytics")] - if let Some(analytics) = re_analytics::Analytics::global_or_init() { - use crate::viewer_analytics::event; - - analytics.record(event::identify( - analytics.config(), - build_info.clone(), - &app_env, - )); - analytics.record(event::viewer_started( - &app_env, - &creation_context.egui_ctx, - _adapter_backend, - _device_tier, - )); - } - - let panel_state_overrides = startup_options.panel_state_overrides; - - let event_dispatcher = startup_options - .on_event - .clone() - .map(ViewerEventDispatcher::new); - - if !state.redap_servers.is_empty() { - command_sender.send_ui(UICommand::ExpandBlueprintPanel); - } - - creation_context.egui_ctx.on_end_pass( - "remove copied text formatting", - Arc::new(|ctx| { - ctx.output_mut(|o| { - for command in &mut o.commands { - if let egui::output::OutputCommand::CopyText(text) = command { - *text = re_format::remove_number_formatting(text); - } - } - }); - }), - ); - - { - // This is a workaround consuming the space and arrow keys so we can use them as timeline shortcuts. - // Egui's built in behavior is to interact with focus, and we don't want that. - // TODO(emilk/egui#7899): allow consuming events before egui uses them to move keyboard focus. - // TODO(emilk/egui#7659): allow disabling certain egui shortcuts. - let command_sender = command_sender.clone(); - creation_context.egui_ctx.on_begin_pass( - "rerun-kb-shortcuts", - Arc::new(move |ctx| { - // egui has already listened for arrow keys before this point, - // so in order for the arrow keys to NOT move the focus, we need to - // undo that focus change here: - let reset_focus_direction = ctx.input_mut(|i| { - i.key_pressed(Key::ArrowLeft) || i.key_pressed(Key::ArrowRight) - }); - - if reset_focus_direction { - ctx.memory_mut(|mem| { - mem.move_focus(FocusDirection::None); - }); - } - - // Consumes the used shortcut (including the "space" key): - if let Some(cmd) = UICommand::listen_for_kb_shortcut(ctx) { - command_sender.send_ui(cmd); - } - }), - ); - } - - Self { - main_thread_token, - build_info, - app_env, - startup_options, - start_time: web_time::Instant::now(), - ram_limit_warner: re_memory::RamLimitWarner::warn_at_fraction_of_max(0.75), - egui_ctx: creation_context.egui_ctx.clone(), - screenshotter, - texture_readback: Default::default(), - - #[cfg(target_arch = "wasm32")] - popstate_listener: None, - - #[cfg(not(target_arch = "wasm32"))] - profiler: Default::default(), - - #[cfg(not(target_arch = "wasm32"))] - profile_capture: None, - - text_log_rx, - component_ui_registry, - component_fallback_registry, - rx_log: Default::default(), - - #[cfg(target_arch = "wasm32")] - open_files_promise: Default::default(), - - state, - background_tasks: Default::default(), - store_hub: Some(StoreHub::new( - blueprint_loader(), - &crate::app_blueprint::setup_welcome_screen_blueprint, - )), - notifications: notifications::NotificationUi::new(creation_context.egui_ctx.clone()), - - memory_panel: Default::default(), - memory_panel_open: false, - cached_app_overhead_bytes: None, - - egui_debug_panel_open: false, - - latest_latency_interest: None, - - frame_time_history: egui::util::History::new(1..100, 0.5), - - command_sender, - command_receiver, - cmd_palette: Default::default(), - - view_class_registry, - - panel_state_overrides_active: true, - panel_state_overrides, - - reflection, - - event_dispatcher, - - connection_registry, - server_latency_trackers: ServerLatencyTrackers::default(), - async_runtime: tokio_runtime, - } - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn set_profiler(&mut self, profiler: re_tracing::Profiler) { - self.profiler = profiler; - } - - pub fn connection_registry(&self) -> &ConnectionRegistryHandle { - &self.connection_registry - } - - pub fn set_examples_manifest_url(&mut self, url: String) { - re_log::info!("Using manifest_url={url:?}"); - self.state.set_examples_manifest_url(&self.egui_ctx, url); - } - - pub fn build_info(&self) -> &re_build_info::BuildInfo { - &self.build_info - } - - pub fn startup_options(&self) -> &StartupOptions { - &self.startup_options - } - - pub fn app_options(&self) -> &AppOptions { - self.state.app_options() - } - - pub fn reflection(&self) -> &re_types_core::reflection::Reflection { - &self.reflection - } - - pub fn app_options_mut(&mut self) -> &mut AppOptions { - self.state.app_options_mut() - } - - pub fn app_env(&self) -> &crate::AppEnvironment { - &self.app_env - } - - /// The active recording [`StoreId`], if any, derived from the current [`Route`]. - pub fn active_recording_id(&self) -> Option<&StoreId> { - self.state.active_recording_id() - } - - /// Open a content URL in the viewer. - pub fn open_url_or_file(&self, url: &str) { - match ViewerOpenUrl::parse_with_options( - url, - &re_data_source::FromUriOptions { - accept_extensionless_http: true, - ..Default::default() - }, - ) { - Ok(url) => { - url.open( - &self.egui_ctx, - &OpenUrlOptions { - follow: false, - recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, - show_loader: true, - }, - &self.command_sender, - ); - } - Err(err) => { - if err.to_string().contains(url) { - re_log::error!("{err}"); - } else { - re_log::error!(?url, "Failed to open URL: {err}"); - } - } - } - } - - pub fn is_screenshotting(&self) -> bool { - self.screenshotter.is_screenshotting() - } - - #[expect(clippy::needless_pass_by_ref_mut)] - pub fn add_log_receiver(&mut self, rx: LogReceiver) { - re_log::debug!("Adding new log receiver: {}", rx.source()); - - // Make sure we wake up when a new message is available: - rx.set_waker({ - let egui_ctx = self.egui_ctx.clone(); - move || { - // Spend a few more milliseconds decoding incoming messages, - // then trigger a repaint (https://github.com/rerun-io/rerun/issues/963): - egui_ctx.request_repaint_after(std::time::Duration::from_millis(10)); - } - }); - - // Add unknown redap servers. - // - // Otherwise we end up in a situation where we have a data from an unknown server, - // which is unnecessary and can get us into a strange ui state. - if let LogSource::RedapGrpcStream { uri, .. } = rx.source() { - self.command_sender - .send_system(SystemCommand::AddRedapServer(uri.origin.clone())); - } - - self.rx_log.add(rx); - } - - /// Update the active [`re_viewer_context::TimeControl`]. And if the blueprint inspection - /// panel is open, also open that time control. - fn move_time(&mut self) { - if let Some(store_hub) = &self.store_hub - && let Some(store_id) = self.active_recording_id() - && let Some(blueprint) = store_hub.active_blueprint_for_app(store_id.application_id()) - { - let default_blueprint = store_hub.default_blueprint_for_app(store_id.application_id()); - - let blueprint_query = self - .state - .get_blueprint_query_for_viewer(blueprint) - .unwrap_or_else(|| { - re_chunk::LatestAtQuery::latest(re_viewer_context::blueprint_timeline()) - }); - - let bp_ctx = AppBlueprintCtx { - command_sender: &self.command_sender, - current_blueprint: blueprint, - default_blueprint, - blueprint_query, - }; - - let stable_dt = self.egui_ctx.input(|i| i.stable_dt); - - if let Some(recording) = store_hub.entity_db(store_id) { - // Are we still connected to the data source for the current store? - let more_data_is_streaming_in = - recording.data_source.as_ref().is_some_and(|store_source| { - self.rx_log - .sources() - .iter() - .any(|s| s.as_ref() == store_source) - }); - - let time_ctrl = self.state.time_control_mut(recording, &bp_ctx); - - // The state diffs are used to trigger callbacks if they are configured. - // If there's no active recording, we should not trigger any callbacks, but since there's an active recording here, - // we want to diff state changes. - let response = time_ctrl.update( - recording, - &re_viewer_context::TimeControlUpdateParams { - stable_dt, - more_data_is_streaming_in, - is_buffering: recording.is_buffering(), - should_diff_state: true, - }, - Some(&bp_ctx), - ); - - if response.needs_repaint == NeedsRepaint::Yes { - self.egui_ctx.request_repaint(); - } - - handle_time_ctrl_event(recording, self.event_dispatcher.as_ref(), &response); - } - - if self.app_options().inspect_blueprint_timeline { - // We ignore most things from the time control response for the blueprint but still - // need to repaint if requested. - let re_viewer_context::TimeControlResponse { - needs_repaint, - playing_change: _, - timeline_change: _, - time_change: _, - } = self.state.blueprint_time_control.update( - bp_ctx.current_blueprint, - &re_viewer_context::TimeControlUpdateParams { - stable_dt, - more_data_is_streaming_in: true, - is_buffering: false, - should_diff_state: false, - }, - None::<&AppBlueprintCtx<'_>>, - ); - - if needs_repaint == NeedsRepaint::Yes { - self.egui_ctx.request_repaint(); - } - - let undo_state = self - .state - .blueprint_undo_state - .entry(blueprint.store_id().clone()) - .or_default(); - // Apply changes to the blueprint time to the undo-state: - if self.state.blueprint_time_control.play_state() == PlayState::Following { - undo_state.redo_all(); - } else if let Some(time) = self.state.blueprint_time_control.time_int() { - undo_state.set_redo_time(time); - } - } - } - } - - pub fn msg_receive_set(&self) -> &LogReceiverSet { - &self.rx_log - } - - /// Adds a new view class to the viewer. - pub fn add_view_class( - &mut self, - ) -> Result<(), ViewClassRegistryError> { - self.view_class_registry.add_class::( - &self.reflection, - &self.state.app_options, - &mut self.component_fallback_registry, - ) - } - - /// Extends an already registered view class with additional systems (visualizers, context systems, fallbacks, etc.). - /// - /// **WARNING:** Many parts of the viewer assume that all views & visualizers are registered before the first frame is rendered. - /// Doing so later in the application life cycle may cause unexpected behavior. - pub fn extend_view_class( - &mut self, - view_class: re_sdk_types::ViewClassIdentifier, - register_fn: impl FnOnce( - &mut re_viewer_context::ViewSystemRegistrator<'_>, - ) -> Result<(), ViewClassRegistryError>, - ) -> Result<(), ViewClassRegistryError> { - self.view_class_registry.extend_class( - view_class, - &self.reflection, - &self.state.app_options, - &mut self.component_fallback_registry, - register_fn, - ) - } - - fn run_pending_system_commands(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) { - re_tracing::profile_function!(); - while let Some((from_where, cmd)) = self.command_receiver.recv_system() { - self.run_system_command(from_where, cmd, store_hub, egui_ctx); - } - } - - fn run_pending_ui_commands( - &mut self, - egui_ctx: &egui::Context, - app_blueprint: &AppBlueprint<'_>, - storage_context: &StorageContext<'_>, - store_context: Option<&ActiveStoreContext<'_>>, - route: &Route, - ) { - re_tracing::profile_function!(); - while let Some(cmd) = self.command_receiver.recv_ui() { - self.run_ui_command( - egui_ctx, - app_blueprint, - storage_context, - store_context, - route, - cmd, - ); - } - } - - /// If we're on web and use web history this updates the - /// web address bar and updates history. - /// - /// Otherwise this updates the viewer tracked history. - fn update_history(&mut self, store_hub: &StoreHub) { - if self.startup_options().web_history_enabled() { - // We don't want to spam the web history API with changes, because - // otherwise it will start complaining about it being an insecure - // operation. - // - // This is a kind of hacky way to fix that: If there are currently any - // inputs, don't update the web address bar. This works for most cases - // because you need to hold down pointer to aggressively scrub, need to - // hold down key inputs to quickly step through the timeline. - #[cfg(target_arch = "wasm32")] - if !self.egui_ctx.egui_is_using_pointer() - && self - .egui_ctx - .input(|input| !input.any_touches() && input.keys_down.is_empty()) - { - self.update_web_history(store_hub); - } - } else { - self.update_viewer_history(store_hub); - } - } - - /// Updates the viewer tracked history - fn update_viewer_history(&mut self, store_hub: &StoreHub) { - let route = self.state.navigation.current(); - let time_ctrl = route - .recording_id() - .and_then(|id| self.state.time_control(id)); - - let selection = self.state.selection_state.selected_items(); - - let Ok(url) = ViewerOpenUrl::from_context_expanded(store_hub, route, time_ctrl, selection) - else { - return; - }; - - self.state.history.update_current_url(url); - } - - /// Updates the web address and web history. - #[cfg(target_arch = "wasm32")] - fn update_web_history(&self, store_hub: &StoreHub) { - let route = self.state.navigation.current(); - let time_ctrl = route - .recording_id() - .and_then(|id| self.state.time_control(id)); - let selection = self.state.selection_state.selected_items(); - - let Ok(url) = ViewerOpenUrl::from_context_expanded(store_hub, route, time_ctrl, selection) - .map(|mut url| { - // We don't want to update the url while playing, so we use the last paused time. - if let Some(fragment) = url.fragment_mut() { - fragment.when = time_ctrl.and_then(|time_ctrl| { - Some(( - *time_ctrl.timeline_name(), - re_log_types::TimeCell { - typ: time_ctrl.time_type()?, - value: time_ctrl.last_paused_time()?.floor().into(), - }, - )) - }); - } - - url - }) - // History entries expect the url parameter, not the full url, therefore don't pass a base url. - .and_then(|url| url.sharable_url(None)) - else { - return; - }; - - re_log::trace!("Updating navigation bar"); - - use crate::web_history::{HistoryEntry, HistoryExt as _, history}; - use crate::web_tools::JsResultExt as _; - - /// Returns the url without the fragment - fn strip_fragment(url: &str) -> &str { - // Split by url code for '#', which is used for fragments. - url.rsplit_once("%23").map_or(url, |(url, _)| url) - } - - if let Some(history) = history().ok_or_log_js_error() { - let current_entry = history.current_entry().ok_or_log_js_error().flatten(); - let new_entry = HistoryEntry::new(url); - if Some(&new_entry) != current_entry.as_ref() { - // If only the fragment has changed, we replace history instead of pushing it. - if current_entry - .and_then(|entry| { - Some(( - entry.to_query_string().ok_or_log_js_error()?, - new_entry.to_query_string().ok_or_log_js_error()?, - )) - }) - .is_some_and(|(current, new)| strip_fragment(¤t) == strip_fragment(&new)) - { - history.replace_entry(new_entry).ok_or_log_js_error(); - } else { - history.push_entry(new_entry).ok_or_log_js_error(); - } - } - } - } - - fn close_recording(&self, store_hub: &mut StoreHub, entry: &RecordingOrTable) { - // TODO(#9464): Find a better successor here. - - if let RecordingOrTable::Recording { store_id } = entry { - store_hub.set_opened(store_id, false); - } - - let data_source = match entry { - RecordingOrTable::Recording { store_id } => { - store_hub.entity_db_entry(store_id).data_source.clone() - } - RecordingOrTable::Table { .. } => None, - }; - if let Some(data_source) = data_source { - // Only certain sources should be closed. - #[expect(clippy::match_same_arms)] - let should_close = match &data_source { - // Specific files should stop streaming when closing them. - LogSource::File { .. } => true, - - // Specific HTTP streams should stop streaming when closing them. - LogSource::HttpStream { .. } => true, - - // Specific GRPC streams should stop streaming when closing them. - // TODO(#10967): We still stream in some data after that. - LogSource::RedapGrpcStream { .. } => true, - - // Don't close generic connections (like to an SDK) that may feed in different recordings over time. - LogSource::RrdWebEvent - | LogSource::JsChannel { .. } - | LogSource::Sdk - | LogSource::Stdin - | LogSource::MessageProxy(_) => false, - }; - - if should_close { - self.rx_log.retain(|r| r.source() != &data_source); - } - } - - store_hub.remove(entry); - } - - fn run_system_command( - &mut self, - sent_from: &std::panic::Location<'_>, // Who sent this command? Useful for debugging! - cmd: SystemCommand, - store_hub: &mut StoreHub, - egui_ctx: &egui::Context, - ) { - re_tracing::profile_function!(cmd.debug_name()); - - match cmd { - SystemCommand::TimeControlCommands { - store_id, - time_commands, - } => { - match store_id.kind() { - StoreKind::Recording => { - store_hub.load_blueprint_and_caches(&store_id, &self.view_class_registry); // Ensure caches and blueprints - let route = Route::LocalRecording { - recording_id: store_id.clone(), - }; - let (storage_ctx, store_ctx) = store_hub.read_context(&route); // Materialize the target blueprint on-demand - - let Some(store_ctx) = store_ctx else { - re_log::debug_panic!( - "No store context found for recording {store_id:?} when handling time control commands sent from {sent_from}. This should never happen for local recording routes.", - ); - re_log::error_once!( - "Can't change time for recording {store_id:?} because it is not active." - ); - return; - }; - - let target_blueprint = store_ctx.blueprint; - let blueprint_query = self - .state - .blueprint_query_for_viewer(Some(target_blueprint)); - - let blueprint_ctx = AppBlueprintCtx { - command_sender: &self.command_sender, - current_blueprint: target_blueprint, - default_blueprint: storage_ctx - .hub - .default_blueprint_for_app(store_id.application_id()), - blueprint_query, - }; - - let time_ctrl = self - .state - .time_control_mut(store_ctx.recording, &blueprint_ctx); - - let response = time_ctrl.handle_time_commands( - Some(&blueprint_ctx), - store_ctx.recording, - &time_commands, - ); - - if response.needs_repaint == NeedsRepaint::Yes { - self.egui_ctx.request_repaint(); - } - - handle_time_ctrl_event( - store_ctx.recording, - self.event_dispatcher.as_ref(), - &response, - ); - } - StoreKind::Blueprint => { - if let Some(target_store) = store_hub.store_bundle().get(&store_id) { - let blueprint_ctx: Option<&AppBlueprintCtx<'_>> = None; - let response = self.state.blueprint_time_control.handle_time_commands( - blueprint_ctx, - target_store, - &time_commands, - ); - - if response.needs_repaint == NeedsRepaint::Yes { - self.egui_ctx.request_repaint(); - } - } - } - } - } - SystemCommand::SetUrlFragment { store_id, fragment } => { - // This adds new system commands, which will be handled later in the loop. - self.go_to_dataset_data(store_id, fragment); - } - SystemCommand::CopyViewerUrl(url) => { - if cfg!(target_arch = "wasm32") { - match combine_with_base_url( - self.startup_options.web_viewer_base_url().as_ref(), - [url], - ) { - Ok(url) => { - self.copy_text(url); - } - Err(err) => { - re_log::error!("{err}"); - } - } - } else { - self.copy_text(url); - } - } - SystemCommand::ActivateApp(app_id) => { - store_hub.load_persisted_blueprints_for_app(&app_id); - if let Some(recording_id) = store_hub.earliest_recording_for_app(&app_id) { - store_hub.load_blueprint_and_caches(&recording_id, &self.view_class_registry); - self.state - .navigation - .replace(Route::LocalRecording { recording_id }); - } else { - // TODO(RR-3713): show a blueprint for it anyway - re_log::warn_once!("Can't switch app-id - we have no recording for it"); - // If we can't go where we want to go, then go nowhere. - } - } - - SystemCommand::CloseApp(app_id) => { - store_hub.close_app(&app_id); - } - - SystemCommand::CloseRecordingOrTable(entry) => { - self.close_recording(store_hub, &entry); - } - - SystemCommand::CloseAllEntries => { - self.state.navigation.reset(); - store_hub.clear_entries(); - - // Stop receiving into the old recordings. - // This is most important when going back to the example screen by using the "Back" - // button in the browser, and there is still a connection downloading an .rrd. - // That's the case of `LogSource::HttpStream`. - // TODO(emilk): exactly what things get kept and what gets cleared? - self.rx_log.retain(|r| match r.source() { - LogSource::File { .. } | LogSource::HttpStream { .. } => false, - - LogSource::JsChannel { .. } - | LogSource::RrdWebEvent - | LogSource::Sdk - | LogSource::RedapGrpcStream { .. } - | LogSource::MessageProxy { .. } - | LogSource::Stdin => true, - }); - } - - SystemCommand::AddReceiver(rx) => { - re_log::debug!("Received AddReceiver"); - self.add_log_receiver(rx); - } - - SystemCommand::SetRoute(new_route) => { - if &new_route == self.state.navigation.current() { - return; - } - - // Suppress loading screen if we're loading a recording that's already loaded, even if only partially. - if let Route::Loading(source) = &new_route - && let Some(re_uri::RedapUri::DatasetData(dataset_uri)) = source.redap_uri() - && store_hub - .store_bundle() - .entity_dbs() - .any(|db| db.store_id() == &dataset_uri.store_id()) - { - return; - } - - if let Some(recording_id) = new_route.recording_id() { - store_hub.set_opened(recording_id, true); - store_hub.load_blueprint_and_caches(recording_id, &self.view_class_registry); - } - - if matches!(new_route, Route::Loading(_)) { - self.state - .selection_state - .set_selection(re_viewer_context::ItemCollection::default()); - } - - self.state.navigation.replace(new_route); - - egui_ctx.request_repaint(); // Make sure we actually see the new mode. - } - - SystemCommand::OpenSettings => { - self.state.navigation.replace(Route::Settings { - previous: Box::new(self.state.navigation.current().clone()), - }); - - #[cfg(feature = "analytics")] - re_analytics::record(|| re_analytics::event::SettingsOpened {}); - } - - SystemCommand::OpenChunkStoreBrowser { - store_id, - selected_chunk, - } => match self.state.navigation.current() { - Route::ChunkStoreBrowser { - store_id: current_store_id, - previous, - .. - } => { - self.state.navigation.replace(Route::ChunkStoreBrowser { - // History/share URLs may carry an explicit store; otherwise keep - // using the current chunk browser store context. - store_id: store_id.or_else(|| current_store_id.clone()), - selected_chunk, - previous: previous.clone(), - }); - } - current => { - self.state.navigation.replace(Route::ChunkStoreBrowser { - store_id: store_id.or_else(|| current.recording_id().cloned()), - selected_chunk, - previous: Box::new(current.clone()), - }); - } - }, - - SystemCommand::ResetRoute => { - self.state.navigation.reset(); - - egui_ctx.request_repaint(); // Make sure we actually see the new mode. - } - - SystemCommand::AddRedapServer(origin) => { - if origin == *re_redap_browser::EXAMPLES_ORIGIN { - return; - } - if self.state.redap_servers.has_server(&origin) { - return; - } - - self.state.redap_servers.add_server(origin.clone()); - - if self.state.navigation.current().recording_id().is_none() { - self.state.navigation.replace(Route::RedapServer(origin)); - } - self.command_sender.send_ui(UICommand::ExpandBlueprintPanel); - } - - SystemCommand::RemoveRedapServer(origin) => { - // Clearing blueprints must happen before closing the recordings (so we can know - // what to close) - store_hub.clear_blueprints_for_origin(&origin); - - // Close any recordings streaming from this server, otherwise their - // still-open connections keep emitting "Failed to connect to remote - // data source" warnings. - let recordings_to_close: Vec<_> = store_hub - .store_bundle() - .recordings_for_origin(&origin) - .map(|db| db.store_id().clone()) - .collect(); - - // Close the recordings before removing the server, to avoid a race - for store_id in recordings_to_close { - self.close_recording(store_hub, &store_id.into()); - } - - self.state - .redap_servers - .remove_server(&origin, &self.connection_registry); - } - - SystemCommand::EditRedapServerModal(command) => { - self.state.redap_servers.open_edit_server_modal(command); - } - - SystemCommand::LoadDataSource(data_source) => { - self.load_data_source(store_hub, egui_ctx, &data_source); - } - - SystemCommand::ResetViewer => self.reset_viewer(store_hub, egui_ctx), - SystemCommand::ClearActiveBlueprintAndEnableHeuristics => { - re_log::debug!("Clear and generate new blueprint"); - store_hub.clear_active_blueprint_and_generate(self.state.navigation.current()); - egui_ctx.request_repaint(); // Many changes take a frame delay to show up. - } - SystemCommand::ClearActiveBlueprint => { - // By clearing the blueprint the default blueprint will be restored - // at the beginning of the next frame. - re_log::debug!("Reset blueprint to default"); - store_hub.clear_active_blueprint(self.state.navigation.current()); - egui_ctx.request_repaint(); // Many changes take a frame delay to show up. - } - - SystemCommand::AppendToStore(store_id, chunks) => { - re_log::trace!( - "{}:{} Update {} entities: {}", - sent_from.file(), - sent_from.line(), - store_id.kind(), - chunks.iter().map(|c| c.entity_path()).join(", ") - ); - - let db = store_hub.entity_db_entry(&store_id); - - // No need to clear undo buffer if we're just appending static data. - // - // It would be nice to be able to undo edits to a recording, but - // we haven't implemented that yet. - if store_id.is_blueprint() && chunks.iter().any(|c| !c.is_static()) { - self.state - .blueprint_undo_state - .entry(store_id.clone()) - .or_default() - .clear_redo_buffer(db); - - if self.app_options().inspect_blueprint_timeline { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id, - time_commands: vec![TimeControlCommand::SetPlayState( - PlayState::Following, - )], - }); - } - } - - for chunk in chunks { - match db.add_chunk(&Arc::new(chunk)) { - Ok(_store_events) => {} - Err(err) => { - re_log::warn_once!("Failed to append chunk: {err}"); - } - } - } - } - - SystemCommand::UndoBlueprint { blueprint_id } => { - let inspect_blueprint_timeline = self.app_options().inspect_blueprint_timeline; - let blueprint_db = store_hub.entity_db_entry(&blueprint_id); - let undo_state = self - .state - .blueprint_undo_state - .entry(blueprint_id.clone()) - .or_default(); - - undo_state.undo(blueprint_db); - - // Update blueprint inspector timeline. - if inspect_blueprint_timeline { - if let Some(redo_time) = undo_state.redo_time() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: blueprint_id, - time_commands: vec![ - TimeControlCommand::SetPlayState(PlayState::Paused), - TimeControlCommand::SetTime(redo_time.into()), - ], - }); - } else { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: blueprint_id, - time_commands: vec![TimeControlCommand::SetPlayState( - PlayState::Following, - )], - }); - } - } - } - SystemCommand::RedoBlueprint { blueprint_id } => { - let inspect_blueprint_timeline = self.app_options().inspect_blueprint_timeline; - let undo_state = self - .state - .blueprint_undo_state - .entry(blueprint_id.clone()) - .or_default(); - - undo_state.redo(); - - // Update blueprint inspector timeline. - if inspect_blueprint_timeline { - if let Some(redo_time) = undo_state.redo_time() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: blueprint_id, - time_commands: vec![ - TimeControlCommand::SetPlayState(PlayState::Paused), - TimeControlCommand::SetTime(redo_time.into()), - ], - }); - } else { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: blueprint_id, - time_commands: vec![TimeControlCommand::SetPlayState( - PlayState::Following, - )], - }); - } - } - } - - SystemCommand::DropEntity(blueprint_id, entity_path) => { - let blueprint_db = store_hub.entity_db_entry(&blueprint_id); - blueprint_db.drop_entity_path_recursive(&entity_path); - } - - #[cfg(debug_assertions)] - SystemCommand::EnableInspectBlueprintTimeline(show) => { - self.app_options_mut().inspect_blueprint_timeline = show; - } - - SystemCommand::SetSelection(set) => { - if let Some(item) = set.selection.single_item() { - // If the selected item has its own page, switch to it. - if let Some(route) = Route::from_item(item) { - if let Route::LocalRecording { recording_id } = &route { - store_hub - .load_blueprint_and_caches(recording_id, &self.view_class_registry); - } - self.state.navigation.replace(route); - } - } - - self.state.selection_state.set_selection(set); - egui_ctx.request_repaint(); // Make sure we actually see the new selection. - } - - SystemCommand::SetFocus(item) => { - self.state.focused_item = Some(item); - } - - SystemCommand::ShowNotification(notification) => { - self.notifications.add(notification); - } - - SystemCommand::ReadbackAndSaveTexture(texture_readback_id) => { - self.texture_readback.push(texture_readback_id); - } - - #[cfg(not(target_arch = "wasm32"))] - SystemCommand::FileSaver(file_saver) => { - if let Err(err) = self.background_tasks.spawn_file_saver(file_saver) { - re_log::error!("Failed to save file: {err}"); - } - } - - SystemCommand::OnAuthChanged(auth) => { - self.state.auth_state = auth; - } - - SystemCommand::SetAuthCredentials { - access_token, - email, - } => { - let credentials = - match re_auth::oauth::Credentials::try_new(access_token, None, email) { - Ok(credentials) => credentials, - Err(err) => { - re_log::error!("Failed to create credentials: {err}"); - return; - } - }; - if let Err(err) = credentials.ensure_stored() { - re_log::error!("Failed to store credentials: {err}"); - } - } - SystemCommand::Logout => { - let signed_out_url = self - .startup_options - .login - .as_ref() - .map(|l| l.signed_out_url.as_str()); - match re_auth::oauth::clear_credentials(signed_out_url) { - Ok(Some(outcome)) => { - // Open the WorkOS logout URL to also end the browser session. - // This opens in a new tab/window so the viewer state is preserved. - // WorkOS clears its session cookies and redirects to /signed-out. - egui_ctx.open_url(egui::output::OpenUrl { - url: outcome.logout_url, - new_tab: true, - }); - } - Ok(None) => { - re_log::debug!("No session to logout from"); - } - Err(err) => { - re_log::error!("Failed to logout: {err}"); - } - } - let logged_out_origins = self.state.redap_servers.logout(); - - // Close any open recordings that came from the logged-out servers. - store_hub.retain_recordings(|db| { - let Some(data_source) = &db.data_source else { - return true; - }; - match data_source { - LogSource::RedapGrpcStream { uri, .. } => { - !logged_out_origins.contains(&uri.origin) - } - _ => true, - } - }); - - // Also stop receiving data from those servers. - self.rx_log.retain(|r| match r.source() { - LogSource::RedapGrpcStream { uri, .. } => { - !logged_out_origins.contains(&uri.origin) - } - _ => true, - }); - } - SystemCommand::SaveScreenshot { target, view_id } => { - if let Some(view_id) = view_id { - // Screenshot a specific view - if let Some(view_info) = self.egui_ctx.memory_mut(|mem| { - mem.caches - .cache::() - .get(&view_id) - .cloned() - }) { - let re_viewer_context::PublishedViewInfo { name, rect } = view_info; - let rect = rect.shrink(2.5); // Hacky: Shrink so we don't accidentally include the border of the view. - if !rect.is_positive() { - re_log::warn!("View too small for a screenshot"); - return; - } - - self.egui_ctx - .send_viewport_cmd(egui::ViewportCommand::Screenshot( - egui::UserData::new(re_viewer_context::ScreenshotInfo { - ui_rect: Some(rect), - pixels_per_point: self.egui_ctx.pixels_per_point(), - name, - target, - }), - )); - } else { - re_log::warn!("View {view_id} not found for screenshot"); - } - } else { - // Screenshot the entire viewer - self.egui_ctx - .send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::new( - re_viewer_context::ScreenshotInfo { - ui_rect: None, - pixels_per_point: self.egui_ctx.pixels_per_point(), - name: "screenshot".to_owned(), - target, - }, - ))); - } - - // Screenshot commands may be triggered from receiving messages over the network, so we may not actually do any painting right now. - // Make sure we do at least once, so the screenshot gets saved out. - self.egui_ctx.request_repaint(); - - // TODO(#12481): Depending on the platform we a request repaint alone isn't enough to wake up the viewer. - // For now we do a focus switch but this isn't ideal since it breaks the flow of programmatic screenshot taking. - self.egui_ctx - .send_viewport_cmd(egui::ViewportCommand::Focus); - } - } - } - - pub fn auth_error_handler(sender: CommandSender) -> AuthErrorHandler { - Arc::new(move |url, _err| { - sender.send_system(SystemCommand::EditRedapServerModal( - EditRedapServerModalCommand { - origin: url.origin.clone(), - open_on_success: Some(url.to_string()), - title: Some("Authenticate to see this recording".to_owned()), - }, - )); - }) - } - - /// Loads a data source into the viewer. - /// - /// Tries to detect whether the datasource is already present (either still streaming in or already loaded), - /// and if so, will not load the data again. - /// Instead, it will only perform any kind of selection/mode-switching operations associated with loading the given data source. - /// - /// Note that we *do not* change the route here _unconditionally_. - /// For instance if the datasource is a blueprint for a dataset that may be loaded later, - /// we don't want to switch out to it while the user browses a server. - fn load_data_source( - &mut self, - store_hub: &mut StoreHub, - egui_ctx: &egui::Context, - data_source: &LogDataSource, - ) { - re_tracing::profile_function!(); - - // Check if we've already loaded this data source and should just switch to it. - // - // Go through all sources that are still loading and those that are already in the store_hub. - // (if we look only at the one from the store_hub, we might miss those that haven't hit it yet) - let active_sources = self.rx_log.sources(); - // Only consider recordings for dedup, not blueprints. - // Blueprints loaded alongside a recording share the same `data_source`, - // but they should not prevent re-opening a closed recording. - let store_sources = store_hub - .store_bundle() - .recordings() - .filter_map(|db| db.data_source.as_ref()); - let mut all_sources = store_sources.chain(active_sources.iter().map(|s| s.as_ref())); - - match data_source { - LogDataSource::HttpUrl { url, follow } => { - let new_source = LogSource::HttpStream { - url: url.to_string(), - follow: *follow, - }; - - if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { - if let Some(entity_db) = store_hub.find_recording_store_by_source(&new_source) { - if *follow { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: entity_db.store_id().clone(), - time_commands: vec![TimeControlCommand::SetPlayState( - PlayState::Following, - )], - }); - } - - let store_id = entity_db.store_id().clone(); - debug_assert!(store_id.is_recording()); // `find_recording_store_by_source` should have filtered for recordings rather than blueprints. - drop(all_sources); - self.make_store_active_and_highlight(store_hub, egui_ctx, &store_id); - } - return; - } - } - - #[cfg(not(target_arch = "wasm32"))] - LogDataSource::FilePath { path, follow, .. } => { - let new_source = LogSource::File { - path: path.clone(), - follow: *follow, - }; - if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { - drop(all_sources); - self.try_make_recording_from_source_active(egui_ctx, store_hub, &new_source); - return; - } - } - - LogDataSource::FileContents(_file_source, _file_contents) => { - // For raw file contents we currently can't determine whether we're already receiving them. - } - - #[cfg(not(target_arch = "wasm32"))] - LogDataSource::Stdin => { - let new_source = LogSource::Stdin; - if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { - drop(all_sources); - self.try_make_recording_from_source_active(egui_ctx, store_hub, &new_source); - return; - } - } - - LogDataSource::RedapDatasetSegment { uri, open_behavior } => { - let new_source = LogSource::RedapGrpcStream { - uri: uri.clone(), - open_behavior: *open_behavior, - }; - if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { - // We're already receiving from the exact same data source! - // But we still should navigate if requested according to the fragments if any. - drop(all_sources); - match *open_behavior { - RecordingOpenBehavior::Background => {} - RecordingOpenBehavior::Open => { - store_hub.set_opened(&uri.store_id(), true); - } - RecordingOpenBehavior::OpenAndSelect => { - // First make the recording itself active. - // `go_to_dataset_data` may override the selection again, but this is important regardless, - // since `go_to_dataset_data` does not change the active recording. - self.make_store_active_and_highlight( - store_hub, - egui_ctx, - &uri.store_id(), - ); - } - } - - // Note that applying the fragment changes the per-recording settings like the active time cursor. - // Therefore, we apply it even when open_behavior is Background. - self.go_to_dataset_data(uri.store_id(), uri.fragment.clone()); - - return; - } - } - - LogDataSource::RedapProxy(uri) => { - let new_source = LogSource::MessageProxy(uri.clone()); - if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { - drop(all_sources); - self.try_make_recording_from_source_active(egui_ctx, store_hub, &new_source); - return; - } - } - } - - let sender = self.command_sender.clone(); - let stream = data_source - .clone() - .stream(Self::auth_error_handler(sender), &self.connection_registry); - - #[cfg(feature = "analytics")] - if let Some(analytics) = re_analytics::Analytics::global_or_init() { - let data_source_analytics = data_source.analytics(); - analytics.record(re_analytics::event::LoadDataSource { - source_type: data_source_analytics.source_type, - file_extension: data_source_analytics.file_extension, - file_source: data_source_analytics.file_source, - started_successfully: stream.is_ok(), - }); - } - - match stream { - Ok(rx) => self.add_log_receiver(rx), - Err(err) => { - re_log::error!("Failed to open data source: {}", re_error::format(err)); - } - } - } - - /// Applies a fragment. - /// - /// Does *not* switch the active recording. - fn go_to_dataset_data(&self, store_id: StoreId, fragment: re_uri::Fragment) { - let re_uri::Fragment { - selection, - when, - time_selection, - } = fragment; - - if let Some(selection) = selection { - let re_log_types::DataPath { - entity_path, - instance, - component, - } = selection; - - let item = if let Some(component) = component { - Item::from(re_log_types::ComponentPath::new(entity_path, component)) - } else if let Some(instance) = instance { - Item::from(InstancePath::instance(entity_path, instance)) - } else { - Item::from(entity_path) - }; - - self.command_sender - .send_system(SystemCommand::set_selection(item.clone())); - } - - let mut time_commands = Vec::new(); - if let Some(time_selection) = time_selection { - time_commands.push(TimeControlCommand::SetActiveTimeline( - *time_selection.timeline.name(), - )); - time_commands.push(TimeControlCommand::SetTimeSelection(time_selection.range)); - time_commands.push(TimeControlCommand::SetLoopMode(LoopMode::Selection)); - } - - if let Some((timeline, timecell)) = when { - time_commands.push(TimeControlCommand::SetActiveTimeline(timeline)); - time_commands.push(TimeControlCommand::SetPlayState(PlayState::Paused)); - time_commands.push(TimeControlCommand::SetTime(timecell.value.into())); - } - - if !time_commands.is_empty() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id, - time_commands, - }); - } - } - - fn run_ui_command( - &mut self, - egui_ctx: &egui::Context, - app_blueprint: &AppBlueprint<'_>, - storage_context: &StorageContext<'_>, - store_context: Option<&ActiveStoreContext<'_>>, - route: &Route, - cmd: UICommand, - ) { - let mut force_store_info = false; - let active_store_id = store_context - .map(|ctx| ctx.recording_store_id().clone()) - // Don't redirect data to the welcome screen. - .filter(|store_id| store_id.application_id() != StoreHub::welcome_screen_app_id()) - .unwrap_or_else(|| { - // If we don't have any application ID to recommend (which means we are on the welcome screen), - // then just generate a new one using a UUID. - let application_id = ApplicationId::random(); - - // NOTE: We don't override blueprints' store IDs anyhow, so it is sound to assume that - // this can only be a recording. - let recording_id = RecordingId::random(); - - // We're creating a recording just-in-time, directly from the viewer. - // We need those store infos or the data will just be silently ignored. - force_store_info = true; - - StoreId::recording(application_id, recording_id) - }); - - match cmd { - UICommand::SaveRecording => { - #[cfg(target_arch = "wasm32")] // Web - { - if let Err(err) = save_active_recording(self, store_context, None) { - re_log::error!("Failed to save recording: {err}"); - } - } - - #[cfg(not(target_arch = "wasm32"))] // Native - { - let mut selected_stores = vec![]; - for item in self.state.selection_state.selected_items().iter_items() { - match item { - Item::AppId(selected_app_id) => { - for recording in storage_context.bundle.recordings() { - if recording.application_id() == selected_app_id { - selected_stores.push(recording.store_id().clone()); - } - } - } - Item::StoreId(store_id) => { - selected_stores.push(store_id.clone()); - } - _ => {} - } - } - - let selected_stores = selected_stores - .iter() - .filter_map(|store_id| storage_context.bundle.get(store_id)) - .collect_vec(); - - if selected_stores.is_empty() { - if let Err(err) = save_active_recording(self, store_context, None) { - re_log::error!("Failed to save recording: {err}"); - } - } else if selected_stores.len() == 1 { - // Common case: saving a single recording. - // In this case we want the user to be able to pick a file name (not just a folder): - if let Err(err) = save_recording(self, selected_stores[0], None) { - re_log::error!("Failed to save recording: {err}"); - } - } else { - // Save all selected recordings to a folder: - if let Some(folder) = rfd::FileDialog::new() - .set_title("Save recordings to folder") - .pick_folder() - { - self.save_many_recordings(&selected_stores, &folder); - } else { - re_log::info!("No folder selected - recordings not saved."); - } - } - } - } - UICommand::SaveRecordingSelection => { - if let Err(err) = save_active_recording( - self, - store_context, - self.state.loop_selection(store_context), - ) { - re_log::error!("Failed to save recording: {err}"); - } - } - - UICommand::SaveBlueprint => { - if let Err(err) = save_blueprint(self, store_context) { - re_log::error!("Failed to save blueprint: {err}"); - } - } - - #[cfg(not(target_arch = "wasm32"))] - UICommand::Open => { - for file_path in open_file_dialog_native(self.main_thread_token) { - self.command_sender - .send_system(SystemCommand::LoadDataSource(LogDataSource::FilePath { - file_source: FileSource::FileDialog { - recommended_store_id: None, - force_store_info, - }, - path: file_path, - follow: false, - })); - } - } - #[cfg(target_arch = "wasm32")] - UICommand::Open => { - let egui_ctx = egui_ctx.clone(); - - let promise = poll_promise::Promise::spawn_local(async move { - let file = async_open_rrd_dialog().await; - egui_ctx.request_repaint(); // Wake ui thread - file - }); - - self.open_files_promise = Some(PendingFilePromise { - recommended_store_id: None, - force_store_info, - promise, - }); - } - - #[cfg(not(target_arch = "wasm32"))] - UICommand::Import => { - for file_path in open_file_dialog_native(self.main_thread_token) { - self.command_sender - .send_system(SystemCommand::LoadDataSource(LogDataSource::FilePath { - file_source: FileSource::FileDialog { - recommended_store_id: Some(active_store_id.clone()), - force_store_info, - }, - path: file_path, - follow: false, - })); - } - } - #[cfg(target_arch = "wasm32")] - UICommand::Import => { - let egui_ctx = egui_ctx.clone(); - - let promise = poll_promise::Promise::spawn_local(async move { - let file = async_open_rrd_dialog().await; - egui_ctx.request_repaint(); // Wake ui thread - file - }); - - self.open_files_promise = Some(PendingFilePromise { - recommended_store_id: Some(active_store_id.clone()), - force_store_info, - promise, - }); - } - - UICommand::OpenUrl => { - self.state.open_url_modal.open(); - } - - UICommand::CloseCurrentRecording => { - let cur_rec = store_context.map(|ctx| ctx.recording.store_id()); - if let Some(cur_rec) = cur_rec { - self.command_sender - .send_system(SystemCommand::CloseRecordingOrTable(cur_rec.clone().into())); - } - } - UICommand::CloseAllEntries => { - self.command_sender - .send_system(SystemCommand::CloseAllEntries); - } - - UICommand::NextRecording => { - self.state - .recording_panel - .send_command(re_recording_panel::RecordingPanelCommand::SelectNextRecording); - } - UICommand::PreviousRecording => { - self.state.recording_panel.send_command( - re_recording_panel::RecordingPanelCommand::SelectPreviousRecording, - ); - } - - UICommand::NavigateBack => { - if let Some(url) = self.state.history.go_back() { - url.clone().open( - egui_ctx, - &OpenUrlOptions { - follow: true, - recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, - show_loader: true, - }, - &self.command_sender, - ); - } - } - UICommand::NavigateForward => { - if let Some(url) = self.state.history.go_forward() { - url.clone().open( - egui_ctx, - &OpenUrlOptions { - follow: true, - recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, - show_loader: true, - }, - &self.command_sender, - ); - } - } - - UICommand::Undo => { - if let Some(store_context) = store_context { - let blueprint_id = store_context.blueprint.store_id().clone(); - self.command_sender - .send_system(SystemCommand::UndoBlueprint { blueprint_id }); - } - } - UICommand::Redo => { - if let Some(store_context) = store_context { - let blueprint_id = store_context.blueprint.store_id().clone(); - self.command_sender - .send_system(SystemCommand::RedoBlueprint { blueprint_id }); - } - } - - #[cfg(not(target_arch = "wasm32"))] - UICommand::Quit => { - egui_ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - - UICommand::OpenWebHelp => { - egui_ctx.open_url(egui::output::OpenUrl { - url: "https://www.rerun.io/docs/getting-started/navigating-the-viewer" - .to_owned(), - new_tab: true, - }); - } - - UICommand::OpenRerunDiscord => { - egui_ctx.open_url(egui::output::OpenUrl { - url: "https://discord.gg/PXtCgFBSmH".to_owned(), - new_tab: true, - }); - } - - UICommand::ResetViewer => self.command_sender.send_system(SystemCommand::ResetViewer), - UICommand::ClearActiveBlueprint => { - self.command_sender - .send_system(SystemCommand::ClearActiveBlueprint); - } - UICommand::ClearActiveBlueprintAndEnableHeuristics => { - self.command_sender - .send_system(SystemCommand::ClearActiveBlueprintAndEnableHeuristics); - } - - #[cfg(not(target_arch = "wasm32"))] - UICommand::OpenProfiler => { - self.profiler.start(); - } - - #[cfg(not(target_arch = "wasm32"))] - UICommand::CaptureProfileTrace => { - if self.profile_capture.is_none() { - self.profile_capture = Some(re_tracing::ProfileCapture::start(5)); - egui_ctx.request_repaint(); - } - } - - UICommand::ToggleMemoryPanel => { - self.memory_panel_open ^= true; - } - UICommand::TogglePanelStateOverrides => { - self.panel_state_overrides_active ^= true; - } - UICommand::ToggleTopPanel => { - app_blueprint.toggle_top_panel(&self.command_sender); - } - UICommand::ToggleBlueprintPanel => { - app_blueprint.toggle_blueprint_panel(&self.command_sender); - } - UICommand::ExpandBlueprintPanel => { - if !app_blueprint.blueprint_panel_state().is_expanded() { - app_blueprint.toggle_blueprint_panel(&self.command_sender); - } - } - UICommand::ToggleSelectionPanel => { - app_blueprint.toggle_selection_panel(&self.command_sender); - } - UICommand::ExpandSelectionPanel => { - if !app_blueprint.selection_panel_state().is_expanded() { - app_blueprint.toggle_selection_panel(&self.command_sender); - } - } - UICommand::ToggleTimePanel => app_blueprint.toggle_time_panel(&self.command_sender), - - UICommand::ToggleChunkStoreBrowser => match self.state.navigation.current() { - Route::ChunkStoreBrowser { previous, .. } => { - self.state.navigation.replace((**previous).clone()); - } - - current => { - self.state.navigation.replace(Route::ChunkStoreBrowser { - store_id: current.recording_id().cloned(), - selected_chunk: None, - previous: Box::new(current.clone()), - }); - } - }, - - #[cfg(debug_assertions)] - UICommand::ToggleBlueprintInspectionPanel => { - self.app_options_mut().inspect_blueprint_timeline ^= true; - } - - #[cfg(debug_assertions)] - UICommand::ToggleEguiDebugPanel => { - self.egui_debug_panel_open ^= true; - } - - UICommand::ToggleFullscreen => { - self.toggle_fullscreen(); - } - - UICommand::Settings => { - self.command_sender.send_system(SystemCommand::OpenSettings); - } - - #[cfg(not(target_arch = "wasm32"))] - UICommand::ZoomIn => { - let mut zoom_factor = egui_ctx.zoom_factor(); - zoom_factor += 0.1; - zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); - zoom_factor = (zoom_factor * 10.).round() / 10.; - egui_ctx.set_zoom_factor(zoom_factor); - } - #[cfg(not(target_arch = "wasm32"))] - UICommand::ZoomOut => { - let mut zoom_factor = egui_ctx.zoom_factor(); - zoom_factor -= 0.1; - zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); - zoom_factor = (zoom_factor * 10.).round() / 10.; - egui_ctx.set_zoom_factor(zoom_factor); - } - #[cfg(not(target_arch = "wasm32"))] - UICommand::ZoomReset => { - egui_ctx.set_zoom_factor(1.0); - } - - UICommand::ToggleCommandPalette => { - self.cmd_palette.toggle(); - } - - UICommand::PlaybackTogglePlayPause => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::TogglePlayPause], - }); - } - } - UICommand::PlaybackFollow => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::SetPlayState( - PlayState::Following, - )], - }); - } - } - UICommand::PlaybackStepBack => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::StepTimeBack], - }); - } - } - UICommand::PlaybackStepForward => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::StepTimeForward], - }); - } - } - UICommand::PlaybackBack => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::Move { - direction: MoveDirection::Back, - speed: MoveSpeed::Normal, - }], - }); - } - } - UICommand::PlaybackForward => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::Move { - direction: MoveDirection::Forward, - speed: MoveSpeed::Normal, - }], - }); - } - } - UICommand::PlaybackBackFast => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::Move { - direction: MoveDirection::Back, - speed: MoveSpeed::Fast, - }], - }); - } - } - UICommand::PlaybackForwardFast => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::Move { - direction: MoveDirection::Forward, - speed: MoveSpeed::Fast, - }], - }); - } - } - UICommand::PlaybackBeginning => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::MoveBeginning], - }); - } - } - UICommand::PlaybackEnd => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::MoveEnd], - }); - } - } - UICommand::PlaybackRestart => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::Restart], - }); - } - } - - UICommand::PlaybackSpeed(speed) => { - if let Some(store_id) = route.recording_id() { - self.command_sender - .send_system(SystemCommand::TimeControlCommands { - store_id: store_id.clone(), - time_commands: vec![TimeControlCommand::SetSpeed(speed.0.0)], - }); - } - } - - #[cfg(not(target_arch = "wasm32"))] - UICommand::ScreenshotWholeApp => { - self.screenshotter.request_screenshot(egui_ctx); - } - #[cfg(not(target_arch = "wasm32"))] - UICommand::PrintChunkStore => { - if let Some(ctx) = store_context { - let text = format!("{}", ctx.recording.storage_engine().store()); - egui_ctx.copy_text(text.clone()); - println!("{text}"); - } - } - #[cfg(not(target_arch = "wasm32"))] - UICommand::PrintBlueprintStore => { - if let Some(ctx) = store_context { - let text = format!("{}", ctx.blueprint.storage_engine().store()); - egui_ctx.copy_text(text.clone()); - println!("{text}"); - } - } - #[cfg(not(target_arch = "wasm32"))] - UICommand::PrintPrimaryCache => { - if let Some(ctx) = store_context { - let text = format!("{:?}", ctx.recording.storage_engine().cache()); - egui_ctx.copy_text(text.clone()); - println!("{text}"); - } - } - - #[cfg(debug_assertions)] - UICommand::ResetEguiMemory => { - egui_ctx.memory_mut(|mem| *mem = Default::default()); - - // re-apply style, which is lost when resetting memory - re_ui::apply_style_and_install_loaders(egui_ctx); - } - - UICommand::Share => { - let selection = self.state.selection_state.selected_items(); - let rec_cfg = route - .recording_id() - .and_then(|id| self.state.time_controls.get(id)); - if let Err(err) = - self.state - .share_modal - .open(storage_context.hub, route, rec_cfg, selection) - { - re_log::error!("Cannot share link to current screen: {err}"); - } - } - UICommand::CopyDirectLink => { - match ViewerOpenUrl::from_route(storage_context.hub, route) { - Ok(url) => self.run_copy_link_command(&url), - Err(err) => re_log::error!("{err}"), - } - } - - UICommand::CopyTimeSelectionLink => { - match ViewerOpenUrl::from_route(storage_context.hub, route) { - Ok(mut url) => { - if let Some(fragment) = url.fragment_mut() { - let time_ctrl = route - .recording_id() - .and_then(|id| self.state.time_control(id)); - - if let Some(time_ctrl) = &time_ctrl - && let Some(time_selection) = time_ctrl.time_selection() - && let Some(timeline) = time_ctrl.timeline() - { - fragment.time_selection = Some(re_uri::TimeSelection { - timeline: *timeline, - range: time_selection.to_int(), - }); - } else { - re_log::warn!("No timeline selection to copy"); - } - } else { - re_log::warn!( - "The current recording doesn't support sharing a time range" - ); - } - - self.run_copy_link_command(&url); - } - Err(err) => re_log::error!("{err}"), - } - } - - #[cfg(target_arch = "wasm32")] - UICommand::RestartWithWebGl => { - if crate::web_tools::set_url_parameter_and_refresh("renderer", "webgl").is_err() { - re_log::error!("Failed to set URL parameter `renderer=webgl` & refresh page."); - } - } - - #[cfg(target_arch = "wasm32")] - UICommand::RestartWithWebGpu => { - if crate::web_tools::set_url_parameter_and_refresh("renderer", "webgpu").is_err() { - re_log::error!("Failed to set URL parameter `renderer=webgpu` & refresh page."); - } - } - - UICommand::CopyEntityHierarchy => { - self.copy_entity_hierarchy_to_clipboard(egui_ctx, store_context); - } - - UICommand::AddRedapServer => { - self.state.redap_servers.open_add_server_modal(); - } - } - } - - #[cfg(not(target_arch = "wasm32"))] - fn save_many_recordings(&mut self, stores: &[&EntityDb], folder: &std::path::Path) { - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - - use re_log::ResultExt as _; - use tap::Pipe as _; - - re_tracing::profile_function!(); - - let num_stores = stores.len(); - let any_error = Arc::new(AtomicBool::new(false)); - let num_remaining = Arc::new(AtomicUsize::new(stores.len())); - - re_log::info!("Saving {num_stores} recordings to {}…", folder.display()); - - for store in stores { - let messages = store.to_messages(None).collect_vec(); - - let file_name = if let Some(rec_name) = store - .recording_info_property::( - re_sdk_types::archetypes::RecordingInfo::descriptor_name().component, - ) { - rec_name.to_string() - } else { - format!("{}-{}", store.application_id(), store.recording_id()) - } - .pipe(|name| sanitize_file_name(&name)) - .pipe(|stem| format!("{stem}.rrd")); - - let file_path = folder.join(file_name.clone()); - let any_error = any_error.clone(); - let num_remaining = num_remaining.clone(); - let folder = folder.display().to_string(); - - self.background_tasks - .spawn_threaded_promise(file_name, move || { - let res = crate::saving::encode_to_file( - re_build_info::CrateVersion::LOCAL, - &file_path, - messages.into_iter(), - ); - - if res.is_err() { - any_error.store(true, Ordering::Relaxed); - } - - let num_remaining = num_remaining.fetch_sub(1, Ordering::Relaxed) - 1; - - if num_remaining == 0 { - if any_error.load(Ordering::Relaxed) { - re_log::error!("Some recordings failed to save."); - } else { - re_log::info!("{num_stores} recordings successfully saved to {folder}"); - } - } - - res - }) - .ok_or_log_error_once(); - } - } - - fn run_copy_link_command(&mut self, content_url: &ViewerOpenUrl) { - let base_url = self.startup_options.web_viewer_base_url(); - - match content_url.sharable_url(base_url.as_ref()) { - Ok(url) => { - self.copy_text(url); - } - Err(err) => { - re_log::error!("{err}"); - } - } - } - - /// Copies text to the clipboard, and gives a notification about it. - fn copy_text(&mut self, url: String) { - self.notifications - .success(format!("Copied {url:?} to clipboard")); - self.egui_ctx.copy_text(url); - } - - fn copy_entity_hierarchy_to_clipboard( - &mut self, - egui_ctx: &egui::Context, - store_context: Option<&ActiveStoreContext<'_>>, - ) { - let Some(entity_db) = store_context.as_ref().map(|ctx| ctx.recording) else { - re_log::warn!("Could not copy entity hierarchy: No active recording"); - return; - }; - - use std::fmt::Write as _; - - let mut hierarchy_text = String::new(); - - // Add application ID and recording ID header - write!( - hierarchy_text, - "Application ID: {}\nRecording ID: {}\n\n", - entity_db.application_id(), - entity_db.recording_id() - ) - .ok(); - - hierarchy_text.push_str(&entity_db.format_with_components()); - - if hierarchy_text.is_empty() { - hierarchy_text = "(no entities)".to_owned(); - } - - egui_ctx.copy_text(hierarchy_text.clone()); - self.notifications - .success("Copied entity hierarchy with schema to clipboard".to_owned()); - } - - fn memory_panel_ui( - &mut self, - ui: &mut egui::Ui, - gpu_resource_stats: &WgpuResourcePoolStatistics, - mem_usage_tree: Option, - store_stats: Option<&StoreHubStats>, - storage_context: &re_viewer_context::StorageContext<'_>, - ) { - let frame = egui::Frame { - fill: ui.visuals().panel_fill, - ..ui.tokens().bottom_panel_frame() - }; - - egui::Panel::bottom("memory_panel") - .default_size(300.0) - .resizable(true) - .frame(frame) - .show_animated_inside(ui, self.memory_panel_open, |ui| { - self.memory_panel.ui( - ui, - &self.state.app_options().memory_limit, - mem_usage_tree, - gpu_resource_stats, - store_stats, - storage_context, - ); - }); - } - - fn egui_debug_panel_ui(&self, ui: &mut egui::Ui) { - let egui_ctx = ui.ctx().clone(); - - egui::Panel::left("style_panel") - .default_size(300.0) - .resizable(true) - .frame(ui.tokens().top_panel_frame()) - .show_animated_inside(ui, self.egui_debug_panel_open, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - if ui - .button("request_discard") - .on_hover_text("Request a second layout pass. Just for testing.") - .clicked() - { - ui.request_discard("testing"); - } - - egui::CollapsingHeader::new("egui settings") - .default_open(false) - .show(ui, |ui| { - egui_ctx.settings_ui(ui); - }); - - egui::CollapsingHeader::new("egui inspection") - .default_open(false) - .show(ui, |ui| { - egui_ctx.inspection_ui(ui); - }); - }); - }); - } - - /// Top-level ui function. - /// - /// Shows the viewer ui. - #[expect(clippy::too_many_arguments)] - fn ui_impl( - &mut self, - ui: &mut egui::Ui, - frame: &eframe::Frame, - app_blueprint: &AppBlueprint<'_>, - gpu_resource_stats: &WgpuResourcePoolStatistics, - store_context: Option<&ActiveStoreContext<'_>>, - storage_context: &StorageContext<'_>, - mem_usage_tree: Option, - store_stats: Option<&StoreHubStats>, - ) { - let mut main_panel_frame = egui::Frame::default(); - if re_ui::CUSTOM_WINDOW_DECORATIONS { - // Add some margin so that we can later paint an outline around it all. - main_panel_frame.inner_margin = 1.0.into(); - } - - egui::CentralPanel::default() - .frame(main_panel_frame) - .show_inside(ui, |ui| { - paint_background_fill(ui); - - crate::ui::mobile_warning_ui(ui); - - crate::ui::top_panel( - frame, - self, - app_blueprint, - store_context, - storage_context.hub, - gpu_resource_stats, - ui, - ); - - self.memory_panel_ui( - ui, - gpu_resource_stats, - mem_usage_tree, - store_stats, - storage_context, - ); - - self.egui_debug_panel_ui(ui); - - let egui_renderer = &mut frame - .wgpu_render_state() - .expect("Failed to get frame render state") - .renderer - .write(); - - if let Some(render_ctx) = egui_renderer - .callback_resources - .get_mut::() - { - render_ctx.begin_frame(); // This may actually be called multiple times per egui frame, if we have a multi-pass layout frame. - - // In some (rare) circumstances we run two egui passes in a single frame. - // This happens on call to `egui::Context::request_discard`. - let is_start_of_new_frame = ui.current_pass_index() == 0; - - if is_start_of_new_frame { - self.state.redap_servers.on_frame_start( - &self.connection_registry, - &self.async_runtime, - &self.egui_ctx, - self.startup_options.login_enabled(), - ); - } - - self.texture_readback.poll_and_save_texture_readbacks( - render_ctx, - ui, - &self.command_sender, - ); - - // TODO(RR-3033): `AppState::show` still expects a non-optional `ActiveStoreContext`; fall back to a sentinel empty context for no-store routes. - let empty_store_context = ActiveStoreContext::empty(); - let active_store_context = store_context.unwrap_or(&empty_store_context); - - self.state.show( - &self.app_env, - &self.startup_options, - app_blueprint, - ui, - render_ctx, - active_store_context, - storage_context, - &self.reflection, - &self.component_ui_registry, - &self.component_fallback_registry, - &self.view_class_registry, - &self.rx_log, - &self.command_sender, - &WelcomeScreenState { - hide_examples: self.startup_options.hide_welcome_screen, - opacity: self.welcome_screen_opacity(ui), - }, - self.event_dispatcher.as_ref(), - &self.connection_registry, - &self.async_runtime, - ); - render_ctx.before_submit(); - - self.show_text_logs_as_notifications(); - } - }); - - if self.app_options().show_notification_toasts { - self.notifications.show_toasts(ui); - } - } - - /// Show recent text log messages to the user as toast notifications. - fn show_text_logs_as_notifications(&mut self) { - re_tracing::profile_function!(); - - while let Ok(message) = self.text_log_rx.try_recv() { - self.notifications.add_log(message); - } - } - - fn receive_messages(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) { - re_tracing::profile_function!(); - - let start = web_time::Instant::now(); - - while let Some((channel_source, msg)) = self.rx_log.try_recv() { - re_log::trace!("Received a message from {channel_source:?}"); // Used by `test_ui_wakeup` test app! - - let msg = match msg.payload { - re_log_channel::SmartMessagePayload::Msg(msg) => msg, - - re_log_channel::SmartMessagePayload::Flush { on_flush_done } => { - re_tracing::profile_scope!("on_flush_done"); - on_flush_done(); - continue; - } - - re_log_channel::SmartMessagePayload::Quit(err) => { - if let Some(err) = err { - re_log::warn!( - "Data source has left unexpectedly: {err}, source: {}", - msg.source - ); - } else { - re_log::debug!("Data source {} has finished", msg.source); - } - continue; - } - }; - - // We centralize "new store" detection and `data_source` attachment here, so that the `on_new_store` - // side effects (like `set_opened(true)` for `OpenAndSelect`) fire regardless of which message type - // happens to come first. - let msg_store_id = match &msg { - DataSourceMessage::RrdManifest(store_id, _) - | DataSourceMessage::RrdManifestComplete(store_id) => Some(store_id.clone()), - DataSourceMessage::LogMsg(log_msg) => Some(log_msg.store_id().clone()), - DataSourceMessage::TableMsg(_) | DataSourceMessage::UiCommand(_) => None, - }; - - let maybe_new_store = msg_store_id - .as_ref() - .filter(|sid| !store_hub.store_bundle().contains(sid)); - - if let Some(sid) = &msg_store_id { - let entity_db = store_hub.entity_db_entry(sid); - if entity_db.data_source.is_none() { - entity_db.data_source = Some((*channel_source).clone()); - } - } - - match msg { - DataSourceMessage::RrdManifest(store_id, rrd_manifest) => { - let entity_db = store_hub.entity_db_entry(&store_id); - let store_events = entity_db.add_rrd_manifest_message(rrd_manifest); - - if let Some((entity_db, cache)) = - store_hub.entity_db_and_cache(&store_id, &self.view_class_registry) - { - cache.on_store_events(&store_events, entity_db); - } - } - - DataSourceMessage::RrdManifestComplete(store_id) => { - let entity_db = store_hub.entity_db_entry(&store_id); - entity_db.mark_rrd_manifest_complete(); - } - - DataSourceMessage::LogMsg(msg) => { - self.receive_log_msg(&msg, store_hub, egui_ctx, &channel_source); - } - - DataSourceMessage::TableMsg(table) => { - self.receive_table_msg(store_hub, egui_ctx, table); - } - - DataSourceMessage::UiCommand(ui_command) => { - self.receive_data_source_ui_command(ui_command, &channel_source); - } - } - - // Handle any action that is triggered by a new store _after_ processing the message - // that caused it. - if let Some(sid) = &maybe_new_store { - self.on_new_store(egui_ctx, sid, &channel_source, store_hub); - } - - if start.elapsed() > web_time::Duration::from_millis(10) { - egui_ctx.request_repaint(); // make sure we keep receiving messages asap - break; // don't block the main thread for too long - } - } - - // Run pending system commands in case any of the messages resulted in additional commands. - // This avoid further frame delays on these commands. - self.run_pending_system_commands(store_hub, egui_ctx); - } - - /// There is logic duplicated between this and [`Self::prefetch_chunks`]. - /// Make sure they are kept in sync! - fn receive_log_msg( - &mut self, - msg: &LogMsg, - store_hub: &mut StoreHub, - egui_ctx: &egui::Context, - channel_source: &LogSource, - ) { - re_tracing::profile_function!(); - - let store_id = msg.store_id(); - - if store_hub.is_active_blueprint(store_id) { - // TODO(#5514): handle loading of active blueprints. - re_log::warn_once!( - "Loading a blueprint {store_id:?} that is active. See https://github.com/rerun-io/rerun/issues/5514 for details." - ); - } - - // NOTE: store materialization, `data_source` attachment, and the `on_new_store` - // dispatch are handled in `receive_messages` so that they also fire for stores first - // introduced by `RrdManifest` / `RrdManifestComplete` messages. - let entity_db = store_hub.entity_db_entry(store_id); - let was_empty = entity_db.num_physical_chunks() == 0; - let entity_db_add_result = entity_db.add_log_msg(msg); - - match entity_db_add_result { - Ok(store_events) => { - self.process_store_events_for_db(store_hub, store_id, &store_events); - } - - Err(err) => { - re_log::error_once!("Failed to add incoming msg: {err}"); - } - } - - // Need to reborrow as read-only since we passed store_hub as mutable earlier. - let entity_db = store_hub - .entity_db(store_id) - .expect("Just queried it mutable and that was fine."); - - // Note: some of the logic above is duplicated in `fn prefetch_chunks`. - // Make sure they are kept in sync! - - let is_empty = entity_db.num_physical_chunks() == 0; - if was_empty && !is_empty { - // Hack: we cannot go to a specific timeline or entity until we know about it. - // Now we _hopefully_ do. The `LogMsg` could also belong to the blueprint, so - // we need to check for that as well. - if let LogSource::RedapGrpcStream { uri, .. } = channel_source - && &uri.store_id() == store_id - { - self.go_to_dataset_data(uri.store_id(), uri.fragment.clone()); - } - } - - #[expect(clippy::match_same_arms)] - match &msg { - LogMsg::SetStoreInfo(_) => { - // Causes a new store typically. But that's handled below via `on_new_store`. - } - - LogMsg::ArrowMsg(_, _) => { - // Handled by `EntityDb::add`. - } - - LogMsg::BlueprintActivationCommand(cmd) => match store_id.kind() { - StoreKind::Recording => { - re_log::debug!( - "Unexpected `BlueprintActivationCommand` message for {store_id:?}" - ); - } - StoreKind::Blueprint => { - if let Some(info) = entity_db.store_info() { - re_log::trace!( - "Activating blueprint that was loaded from {channel_source}" - ); - let app_id = info.application_id().clone(); - if cmd.make_default { - store_hub - .set_default_blueprint_for_app(store_id) - .unwrap_or_else(|err| { - re_log::warn!("Failed to make blueprint default: {err}"); - }); - } - if cmd.make_active { - store_hub - .set_cloned_blueprint_active_for_app(store_id) - .unwrap_or_else(|err| { - re_log::warn!("Failed to make blueprint active: {err}"); - }); - - // Switch to this app, e.g. on drag-and-drop of a blueprint file - - if self.state.navigation.current().app_id() != Some(&app_id) { - // Switch to this app: - - store_hub.load_persisted_blueprints_for_app(&app_id); - if let Some(recording_id) = - store_hub.earliest_recording_for_app(&app_id) - { - store_hub.load_blueprint_and_caches( - &recording_id, - &self.view_class_registry, - ); - self.state - .selection_state - .set_selection(Item::StoreId(recording_id.clone())); - self.state - .navigation - .replace(Route::LocalRecording { recording_id }); - } else { - // TODO(RR-3713): show a blueprint for it anyway - re_log::debug_once!( - "Received BlueprintActivationCommand for app '{app_id}', but we have no recording for it" - ); - } - } - - // If the viewer is in the background, tell the user that it has received something new. - egui_ctx.send_viewport_cmd( - egui::ViewportCommand::RequestUserAttention( - egui::UserAttentionType::Informational, - ), - ); - } - } else { - re_log::warn!( - "Got ActivateStore message without first receiving a SetStoreInfo" - ); - } - } - }, - } - } - - fn process_store_events_for_db( - &self, - store_hub: &mut StoreHub, - store_id: &StoreId, - store_events: &[re_chunk_store::ChunkStoreEvent], - ) { - re_tracing::profile_function!(); - - // Keep all caches up to date, even if they're in the background. - // This ensures that when we switch to a different recording, the caches are already valid. - if let Some((entity_db, cache)) = - store_hub.entity_db_and_cache(store_id, &self.view_class_registry) - { - cache.on_store_events(store_events, entity_db); - } - - self.validate_loaded_events(store_events); - } - - fn receive_table_msg( - &self, - store_hub: &mut StoreHub, - egui_ctx: &egui::Context, - table: TableMsg, - ) { - re_tracing::profile_function!(); - - let TableMsg { id, data } = table; - - // TODO(grtlr): For now we don't append anything to existing stores and always replace. - // TODO(ab): When we actually append to existing table, we will have to clear the UI - // cache by calling `DataFusionTableWidget::clear_state`. - let store = TableStore::default(); - if let Err(err) = store.add_record_batch(data) { - re_log::error!("Failed to load table {id}: {err}"); - } else { - if store_hub.insert_table_store(id.clone(), store).is_some() { - re_log::debug!("Overwritten table store with id: `{id}`"); - } else { - re_log::debug!("Inserted table store with id: `{id}`"); - } - self.command_sender - .send_system(SystemCommand::set_selection( - re_viewer_context::Item::TableId(id), - )); - - // If the viewer is in the background, tell the user that it has received something new. - egui_ctx.send_viewport_cmd(egui::ViewportCommand::RequestUserAttention( - egui::UserAttentionType::Informational, - )); - } - } - - fn on_new_store( - &mut self, - egui_ctx: &egui::Context, - store_id: &StoreId, - channel_source: &LogSource, - store_hub: &mut StoreHub, - ) { - match channel_source.open_behavior() { - RecordingOpenBehavior::Background => {} - - RecordingOpenBehavior::Open => { - if store_id.kind() == StoreKind::Recording { - store_hub.set_opened(store_id, true); - } - } - - RecordingOpenBehavior::OpenAndSelect => { - // Set the recording-id after potentially creating the store in the hub. - // This ordering is important because the `StoreHub` internally - // updates the app-id when changing the recording. - match store_id.kind() { - StoreKind::Recording => { - re_log::trace!("Opening a new recording: '{store_id:?}'"); - self.make_store_active_and_highlight(store_hub, egui_ctx, store_id); - } - StoreKind::Blueprint => { - // We wait with activating blueprints until they are fully loaded, - // so that we don't run heuristics on half-loaded blueprints. - // Otherwise on a mixed connection (SDK sending both blueprint and recording) - // the blueprint won't be activated until the whole _recording_ has finished loading. - } - } - } - } - - let entity_db = store_hub.entity_db_entry(store_id); - let is_example = entity_db.store_class().is_example(); - - if cfg!(target_arch = "wasm32") && !self.startup_options.is_in_notebook && !is_example { - use std::sync::Once; - static ONCE: Once = Once::new(); - ONCE.call_once(|| { - // Tell the user there is a faster native viewer they can use instead of the web viewer: - let notification = re_ui::notifications::Notification::new( - re_ui::notifications::NotificationLevel::Tip, "For better performance, try the native Rerun Viewer!").with_link( - re_ui::Link { - text: "Install…".into(), - url: "https://rerun.io/docs/overview/installing-rerun/viewer#installing-the-viewer".into(), - } - ) - .no_toast() - .permanent_dismiss_id(egui::Id::new("install_native_viewer_prompt")); - self.command_sender - .send_system(SystemCommand::ShowNotification(notification)); - }); - } - - if entity_db.store_kind() == StoreKind::Recording { - #[cfg(feature = "analytics")] - if let Some(analytics) = re_analytics::Analytics::global_or_init() - && let Some(event) = - crate::viewer_analytics::event::open_recording(&self.app_env, entity_db) - { - analytics.record(event); - } - - if let Some(event_dispatcher) = self.event_dispatcher.as_ref() { - event_dispatcher.on_recording_open(entity_db); - } - } - } - - fn receive_data_source_ui_command( - &self, - ui_command: DataSourceUiCommand, - channel_source: &LogSource, - ) { - re_tracing::profile_function!(); - match ui_command { - DataSourceUiCommand::SetUrlFragment { store_id, fragment } => { - match re_uri::Fragment::from_str(&fragment) { - Ok(fragment) => { - self.command_sender - .send_system(SystemCommand::SetUrlFragment { store_id, fragment }); - } - - Err(err) => { - re_log::warn!( - "Failed to parse fragment received from {channel_source:?}: {err}" - ); - } - } - } - - DataSourceUiCommand::SaveScreenshot { file_path, view_id } => { - let view_id = if let Some(view_id) = view_id { - if let Ok(view_id) = uuid::Uuid::parse_str(&view_id) { - Some(view_id.into()) - } else { - re_log::error!( - "Failed to parse view id from {view_id:?}. Expected a UUID." - ); - return; - } - } else { - None - }; - - self.command_sender - .send_system(SystemCommand::SaveScreenshot { - target: re_viewer_context::ScreenshotTarget::SaveToPath(file_path), - view_id, - }); - } - } - } - - /// Makes the first recording store active that is found for a given data source if any. - fn try_make_recording_from_source_active( - &mut self, - egui_ctx: &egui::Context, - store_hub: &mut StoreHub, - new_source: &LogSource, - ) { - if let Some(entity_db) = store_hub.find_recording_store_by_source(new_source) { - let store_id = entity_db.store_id().clone(); - debug_assert!(store_id.is_recording()); // `find_recording_store_by_source` should have filtered for recordings rather than blueprints. - self.make_store_active_and_highlight(store_hub, egui_ctx, &store_id); - } - } - - /// Makes the given store active and request user attention if Rerun in the background. - fn make_store_active_and_highlight( - &mut self, - store_hub: &mut StoreHub, - egui_ctx: &egui::Context, - store_id: &StoreId, - ) { - if store_id.is_blueprint() { - re_log::warn!( - "Can't make a blueprint active: {store_id:?}. This is likely a bug in Rerun." - ); - return; - } - - store_hub.set_opened(store_id, true); - store_hub.load_blueprint_and_caches(store_id, &self.view_class_registry); - self.state.navigation.replace(Route::LocalRecording { - recording_id: store_id.clone(), - }); - - // Also select the new recording: - self.command_sender - .send_system(SystemCommand::set_selection( - re_viewer_context::Item::StoreId(store_id.clone()), - )); - - // If the viewer is in the background, tell the user that it has received something new. - egui_ctx.send_viewport_cmd(egui::ViewportCommand::RequestUserAttention( - egui::UserAttentionType::Informational, - )); - } - - /// After loading some data; check if the loaded data makes sense. - fn validate_loaded_events(&self, store_events: &[re_chunk_store::ChunkStoreEvent]) { - re_tracing::profile_function!(); - - for event in store_events { - let Some(chunk) = event.delta_chunk() else { - continue; - }; - - // For speed, we don't care about the order of the following log statements, so we silence this warning - for component_descr in chunk.components().component_descriptors() { - if let Some(archetype_name) = component_descr.archetype { - if let Some(archetype) = self.reflection.archetypes.get(&archetype_name) { - for &view_type in archetype.view_types { - if !cfg!(feature = "map_view") && view_type == "MapView" { - re_log::warn_once!( - "Found map-related archetype, but viewer was not compiled with the `map_view` feature." - ); - } - } - } else { - re_log::trace_once!("Unknown archetype: {archetype_name}"); - } - } - } - } - } - - fn purge_memory_if_needed(&mut self, store_hub: &mut StoreHub) { - re_tracing::profile_function!(); - - use re_format::format_bytes; - use re_memory::MemoryUse; - - let limit = self.app_options().memory_limit; - let mem_use_before = MemoryUse::capture(); - - if let Some(minimum_fraction_to_purge) = limit.is_exceeded_by(&mem_use_before) { - re_log::info_once!("Reached memory limit of {limit}. Freeing up data…"); - - let fraction_to_purge = (minimum_fraction_to_purge + 0.2).clamp(0.25, 1.0); - - re_log::trace!("RAM limit: {limit}"); - if let Some(resident) = mem_use_before.resident { - re_log::trace!("Resident: {}", format_bytes(resident as _),); - } - if let Some(counted) = mem_use_before.counted { - re_log::trace!("Counted: {}", format_bytes(counted as _)); - } - - re_tracing::profile_scope!("pruning"); - if let Some(counted) = mem_use_before.counted { - re_log::trace!( - "Attempting to purge {:.1}% of used RAM ({})…", - 100.0 * fraction_to_purge, - format_bytes(counted as f64 * fraction_to_purge as f64) - ); - } - - store_hub.purge_fraction_of_ram( - fraction_to_purge, - self.active_recording_id(), - &|store_id| self.state.time_cursor_for(store_id).map(|t| t.time_cursor), - ); - - let mem_use_after = MemoryUse::capture(); - - let freed_memory = mem_use_before - mem_use_after; - - if let (Some(counted_before), Some(counted_diff)) = - (mem_use_before.counted, freed_memory.counted) - && 0 < counted_diff - { - re_log::debug!( - "GC result: -{} (-{:.1}%).", - format_bytes(counted_diff as _), - 100.0 * counted_diff as f32 / counted_before as f32 - ); - } - - // Cache app overhead = total memory use minus all recording chunk data. - // This captures fonts, UI state, indices, and other unevictable memory. - if let Some(current_mem_use) = mem_use_after.counted.or(mem_use_after.resident) { - let total_chunk_bytes: u64 = store_hub - .store_bundle() - .recordings() - .map(|r| r.byte_size_of_physical_chunks()) - .sum(); - self.cached_app_overhead_bytes = - Some(current_mem_use.saturating_sub(total_chunk_bytes)); - } - - self.memory_panel.note_memory_purge(); - } - } - - /// Reset the viewer to how it looked the first time you ran it. - fn reset_viewer(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) { - self.state = Default::default(); - - store_hub.clear_all_cloned_blueprints(); - - // Reset egui: - egui_ctx.memory_mut(|mem| *mem = Default::default()); - - // Restore style: - re_ui::apply_style_and_install_loaders(egui_ctx); - - if let Err(err) = crate::reset_viewer_persistence() { - re_log::warn!("Failed to reset viewer: {err}"); - } - } - - pub fn recording_db(&self) -> Option<&EntityDb> { - let store_hub = self.store_hub.as_ref()?; - let recording_id = self.active_recording_id()?; - store_hub.entity_db(recording_id) - } - - // NOTE: Relying on `self` is dangerous, as this is called during a time where some internal - // fields may have been temporarily `take()`n out. Keep this a static method. - fn handle_dropping_files( - egui_ctx: &egui::Context, - command_sender: &CommandSender, - route: &Route, - ) { - #![allow(clippy::allow_attributes, clippy::needless_continue)] // false positive, depending on target_arch - - preview_files_being_dropped(egui_ctx); - - let dropped_files = egui_ctx.input_mut(|i| std::mem::take(&mut i.raw.dropped_files)); - - if dropped_files.is_empty() { - return; - } - - egui_ctx.request_repaint(); - - let mut force_store_info = false; - - for file in dropped_files { - let active_store_id = route - .recording_id() - .cloned() - // Don't redirect data to the welcome screen. - .filter(|store_id| store_id.application_id() != StoreHub::welcome_screen_app_id()) - .unwrap_or_else(|| { - // When we're on the welcome screen, there is no recording ID to recommend. - // But we want one, otherwise multiple things being dropped simultaneously on the - // welcome screen would end up in different recordings! - - // If we don't have any application ID to recommend (which means we are on the welcome screen), - // then we use the file path as the application ID or the file name if there is no path (on web builds). - let application_id = file - .path - .clone() - .map(|p| ApplicationId::from(p.display().to_string())) - .unwrap_or_else(|| ApplicationId::from(file.name.clone())); - - // NOTE: We don't override blueprints' store IDs anyhow, so it is sound to assume that - // this can only be a recording. - let recording_id = RecordingId::random(); - - // We're creating a recording just-in-time, directly from the viewer. - // We need those store infos or the data will just be silently ignored. - force_store_info = true; - - StoreId::recording(application_id, recording_id) - }); - - if let Some(bytes) = file.bytes { - // This is what we get on Web. - command_sender.send_system(SystemCommand::LoadDataSource( - LogDataSource::FileContents( - FileSource::DragAndDrop { - recommended_store_id: Some(active_store_id.clone()), - force_store_info, - }, - FileContents { - name: file.name.clone(), - bytes: bytes.clone(), - }, - ), - )); - - continue; - } - - #[cfg(not(target_arch = "wasm32"))] - if let Some(path) = file.path { - command_sender.send_system(SystemCommand::LoadDataSource( - LogDataSource::FilePath { - file_source: FileSource::DragAndDrop { - recommended_store_id: Some(active_store_id.clone()), - force_store_info, - }, - path, - follow: false, - }, - )); - } - } - } - - fn should_fade_in_welcome_screen(&self) -> bool { - if let Some(expect_data_soon) = self.startup_options.expect_data_soon { - return expect_data_soon; - } - - // The reason for the fade-in is to avoid the welcome screen - // flickering quickly before receiving some data. - // So: if we expect data very soon, we do a fade-in. - - for source in self.rx_log.sources() { - match &*source { - LogSource::File { .. } - | LogSource::HttpStream { .. } - | LogSource::RedapGrpcStream { .. } - | LogSource::Stdin - | LogSource::RrdWebEvent - | LogSource::Sdk - | LogSource::JsChannel { .. } => { - return true; // We expect data soon, so fade-in - } - - LogSource::MessageProxy { .. } => { - // We start a gRPC server by default in native rerun, i.e. when just running `rerun`, - // and in that case fading in the welcome screen would be slightly annoying. - // However, we also use the gRPC server for sending data from the logging SDKs - // when they call `spawn()`, and in that case we really want to fade in the welcome screen. - // Therefore `spawn()` uses the special `--expect-data-soon` flag - // (handled earlier in this function), so here we know we are in the other case: - // a user calling `rerun` in their terminal (don't fade in). - } - } - } - - false // No special sources (or no sources at all), so don't fade in - } - - /// Handle fading in the welcome screen, if we should. - fn welcome_screen_opacity(&self, egui_ctx: &egui::Context) -> f32 { - if self.should_fade_in_welcome_screen() { - // The reason for this delay is to avoid the welcome screen - // flickering quickly before receiving some data. - // The only time it has for that is between the call to `spawn` and sending the recording info, - // which should happen _right away_, so we only need a small delay. - // Why not skip the wlecome screen completely when we expect the data? - // Because maybe the data never comes. - let sec_since_first_shown = self.start_time.elapsed().as_secs_f32(); - let opacity = egui::remap_clamp(sec_since_first_shown, 0.4..=0.6, 0.0..=1.0); - if opacity < 1.0 { - egui_ctx.request_repaint(); - } - opacity - } else { - 1.0 - } - } - - pub(crate) fn toggle_fullscreen(&self) { - #[cfg(not(target_arch = "wasm32"))] - { - let fullscreen = self - .egui_ctx - .input(|i| i.viewport().fullscreen.unwrap_or(false)); - self.egui_ctx - .send_viewport_cmd(egui::ViewportCommand::Fullscreen(!fullscreen)); - } - - #[cfg(target_arch = "wasm32")] - { - if let Some(options) = &self.startup_options.fullscreen_options { - // Tell JS to toggle fullscreen. - if let Err(err) = options.on_toggle.call0() { - re_log::error!("{}", crate::web_tools::string_from_js_value(err)); - } - } - } - } - - #[cfg(target_arch = "wasm32")] - pub(crate) fn is_fullscreen_allowed(&self) -> bool { - self.startup_options.fullscreen_options.is_some() - } - - #[cfg(target_arch = "wasm32")] - pub(crate) fn is_fullscreen_mode(&self) -> bool { - if let Some(options) = &self.startup_options.fullscreen_options { - // Ask JS if fullscreen is on or not. - match options.get_state.call0() { - Ok(v) => return v.is_truthy(), - Err(err) => re_log::error_once!("{}", crate::web_tools::string_from_js_value(err)), - } - } - - false - } - - #[allow(clippy::allow_attributes, clippy::needless_pass_by_ref_mut)] // False positive on wasm - fn process_screenshot_result( - &mut self, - image: &Arc, - user_data: &egui::UserData, - ) { - use re_viewer_context::ScreenshotInfo; - - if let Some(info) = user_data - .data - .as_ref() - .and_then(|data| data.downcast_ref::()) - { - let ScreenshotInfo { - ui_rect, - pixels_per_point, - name, - target, - } = (*info).clone(); - - let rgba = if let Some(ui_rect) = ui_rect { - Arc::new(image.region(&ui_rect, Some(pixels_per_point))) - } else { - image.clone() - }; - - match target { - re_viewer_context::ScreenshotTarget::CopyToClipboard => { - self.egui_ctx.copy_image((*rgba).clone()); - } - - re_viewer_context::ScreenshotTarget::SaveToPathFromFileDialog => { - use image::ImageEncoder as _; - let mut png_bytes: Vec = Vec::new(); - if let Err(err) = image::codecs::png::PngEncoder::new(&mut png_bytes) - .write_image( - rgba.as_raw(), - rgba.width() as u32, - rgba.height() as u32, - image::ExtendedColorType::Rgba8, - ) - { - re_log::error!("Failed to encode screenshot as PNG: {err}"); - } else { - let file_name = format!("{name}.png"); - self.command_sender.save_file_dialog( - self.main_thread_token, - &file_name, - "Save screenshot".to_owned(), - png_bytes, - ); - } - } - - re_viewer_context::ScreenshotTarget::SaveToPath(file_path) => { - #[cfg(not(target_arch = "wasm32"))] - { - let rgba = rgba.clone(); - let Some(rgba_image) = image::RgbaImage::from_vec( - rgba.width() as _, - rgba.height() as _, - bytemuck::pod_collect_to_vec(&rgba.pixels), - ) else { - re_log::error!("Failed to create image from screenshot data"); - return; - }; - - // Convert to RGB8 so it works with JPG and other formats that don't support alpha. - // (There's nothing interesting in the alpha channel anyways.) - let rgb_image = image::DynamicImage::ImageRgba8(rgba_image).to_rgb8(); - - match rgb_image.save(&file_path) { - Ok(()) => { - re_log::info!("Saved screenshot to {file_path:?}"); - } - Err(err) => { - re_log::error!(?file_path, "Failed to save screenshot: {err}"); - // Image library has the bad habit of creating the file even when it fails e.g. due to unsupported format. Remove it again. - std::fs::remove_file(&file_path).ok(); - } - } - } - #[cfg(target_arch = "wasm32")] - { - re_log::error!( - "Saving screenshots to a path is not supported on web. Attempted to save to: {file_path:?}" - ); - } - } - } - } else { - #[cfg(not(target_arch = "wasm32"))] // no full-app screenshotting on web - self.screenshotter.save(&self.egui_ctx, image); - } - } - - /// Receive in-transit chunks (previously prefetched): - fn receive_fetched_chunks(&self, store_hub: &mut StoreHub) { - re_tracing::profile_function!(); - - let store_ids: Vec<_> = store_hub - .store_bundle() - .recordings() - .map(|db| db.store_id().clone()) - .collect(); - - for store_id in store_ids { - let db = store_hub.entity_db_entry(&store_id); - - if cfg!(debug_assertions) && db.can_fetch_chunks_from_redap() { - re_tracing::profile_scope!("debug-sanity-check"); - let storage_engine = db.storage_engine(); - let store = storage_engine.store(); - - #[expect(clippy::iter_over_hash_type)] // sanity checks don't care about order - for missing_chunk_id in store.tracked_chunk_ids().missing_virtual { - let roots = store.find_root_chunks(&missing_chunk_id); - debug_assert!(!roots.is_empty(), "Missing chunk has no roots"); - - let all_roots_are_fully_loaded = roots.iter().all(|root_id| { - let root_info = db.rrd_manifest_index().root_chunk_info(root_id); - if let Some(root_info) = root_info { - root_info.is_fully_loaded() - } else { - re_log::debug_warn_once!("Failed to find root chunk"); - false - } - }); - - if all_roots_are_fully_loaded { - re_log::warn_once!( - "A chunk was reported missing, but all its roots are marked as fully loaded." - ); - re_log::debug_once!( - "Missing: {missing_chunk_id}, roots: {roots:?}, Chunk lineage: {}", - store.format_lineage(&missing_chunk_id) - ); - } - } - } - - if db.can_fetch_chunks_from_redap() { - re_tracing::profile_scope!("recording"); - - let mut store_events = Vec::new(); - for chunk in db - .rrd_manifest_index_mut() - .chunk_requests_mut() - .receive_finished(self.egui_ctx.time()) - { - match db.add_chunk(&std::sync::Arc::new(chunk)) { - Ok(events) => { - store_events.extend(events); - } - Err(err) => { - re_log::warn_once!("add_chunk failed: {err}"); - } - } - } - - self.process_store_events_for_db(store_hub, &store_id, &store_events); - - // Need to reborrow since we pass `&mut store_hub` above. - let db = store_hub.entity_db_entry(&store_id); - - // Note: some of the logic above is duplicated in `fn receive_log_msg`. - // Make sure they are kept in sync! - - // We cancel right after resoliving (above), so that - // we give each fetch as much time as possible to finish. - db.rrd_manifest_index_mut() - .cancel_outdated_requests(self.egui_ctx.time()); - - if db.rrd_manifest_index_mut().chunk_requests().has_pending() { - self.egui_ctx.request_repaint(); // check back for more - } - } - } - } - - /// Prefetch chunks for the open recording (stream from server) - /// - /// There is logic duplicated between this and [`Self::receive_log_msg`]. - /// Make sure they are kept in sync! - fn prefetch_chunks(&self, store_hub: &mut StoreHub) { - re_tracing::profile_function!(); - - use crate::prefetch_chunks::{RecordingOpenKind, RecordingPrefetchInfo}; - use re_entity_db::ChunkPrefetchOptions; - - let active_recording_id = self.active_recording_id(); - - // Fixed overhead for the app (fonts, icons, caches, etc.) that we cannot purge. - // We also want some headroom for spikes. - const APP_OVERHEAD_BYTES: u64 = 300_000_000; - - // When we have a measured overhead we need less extra headroom. - // When we don't, use a larger fraction to be safe. - const FIXED_FRACTION_OVERHEAD: f32 = 0.10; - const FALLBACK_FIXED_FRACTION_OVERHEAD: f32 = 0.20; - - let overhead = self.cached_app_overhead_bytes.unwrap_or(APP_OVERHEAD_BYTES); - let fixed_fraction_overhead = if self.cached_app_overhead_bytes.is_some() { - FIXED_FRACTION_OVERHEAD - } else { - FALLBACK_FIXED_FRACTION_OVERHEAD - }; - - let memory_limit = self - .app_options() - .memory_limit - .saturating_sub(overhead) - .split(fixed_fraction_overhead) - .1; - - if memory_limit == re_memory::MemoryLimit::ZERO { - re_log::warn_once!("Very little memory budget left for prefetching recordings."); - } - - let mut recordings_info: HashMap = HashMap::default(); - - for recording in store_hub.store_bundle().recordings() { - if recording.is_downloading_first_part_of_manifest() { - // We need at least ONE part of the manifest before prefetching chunks. - continue; - } - - let is_active = Some(recording.store_id()) == active_recording_id; - let usage = store_hub.usage(recording.store_id()); - - let open_kind = if is_active { - RecordingOpenKind::Active - } else if usage.was_preview() { - RecordingOpenKind::Preview - } else if usage.opened { - RecordingOpenKind::Inactive - } else { - continue; - }; - - let time_cursor = self.state.time_cursor_for(recording.store_id()); - if let Some(redap_uri) = recording.redap_uri() { - let store_id = recording.store_id().clone(); - recordings_info.insert( - store_id.clone(), - RecordingPrefetchInfo { - store_id, - open_kind, - time_cursor, - origin: redap_uri.origin.clone(), - }, - ); - } - } - - let total_bytes_in_memory = memory_limit.at_least(100_000_000).as_bytes(); - - crate::prefetch_chunks::prefetch_chunks_for_recordings( - &self.egui_ctx, - store_hub.store_bundle_mut(), - &recordings_info, - total_bytes_in_memory, - self.connection_registry(), - &ChunkPrefetchOptions { - max_fetch_stage: self.app_options().max_fetch_stage, - ..ChunkPrefetchOptions::default() - }, - ); - } -} - -#[cfg(target_arch = "wasm32")] -fn blueprint_loader() -> BlueprintPersistence { - // TODO(#2579): implement persistence for web - BlueprintPersistence { - loader: None, - saver: None, - validator: Some(Box::new(crate::blueprint::is_valid_blueprint)), - deleter: None, - } -} - -#[cfg(not(target_arch = "wasm32"))] -fn blueprint_loader() -> BlueprintPersistence { - use re_entity_db::StoreBundle; - - fn load_blueprint_from_disk(app_id: &ApplicationId) -> anyhow::Result> { - let blueprint_path = crate::saving::default_blueprint_path(app_id)?; - if !blueprint_path.exists() { - return Ok(None); - } - - re_log::debug!("Trying to load blueprint for {app_id} from {blueprint_path:?}"); - - if let Some(bundle) = crate::loading::load_blueprint_file(&blueprint_path) { - for store in bundle.entity_dbs() { - if store.store_kind() == StoreKind::Blueprint - && !crate::blueprint::is_valid_blueprint(store) - { - re_log::warn_once!( - "Blueprint for {app_id} at {blueprint_path:?} appears invalid - will ignore. This is expected if you have just upgraded Rerun versions." - ); - return Ok(None); - } - } - Ok(Some(bundle)) - } else { - Ok(None) - } - } - - #[cfg(not(target_arch = "wasm32"))] - fn save_blueprint_to_disk(app_id: &ApplicationId, blueprint: &EntityDb) -> anyhow::Result<()> { - let blueprint_path = crate::saving::default_blueprint_path(app_id)?; - - let messages = blueprint.to_messages(None); - let rrd_version = blueprint - .store_info() - .and_then(|info| info.store_version) - .unwrap_or(re_build_info::CrateVersion::LOCAL); - - // TODO(jleibs): Should we push this into a background thread? Blueprints should generally - // be small & fast to save, but maybe not once we start adding big pieces of user data? - crate::saving::encode_to_file(rrd_version, &blueprint_path, messages)?; - - re_log::debug!("Saved blueprint for {app_id} to {blueprint_path:?}"); - - Ok(()) - } - - BlueprintPersistence { - loader: Some(Box::new(load_blueprint_from_disk)), - saver: Some(Box::new(save_blueprint_to_disk)), - validator: Some(Box::new(crate::blueprint::is_valid_blueprint)), - deleter: Some(Box::new(crate::saving::delete_blueprint)), - } -} - -impl eframe::App for App { - fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] { - if re_ui::CUSTOM_WINDOW_DECORATIONS { - [0.; 4] // transparent - } else if visuals.dark_mode { - [0., 0., 0., 1.] - } else { - [1., 1., 1., 1.] - } - } - - fn save(&mut self, storage: &mut dyn eframe::Storage) { - if !self.startup_options.persist_state { - return; - } - - re_tracing::profile_function!(); - - storage.set_string(RERUN_VERSION_KEY, self.build_info.version.to_string()); - - // Save the app state - eframe::set_value(storage, eframe::APP_KEY, &self.state); - eframe::set_value( - storage, - REDAP_TOKEN_KEY, - &self.connection_registry.dump_tokens(), - ); - - // Save the blueprints - // TODO(#2579): implement web-storage for blueprints as well - if let Some(hub) = &mut self.store_hub { - if self.state.app_options.blueprint_gc { - hub.gc_blueprints(&self.state.blueprint_undo_state); - } - - if let Err(err) = hub.save_app_blueprints() { - re_log::error!("Saving blueprints failed: {err}"); - } - } else { - re_log::error!("Could not save blueprints: the store hub is not available"); - } - } - - /// Called before each call to `ui`, but ALSO when the app is - /// hidden (occluded, minimized, …) if something has called `request_repaint`. - /// - /// We put things here that are unrelated to the UI, - /// and that we still want to happen if the application is hidden. - fn logic(&mut self, egui_ctx: &egui::Context, _frame: &mut eframe::Frame) { - // Temporarily take the `StoreHub` out of the Viewer so it doesn't interfere with mutability - let mut store_hub = self - .store_hub - .take() - .expect("Failed to take store hub from the Viewer"); - - { - // Respect memory budget: - self.purge_memory_if_needed(&mut store_hub); // Call BEFORE `begin_frame_caches` - - if self.app_options().blueprint_gc { - store_hub.gc_blueprints(&self.state.blueprint_undo_state); - } - } - - { - // Download/ingest data: - self.receive_messages(&mut store_hub, egui_ctx); - self.receive_fetched_chunks(&mut store_hub); - self.prefetch_chunks(&mut store_hub); - } - - self.run_pending_system_commands(&mut store_hub, egui_ctx); - - { - // We also need to check for Ui commands, especially `UiCommand::Quit`. - - let route = self.state.navigation.current().clone(); - let (storage_context, store_context) = store_hub.read_context(&route); - - let blueprint = store_context.as_ref().map(|ctx| ctx.blueprint); - let blueprint_query = self.state.blueprint_query_for_viewer(blueprint); - - let app_blueprint = AppBlueprint::new( - blueprint, - &blueprint_query, - egui_ctx, - self.panel_state_overrides_active - .then_some(self.panel_state_overrides), - ); - - self.run_pending_ui_commands( - egui_ctx, - &app_blueprint, - &storage_context, - store_context.as_ref(), - &route, - ); - } - - self.state.cleanup(&store_hub); - - // Return the `StoreHub` to the Viewer so we have it on the next frame - self.store_hub = Some(store_hub); - } - - /// Called when application need to be repainted - fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { - #[cfg(all(not(target_arch = "wasm32"), feature = "perf_telemetry_tracy"))] - re_perf_telemetry::external::tracing_tracy::client::frame_mark(); - - #[cfg(not(target_arch = "wasm32"))] - if let Some(capture) = &self.profile_capture { - if capture.is_done() { - if let Some(capture) = self.profile_capture.take() - && let Err(err) = save_profile_trace(&capture.finish()) - { - re_log::error!("Failed to save profile trace: {err}"); - } - } else { - ui.ctx().request_repaint(); - } - } - - if let Some(seconds) = frame.info().cpu_usage { - self.frame_time_history.add(ui.input(|i| i.time), seconds); - } - - // NOTE: Memory stats can be very costly to compute, so only do so if the memory panel is opened. - let mem_usage_tree = self - .memory_panel_open - .then(|| re_byte_size::NamedMemUsageTree::new("App", self.capture_mem_usage_tree())); - - #[cfg(target_arch = "wasm32")] - if self.startup_options.enable_history { - // Handle pressing the back/forward mouse buttons explicitly, since eframe catches those. - let back_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Extra1)); - let fwd_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Extra2)); - - if back_pressed { - crate::web_history::go_back(); - } - if fwd_pressed { - crate::web_history::go_forward(); - } - } - - self.server_latency_trackers - .update(&self.connection_registry); - - // We move the time at the very start of the frame, - // so that we always show the latest data when we're in "follow" mode. - self.move_time(); - - // Temporarily take the `StoreHub` out of the Viewer so it doesn't interfere with mutability - let mut store_hub = self - .store_hub - .take() - .expect("Failed to take store hub from the Viewer"); - - // Update data source order so it's based on opening order. - store_hub.update_data_source_order(&self.rx_log.sources()); - - #[cfg(not(target_arch = "wasm32"))] - if let Some(resolution_in_points) = self.startup_options.resolution_in_points.take() { - ui.send_viewport_cmd(egui::ViewportCommand::InnerSize( - resolution_in_points.into(), - )); - } - - #[cfg(not(target_arch = "wasm32"))] - if self.screenshotter.update(ui).quit { - ui.send_viewport_cmd(egui::ViewportCommand::Close); - return; - } - - if self.app_options().memory_limit.is_unlimited() { - // we only warn about high memory usage if the user hasn't specified a limit - self.ram_limit_warner.update(); - } - - #[cfg(target_arch = "wasm32")] - if let Some(PendingFilePromise { - recommended_store_id, - force_store_info, - promise, - }) = &self.open_files_promise - && let Some(files) = promise.ready() - { - for file in files { - self.command_sender - .send_system(SystemCommand::LoadDataSource(LogDataSource::FileContents( - FileSource::FileDialog { - recommended_store_id: recommended_store_id.clone(), - force_store_info: *force_store_info, - }, - file.clone(), - ))); - } - self.open_files_promise = None; - } - - // NOTE: GPU resource stats are cheap to compute so we always do. - let gpu_resource_stats = { - re_tracing::profile_scope!("gpu_resource_stats"); - - let egui_renderer = frame - .wgpu_render_state() - .expect("Failed to get frame render state") - .renderer - .read(); - - let render_ctx = egui_renderer - .callback_resources - .get::() - .expect("Failed to get render context"); - - // Query statistics before begin_frame as this might be more accurate if there's resources that we recreate every frame. - render_ctx.gpu_resources.statistics() - }; - - // NOTE: Store and caching stats are very costly to compute: only do so if the memory panel - // is opened. - let store_stats = self.memory_panel_open.then(|| store_hub.stats()); - - // do early, before doing too many allocations - let store_bundle_for_streaming = self - .memory_panel_open - .then(|| store_hub.store_bundle() as &re_entity_db::StoreBundle); - self.memory_panel.update( - &gpu_resource_stats, - store_stats.as_ref(), - store_bundle_for_streaming, - ); - - self.purge_memory_if_needed(&mut store_hub); // Call BEFORE `begin_frame_caches` - - // In some (rare) circumstances we run two egui passes in a single frame. - // This happens on call to `egui::Context::request_discard`. - let is_start_of_new_frame = ui.current_pass_index() == 0; - if is_start_of_new_frame { - // IMPORTANT: only call this once per FRAME even if we run multiple passes. - // Otherwise we might incorrectly evict something that was invisible in the first (discarded) pass. - store_hub.begin_frame_caches(self.active_recording_id()); // Call AFTER `purge_memory_if_needed` - } - - file_saver_progress_ui(ui, &mut self.background_tasks); // toasts for background file saver - - // Make sure some app is active - // Must be called before `read_context` below. - if let Route::Loading(source) = self.state.navigation.current() { - if !self.msg_receive_set().contains(source) { - // The stream finished and may have produced a recording without triggering - // automatic navigation. So we try that before defaulting to showing the - // Welcome screen. - let loaded_recording = store_hub - .find_recording_store_by_source(source) - .map(|db| db.store_id().clone()); - - if let Some(store_id) = loaded_recording { - re_log::debug!("Stream completed, navigating to loaded recording {store_id:?}"); - store_hub.load_blueprint_and_caches(&store_id, &self.view_class_registry); - self.state.navigation.replace(Route::LocalRecording { - recording_id: store_id, - }); - } else { - re_log::debug!("No recording found from loading source, resetting navigation"); - self.state.navigation.reset(); - } - } - } else if !matches!( - self.state.navigation.current(), - Route::ChunkStoreBrowser { .. } - ) { - // If the current route points to a stale recording, find a new valid state. - let route_is_valid = self - .state - .navigation - .current() - .recording_id() - .is_none_or(|recording_id| store_hub.entity_db(recording_id).is_some()); - - if !route_is_valid { - let any_other_app_id: Option = store_hub - .store_bundle() - .entity_dbs() - .map(|db| db.application_id()) - .filter(|app_id| *app_id != StoreHub::welcome_screen_app_id()) - .min() - .cloned(); - if let Some(app_id) = any_other_app_id { - store_hub.load_persisted_blueprints_for_app(&app_id); - if let Some(recording_id) = store_hub.earliest_recording_for_app(&app_id) { - store_hub - .load_blueprint_and_caches(&recording_id, &self.view_class_registry); - self.state - .selection_state - .set_selection(Item::StoreId(recording_id.clone())); - self.state - .navigation - .replace(Route::LocalRecording { recording_id }); - } else { - self.state.navigation.reset(); - } - } else { - self.state.navigation.reset(); - } - } - } - - { - let (storage_context, store_context) = - store_hub.read_context(self.state.navigation.current()); - - let blueprint = store_context.as_ref().map(|ctx| ctx.blueprint); - let blueprint_query = self.state.blueprint_query_for_viewer(blueprint); - - let app_blueprint = AppBlueprint::new( - blueprint, - &blueprint_query, - ui, - self.panel_state_overrides_active - .then_some(self.panel_state_overrides), - ); - - self.ui_impl( - ui, - frame, - &app_blueprint, - &gpu_resource_stats, - store_context.as_ref(), - &storage_context, - mem_usage_tree, - store_stats.as_ref(), - ); - - if re_ui::CUSTOM_WINDOW_DECORATIONS { - // Paint the main window frame on top of everything else - paint_native_window_frame(ui); - } - - if let Some(cmd) = self - .cmd_palette - .show(ui, &crate::open_url_description::command_palette_parse_url) - { - match cmd { - re_ui::CommandPaletteAction::UiCommand(cmd) => { - self.command_sender.send_ui(cmd); - } - re_ui::CommandPaletteAction::OpenUrl(url_desc) => { - match ViewerOpenUrl::parse_with_options( - &url_desc.url, - &re_data_source::FromUriOptions { - accept_extensionless_http: true, - ..Default::default() - }, - ) { - Ok(url) => { - url.open( - ui, - &OpenUrlOptions { - follow: false, - recording_open_behavior: - RecordingOpenBehavior::OpenAndSelect, - show_loader: true, - }, - &self.command_sender, - ); - } - Err(err) => { - re_log::warn!("{err}"); - } - } - - // Note that we can't use `ui.open_url(egui::OpenUrl::same_tab(uri))` here because.. - // * the url redirect in `check_for_clicked_hyperlinks` wouldn't be hit - // * we don't actually want to open any URLs in the browser here ever, only ever into the current viewer - } - } - } - - let route = self.state.navigation.current().clone(); - Self::handle_dropping_files(ui, &self.command_sender, &route); - - // Run pending commands last (so we don't have to wait for a repaint before they are run): - self.run_pending_ui_commands( - ui, - &app_blueprint, - &storage_context, - store_context.as_ref(), - &route, - ); - } - self.run_pending_system_commands(&mut store_hub, ui); - - self.update_history(&store_hub); - - // Return the `StoreHub` to the Viewer so we have it on the next frame - self.store_hub = Some(store_hub); - - { - // Check for returned screenshots: - let screenshots: Vec<_> = ui.input(|i| { - i.raw - .events - .iter() - .filter_map(|event| { - if let egui::Event::Screenshot { - image, user_data, .. - } = event - { - Some((image.clone(), user_data.clone())) - } else { - None - } - }) - .collect() - }); - - for (image, user_data) in screenshots { - self.process_screenshot_result(&image, &user_data); - } - } - } - - #[cfg(target_arch = "wasm32")] - fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { - Some(&mut *self) - } -} - -fn paint_background_fill(ui: &egui::Ui) { - // This is required because the streams view (time panel) - // has rounded top corners, which leaves a gap. - // So we fill in that gap (and other) here. - // Of course this does some over-draw, but we have to live with that. - - let tokens = ui.tokens(); - - ui.painter().rect_filled( - ui.max_rect().shrink(0.5), - tokens.native_window_corner_radius(), - ui.visuals().panel_fill, - ); -} - -fn paint_native_window_frame(egui_ctx: &egui::Context) { - let tokens = egui_ctx.tokens(); - - let painter = egui::Painter::new( - egui_ctx.clone(), - egui::LayerId::new(egui::Order::TOP, egui::Id::new("native_window_frame")), - egui::Rect::EVERYTHING, - ); - - painter.rect_stroke( - egui_ctx.content_rect(), - tokens.native_window_corner_radius(), - egui_ctx.tokens().native_frame_stroke, - egui::StrokeKind::Inside, - ); -} - -fn preview_files_being_dropped(egui_ctx: &egui::Context) { - use egui::{Align2, Id, LayerId, Order, TextStyle}; - - // Preview hovering files: - if !egui_ctx.input(|i| i.raw.hovered_files.is_empty()) { - use std::fmt::Write as _; - - let mut text = "Drop to load:\n".to_owned(); - egui_ctx.input(|input| { - for file in &input.raw.hovered_files { - if let Some(path) = &file.path { - write!(text, "\n{}", path.display()).ok(); - } else if !file.mime.is_empty() { - write!(text, "\n{}", file.mime).ok(); - } - } - }); - - let painter = - egui_ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target"))); - - let screen_rect = egui_ctx.content_rect(); - painter.rect_filled( - screen_rect, - 0.0, - egui_ctx - .global_style() - .visuals - .extreme_bg_color - .gamma_multiply_u8(192), - ); - painter.text( - screen_rect.center(), - Align2::CENTER_CENTER, - text, - TextStyle::Body.resolve(&egui_ctx.global_style()), - egui_ctx.global_style().visuals.strong_text_color(), - ); - } -} - -// ---------------------------------------------------------------------------- - -fn file_saver_progress_ui(egui_ctx: &egui::Context, background_tasks: &mut BackgroundTasks) { - if background_tasks.is_file_save_in_progress() { - // There's already a file save running in the background. - - if let Some(res) = background_tasks.poll_file_saver_promise() { - // File save promise has returned. - match res { - Ok(path) => { - re_log::info!("File saved to {path:?}."); // this will also show a notification the user - } - Err(err) => { - re_log::error!("{err}"); // this will also show a notification the user - } - } - } else { - // File save promise is still running in the background. - - // NOTE: not a toast, want something a bit more discreet here. - egui::Window::new("file_saver_spin") - .anchor(egui::Align2::RIGHT_BOTTOM, egui::Vec2::ZERO) - .title_bar(false) - .enabled(false) - .auto_sized() - .show(egui_ctx, |ui| { - ui.horizontal(|ui| { - ui.loading_indicator("Writing file to disk"); - ui.label("Writing file to disk…"); - }) - }); - } - } -} - -/// [This may only be called on the main thread](https://docs.rs/rfd/latest/rfd/#macos-non-windowed-applications-async-and-threading). -#[cfg(not(target_arch = "wasm32"))] -fn open_file_dialog_native(_: crate::MainThreadToken) -> Vec { - re_tracing::profile_function!(); - - let supported: Vec<_> = if re_importer::iter_external_importers().len() == 0 { - re_importer::supported_extensions().collect() - } else { - vec![] - }; - - let mut dialog = rfd::FileDialog::new(); - - // If there's at least one external loader registered, then literally anything goes! - if !supported.is_empty() { - dialog = dialog.add_filter("Supported files", &supported); - } - - dialog.pick_files().unwrap_or_default() -} - -#[cfg(target_arch = "wasm32")] -async fn async_open_rrd_dialog() -> Vec { - let supported: Vec<_> = re_importer::supported_extensions().collect(); - - let files = rfd::AsyncFileDialog::new() - .add_filter("Supported files", &supported) - .pick_files() - .await - .unwrap_or_default(); - - let mut file_contents = Vec::with_capacity(files.len()); - - for file in files { - let file_name = file.file_name(); - re_log::debug!("Reading {file_name}…"); - let bytes = file.read().await; - re_log::debug!( - "{file_name} was {}", - re_format::format_bytes(bytes.len() as _) - ); - file_contents.push(re_data_source::FileContents { - name: file_name, - bytes: bytes.into(), - }); - } - - file_contents -} - -fn save_active_recording( - app: &mut App, - store_context: Option<&ActiveStoreContext<'_>>, - loop_selection: Option<(TimelineName, re_log_types::AbsoluteTimeRangeF)>, -) -> anyhow::Result<()> { - let Some(entity_db) = store_context.as_ref().map(|view| view.recording) else { - // NOTE: Can only happen if saving through the command palette. - anyhow::bail!("No recording data to save"); - }; - - save_recording(app, entity_db, loop_selection) -} - -fn save_recording( - app: &mut App, - entity_db: &EntityDb, - loop_selection: Option<(TimelineName, re_log_types::AbsoluteTimeRangeF)>, -) -> anyhow::Result<()> { - let rrd_version = entity_db - .store_info() - .and_then(|info| info.store_version) - .unwrap_or(re_build_info::CrateVersion::LOCAL); - - let file_name = if let Some(recording_name) = entity_db - .recording_info_property::( - re_sdk_types::archetypes::RecordingInfo::descriptor_name().component, - ) { - format!("{}.rrd", sanitize_file_name(&recording_name)) - } else { - "data.rrd".to_owned() - }; - - let title = if loop_selection.is_some() { - "Save loop selection" - } else { - "Save recording" - }; - - save_entity_db( - app, - rrd_version, - file_name, - title.to_owned(), - entity_db.to_messages(loop_selection), - ) -} - -fn save_blueprint( - app: &mut App, - store_context: Option<&ActiveStoreContext<'_>>, -) -> anyhow::Result<()> { - let Some(store_context) = store_context else { - anyhow::bail!("No blueprint to save"); - }; - - re_tracing::profile_function!(); - - let rrd_version = store_context - .blueprint - .store_info() - .and_then(|info| info.store_version) - .unwrap_or(re_build_info::CrateVersion::LOCAL); - - // We change the recording id to a new random one, - // otherwise when saving and loading a blueprint file, we can end up - // in a situation where the store_id we're loading is the same as the currently active one, - // which mean they will merge in a strange way. - // This is also related to https://github.com/rerun-io/rerun/issues/5295 - let new_store_id = store_context - .blueprint - .store_id() - .clone() - .with_recording_id(RecordingId::random()); - - let mut saved_blueprint = store_context - .blueprint - .clone_with_new_id(new_store_id) - .context("Cloning current blueprint")?; - - if let Some(undo_state) = app - .state - .blueprint_undo_state - .get(store_context.blueprint.store_id()) - { - // We don't actually want to edit the undo state when saving, - // just clear the redo-buffer section of what we save. - undo_state.clone().clear_redo_buffer(&mut saved_blueprint); - } - - let messages = saved_blueprint.to_messages(None); - - let file_name = format!( - "{}.rbl", - crate::saving::sanitize_app_id(store_context.application_id()) - ); - let title = "Save blueprint"; - - save_entity_db(app, rrd_version, file_name, title.to_owned(), messages) -} - -#[cfg(not(target_arch = "wasm32"))] -fn save_profile_trace(view: &re_tracing::reexports::puffin::FrameView) -> anyhow::Result<()> { - let Some(path) = rfd::FileDialog::new() - .set_file_name("rerun.puffin") - .set_title("Save profile trace") - .add_filter("Puffin profile", &["puffin"]) - .save_file() - else { - re_log::info!("Profile trace capture cancelled by user."); - return Ok(()); - }; - - let file = std::fs::File::create(&path)?; - let mut writer = std::io::BufWriter::new(file); - view.write(&mut writer)?; - - re_log::info!("Saved profile trace to {}", path.display()); - Ok(()) -} - -// TODO(emilk): unify this with `ViewerContext::save_file_dialog` -#[allow(clippy::allow_attributes, clippy::needless_pass_by_ref_mut)] // `app` is only used on native -#[allow(clippy::unnecessary_wraps)] // cannot return error on web -fn save_entity_db( - #[allow(clippy::allow_attributes, unused_variables)] app: &mut App, // only used on native - rrd_version: CrateVersion, - file_name: String, - title: String, - messages: impl Iterator>, -) -> anyhow::Result<()> { - re_tracing::profile_function!(); - - // TODO(#6984): Ideally we wouldn't collect at all and just stream straight to the - // encoder from the store. - // - // From a memory usage perspective this isn't too bad though: the data within is still - // refcounted straight from the store in any case. - // - // It just sucks latency-wise. - let messages = messages.collect::>(); - - // Web - #[cfg(target_arch = "wasm32")] - { - wasm_bindgen_futures::spawn_local(async move { - if let Err(err) = - async_save_dialog(rrd_version, &file_name, &title, messages.into_iter()).await - { - re_log::error!("File saving failed: {err}"); - } - }); - } - - // Native - #[cfg(not(target_arch = "wasm32"))] - { - let path = { - re_tracing::profile_scope!("file_dialog"); - rfd::FileDialog::new() - .set_file_name(file_name) - .set_title(title) - .save_file() - }; - if let Some(path) = path { - app.background_tasks.spawn_file_saver(move || { - crate::saving::encode_to_file(rrd_version, &path, messages.into_iter())?; - Ok(path) - })?; - } - } - - Ok(()) -} - -#[cfg(target_arch = "wasm32")] -async fn async_save_dialog( - rrd_version: CrateVersion, - file_name: &str, - title: &str, - messages: impl Iterator>, -) -> anyhow::Result<()> { - use anyhow::Context as _; - - let file_handle = rfd::AsyncFileDialog::new() - .set_file_name(file_name) - .set_title(title) - .save_file() - .await; - - let Some(file_handle) = file_handle else { - return Ok(()); // aborted - }; - - let options = re_log_encoding::rrd::EncodingOptions::PROTOBUF_COMPRESSED; - let mut bytes = Vec::new(); - re_log_encoding::Encoder::encode_into(rrd_version, options, messages, &mut bytes)?; - file_handle.write(&bytes).await.context("Failed to save") -} - -/// Propagates [`re_viewer_context::TimeControlResponse`] to [`ViewerEventDispatcher`]. -fn handle_time_ctrl_event( - recording: &EntityDb, - events: Option<&ViewerEventDispatcher>, - response: &re_viewer_context::TimeControlResponse, -) { - let Some(events) = events else { - return; - }; - - if let Some(playing) = response.playing_change { - events.on_play_state_change(recording, playing); - } - - if let Some((timeline, time)) = response.timeline_change { - events.on_timeline_change(recording, timeline, time); - } - - if let Some(time) = response.time_change { - events.on_time_update(recording, time); - } -} - -impl MemUsageTreeCapture for App { - fn capture_mem_usage_tree(&self) -> MemUsageTree { - re_tracing::profile_function!(); - let mut node = re_byte_size::MemUsageNode::default(); - node.add("state", self.state.capture_mem_usage_tree()); - node.add("rx_log", self.rx_log.capture_mem_usage_tree()); - node.add("store_hub", self.store_hub.capture_mem_usage_tree()); - node.add( - "store_subscribers", - re_chunk_store::ChunkStore::capture_all_subscribers_mem_usage_tree(), - ); - - let mut globals = re_byte_size::MemUsageNode::new(); - globals.add( - "forgiving_parse_cache", - re_log_types::forgiving_parse_cache_bytes_used(), - ); - globals.add("string_interner", re_string_interner::bytes_used() as u64); - node.add("globals", globals.into_tree()); - - node.into_tree() - } -} diff --git a/crates/viewer/re_viewer/src/app/add_data_source.rs b/crates/viewer/re_viewer/src/app/add_data_source.rs new file mode 100644 index 000000000000..dbc417374ac9 --- /dev/null +++ b/crates/viewer/re_viewer/src/app/add_data_source.rs @@ -0,0 +1,531 @@ +use re_data_source::LogDataSource; +use re_entity_db::LogSource; +use re_log_channel::{LogReceiver, RecordingOpenBehavior}; +use re_log_types::StoreId; +use re_viewer_context::{StoreHub, SystemCommand, SystemCommandSender as _}; + +use super::App; + +#[cfg(not(target_arch = "wasm32"))] +use std::path::Path; + +use anyhow::Context as _; +use re_protos::cloud::v1alpha1::ext::DataSource; +use re_protos::common::v1alpha1::ext::IfDuplicateBehavior; +#[cfg(not(target_arch = "wasm32"))] +use tokio_util::compat::TokioAsyncReadCompatExt as _; + +impl App { + #[expect(clippy::needless_pass_by_ref_mut)] + pub fn add_log_receiver(&mut self, rx: LogReceiver) { + re_log::debug!("Adding new log receiver: {}", rx.source()); + + // Make sure we wake up when a new message is available: + rx.set_waker({ + let egui_ctx = self.egui_ctx.clone(); + move || { + // Spend a few more milliseconds decoding incoming messages, + // then trigger a repaint (https://github.com/rerun-io/rerun/issues/963): + egui_ctx.request_repaint_after(std::time::Duration::from_millis(10)); + } + }); + + // Add unknown redap servers. + // + // Otherwise we end up in a situation where we have a data from an unknown server, + // which is unnecessary and can get us into a strange ui state. + if let LogSource::RedapGrpcStream { uri, .. } = rx.source() { + if self.connection_registry.is_internal_origin(&uri.origin) { + self.rx_log.add(rx); + return; + } + + self.command_sender + .send_system(SystemCommand::AddRedapServer(uri.origin.clone())); + } + + self.rx_log.add(rx); + } + + /// Add a tracker for memory external to the viewer but in the same process. + pub fn add_external_memory_user(&mut self, user: Box) { + self.external_memory_users.add(user); + } + + /// Loads a data source into the viewer. + /// + /// Tries to detect whether the datasource is already present (either still streaming in or already loaded), + /// and if so, will not load the data again. + /// Instead, it will only perform any kind of selection/mode-switching operations associated with loading the given data source. + /// + /// Note that we *do not* change the route here _unconditionally_. + /// For instance if the datasource is a blueprint for a dataset that may be loaded later, + /// we don't want to switch out to it while the user browses a server. + pub(super) fn load_data_source( + &mut self, + store_hub: &mut StoreHub, + egui_ctx: &egui::Context, + data_source: &LogDataSource, + ) { + re_tracing::profile_function!(); + + // Check if we've already loaded this data source and should just switch to it. + // + // Go through all sources that are still loading and those that are already in the store_hub. + // (if we look only at the one from the store_hub, we might miss those that haven't hit it yet) + let active_sources = self.rx_log.sources(); + // Only consider recordings for dedup, not blueprints. + // Blueprints loaded alongside a recording share the same `data_source`, + // but they should not prevent re-opening a closed recording. + let store_sources = store_hub + .store_bundle() + .recordings() + .filter_map(|db| db.data_source.as_ref()); + let mut all_sources = + std::iter::chain(store_sources, active_sources.iter().map(|s| s.as_ref())); + + match data_source { + LogDataSource::HttpUrl { url } => { + let new_source = LogSource::HttpStream { + url: url.to_string(), + }; + + if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { + if let Some(entity_db) = store_hub.find_recording_store_by_source(&new_source) { + let store_id = entity_db.store_id().clone(); + re_log::debug_assert!(store_id.is_recording()); // `find_recording_store_by_source` should have filtered for recordings rather than blueprints. + drop(all_sources); + self.make_store_active_and_highlight(store_hub, egui_ctx, &store_id); + } + return; + } + } + + #[cfg(not(target_arch = "wasm32"))] + LogDataSource::FilePath { path, .. } => { + // If the internal catalog is enabled, route `.rrd` files through it. + if path.extension().is_some_and(|ext| ext == "rrd") + && self.app_options().experimental.use_internal_catalog + && self.connection_registry.internal_origin().is_some() + { + let path = path.clone(); + let connection_registry = self.connection_registry.clone(); + let sender = self.command_sender.clone(); + self.async_runtime.spawn_future(async move { + match register_local_file(&connection_registry, &path).await { + Ok(uri) => { + // Refresh the dataset if its open + sender.send_system(SystemCommand::RefreshRedapEntry { + origin: uri.origin.clone(), + entry_id: uri.dataset_id.into(), + }); + sender.send_system(SystemCommand::LoadDataSource( + LogDataSource::RedapDatasetSegment { + uri, + open_behavior: RecordingOpenBehavior::OpenAndSelect, + }, + )); + } + Err(err) => { + re_log::error!( + "Failed to load file via the Viewer catalog: {err}\nFile path: {}", + path.display(), + ); + } + } + }); + return; + } + + let new_source = LogSource::File { path: path.clone() }; + if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { + drop(all_sources); + self.try_make_recording_from_source_active(egui_ctx, store_hub, &new_source); + return; + } + } + + LogDataSource::FileContents(_file_source, file_contents) => { + if self + .try_register_via_internal_catalog(file_contents) + .is_break() + { + return; + } + + // For raw file contents we currently can't determine whether we're already receiving them. + } + + #[cfg(not(target_arch = "wasm32"))] + LogDataSource::Stdin => { + let new_source = LogSource::Stdin; + if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { + drop(all_sources); + self.try_make_recording_from_source_active(egui_ctx, store_hub, &new_source); + return; + } + } + + LogDataSource::RedapDatasetSegment { uri, open_behavior } => { + let new_source = LogSource::RedapGrpcStream { + uri: uri.clone(), + open_behavior: *open_behavior, + table_blueprint: None, + }; + if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { + // We're already receiving from the exact same data source! + // But we still should navigate if requested according to the fragments if any. + drop(all_sources); + match *open_behavior { + RecordingOpenBehavior::Background => {} + RecordingOpenBehavior::Open => { + store_hub.set_opened(&uri.store_id(), true); + } + RecordingOpenBehavior::OpenAndSelect => { + // First make the recording itself active. + // `go_to_dataset_data` may override the selection again, but this is important regardless, + // since `go_to_dataset_data` does not change the active recording. + // `make_store_active_and_highlight` also fetches the blueprint we skipped + // while this was a preview. + self.make_store_active_and_highlight( + store_hub, + egui_ctx, + &uri.store_id(), + ); + } + } + + // Note that applying the fragment changes the per-recording settings like the active time cursor. + // Therefore, we apply it even when open_behavior is Background. + self.go_to_dataset_data(uri.store_id(), uri.fragment.clone()); + + return; + } + } + + LogDataSource::RedapProxy(uri) => { + let new_source = LogSource::MessageProxy(uri.clone()); + if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { + drop(all_sources); + self.try_make_recording_from_source_active(egui_ctx, store_hub, &new_source); + return; + } + } + } + + let stream = data_source.clone().stream_with_options( + Self::auth_error_handler(self.command_sender.clone()), + &self.connection_registry, + if let LogDataSource::RedapDatasetSegment { open_behavior, .. } = &data_source + && matches!(open_behavior, RecordingOpenBehavior::Background) + { + // Previews skip the blueprint; we fetch it later if the user opens the recording for real. + re_redap_client::StreamingOptions { + download: re_redap_client::SegmentDownload::SEGMENT, + ..Default::default() + } + } else { + Default::default() + }, + ); + + #[cfg(feature = "analytics")] + if let Some(analytics) = re_analytics::Analytics::global_or_init() { + let data_source_analytics = data_source.analytics(); + analytics.record(re_analytics::event::LoadDataSource { + source_type: data_source_analytics.source_type, + file_extension: data_source_analytics.file_extension, + file_source: data_source_analytics.file_source, + started_successfully: stream.is_ok(), + }); + } + + match stream { + Ok(rx) => self.add_log_receiver(rx), + Err(err) => { + re_log::error!("Failed to open data source: {}", re_error::format(err)); + } + } + } + + /// Fetch the server blueprint for a recording that was streamed as a preview, which skips it. + /// + /// Does nothing unless the recording hasn't fetched its blueprint. + pub(super) fn fetch_pending_blueprint(&mut self, store_hub: &mut StoreHub, store_id: &StoreId) { + if !store_hub.is_blueprint_pending(store_id) { + return; + } + + let Some(LogSource::RedapGrpcStream { uri, .. }) = store_hub + .entity_db(store_id) + .and_then(|db| db.data_source.clone()) + else { + return; + }; + let data_source = LogDataSource::RedapDatasetSegment { + uri: uri.without_fragment(), + open_behavior: RecordingOpenBehavior::Background, + }; + match data_source.stream_with_options( + Self::auth_error_handler(self.command_sender.clone()), + &self.connection_registry, + re_redap_client::StreamingOptions { + download: re_redap_client::SegmentDownload::BLUEPRINT, + ..Default::default() + }, + ) { + Ok(rx) => { + store_hub.set_blueprint_pending(store_id, false); + self.add_log_receiver(rx); + } + Err(err) => { + re_log::error!("Failed to fetch blueprint: {}", re_error::format(err)); + } + } + } + + /// Makes the first recording store active that is found for a given data source if any. + fn try_make_recording_from_source_active( + &mut self, + egui_ctx: &egui::Context, + store_hub: &mut StoreHub, + new_source: &LogSource, + ) { + if let Some(entity_db) = store_hub.find_recording_store_by_source(new_source) { + let store_id = entity_db.store_id().clone(); + re_log::debug_assert!(store_id.is_recording()); // `find_recording_store_by_source` should have filtered for recordings rather than blueprints. + self.make_store_active_and_highlight(store_hub, egui_ctx, &store_id); + } + } + + /// On Wasm with the internal catalog enabled, register a dropped `.rrd`'s contents with the + /// in-process catalog and open the resulting segment, returning [`ControlFlow::Break`] when it + /// took ownership of the load. Other builds have nothing to route through and return + /// [`ControlFlow::Continue`]. + #[cfg(target_arch = "wasm32")] + fn try_register_via_internal_catalog( + &self, + file_contents: &re_data_source::FileContents, + ) -> std::ops::ControlFlow<()> { + use std::ops::ControlFlow; + + let is_rrd = file_contents + .path + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("rrd")); + if !(is_rrd + && self.app_options().experimental.use_internal_catalog + && self.connection_registry.internal_origin().is_some()) + { + return ControlFlow::Continue(()); + } + + let file_contents = file_contents.clone(); + let connection_registry = self.connection_registry.clone(); + let sender = self.command_sender.clone(); + self.async_runtime.spawn_future(async move { + match register_opfs_file(&connection_registry, &file_contents).await { + Ok(uri) => { + sender.send_system(SystemCommand::RefreshRedapEntry { + origin: uri.origin.clone(), + entry_id: uri.dataset_id.into(), + }); + sender.send_system(SystemCommand::LoadDataSource( + LogDataSource::RedapDatasetSegment { + uri, + open_behavior: RecordingOpenBehavior::OpenAndSelect, + }, + )); + } + Err(err) => { + re_log::error!( + "Failed to load file via the Viewer catalog: {err}\nFile path: {}", + file_contents.path.display(), + ); + } + } + }); + + ControlFlow::Break(()) + } + + #[cfg(not(target_arch = "wasm32"))] + #[expect(clippy::unused_self)] + fn try_register_via_internal_catalog( + &self, + _file_contents: &re_data_source::FileContents, + ) -> std::ops::ControlFlow<()> { + std::ops::ControlFlow::Continue(()) + } +} + +/// Register a local `.rrd` file with the catalog server. +#[cfg(not(target_arch = "wasm32"))] +async fn register_local_file( + connection_registry: &re_redap_client::ConnectionRegistryHandle, + path: &Path, +) -> anyhow::Result { + let abs_path = std::path::absolute(path).with_context(|| { + format!( + "failed to resolve absolute path\nFile path: {}", + path.display() + ) + })?; + let file_url = url::Url::from_file_path(&abs_path).map_err(|()| { + anyhow::anyhow!( + "not an absolute file path\nFile path: {}", + abs_path.display() + ) + })?; + + let dataset_name = async { + let mut file = tokio::fs::File::open(&abs_path) + .await + .with_context(|| { + format!( + "failed to open RRD for application id extraction\nFile path: {}", + abs_path.display(), + ) + })? + .compat(); + rrd_dataset_name(&mut file).await + } + .await + .unwrap_or_else(|err| { + re_log::warn!( + "Failed to read application id from RRD: {err}\nFile path: {}", + abs_path.display(), + ); + abs_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("recording") + .to_owned() + }); + + register_file(connection_registry, dataset_name, file_url).await +} + +#[cfg(target_arch = "wasm32")] +async fn register_opfs_file( + connection_registry: &re_redap_client::ConnectionRegistryHandle, + file_contents: &re_data_source::FileContents, +) -> anyhow::Result { + let mut reader = futures::io::Cursor::new(file_contents.bytes.clone()); + let dataset_name = rrd_dataset_name(&mut reader).await.with_context(|| { + format!( + "failed to read application id from RRD\nFile path: {}", + file_contents.path.display(), + ) + })?; + let fingerprint = re_log_encoding::RrdFingerprint::compute_for_rrd(&mut reader) + .await + .with_context(|| { + format!( + "failed to fingerprint RRD\nFile path: {}", + file_contents.path.display(), + ) + })?; + let fingerprint = fingerprint + .as_bytes() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + + let file_name = file_contents + .path + .file_name() + .filter(|file_name| !file_name.is_empty()) + .context("OPFS upload path has no file name")? + .to_str() + .context("OPFS upload file name is not UTF-8")?; + + // The web file picker yields only a base name, so key uploads on the RRD fingerprint to avoid + // path collisions. Identical re-uploads deduplicate to the same path. + let path = std::path::PathBuf::from("/uploads") + .join(&fingerprint) + .join(file_name); + + let file_exists = match re_server::opfs::metadata(&path).await { + Ok(metadata) => metadata.is_file(), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, + Err(err) => { + return Err(err).with_context(|| { + format!( + "failed to inspect OPFS upload file\nFile path: {}", + path.display() + ) + }); + } + }; + + if !file_exists { + re_server::opfs::write(&path, file_contents.bytes.clone()) + .await + .with_context(|| { + format!( + "failed to write OPFS upload file\nFile path: {}", + path.display() + ) + })?; + } + + // `Url::from_file_path` is unavailable on `wasm32-unknown-unknown`, so build the `file://` URL + // for the same on-disk location by hand. Both fallible steps are infallible for a known base. + let mut file_url = url::Url::parse("file:///").expect("`file:///` is a valid base URL"); + file_url + .path_segments_mut() + .expect("`file:///` is a base URL") + .extend(["uploads", fingerprint.as_str(), file_name]); + + register_file(connection_registry, dataset_name, file_url).await +} + +async fn register_file( + connection_registry: &re_redap_client::ConnectionRegistryHandle, + dataset_name: String, + file_url: url::Url, +) -> anyhow::Result { + let origin = connection_registry + .internal_origin() + .context("internal catalog is not running")?; + let mut client = connection_registry.client(origin.clone()).await?; + let data_source = DataSource::new_rrd_url(file_url); + + let (dataset_id, segment_id) = client + .ensure_dataset_and_register( + &dataset_name, + vec![data_source], + IfDuplicateBehavior::Overwrite, + ) + .await?; + + Ok(re_uri::DatasetSegmentUri { + origin, + dataset_id: dataset_id.id, + segment_id, + fragment: Default::default(), + }) +} + +async fn rrd_dataset_name( + reader: &mut impl re_log_encoding::AsyncReadAt, +) -> anyhow::Result { + let store_ids = re_log_encoding::enumerate_rrd_stores(reader).await?; + let first_application_id = store_ids + .first() + .map(re_log_types::StoreId::application_id) + .context("no application id found in RRD")?; + + if store_ids + .iter() + .any(|store_id| store_id.application_id() != first_application_id) + { + re_log::warn!( + "RRD contains multiple application ids; using the first as the dataset name: {first_application_id}" + ); + } + + Ok(first_application_id.to_string()) +} diff --git a/crates/viewer/re_viewer/src/app/command_handling.rs b/crates/viewer/re_viewer/src/app/command_handling.rs new file mode 100644 index 000000000000..419ab214a626 --- /dev/null +++ b/crates/viewer/re_viewer/src/app/command_handling.rs @@ -0,0 +1,1971 @@ +use anyhow::Context as _; +use itertools::Itertools as _; +use re_build_info::CrateVersion; +use re_chunk::TimelineName; +use re_entity_db::{EntityDb, LogSource}; +use re_log_channel::RecordingOpenBehavior; +use re_log_types::{ApplicationId, LogMsg, RecordingId, StoreId, StoreKind}; +use re_sdk_types::blueprint::components::PlayState; +use re_ui::{RecordingCommand, UICommand, UICommandSender as _}; +use re_viewer_context::open_url::{OpenUrlOptions, ViewerOpenUrl}; +use re_viewer_context::{ + ActiveStoreContext, AppBlueprintCtx, NeedsRepaint, Route, StorageContext, StoreHub, + SystemCommand, open_url::combine_with_base_url, +}; +use re_viewer_context::{ + MoveDirection, MoveSpeed, RecordingOrTable, SystemCommandSender as _, TimeControlCommand, + sanitize_file_name, +}; +use std::sync::Arc; + +use super::App; +use crate::{app_blueprint::AppBlueprint, event::ViewerEventDispatcher}; + +#[cfg(not(target_arch = "wasm32"))] +const MIN_ZOOM_FACTOR: f32 = 0.2; +#[cfg(not(target_arch = "wasm32"))] +const MAX_ZOOM_FACTOR: f32 = 5.0; + +/// How [`App::close_recording`] should treat a recording that's still rendered as a preview. +#[derive(Clone, Copy, PartialEq, Eq)] +enum CloseRecording { + /// A recording still rendered as a preview stays loaded and streaming, just no longer in the + /// recording list. + KeepPreview, + + /// Fully remove the recording even if it's still rendered as a preview. + /// Used when the recording's server is going away, so leaving it loaded makes no sense. + Force, +} + +impl App { + pub(super) fn run_pending_system_commands( + &mut self, + store_hub: &mut StoreHub, + egui_ctx: &egui::Context, + ) { + re_tracing::profile_function!(); + while let Some((from_where, cmd)) = self.command_receiver.recv_system() { + self.run_system_command(from_where, cmd, store_hub, egui_ctx); + } + } + + pub(super) fn run_pending_ui_commands( + &mut self, + egui_ctx: &egui::Context, + app_blueprint: &AppBlueprint<'_>, + storage_context: &StorageContext<'_>, + store_context: Option<&ActiveStoreContext<'_>>, + route: &Route, + ) { + re_tracing::profile_function!(); + while let Some(cmd) = self.command_receiver.recv_ui() { + self.run_ui_command( + egui_ctx, + app_blueprint, + storage_context, + store_context, + route, + cmd, + ); + } + } + + fn run_system_command( + &mut self, + sent_from: &std::panic::Location<'_>, // Who sent this command? Useful for debugging! + cmd: SystemCommand, + store_hub: &mut StoreHub, + egui_ctx: &egui::Context, + ) { + re_tracing::profile_function!(cmd.debug_name()); + + match cmd { + SystemCommand::TimeControlCommands { + store_id, + time_commands, + } => { + match store_id.kind() { + StoreKind::Recording => { + let usage = store_hub.usage(&store_id); + + if usage.was_preview() + && let Some(preview_state) = &mut self.state.view_states.preview_state + && let Some(time_control) = + preview_state.recording_time_control_mut(&store_id) + && let Some(db) = store_hub.entity_db(&store_id) + { + let response = time_control.handle_time_commands( + None::<&AppBlueprintCtx<'_>>, + db, + &time_commands, + ); + + if response.needs_repaint == NeedsRepaint::Yes { + self.egui_ctx.request_repaint(); + } + + return; + } + + store_hub.load_blueprint_and_caches(&store_id, &self.view_class_registry); // Ensure caches and blueprints + store_hub.ensure_active_blueprint_for_app(store_id.application_id()); // Materialize the target blueprint on-demand + + let Some(target_blueprint) = + store_hub.active_blueprint_for_app(store_id.application_id()) + else { + re_log::debug_panic!( + "No active blueprint found for recording {store_id:?} when handling time control commands sent from {sent_from}. This should never happen for local recording routes.", + ); + re_log::error_once!( + "Can't change time for recording {store_id:?} because it is not active." + ); + return; + }; + + let blueprint_query = self + .state + .blueprint_query_for_viewer(Some(target_blueprint)); + + let blueprint_ctx = AppBlueprintCtx { + command_sender: &self.command_sender, + current_blueprint: target_blueprint, + default_blueprint: store_hub + .default_blueprint_for_app(store_id.application_id()), + blueprint_query, + }; + + let Some(recording) = store_hub.entity_db(&store_id) else { + re_log::error_once!( + "Can't change time for recording {store_id:?} because it is not loaded." + ); + return; + }; + + let time_ctrl = self.state.time_control_mut(recording, &blueprint_ctx); + + let response = time_ctrl.handle_time_commands( + Some(&blueprint_ctx), + recording, + &time_commands, + ); + + if response.needs_repaint == NeedsRepaint::Yes { + self.egui_ctx.request_repaint(); + } + + handle_time_ctrl_event( + recording, + self.event_dispatcher.as_ref(), + &response, + ); + } + StoreKind::Blueprint => { + if let Some(target_store) = store_hub.store_bundle().get(&store_id) { + let blueprint_ctx: Option<&AppBlueprintCtx<'_>> = None; + let response = self.state.blueprint_time_control.handle_time_commands( + blueprint_ctx, + target_store, + &time_commands, + ); + + if response.needs_repaint == NeedsRepaint::Yes { + self.egui_ctx.request_repaint(); + } + } + } + } + } + SystemCommand::SetUrlFragment { store_id, fragment } => { + // This adds new system commands, which will be handled later in the loop. + self.go_to_dataset_data(store_id, fragment); + } + SystemCommand::CopyViewerUrl(url) => { + if cfg!(target_arch = "wasm32") { + match combine_with_base_url( + self.startup_options.web_viewer_base_url().as_ref(), + [url], + ) { + Ok(url) => { + self.copy_text(url); + } + Err(err) => { + re_log::error!("{err}"); + } + } + } else { + self.copy_text(url); + } + } + SystemCommand::ActivateApp(app_id) => { + store_hub.load_persisted_blueprints_for_app(&app_id); + if let Some(recording_id) = store_hub.earliest_recording_for_app(&app_id) { + store_hub.load_blueprint_and_caches(&recording_id, &self.view_class_registry); + self.state + .navigation + .replace(Route::LocalRecording { recording_id }); + } else { + // TODO(RR-3713): show a blueprint for it anyway + re_log::warn_once!("Can't switch app-id - we have no recording for it"); + // If we can't go where we want to go, then go nowhere. + } + } + + SystemCommand::CloseApp(app_id) => { + store_hub.close_app(&app_id); + } + + SystemCommand::CloseRecordingOrTable(entry) => { + // The active recording we're closing, if that's what this is. When set, we move off + // it after closing. + let active_being_closed = match &entry { + RecordingOrTable::Recording { store_id } + if self.state.active_recording_id() == Some(store_id) => + { + Some(store_id.clone()) + } + _ => None, + }; + + let new_navigation = active_being_closed.as_ref().and_then(|closing| { + // Look back through history for the closest entry that's still an open destination. + let back_target = self + .state + .history + .find_back(|url| { + self.is_back_destination_open(store_hub, url, Some(closing)) + }) + .cloned(); + + back_target.or_else(|| { + ViewerOpenUrl::from_route( + store_hub, + &Self::fallback_route_after_close(store_hub, closing), + ) + .ok() + }) + }); + + self.close_recording(store_hub, &entry, CloseRecording::KeepPreview); + + if let Some(new_navigation) = new_navigation { + self.navigate_to(egui_ctx, &new_navigation); + } else if active_being_closed.is_some() { + self.state.navigation.reset(); + } + } + + SystemCommand::CloseAllEntries => { + self.state.navigation.reset(); + store_hub.clear_entries(); + + // Stop receiving into the old recordings. + // This is most important when going back to the example screen by using the "Back" + // button in the browser, and there is still a connection downloading an .rrd. + // That's the case of `LogSource::HttpStream`. + // TODO(emilk): exactly what things get kept and what gets cleared? + self.rx_log.retain(|r| match r.source() { + LogSource::File { .. } | LogSource::HttpStream { .. } => false, + + LogSource::JsChannel { .. } + | LogSource::RrdWebEvent + | LogSource::Sdk + | LogSource::RedapGrpcStream { .. } + | LogSource::MessageProxy { .. } + | LogSource::Stdin => true, + }); + } + + SystemCommand::AddReceiver(rx) => { + re_log::debug!("Received AddReceiver"); + self.add_log_receiver(rx); + } + + SystemCommand::SetRoute(new_route) => { + if &new_route == self.state.navigation.current() { + return; + } + + self.state.view_states.preview_state = None; + + // Suppress loading screen if we're loading a recording that's already loaded, even if only partially. + if let Route::Loading(source) = &new_route + && let Some(re_uri::RedapUri::DatasetData(dataset_uri)) = source.redap_uri() + && store_hub + .store_bundle() + .entity_dbs() + .any(|db| db.store_id() == &dataset_uri.store_id()) + { + return; + } + + if let Some(recording_id) = new_route.recording_id() { + store_hub.set_opened(recording_id, true); + store_hub.load_blueprint_and_caches(recording_id, &self.view_class_registry); + // If we're navigating to a recording that was only ever a preview, fetch the + // blueprint we skipped while previewing it. + self.fetch_pending_blueprint(store_hub, recording_id); + } + + if matches!(new_route, Route::Loading(_)) { + self.state + .selection_state + .set_selection(re_viewer_context::ItemCollection::default()); + } + + self.state.navigation.replace(new_route); + + egui_ctx.request_repaint(); // Make sure we actually see the new mode. + } + + SystemCommand::OpenSettings => { + self.state.navigation.replace(Route::Settings { + return_route: Box::new(self.state.navigation.current().clone()), + }); + + #[cfg(feature = "analytics")] + re_analytics::record(|| re_analytics::event::SettingsOpened {}); + } + + SystemCommand::OpenChunkStoreBrowser { + store_id, + selected_chunk, + } => match self.state.navigation.current() { + Route::ChunkStoreBrowser { + store_id: current_store_id, + return_route, + .. + } => { + self.state.navigation.replace(Route::ChunkStoreBrowser { + // History/share URLs may carry an explicit store; otherwise keep + // using the current chunk browser store context. + store_id: store_id.or_else(|| current_store_id.clone()), + selected_chunk, + return_route: return_route.clone(), + }); + } + current => { + self.state.navigation.replace(Route::ChunkStoreBrowser { + store_id: store_id.or_else(|| current.recording_id().cloned()), + selected_chunk, + return_route: Box::new(current.clone()), + }); + } + }, + + SystemCommand::ResetRoute => { + self.state.navigation.reset(); + + egui_ctx.request_repaint(); // Make sure we actually see the new mode. + } + + SystemCommand::AddRedapServer(origin) => { + if origin == *re_redap_browser::EXAMPLES_ORIGIN { + return; + } + if self.state.redap_servers.has_server(&origin) { + return; + } + + self.state.redap_servers.add_server(origin.clone()); + + if self.state.navigation.current().recording_id().is_none() { + self.state.navigation.replace(Route::RedapServer(origin)); + } + self.command_sender.send_ui(UICommand::ExpandBlueprintPanel); + } + + SystemCommand::RefreshRedapServer(origin) => { + // Only refresh servers we already know about; adding a new server already fetches + // its catalog, so there's nothing to refresh in that case. + if self.state.redap_servers.has_server(&origin) { + self.state + .redap_servers + .send_command(re_redap_browser::Command::RefreshCollection(origin)); + } + } + + SystemCommand::RefreshRedapEntry { origin, entry_id } => { + self.state + .redap_servers + .refresh_entry(&origin, entry_id, egui_ctx); + } + + SystemCommand::RemoveRedapServer(origin) => { + // Clearing blueprints must happen before closing the recordings (so we can know + // what to close) + store_hub.clear_blueprints_for_origin(&origin); + + // Close any recordings streaming from this server, otherwise their + // still-open connections keep emitting "Failed to connect to remote + // data source" warnings. + let recordings_to_close: Vec<_> = store_hub + .store_bundle() + .recordings_for_origin(&origin) + .map(|db| db.store_id().clone()) + .collect(); + + // Were we viewing one of the recordings we're about to close? Then we need to move + // off it once the server is gone. + let viewing_closed_recording = self + .state + .active_recording_id() + .is_some_and(|active| recordings_to_close.contains(active)); + + // Close the recordings before removing the server, to avoid a race. + // `Force` because a recording rendered as a preview last frame would otherwise stay + // loaded and streaming even though its server is being removed. + for store_id in recordings_to_close { + self.close_recording(store_hub, &store_id.into(), CloseRecording::Force); + } + + self.state + .redap_servers + .remove_server(&origin, &self.connection_registry); + + let current_route = self.state.navigation.current(); + let on_removed_server = match current_route { + Route::RedapServer(route_origin) + | Route::RedapEntry { + origin: route_origin, + .. + } => route_origin == &origin, + _ => false, + }; + + if on_removed_server || viewing_closed_recording { + if let Some(url) = self + .state + .history + .find_back(|url| self.is_back_destination_open(store_hub, url, None)) + .cloned() + { + self.navigate_to(egui_ctx, &url); + } else { + self.state.navigation.reset(); + } + } + } + + SystemCommand::EditRedapServerModal(command) => { + self.state.redap_servers.open_edit_server_modal(command); + } + + SystemCommand::RedapServer(command) => { + let re_ui::RedapServerCommand { origin, kind } = command; + match kind { + re_ui::RedapServerCommandKind::Refresh => { + self.command_sender + .send_system(SystemCommand::RefreshRedapServer(origin)); + } + re_ui::RedapServerCommandKind::Edit => { + self.command_sender + .send_system(SystemCommand::EditRedapServerModal( + re_viewer_context::EditRedapServerModalCommand::new(origin), + )); + } + re_ui::RedapServerCommandKind::CopyUrl => { + let url = origin.to_string(); + re_log::info!("Copied {url:?} to clipboard"); + egui_ctx.copy_text(url); + } + re_ui::RedapServerCommandKind::Remove => { + self.command_sender + .send_system(SystemCommand::RemoveRedapServer(origin)); + } + } + } + + SystemCommand::Table(command) => { + let re_ui::TableCommand { + origin, + entry_id, + kind, + } = command; + match kind { + re_ui::TableCommandKind::Refresh => { + self.state + .redap_servers + .refresh_entry(&origin, entry_id, egui_ctx); + } + } + } + + SystemCommand::LoadDataSource(data_source) => { + self.load_data_source(store_hub, egui_ctx, &data_source); + } + + SystemCommand::ResetViewer => self.reset_viewer(store_hub, egui_ctx), + SystemCommand::ClearActiveBlueprintAndEnableHeuristics => { + re_log::debug!("Clear and generate new blueprint"); + store_hub.clear_active_blueprint_and_generate(self.state.navigation.current()); + egui_ctx.request_repaint(); // Many changes take a frame delay to show up. + } + SystemCommand::ClearActiveBlueprint => { + // By clearing the blueprint the default blueprint will be restored + // at the beginning of the next frame. + re_log::debug!("Reset blueprint to default"); + store_hub.clear_active_blueprint(self.state.navigation.current()); + egui_ctx.request_repaint(); // Many changes take a frame delay to show up. + } + + SystemCommand::AppendToStore(store_id, chunks) => { + re_log::trace!( + "{}:{} Update {} entities: {}", + sent_from.file(), + sent_from.line(), + store_id.kind(), + chunks.iter().map(|c| c.entity_path()).join(", ") + ); + + let db = store_hub.entity_db_entry(&store_id); + + // No need to clear undo buffer if we're just appending static data. + // + // It would be nice to be able to undo edits to a recording, but + // we haven't implemented that yet. + if store_id.is_blueprint() && chunks.iter().any(|c| !c.is_static()) { + self.state + .blueprint_undo_state + .entry(store_id.clone()) + .or_default() + .clear_redo_buffer(db); + + if self.app_options().inspect_blueprint_timeline { + self.command_sender + .send_system(SystemCommand::TimeControlCommands { + store_id, + time_commands: vec![TimeControlCommand::SetPlayState( + PlayState::Following, + )], + }); + } + } + + for chunk in chunks { + match db.add_chunk(&Arc::new(chunk)) { + Ok(_store_events) => {} + Err(err) => { + re_log::warn_once!("Failed to append chunk: {err}"); + } + } + } + } + + SystemCommand::UndoBlueprint { blueprint_id } => { + let inspect_blueprint_timeline = self.app_options().inspect_blueprint_timeline; + let blueprint_db = store_hub.entity_db_entry(&blueprint_id); + let undo_state = self + .state + .blueprint_undo_state + .entry(blueprint_id.clone()) + .or_default(); + + undo_state.undo(blueprint_db); + + // Update blueprint inspector timeline. + if inspect_blueprint_timeline { + if let Some(redo_time) = undo_state.redo_time() { + self.command_sender + .send_system(SystemCommand::TimeControlCommands { + store_id: blueprint_id, + time_commands: vec![ + TimeControlCommand::SetPlayState(PlayState::Paused), + TimeControlCommand::SetTime(redo_time.into()), + ], + }); + } else { + self.command_sender + .send_system(SystemCommand::TimeControlCommands { + store_id: blueprint_id, + time_commands: vec![TimeControlCommand::SetPlayState( + PlayState::Following, + )], + }); + } + } + } + SystemCommand::RedoBlueprint { blueprint_id } => { + let inspect_blueprint_timeline = self.app_options().inspect_blueprint_timeline; + let undo_state = self + .state + .blueprint_undo_state + .entry(blueprint_id.clone()) + .or_default(); + + undo_state.redo(); + + // Update blueprint inspector timeline. + if inspect_blueprint_timeline { + if let Some(redo_time) = undo_state.redo_time() { + self.command_sender + .send_system(SystemCommand::TimeControlCommands { + store_id: blueprint_id, + time_commands: vec![ + TimeControlCommand::SetPlayState(PlayState::Paused), + TimeControlCommand::SetTime(redo_time.into()), + ], + }); + } else { + self.command_sender + .send_system(SystemCommand::TimeControlCommands { + store_id: blueprint_id, + time_commands: vec![TimeControlCommand::SetPlayState( + PlayState::Following, + )], + }); + } + } + } + + SystemCommand::DropEntity(blueprint_id, entity_path) => { + let blueprint_db = store_hub.entity_db_entry(&blueprint_id); + blueprint_db.drop_entity_path_recursive(&entity_path); + } + + #[cfg(debug_assertions)] + SystemCommand::EnableInspectBlueprintTimeline(show) => { + self.app_options_mut().inspect_blueprint_timeline = show; + } + + SystemCommand::SetSelection(set) => { + if let Some(item) = set.selection.single_item() { + // If the selected item has its own page, switch to it. + if let Some(route) = Route::from_item(item) { + if let Route::LocalRecording { recording_id } = &route { + store_hub + .load_blueprint_and_caches(recording_id, &self.view_class_registry); + } + self.state.navigation.replace(route); + } + } + + self.state.selection_state.set_selection(set); + egui_ctx.request_repaint(); // Make sure we actually see the new selection. + } + + SystemCommand::SetFocus(item) => { + self.state.focused_item = Some(item); + } + + SystemCommand::ShowNotification(notification) => { + self.notifications.add(notification); + } + + SystemCommand::ReadbackAndSaveTexture { texture, action } => { + self.texture_readback.push(texture, action); + } + + #[cfg(not(target_arch = "wasm32"))] + SystemCommand::FileSaver(file_saver) => { + if let Err(err) = self.background_tasks.spawn_file_saver(file_saver) { + re_log::error!("Failed to save file: {err}"); + } + } + + SystemCommand::OnAuthChanged(auth) => { + self.state.auth_state = auth; + } + + SystemCommand::SetAuthCredentials { + access_token, + email, + } => { + let credentials = + match re_auth::oauth::Credentials::try_new(access_token, None, email) { + Ok(credentials) => credentials, + Err(err) => { + re_log::error!("Failed to create credentials: {err}"); + return; + } + }; + if let Err(err) = credentials.ensure_stored() { + re_log::error!("Failed to store credentials: {err}"); + } + } + SystemCommand::Logout => { + let signed_out_url = self + .startup_options + .login + .as_ref() + .map(|l| l.signed_out_url.as_str()); + match re_auth::oauth::clear_credentials(signed_out_url) { + Ok(Some(outcome)) => { + // Open the WorkOS logout URL to also end the browser session. + // This opens in a new tab/window so the viewer state is preserved. + // WorkOS clears its session cookies and redirects to /signed-out. + egui_ctx.open_url(egui::output::OpenUrl { + url: outcome.logout_url, + new_tab: true, + }); + } + Ok(None) => { + re_log::debug!("No session to logout from"); + } + Err(err) => { + re_log::error!("Failed to logout: {err}"); + } + } + let logged_out_origins = self.state.redap_servers.logout(); + + // Close any open recordings that came from the logged-out servers. + store_hub.retain_recordings(|db| { + let Some(data_source) = &db.data_source else { + return true; + }; + match data_source { + LogSource::RedapGrpcStream { uri, .. } => { + !logged_out_origins.contains(&uri.origin) + } + _ => true, + } + }); + + // Also stop receiving data from those servers. + self.rx_log.retain(|r| match r.source() { + LogSource::RedapGrpcStream { uri, .. } => { + !logged_out_origins.contains(&uri.origin) + } + _ => true, + }); + } + SystemCommand::SaveScreenshot { + target, + view_id, + notify, + } => { + if let Some(view_id) = view_id { + // Screenshot a specific view + if let Some(view_info) = self.egui_ctx.memory_mut(|mem| { + mem.caches + .cache::() + .get(&view_id) + .cloned() + }) { + let re_viewer_context::PublishedViewInfo { name, rect } = view_info; + let rect = rect.shrink(2.5); // Hacky: Shrink so we don't accidentally include the border of the view. + if !rect.is_positive() { + re_log::warn!("View too small for a screenshot"); + return; + } + + self.egui_ctx + .send_viewport_cmd(egui::ViewportCommand::Screenshot( + egui::UserData::new(re_viewer_context::ScreenshotInfo { + ui_rect: Some(rect), + pixels_per_point: self.egui_ctx.pixels_per_point(), + name, + target, + notify, + }), + )); + } else { + re_log::warn!("View {view_id} not found for screenshot"); + } + } else { + // Screenshot the entire viewer + self.egui_ctx + .send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::new( + re_viewer_context::ScreenshotInfo { + ui_rect: None, + pixels_per_point: self.egui_ctx.pixels_per_point(), + name: "screenshot".to_owned(), + target, + notify, + }, + ))); + } + + // Screenshot commands may be triggered from receiving messages over the network, so we may not actually do any painting right now. + // Make sure we do at least once, so the screenshot gets saved out. + self.egui_ctx.request_repaint(); + + // TODO(#12481): Depending on the platform we a request repaint alone isn't enough to wake up the viewer. + // For now we do a focus switch but this isn't ideal since it breaks the flow of programmatic screenshot taking. + self.egui_ctx + .send_viewport_cmd(egui::ViewportCommand::Focus); + } + } + } + + fn run_ui_command( + &mut self, + egui_ctx: &egui::Context, + app_blueprint: &AppBlueprint<'_>, + storage_context: &StorageContext<'_>, + store_context: Option<&ActiveStoreContext<'_>>, + route: &Route, + cmd: UICommand, + ) { + let mut force_store_info = false; + let active_store_id = store_context + .map(|ctx| ctx.recording_store_id().clone()) + // Don't redirect data to the welcome screen. + .filter(|store_id| store_id.application_id() != StoreHub::welcome_screen_app_id()) + .unwrap_or_else(|| { + // If we don't have any application ID to recommend (which means we are on the welcome screen), + // then just generate a new one using a UUID. + let application_id = ApplicationId::random(); + + // NOTE: We don't override blueprints' store IDs anyhow, so it is sound to assume that + // this can only be a recording. + let recording_id = RecordingId::random(); + + // We're creating a recording just-in-time, directly from the viewer. + // We need those store infos or the data will just be silently ignored. + force_store_info = true; + + StoreId::recording(application_id, recording_id) + }); + + match cmd { + #[cfg(not(target_arch = "wasm32"))] + UICommand::Open => { + use re_data_source::LogDataSource; + use re_log_types::FileSource; + for file_path in open_file_dialog_native(self.main_thread_token) { + self.command_sender + .send_system(SystemCommand::LoadDataSource(LogDataSource::FilePath { + file_source: FileSource::FileDialog { + recommended_store_id: None, + force_store_info, + }, + path: file_path, + })); + } + } + #[cfg(target_arch = "wasm32")] + UICommand::Open => { + let egui_ctx = egui_ctx.clone(); + + let promise = poll_promise::Promise::spawn_local(async move { + let file = async_open_rrd_dialog().await; + egui_ctx.request_repaint(); // Wake ui thread + file + }); + + self.open_files_promise = Some(super::PendingFilePromise { + recommended_store_id: None, + force_store_info, + promise, + }); + } + + #[cfg(not(target_arch = "wasm32"))] + UICommand::Import => { + use re_data_source::LogDataSource; + use re_log_types::FileSource; + for file_path in open_file_dialog_native(self.main_thread_token) { + self.command_sender + .send_system(SystemCommand::LoadDataSource(LogDataSource::FilePath { + file_source: FileSource::FileDialog { + recommended_store_id: Some(active_store_id.clone()), + force_store_info, + }, + path: file_path, + })); + } + } + #[cfg(target_arch = "wasm32")] + UICommand::Import => { + let egui_ctx = egui_ctx.clone(); + + let promise = poll_promise::Promise::spawn_local(async move { + let file = async_open_rrd_dialog().await; + egui_ctx.request_repaint(); // Wake ui thread + file + }); + + self.open_files_promise = Some(super::PendingFilePromise { + recommended_store_id: Some(active_store_id.clone()), + force_store_info, + promise, + }); + } + + UICommand::OpenUrl => { + self.state.open_url_modal.open(); + } + + UICommand::CloseAllEntries => { + self.command_sender + .send_system(SystemCommand::CloseAllEntries); + } + + UICommand::NextRecording => { + self.state + .recording_panel + .send_command(re_recording_panel::RecordingPanelCommand::SelectNextRecording); + } + UICommand::PreviousRecording => { + self.state.recording_panel.send_command( + re_recording_panel::RecordingPanelCommand::SelectPreviousRecording, + ); + } + + UICommand::NavigateBack => { + if let Some(url) = self.state.history.go_back() { + url.clone().open( + egui_ctx, + &OpenUrlOptions { + recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, + show_loader: true, + }, + &self.command_sender, + ); + } + } + UICommand::NavigateForward => { + if let Some(url) = self.state.history.go_forward() { + url.clone().open( + egui_ctx, + &OpenUrlOptions { + recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, + show_loader: true, + }, + &self.command_sender, + ); + } + } + + #[cfg(not(target_arch = "wasm32"))] + UICommand::Quit => { + egui_ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + + UICommand::OpenWebsite => { + egui_ctx.open_url(egui::output::OpenUrl { + url: "https://rerun.io/".to_owned(), + new_tab: true, + }); + } + UICommand::OpenWebHelp => { + egui_ctx.open_url(egui::output::OpenUrl { + url: "https://rerun.io/docs/getting-started/navigating-the-viewer".to_owned(), + new_tab: true, + }); + } + UICommand::OpenRerunDiscord => { + egui_ctx.open_url(egui::output::OpenUrl { + url: "https://discord.gg/PXtCgFBSmH".to_owned(), + new_tab: true, + }); + } + + UICommand::ResetViewer => self.command_sender.send_system(SystemCommand::ResetViewer), + #[cfg(not(target_arch = "wasm32"))] + UICommand::OpenProfiler => { + self.profiler.start(); + } + + #[cfg(not(target_arch = "wasm32"))] + UICommand::CaptureProfileTrace => { + if self.profile_capture.is_none() { + self.profile_capture = Some(re_tracing::ProfileCapture::start(5)); + egui_ctx.request_repaint(); + } + } + + UICommand::ToggleDevPanel => { + self.dev_panel_open ^= true; + } + UICommand::TogglePanelStateOverrides => { + self.panel_state_overrides_active ^= true; + } + UICommand::ToggleTopPanel => { + app_blueprint.toggle_top_panel(&self.command_sender); + } + UICommand::ToggleBlueprintPanel => { + app_blueprint.toggle_blueprint_panel(&self.command_sender); + } + UICommand::ExpandBlueprintPanel => { + if !app_blueprint.blueprint_panel_state().is_expanded() { + app_blueprint.toggle_blueprint_panel(&self.command_sender); + } + } + UICommand::ToggleSelectionPanel => { + app_blueprint.toggle_selection_panel(&self.command_sender); + } + UICommand::ExpandSelectionPanel => { + if !app_blueprint.selection_panel_state().is_expanded() { + app_blueprint.toggle_selection_panel(&self.command_sender); + } + } + #[cfg(debug_assertions)] + UICommand::ToggleEguiDebugPanel => { + self.egui_debug_panel_open ^= true; + } + + UICommand::ToggleFullscreen => { + self.toggle_fullscreen(); + } + + UICommand::Settings => { + self.command_sender.send_system(SystemCommand::OpenSettings); + } + + #[cfg(not(target_arch = "wasm32"))] + UICommand::ZoomIn => { + let mut zoom_factor = egui_ctx.zoom_factor(); + zoom_factor += 0.1; + zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); + zoom_factor = (zoom_factor * 10.).round() / 10.; + egui_ctx.set_zoom_factor(zoom_factor); + } + #[cfg(not(target_arch = "wasm32"))] + UICommand::ZoomOut => { + let mut zoom_factor = egui_ctx.zoom_factor(); + zoom_factor -= 0.1; + zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); + zoom_factor = (zoom_factor * 10.).round() / 10.; + egui_ctx.set_zoom_factor(zoom_factor); + } + #[cfg(not(target_arch = "wasm32"))] + UICommand::ZoomReset => { + egui_ctx.set_zoom_factor(1.0); + } + + UICommand::ToggleCommandPalette => { + self.cmd_palette.toggle(); + } + + #[cfg(not(target_arch = "wasm32"))] + UICommand::ScreenshotWholeApp => { + self.screenshotter.request_screenshot(egui_ctx); + } + #[cfg(debug_assertions)] + UICommand::ResetEguiMemory => { + egui_ctx.memory_mut(|mem| *mem = Default::default()); + + // re-apply style, which is lost when resetting memory + re_ui::apply_style_and_install_loaders(egui_ctx); + } + + UICommand::Share => { + let selection = self.state.selection_state.selected_items(); + let rec_cfg = route + .recording_id() + .and_then(|id| self.state.time_controls.get(id)); + if let Err(err) = + self.state + .share_modal + .open(storage_context.hub, route, rec_cfg, selection) + { + re_log::error!("Cannot share link to current screen: {err}"); + } + } + UICommand::CopyDirectLink => { + match ViewerOpenUrl::from_route(storage_context.hub, route) { + Ok(url) => self.run_copy_link_command(&url), + Err(err) => re_log::error!("{err}"), + } + } + + UICommand::CopyTimeSelectionLink => { + match ViewerOpenUrl::from_route(storage_context.hub, route) { + Ok(mut url) => { + if let Some(fragment) = url.fragment_mut() { + let time_ctrl = route + .recording_id() + .and_then(|id| self.state.time_control(id)); + + if let Some(time_ctrl) = &time_ctrl + && let Some(time_selection) = time_ctrl.time_selection() + && let Some(timeline) = time_ctrl.timeline() + { + fragment.time_selection = Some(re_uri::TimeSelection { + timeline: *timeline, + range: time_selection.to_int(), + }); + } else { + re_log::warn!("No timeline selection to copy"); + } + } else { + re_log::warn!( + "The current recording doesn't support sharing a time range" + ); + } + + self.run_copy_link_command(&url); + } + Err(err) => re_log::error!("{err}"), + } + } + + #[cfg(target_arch = "wasm32")] + UICommand::RestartWithWebGl => { + if crate::web_tools::set_url_parameter_and_refresh("renderer", "webgl").is_err() { + re_log::error!("Failed to set URL parameter `renderer=webgl` & refresh page."); + } + } + + #[cfg(target_arch = "wasm32")] + UICommand::RestartWithWebGpu => { + if crate::web_tools::set_url_parameter_and_refresh("renderer", "webgpu").is_err() { + re_log::error!("Failed to set URL parameter `renderer=webgpu` & refresh page."); + } + } + + UICommand::CopyEntityHierarchy => { + self.copy_entity_hierarchy_to_clipboard(egui_ctx, store_context); + } + + UICommand::AddRedapServer => { + self.state.redap_servers.open_add_server_modal(); + } + } + } + + pub(super) fn run_pending_recording_commands( + &mut self, + egui_ctx: &egui::Context, + app_blueprint: &AppBlueprint<'_>, + storage_context: &StorageContext<'_>, + store_context: Option<&ActiveStoreContext<'_>>, + ) { + re_tracing::profile_function!(); + while let Some(cmd) = self.command_receiver.recv_recording() { + self.run_recording_command( + egui_ctx, + app_blueprint, + storage_context, + store_context, + cmd, + ); + } + } + + #[allow(clippy::allow_attributes, unused_variables)] // some parameters are only used on some platforms + fn run_recording_command( + &mut self, + egui_ctx: &egui::Context, + app_blueprint: &AppBlueprint<'_>, + storage_context: &StorageContext<'_>, + store_context: Option<&ActiveStoreContext<'_>>, + cmd: RecordingCommand, + ) { + use re_ui::RecordingCommandKind; + + let RecordingCommand { recording_id, kind } = cmd; + + match kind { + RecordingCommandKind::Save => { + #[cfg(target_arch = "wasm32")] // Web + { + if let Err(err) = save_active_recording(self, store_context) { + re_log::error!("Failed to save recording: {err}"); + } + } + + #[cfg(not(target_arch = "wasm32"))] // Native + { + let mut selected_stores = vec![]; + for item in self.state.selection_state.selected_items().iter_items() { + use re_viewer_context::Item; + + match item { + Item::AppId(selected_app_id) => { + for recording in storage_context.bundle.recordings() { + if recording.application_id() == selected_app_id { + selected_stores.push(recording.store_id().clone()); + } + } + } + Item::StoreId(store_id) => { + selected_stores.push(store_id.clone()); + } + _ => {} + } + } + + let selected_stores = selected_stores + .iter() + .filter_map(|store_id| storage_context.bundle.get(store_id)) + .collect_vec(); + + if selected_stores.is_empty() { + if let Err(err) = save_active_recording(self, store_context) { + re_log::error!("Failed to save recording: {err}"); + } + } else if selected_stores.len() == 1 { + // Common case: saving a single recording. + // In this case we want the user to be able to pick a file name (not just a folder): + if let Err(err) = save_recording(self, selected_stores[0], None) { + re_log::error!("Failed to save recording: {err}"); + } + } else { + // Save all selected recordings to a folder: + if let Some(folder) = rfd::FileDialog::new() + .set_title("Save recordings to folder") + .pick_folder() + { + self.save_many_recordings(&selected_stores, &folder); + } else { + re_log::info!("No folder selected - recordings not saved."); + } + } + } + } + RecordingCommandKind::SaveTimeSelection => { + if let Err(err) = save_active_recording(self, store_context) { + re_log::error!("Failed to save recording: {err}"); + } + } + + RecordingCommandKind::SaveBlueprint => { + if let Err(err) = save_blueprint(self, store_context) { + re_log::error!("Failed to save blueprint: {err}"); + } + } + + RecordingCommandKind::Close => { + self.command_sender + .send_system(SystemCommand::CloseRecordingOrTable(recording_id.into())); + } + RecordingCommandKind::Undo => { + if let Some(store_context) = store_context { + let blueprint_id = store_context.blueprint.store_id().clone(); + self.command_sender + .send_system(SystemCommand::UndoBlueprint { blueprint_id }); + } + } + RecordingCommandKind::Redo => { + if let Some(store_context) = store_context { + let blueprint_id = store_context.blueprint.store_id().clone(); + self.command_sender + .send_system(SystemCommand::RedoBlueprint { blueprint_id }); + } + } + + RecordingCommandKind::AddViewOrContainer => { + if let Some(ctx) = store_context { + let blueprint_query = + self.state.blueprint_query_for_viewer(Some(ctx.blueprint)); + let viewport = re_viewport_blueprint::ViewportBlueprint::from_db( + ctx.blueprint, + &blueprint_query, + ); + + // If a single container is selected, we use it as target. + // Otherwise, we target the root container. + let target_container_id = + if let Some(re_viewer_context::Item::Container(container_id)) = + self.state.selection_state.selected_items().single_item() + { + *container_id + } else { + viewport.root_container + }; + + re_viewport_blueprint::ui::show_add_view_or_container_modal( + target_container_id, + ); + } + } + RecordingCommandKind::ClearActiveBlueprint => { + self.command_sender + .send_system(SystemCommand::ClearActiveBlueprint); + } + RecordingCommandKind::ClearActiveBlueprintAndEnableHeuristics => { + self.command_sender + .send_system(SystemCommand::ClearActiveBlueprintAndEnableHeuristics); + } + + RecordingCommandKind::ToggleTimePanel => { + app_blueprint.toggle_time_panel(&self.command_sender); + } + + RecordingCommandKind::ToggleChunkStoreBrowser => { + match self.state.navigation.current() { + Route::ChunkStoreBrowser { return_route, .. } => { + self.state.navigation.replace((**return_route).clone()); + } + + current => { + self.state.navigation.replace(Route::ChunkStoreBrowser { + store_id: current.recording_id().cloned(), + selected_chunk: None, + return_route: Box::new(current.clone()), + }); + } + } + } + + #[cfg(debug_assertions)] + RecordingCommandKind::ToggleBlueprintInspectionPanel => { + self.app_options_mut().inspect_blueprint_timeline ^= true; + } + + RecordingCommandKind::PlaybackTogglePlayPause + | RecordingCommandKind::PlaybackFollow + | RecordingCommandKind::PlaybackStepBack + | RecordingCommandKind::PlaybackStepForward + | RecordingCommandKind::PlaybackBack + | RecordingCommandKind::PlaybackForward + | RecordingCommandKind::PlaybackBackFast + | RecordingCommandKind::PlaybackForwardFast + | RecordingCommandKind::PlaybackBeginning + | RecordingCommandKind::PlaybackEnd + | RecordingCommandKind::PlaybackRestart + | RecordingCommandKind::PlaybackSpeed(_) => { + if let Some(time_command) = playback_time_command(kind) { + self.command_sender + .send_system(SystemCommand::TimeControlCommands { + store_id: recording_id, + time_commands: vec![time_command], + }); + } + } + + #[cfg(not(target_arch = "wasm32"))] + RecordingCommandKind::PrintChunkStore => { + if let Some(ctx) = store_context { + let text = format!("{}", ctx.recording.storage_engine().store()); + egui_ctx.copy_text(text.clone()); + println!("{text}"); + } + } + #[cfg(not(target_arch = "wasm32"))] + RecordingCommandKind::PrintBlueprintStore => { + if let Some(ctx) = store_context { + let text = format!("{}", ctx.blueprint.storage_engine().store()); + egui_ctx.copy_text(text.clone()); + println!("{text}"); + } + } + #[cfg(not(target_arch = "wasm32"))] + RecordingCommandKind::PrintPrimaryCache => { + if let Some(ctx) = store_context { + let text = format!("{:?}", ctx.recording.storage_engine().cache()); + egui_ctx.copy_text(text.clone()); + println!("{text}"); + } + } + } + } + + #[cfg(not(target_arch = "wasm32"))] + fn save_many_recordings(&mut self, stores: &[&EntityDb], folder: &std::path::Path) { + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + use itertools::Itertools as _; + use re_log::ResultExt as _; + use re_viewer_context::sanitize_file_name; + use tap::Pipe as _; + + re_tracing::profile_function!(); + + let num_stores = stores.len(); + let any_error = Arc::new(AtomicBool::new(false)); + let num_remaining = Arc::new(AtomicUsize::new(stores.len())); + + re_log::info!("Saving {num_stores} recordings to {}…", folder.display()); + + for store in stores { + let messages = store.to_messages(None).collect_vec(); + + let file_name = if let Some(rec_name) = store + .recording_info_property::( + re_sdk_types::archetypes::RecordingInfo::descriptor_name().component, + ) { + rec_name.to_string() + } else { + format!("{}-{}", store.application_id(), store.recording_id()) + } + .pipe(|name| sanitize_file_name(&name)) + .pipe(|stem| format!("{stem}.rrd")); + + let file_path = folder.join(file_name.clone()); + let any_error = any_error.clone(); + let num_remaining = num_remaining.clone(); + let folder = folder.display().to_string(); + + self.background_tasks + .spawn_threaded_promise(file_name, move || { + let res = crate::saving::encode_to_file( + re_build_info::CrateVersion::LOCAL, + &file_path, + messages.into_iter(), + ); + + if res.is_err() { + any_error.store(true, Ordering::Relaxed); + } + + let num_remaining = num_remaining.fetch_sub(1, Ordering::Relaxed) - 1; + + if num_remaining == 0 { + if any_error.load(Ordering::Relaxed) { + re_log::error!("Some recordings failed to save."); + } else { + re_log::info!("{num_stores} recordings successfully saved to {folder}"); + } + } + + res + }) + .ok_or_log_error_once(); + } + } + + fn run_copy_link_command(&mut self, content_url: &ViewerOpenUrl) { + let base_url = self.startup_options.web_viewer_base_url(); + + match content_url.sharable_url(base_url.as_ref()) { + Ok(url) => { + self.copy_text(url); + } + Err(err) => { + re_log::error!("{err}"); + } + } + } + + /// Copies text to the clipboard, and gives a notification about it. + fn copy_text(&mut self, url: String) { + self.notifications + .success(format!("Copied {url:?} to clipboard")); + self.egui_ctx.copy_text(url); + } + + fn copy_entity_hierarchy_to_clipboard( + &mut self, + egui_ctx: &egui::Context, + store_context: Option<&ActiveStoreContext<'_>>, + ) { + let Some(entity_db) = store_context.as_ref().map(|ctx| ctx.recording) else { + re_log::warn!("Could not copy entity hierarchy: No active recording"); + return; + }; + + use std::fmt::Write as _; + + let mut hierarchy_text = String::new(); + + // Add application ID and recording ID header + write!( + hierarchy_text, + "Application ID: {}\nRecording ID: {}\n\n", + entity_db.application_id(), + entity_db.recording_id() + ) + .ok(); + + hierarchy_text.push_str(&entity_db.format_with_components()); + + if hierarchy_text.is_empty() { + hierarchy_text = "(no entities)".to_owned(); + } + + egui_ctx.copy_text(hierarchy_text.clone()); + self.notifications + .success("Copied entity hierarchy with schema to clipboard".to_owned()); + } + + /// Reset the viewer to how it looked the first time you ran it. + fn reset_viewer(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) { + self.state = Default::default(); + + store_hub.clear_all_cloned_blueprints(); + + // Reset egui: + egui_ctx.memory_mut(|mem| *mem = Default::default()); + + // Restore style: + re_ui::apply_style_and_install_loaders(egui_ctx); + + if let Err(err) = crate::reset_viewer_persistence() { + re_log::warn!("Failed to reset viewer: {err}"); + } + } + + pub(crate) fn toggle_fullscreen(&self) { + #[cfg(not(target_arch = "wasm32"))] + { + let fullscreen = self + .egui_ctx + .input(|i| i.viewport().fullscreen.unwrap_or(false)); + self.egui_ctx + .send_viewport_cmd(egui::ViewportCommand::Fullscreen(!fullscreen)); + } + + #[cfg(target_arch = "wasm32")] + { + if let Some(options) = &self.startup_options.fullscreen_options { + // Tell JS to toggle fullscreen. + if let Err(err) = options.on_toggle.call0() { + re_log::error!("{}", crate::web_tools::string_from_js_value(err)); + } + } + } + } + + #[cfg(target_arch = "wasm32")] + pub(crate) fn is_fullscreen_allowed(&self) -> bool { + self.startup_options.fullscreen_options.is_some() + } + + #[cfg(target_arch = "wasm32")] + pub(crate) fn is_fullscreen_mode(&self) -> bool { + if let Some(options) = &self.startup_options.fullscreen_options { + // Ask JS if fullscreen is on or not. + match options.get_state.call0() { + Ok(v) => return v.is_truthy(), + Err(err) => re_log::error_once!("{}", crate::web_tools::string_from_js_value(err)), + } + } + + false + } + + /// Opens `url` through the normal navigation flow. + fn navigate_to(&self, egui_ctx: &egui::Context, url: &ViewerOpenUrl) { + url.clone().open( + egui_ctx, + &OpenUrlOptions { + recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, + show_loader: true, + }, + &self.command_sender, + ); + } + + /// Whether navigating back to `url` still leads to an open route. + /// + /// `closing` is the recording being closed, if any. + fn is_back_destination_open( + &self, + store_hub: &StoreHub, + url: &ViewerOpenUrl, + closing: Option<&StoreId>, + ) -> bool { + // A redap origin is reachable as long as its server is still registered. + let origin_reachable = |origin: &re_uri::Origin| { + origin == &*re_redap_browser::EXAMPLES_ORIGIN + || self.state.redap_servers.has_server(origin) + }; + + // Whether some still-open recording, other than the one we're closing, was loaded from + // `url`. + let recording_loaded_from = |url: &ViewerOpenUrl| { + store_hub.store_bundle().recordings().any(|db| { + closing.is_none_or(|closing| db.store_id() != closing) + && store_hub.is_opened(db.store_id()) + && db.data_source.as_ref().is_some_and(|source| { + ViewerOpenUrl::from_data_source(source).is_ok_and(|loaded| &loaded == url) + }) + }) + }; + + match url { + // Points into the recording we're closing, so there's nothing left to step back to. + ViewerOpenUrl::IntraRecordingSelection(_) => false, + + ViewerOpenUrl::WebViewerUrl { url_parameters, .. } => { + if url_parameters.len() == 1 { + self.is_back_destination_open(store_hub, url_parameters.first(), closing) + } else { + false + } + } + + // Not a place to land on after closing a recording. + ViewerOpenUrl::Settings => false, + + // Resolve to a recording loaded from that source: only step back while it's still open. + ViewerOpenUrl::HttpUrl(_) | ViewerOpenUrl::WebEventListener => { + recording_loaded_from(url) + } + #[cfg(not(target_arch = "wasm32"))] + ViewerOpenUrl::FilePath(_) => recording_loaded_from(url), + + // Resolves to a recording: it must still be loaded, and not be the one we're closing. + // We don't re-stream a closed recording, that would land us on a blank, unloaded one. + ViewerOpenUrl::RedapDatasetSegment(uri) => { + let store_id = uri.store_id(); + closing.is_none_or(|closing| &store_id != closing) && store_hub.is_opened(&store_id) + } + + // Redap destinations are reachable while their server is still registered. + ViewerOpenUrl::RedapProxy(uri) => origin_reachable(&uri.origin), + ViewerOpenUrl::RedapCatalog(uri) => origin_reachable(&uri.origin), + ViewerOpenUrl::RedapEntry(uri) => origin_reachable(&uri.origin), + ViewerOpenUrl::RedapFolder(uri) => origin_reachable(&uri.origin), + + // Tied to a specific store: it must exist, still be opened, and not be the one we're + // closing. Without an explicit store it falls back to the active recording, which is the + // one we're closing, so there's nowhere to step back to. + ViewerOpenUrl::ChunkStoreBrowser { recording_id, .. } => { + recording_id.as_ref().is_some_and(|id| { + closing.is_none_or(|closing| id != closing) && store_hub.is_opened(id) + }) + } + } + } + + /// Where to navigate after closing the active recording `closing`, when there's no usable + /// history entry to step back to. + /// + /// - If viewing a recording in a dataset, go to said dataset. + /// - Otherwise, if in a redap server, go to said redap server. + /// - Otherwise go to the start page. + fn fallback_route_after_close(store_hub: &StoreHub, closing: &StoreId) -> Route { + let redap_uri = store_hub + .entity_db(closing) + .and_then(|db| db.data_source.as_ref()) + .and_then(|source| source.redap_uri()); + + match redap_uri { + Some(re_uri::RedapUri::DatasetData(uri)) => Route::RedapEntry { + origin: uri.origin, + kind: re_viewer_context::RedapEntryKind::Entry(uri.dataset_id.into()), + }, + + Some(uri) if !matches!(uri, re_uri::RedapUri::Proxy(_)) => { + Route::RedapServer(uri.origin().clone()) + } + + _ => Route::welcome_page(), + } + } + + fn close_recording( + &self, + store_hub: &mut StoreHub, + entry: &RecordingOrTable, + mode: CloseRecording, + ) { + if let RecordingOrTable::Recording { store_id } = entry { + store_hub.set_opened(store_id, false); + + // A recording that's still rendered as a preview should stay loaded and streaming, just + // no longer in the recording list. Removing it would make the preview re-download it. + // `Force` overrides this and removes it regardless. + if mode != CloseRecording::Force && store_hub.was_preview(store_id) { + return; + } + } + + let data_source = match entry { + RecordingOrTable::Recording { store_id } => { + store_hub.entity_db_entry(store_id).data_source.clone() + } + RecordingOrTable::Table { .. } => None, + }; + if let Some(data_source) = data_source { + // Only certain sources should be closed. + #[expect(clippy::match_same_arms)] + let should_close = match &data_source { + // Specific files should stop streaming when closing them. + LogSource::File { .. } => true, + + // Specific HTTP streams should stop streaming when closing them. + LogSource::HttpStream { .. } => true, + + // Specific GRPC streams should stop streaming when closing them. + // TODO(#10967): We still stream in some data after that. + LogSource::RedapGrpcStream { .. } => true, + + // Don't close generic connections (like to an SDK) that may feed in different recordings over time. + LogSource::RrdWebEvent + | LogSource::JsChannel { .. } + | LogSource::Sdk + | LogSource::Stdin + | LogSource::MessageProxy(_) => false, + }; + + if should_close { + self.rx_log.retain(|r| r.source() != &data_source); + } + } + + store_hub.remove(entry); + } +} + +/// Propagates [`re_viewer_context::TimeControlResponse`] to [`ViewerEventDispatcher`]. +pub(super) fn handle_time_ctrl_event( + recording: &EntityDb, + events: Option<&ViewerEventDispatcher>, + response: &re_viewer_context::TimeControlResponse, +) { + let Some(events) = events else { + return; + }; + + if let Some(playing) = response.playing_change { + events.on_play_state_change(recording, playing); + } + + if let Some((timeline, time)) = response.timeline_change { + events.on_timeline_change(recording, timeline, time); + } + + if let Some(time) = response.time_change { + events.on_time_update(recording, time); + } +} + +/// [This may only be called on the main thread](https://docs.rs/rfd/latest/rfd/#macos-non-windowed-applications-async-and-threading). +#[cfg(not(target_arch = "wasm32"))] +fn open_file_dialog_native(_: crate::MainThreadToken) -> Vec { + re_tracing::profile_function!(); + + let supported: Vec<_> = if re_importer::iter_external_importers().len() == 0 { + re_importer::supported_extensions().collect() + } else { + vec![] + }; + + let mut dialog = rfd::FileDialog::new(); + + // If there's at least one external loader registered, then literally anything goes! + if !supported.is_empty() { + dialog = dialog.add_filter("Supported files", &supported); + } + + dialog.pick_files().unwrap_or_default() +} + +#[cfg(target_arch = "wasm32")] +async fn async_open_rrd_dialog() -> Vec { + let supported: Vec<_> = re_importer::supported_extensions().collect(); + + let files = rfd::AsyncFileDialog::new() + .add_filter("Supported files", &supported) + .pick_files() + .await + .unwrap_or_default(); + + let mut file_contents = Vec::with_capacity(files.len()); + + for file in files { + let file_name = file.file_name(); + re_log::debug!("Reading {file_name}…"); + let bytes = file.read().await; + re_log::debug!( + "{file_name} was {}", + re_format::format_bytes(bytes.len() as _) + ); + file_contents.push(re_data_source::FileContents { + path: std::path::PathBuf::from(file_name), + bytes: bytes.into(), + }); + } + + file_contents +} + +/// The time-control command a playback [`re_ui::RecordingCommandKind`] maps to. +/// +/// Returns `None` for non-playback kinds. +fn playback_time_command(kind: re_ui::RecordingCommandKind) -> Option { + use re_ui::RecordingCommandKind; + Some(match kind { + RecordingCommandKind::PlaybackTogglePlayPause => TimeControlCommand::TogglePlayPause, + RecordingCommandKind::PlaybackFollow => { + TimeControlCommand::SetPlayState(PlayState::Following) + } + RecordingCommandKind::PlaybackStepBack => TimeControlCommand::StepTimeBack, + RecordingCommandKind::PlaybackStepForward => TimeControlCommand::StepTimeForward, + RecordingCommandKind::PlaybackBack => TimeControlCommand::Move { + direction: MoveDirection::Back, + speed: MoveSpeed::Normal, + }, + RecordingCommandKind::PlaybackForward => TimeControlCommand::Move { + direction: MoveDirection::Forward, + speed: MoveSpeed::Normal, + }, + RecordingCommandKind::PlaybackBackFast => TimeControlCommand::Move { + direction: MoveDirection::Back, + speed: MoveSpeed::Fast, + }, + RecordingCommandKind::PlaybackForwardFast => TimeControlCommand::Move { + direction: MoveDirection::Forward, + speed: MoveSpeed::Fast, + }, + RecordingCommandKind::PlaybackBeginning => TimeControlCommand::MoveBeginning, + RecordingCommandKind::PlaybackEnd => TimeControlCommand::MoveEnd, + RecordingCommandKind::PlaybackRestart => TimeControlCommand::Restart, + RecordingCommandKind::PlaybackSpeed(speed) => TimeControlCommand::SetSpeed(speed.0.0), + _ => return None, + }) +} + +fn save_active_recording( + app: &mut App, + store_context: Option<&ActiveStoreContext<'_>>, +) -> anyhow::Result<()> { + let Some(store_context) = store_context else { + // NOTE: Can only happen if saving through the command palette. + anyhow::bail!("No recording data to save"); + }; + + save_recording(app, store_context.recording, store_context.loop_selection()) +} + +fn save_recording( + app: &mut App, + entity_db: &EntityDb, + loop_selection: Option<(TimelineName, re_log_types::AbsoluteTimeRangeF)>, +) -> anyhow::Result<()> { + let rrd_version = entity_db + .store_info() + .and_then(|info| info.store_version) + .unwrap_or(re_build_info::CrateVersion::LOCAL); + + let file_name = if let Some(recording_name) = entity_db + .recording_info_property::( + re_sdk_types::archetypes::RecordingInfo::descriptor_name().component, + ) { + format!("{}.rrd", sanitize_file_name(&recording_name)) + } else { + "data.rrd".to_owned() + }; + + let title = if loop_selection.is_some() { + "Save loop selection" + } else { + "Save recording" + }; + + save_entity_db( + app, + rrd_version, + file_name, + title.to_owned(), + entity_db.to_messages(loop_selection), + ) +} + +fn save_blueprint( + app: &mut App, + store_context: Option<&ActiveStoreContext<'_>>, +) -> anyhow::Result<()> { + let Some(store_context) = store_context else { + anyhow::bail!("No blueprint to save"); + }; + + re_tracing::profile_function!(); + + let rrd_version = store_context + .blueprint + .store_info() + .and_then(|info| info.store_version) + .unwrap_or(re_build_info::CrateVersion::LOCAL); + + // We change the recording id to a new random one, + // otherwise when saving and loading a blueprint file, we can end up + // in a situation where the store_id we're loading is the same as the currently active one, + // which mean they will merge in a strange way. + // This is also related to https://github.com/rerun-io/rerun/issues/5295 + let new_store_id = store_context + .blueprint + .store_id() + .clone() + .with_recording_id(RecordingId::random()); + + let mut saved_blueprint = store_context + .blueprint + .clone_with_new_id(new_store_id) + .context("Cloning current blueprint")?; + + if let Some(undo_state) = app + .state + .blueprint_undo_state + .get(store_context.blueprint.store_id()) + { + // We don't actually want to edit the undo state when saving, + // just clear the redo-buffer section of what we save. + undo_state.clone().clear_redo_buffer(&mut saved_blueprint); + } + + let messages = saved_blueprint.to_messages(None); + + let file_name = format!( + "{}.rbl", + crate::saving::sanitize_app_id(store_context.application_id()) + ); + let title = "Save blueprint"; + + save_entity_db(app, rrd_version, file_name, title.to_owned(), messages) +} + +// TODO(emilk): unify this with `ViewerContext::save_file_dialog` +#[allow(clippy::allow_attributes, clippy::needless_pass_by_ref_mut)] // `app` is only used on native +#[allow(clippy::unnecessary_wraps)] // cannot return error on web +fn save_entity_db( + #[allow(clippy::allow_attributes, unused_variables)] app: &mut App, // only used on native + rrd_version: CrateVersion, + file_name: String, + title: String, + messages: impl Iterator>, +) -> anyhow::Result<()> { + re_tracing::profile_function!(); + + // TODO(#6984): Ideally we wouldn't collect at all and just stream straight to the + // encoder from the store. + // + // From a memory usage perspective this isn't too bad though: the data within is still + // refcounted straight from the store in any case. + // + // It just sucks latency-wise. + let messages = messages.collect::>(); + + // Web + #[cfg(target_arch = "wasm32")] + { + wasm_bindgen_futures::spawn_local(async move { + if let Err(err) = + async_save_dialog(rrd_version, &file_name, &title, messages.into_iter()).await + { + re_log::error!("File saving failed: {err}"); + } + }); + } + + // Native + #[cfg(not(target_arch = "wasm32"))] + { + let path = { + re_tracing::profile_scope!("file_dialog"); + rfd::FileDialog::new() + .set_file_name(file_name) + .set_title(title) + .save_file() + }; + if let Some(path) = path { + app.background_tasks.spawn_file_saver(move || { + crate::saving::encode_to_file(rrd_version, &path, messages.into_iter())?; + Ok(path) + })?; + } + } + + Ok(()) +} + +#[cfg(target_arch = "wasm32")] +async fn async_save_dialog( + rrd_version: CrateVersion, + file_name: &str, + title: &str, + messages: impl Iterator>, +) -> anyhow::Result<()> { + use anyhow::Context as _; + + let file_handle = rfd::AsyncFileDialog::new() + .set_file_name(file_name) + .set_title(title) + .save_file() + .await; + + let Some(file_handle) = file_handle else { + return Ok(()); // aborted + }; + + let options = re_log_encoding::rrd::EncodingOptions::PROTOBUF_COMPRESSED; + let mut bytes = Vec::new(); + re_log_encoding::Encoder::encode_into(rrd_version, options, messages, &mut bytes)?; + file_handle.write(&bytes).await.context("Failed to save") +} diff --git a/crates/viewer/re_viewer/src/app/logic.rs b/crates/viewer/re_viewer/src/app/logic.rs new file mode 100644 index 000000000000..11aa9ff56be3 --- /dev/null +++ b/crates/viewer/re_viewer/src/app/logic.rs @@ -0,0 +1,1160 @@ +use std::str::FromStr as _; + +use ahash::HashMap; +use re_chunk::TimelineName; +use re_entity_db::LogSource; +use re_log_channel::{ + DataSourceMessage, DataSourceUiCommand, InspectError, RecordingOpenBehavior, + SaveScreenshotError, +}; +use re_log_types::{LogMsg, StoreId, StoreKind, TableMsg, TimeReal, TimeType}; +use re_protos::common::v1alpha1::TimeType as ProtoTimeType; +use re_protos::sdk_comms::v1alpha1::{ + GetViewerStateResponse, SetTimeCursorResponse, TimeCursor, ViewerRecording, ViewerTimeline, +}; +use re_sdk_types::external::uuid; +use re_viewer_context::{ + Item, Route, StoreHub, SystemCommand, SystemCommandSender as _, TableStore, TimeControlCommand, + open_url::{OpenUrlOptions, ViewerOpenUrl}, +}; + +use crate::app_blueprint::AppBlueprint; + +use super::App; + +impl App { + /// Called before each call to `ui`, but ALSO when the app is + /// hidden (occluded, minimized, …) if something has called `request_repaint`. + /// + /// We put things here that are unrelated to the UI, + /// and that we still want to happen if the application is hidden. + pub(super) fn logic_impl(&mut self, egui_ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Temporarily take the `StoreHub` out of the Viewer so it doesn't interfere with mutability + let mut store_hub = self + .store_hub + .take() + .expect("Failed to take store hub from the Viewer"); + + { + // Respect memory budget: + self.purge_memory_if_needed(&mut store_hub); // Call BEFORE `begin_frame_caches` + + if self.app_options().blueprint_gc { + store_hub.gc_blueprints(&self.state.blueprint_undo_state); + } + } + + { + // Download/ingest data: + self.receive_messages(&mut store_hub, egui_ctx); + self.receive_fetched_chunks(&mut store_hub); + self.prefetch_chunks(&mut store_hub); + } + + self.run_pending_system_commands(&mut store_hub, egui_ctx); + + { + // We also need to check for Ui commands, especially `UiCommand::Quit`. + + let route = self.state.navigation.current().clone(); + + // Cloned snapshot of the active recording's time control, so that + // handing out references to it doesn't keep `self` borrowed. Defaults to an + // empty time control on routes without a recording (where it's ignored anyway). + let active_time_ctrl = route + .recording_id() + .and_then(|id| self.state.time_controls.get(id).cloned()) + .unwrap_or_default(); + + let (storage_context, store_context) = + store_hub.read_context(&route, &active_time_ctrl); + + let blueprint = store_context.as_ref().map(|ctx| ctx.blueprint); + let blueprint_query = self.state.blueprint_query_for_viewer(blueprint); + + let app_blueprint = AppBlueprint::new( + blueprint, + &blueprint_query, + egui_ctx, + self.panel_state_overrides_active + .then_some(self.panel_state_overrides), + ); + + self.run_pending_ui_commands( + egui_ctx, + &app_blueprint, + &storage_context, + store_context.as_ref(), + &route, + ); + } + + self.state.cleanup(&store_hub); + + self.sync_native_window_theme(egui_ctx); + + // Return the `StoreHub` to the Viewer so we have it on the next frame + self.store_hub = Some(store_hub); + } + + /// Keep the OS window's appearance in sync with our egui theme. + /// + /// This affects the way the macOS traffic light buttons are painted. Without this, + /// they look wrong when the themes mismatch and the window isn't focused. + // TODO(emilk/egui#8299): Remove once the egui fix lands + fn sync_native_window_theme(&mut self, egui_ctx: &egui::Context) { + let window_theme = match egui_ctx.options(|o| o.theme_preference) { + egui::ThemePreference::System => egui::SystemTheme::SystemDefault, + egui::ThemePreference::Dark => egui::SystemTheme::Dark, + egui::ThemePreference::Light => egui::SystemTheme::Light, + }; + + if self.last_window_theme != Some(window_theme) { + self.last_window_theme = Some(window_theme); + egui_ctx.send_viewport_cmd(egui::ViewportCommand::SetTheme(window_theme)); + } + } + + fn receive_messages(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) { + re_tracing::profile_function!(); + + let start = web_time::Instant::now(); + + while let Some((channel_source, msg)) = self.rx_log.try_recv() { + re_log::trace!("Received a message from {channel_source:?}"); // Used by `test_ui_wakeup` test app! + + if let Some(re_uri::RedapUri::DatasetData(uri)) = channel_source.redap_uri() { + self.connection_registry.clear_uri_error(uri); + } + + let msg = match msg.payload { + re_log_channel::SmartMessagePayload::Msg(msg) => msg, + + re_log_channel::SmartMessagePayload::Flush { on_flush_done } => { + re_tracing::profile_scope!("on_flush_done"); + on_flush_done(); + continue; + } + + re_log_channel::SmartMessagePayload::Quit(err) => { + if let Some(err) = err { + re_log::warn!( + "Data source has left unexpectedly: {err}, source: {}", + msg.source + ); + if let Some(re_uri::RedapUri::DatasetData(uri)) = channel_source.redap_uri() + { + self.connection_registry.set_uri_error(uri, err.to_string()); + } + } else { + re_log::debug!("Data source {} has finished", msg.source); + if let LogSource::RedapGrpcStream { + table_blueprint: Some(table_blueprint), + .. + } = channel_source.as_ref() + && let Err(err) = store_hub.associate_table_blueprint( + table_blueprint.table_id.clone(), + &table_blueprint.blueprint_id, + ) + { + re_log::warn!("Failed to register table blueprint: {err}"); + } + } + continue; + } + }; + + // We centralize "new store" detection and `data_source` attachment here, so that the `on_new_store` + // side effects (like `set_opened(true)` for `OpenAndSelect`) fire regardless of which message type + // happens to come first. + let msg_store_id = match &msg { + DataSourceMessage::RrdManifest(store_id, _) + | DataSourceMessage::RrdManifestComplete(store_id) => Some(store_id.clone()), + DataSourceMessage::LogMsg(log_msg) => Some(log_msg.store_id().clone()), + DataSourceMessage::TableMsg(_) | DataSourceMessage::UiCommand(_) => None, + }; + + let maybe_new_store = msg_store_id + .as_ref() + .filter(|sid| !store_hub.store_bundle().contains(sid)); + + if let Some(sid) = &msg_store_id { + let entity_db = store_hub.entity_db_entry(sid); + if entity_db.data_source.is_none() { + entity_db.data_source = Some((*channel_source).clone()); + } + } + + match msg { + DataSourceMessage::RrdManifest(store_id, rrd_manifest) => { + let entity_db = store_hub.entity_db_entry(&store_id); + let store_events = entity_db.add_rrd_manifest_message(rrd_manifest); + + if let Some((entity_db, cache)) = + store_hub.entity_db_and_cache(&store_id, &self.view_class_registry) + { + cache.on_store_events(&store_events, entity_db); + } + } + + DataSourceMessage::RrdManifestComplete(store_id) => { + let entity_db = store_hub.entity_db_entry(&store_id); + entity_db.mark_rrd_manifest_complete(); + } + + DataSourceMessage::LogMsg(msg) => { + self.receive_log_msg(&msg, store_hub, egui_ctx, &channel_source); + } + + DataSourceMessage::TableMsg(table) => { + self.receive_table_msg(store_hub, egui_ctx, table); + } + + DataSourceMessage::UiCommand(ui_command) => { + self.receive_data_source_ui_command( + ui_command, + &channel_source, + store_hub, + egui_ctx, + ); + } + } + + // Handle any action that is triggered by a new store _after_ processing the message + // that caused it. + if let Some(sid) = &maybe_new_store { + self.on_new_store(egui_ctx, sid, &channel_source, store_hub); + } + + if start.elapsed() > web_time::Duration::from_millis(10) { + egui_ctx.request_repaint(); // make sure we keep receiving messages asap + break; // don't block the main thread for too long + } + } + + // Run pending system commands in case any of the messages resulted in additional commands. + // This avoid further frame delays on these commands. + self.run_pending_system_commands(store_hub, egui_ctx); + } + + /// There is logic duplicated between this and [`Self::prefetch_chunks`]. + /// Make sure they are kept in sync! + fn receive_log_msg( + &mut self, + msg: &LogMsg, + store_hub: &mut StoreHub, + egui_ctx: &egui::Context, + channel_source: &LogSource, + ) { + re_tracing::profile_function!(); + + let store_id = msg.store_id(); + + if store_hub.is_active_blueprint(store_id) { + // TODO(#5514): handle loading of active blueprints. + re_log::warn_once!( + "Loading a blueprint {store_id:?} that is active. See https://github.com/rerun-io/rerun/issues/5514 for details." + ); + } + + // NOTE: store materialization, `data_source` attachment, and the `on_new_store` + // dispatch are handled in `receive_messages` so that they also fire for stores first + // introduced by `RrdManifest` / `RrdManifestComplete` messages. + let entity_db = store_hub.entity_db_entry(store_id); + let was_empty = entity_db.num_physical_chunks() == 0; + let entity_db_add_result = entity_db.add_log_msg(msg); + + match entity_db_add_result { + Ok(store_events) => { + self.process_store_events_for_db(store_hub, store_id, &store_events); + } + + Err(err) => { + re_log::error_once!("Failed to add incoming msg: {err}"); + } + } + + // Need to reborrow as read-only since we passed store_hub as mutable earlier. + let entity_db = store_hub + .entity_db(store_id) + .expect("Just queried it mutable and that was fine."); + + // Note: some of the logic above is duplicated in `fn prefetch_chunks`. + // Make sure they are kept in sync! + + let is_empty = entity_db.num_physical_chunks() == 0; + if was_empty && !is_empty { + // Hack: we cannot go to a specific timeline or entity until we know about it. + // Now we _hopefully_ do. The `LogMsg` could also belong to the blueprint, so + // we need to check for that as well. + if let LogSource::RedapGrpcStream { uri, .. } = channel_source + && &uri.store_id() == store_id + { + self.go_to_dataset_data(uri.store_id(), uri.fragment.clone()); + } + } + + #[expect(clippy::match_same_arms)] + match &msg { + LogMsg::SetStoreInfo(_) => { + // Causes a new store typically. But that's handled below via `on_new_store`. + } + + LogMsg::ArrowMsg(_, _) => { + // Handled by `EntityDb::add`. + } + + LogMsg::BlueprintActivationCommand(cmd) => match store_id.kind() { + StoreKind::Recording => { + re_log::debug!( + "Unexpected `BlueprintActivationCommand` message for {store_id:?}" + ); + } + StoreKind::Blueprint => { + if let Some(info) = entity_db.store_info() { + re_log::trace!( + "Activating blueprint that was loaded from {channel_source}" + ); + let app_id = info.application_id().clone(); + if cmd.make_default { + store_hub + .set_default_blueprint_for_app(store_id) + .unwrap_or_else(|err| { + re_log::warn!("Failed to make blueprint default: {err}"); + }); + } + if cmd.make_active { + store_hub + .set_cloned_blueprint_active_for_app(store_id) + .unwrap_or_else(|err| { + re_log::warn!("Failed to make blueprint active: {err}"); + }); + + // Switch to this app, e.g. on drag-and-drop of a blueprint file + + if self.state.navigation.current().app_id() != Some(&app_id) { + // Switch to this app: + + store_hub.load_persisted_blueprints_for_app(&app_id); + if let Some(recording_id) = + store_hub.earliest_recording_for_app(&app_id) + { + store_hub.load_blueprint_and_caches( + &recording_id, + &self.view_class_registry, + ); + self.state + .selection_state + .set_selection(Item::StoreId(recording_id.clone())); + self.state + .navigation + .replace(Route::LocalRecording { recording_id }); + } else { + // TODO(RR-3713): show a blueprint for it anyway + re_log::debug_once!( + "Received BlueprintActivationCommand for app '{app_id}', but we have no recording for it" + ); + } + } + + // If the viewer is in the background, tell the user that it has received something new. + egui_ctx.send_viewport_cmd( + egui::ViewportCommand::RequestUserAttention( + egui::UserAttentionType::Informational, + ), + ); + } + } else { + re_log::warn!( + "Got ActivateStore message without first receiving a SetStoreInfo" + ); + } + } + }, + } + } + + fn process_store_events_for_db( + &self, + store_hub: &mut StoreHub, + store_id: &StoreId, + store_events: &[re_chunk_store::ChunkStoreEvent], + ) { + re_tracing::profile_function!(); + + // Keep all caches up to date, even if they're in the background. + // This ensures that when we switch to a different recording, the caches are already valid. + if let Some((entity_db, cache)) = + store_hub.entity_db_and_cache(store_id, &self.view_class_registry) + { + cache.on_store_events(store_events, entity_db); + } + + self.validate_loaded_events(store_events); + } + + fn receive_table_msg( + &self, + store_hub: &mut StoreHub, + egui_ctx: &egui::Context, + table: TableMsg, + ) { + re_tracing::profile_function!(); + + let TableMsg { id, data } = table; + + // TODO(grtlr): For now we don't append anything to existing stores and always replace. + // TODO(ab): When we actually append to existing table, we will have to clear the UI + // cache by calling `DataFusionTableWidget::clear_state`. + let store = TableStore::default(); + if let Err(err) = store.add_record_batch(data) { + re_log::error!("Failed to load table {id}: {err}"); + } else { + if store_hub.insert_table_store(id.clone(), store).is_some() { + re_log::debug!("Overwritten table store with id: `{id}`"); + } else { + re_log::debug!("Inserted table store with id: `{id}`"); + } + self.command_sender + .send_system(SystemCommand::set_selection( + re_viewer_context::Item::TableId(id), + )); + + // If the viewer is in the background, tell the user that it has received something new. + egui_ctx.send_viewport_cmd(egui::ViewportCommand::RequestUserAttention( + egui::UserAttentionType::Informational, + )); + } + } + + fn on_new_store( + &mut self, + egui_ctx: &egui::Context, + store_id: &StoreId, + channel_source: &LogSource, + store_hub: &mut StoreHub, + ) { + match channel_source.open_behavior() { + RecordingOpenBehavior::Background => { + // Background streams (previews) skip the blueprint download. + if store_id.kind() == StoreKind::Recording { + store_hub.set_blueprint_pending(store_id, true); + + // The user may have already opened the segment while this preview stream was + // still in flight. + if store_hub.is_opened(store_id) { + self.fetch_pending_blueprint(store_hub, store_id); + } + } + } + + RecordingOpenBehavior::Open => { + if store_id.kind() == StoreKind::Recording { + store_hub.set_opened(store_id, true); + } + } + + RecordingOpenBehavior::OpenAndSelect => { + // Set the recording-id after potentially creating the store in the hub. + // This ordering is important because the `StoreHub` internally + // updates the app-id when changing the recording. + match store_id.kind() { + StoreKind::Recording => { + re_log::trace!("Opening a new recording: '{store_id:?}'"); + self.make_store_active_and_highlight(store_hub, egui_ctx, store_id); + } + StoreKind::Blueprint => { + // We wait with activating blueprints until they are fully loaded, + // so that we don't run heuristics on half-loaded blueprints. + // Otherwise on a mixed connection (SDK sending both blueprint and recording) + // the blueprint won't be activated until the whole _recording_ has finished loading. + } + } + } + } + + let entity_db = store_hub.entity_db_entry(store_id); + let is_example = entity_db.store_class().is_example(); + + if cfg!(target_arch = "wasm32") && !self.startup_options.is_in_notebook && !is_example { + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + // Tell the user there is a faster native viewer they can use instead of the web viewer: + let notification = re_ui::notifications::Notification::new( + re_ui::notifications::NotificationLevel::Tip, "For better performance, try the native Rerun Viewer!").with_link( + re_ui::Link { + text: "Install…".into(), + url: "https://rerun.io/docs/overview/installing-rerun/viewer#installing-the-viewer".into(), + } + ) + .no_toast() + .permanent_dismiss_id(egui::Id::new("install_native_viewer_prompt")); + self.command_sender + .send_system(SystemCommand::ShowNotification(notification)); + }); + } + + if entity_db.store_kind() == StoreKind::Recording { + #[cfg(feature = "analytics")] + if let Some(analytics) = re_analytics::Analytics::global_or_init() + && let Some(event) = + crate::viewer_analytics::event::open_recording(&self.app_env, entity_db) + { + analytics.record(event); + } + + if let Some(event_dispatcher) = self.event_dispatcher.as_ref() { + event_dispatcher.on_recording_open(entity_db); + } + } + } + + fn receive_data_source_ui_command( + &mut self, + ui_command: DataSourceUiCommand, + channel_source: &LogSource, + store_hub: &StoreHub, + egui_ctx: &egui::Context, + ) { + re_tracing::profile_function!(); + match ui_command { + DataSourceUiCommand::SetUrlFragment { store_id, fragment } => { + match re_uri::Fragment::from_str(&fragment) { + Ok(fragment) => { + self.command_sender + .send_system(SystemCommand::SetUrlFragment { store_id, fragment }); + } + + Err(err) => { + re_log::warn!( + "Failed to parse fragment received from {channel_source:?}: {err}" + ); + } + } + } + + DataSourceUiCommand::SaveScreenshot { + file_path, + view_id, + on_done, + } => { + let view_id = if let Some(view_id) = view_id { + if let Ok(view_id) = uuid::Uuid::parse_str(&view_id) { + Some(view_id.into()) + } else { + re_log::error!( + "Failed to parse view id from {view_id:?}. Expected a UUID." + ); + if let Some(on_done) = on_done { + on_done + .unbounded_send(Err(SaveScreenshotError::InvalidViewId { view_id })) + .ok(); + } + return; + } + } else { + None + }; + + if let Some(on_done) = on_done { + self.pending_screenshot_notifiers + .insert(file_path.clone(), on_done); + } + + self.command_sender + .send_system(SystemCommand::SaveScreenshot { + target: re_viewer_context::ScreenshotTarget::SaveToPath(file_path), + view_id, + notify: false, + }); + } + + // Handle a `egui_inspection` request. + DataSourceUiCommand::Inspect { request, on_done } => { + serve_inspect_request(egui_ctx, &request, on_done); + } + + // Report current viewer state (re_viewer_mcp's `GetViewerState`). + DataSourceUiCommand::GetViewerState { on_done } => { + let state = self.collect_viewer_state(store_hub); + on_done.unbounded_send(state).ok(); + } + + // Open a URL in the viewer (re_viewer_mcp's `OpenUrl`). + DataSourceUiCommand::OpenUrl { url, on_done } => { + let result = ViewerOpenUrl::parse_with_options( + &url, + &re_data_source::FromUriOptions { + accept_extensionless_http: true, + }, + ); + match result { + Ok(open_url) => { + open_url.open(egui_ctx, &OpenUrlOptions::default(), &self.command_sender); + on_done.unbounded_send(Ok(())).ok(); + } + Err(err) => { + on_done + .unbounded_send(Err(format!("Failed to open URL {url:?}: {err}"))) + .ok(); + } + } + } + + // Move the time cursor of a recording (re_viewer_mcp's `SetTimeCursor`). + DataSourceUiCommand::SetTimeCursor { + store_id, + timeline, + time, + play, + on_done, + } => { + let result = self.apply_set_time_cursor( + store_hub, + store_id, + timeline.as_deref(), + time, + play, + egui_ctx, + ); + on_done.unbounded_send(result).ok(); + } + } + } + + /// Snapshot the current viewer state for `re_viewer_mcp`'s `GetViewerState`: + /// the active recording, the current page as a sharable URL, and every open recording's + /// timelines with their time ranges and current time cursor. + fn collect_viewer_state(&self, store_hub: &StoreHub) -> GetViewerStateResponse { + let active_id = self.state.active_recording_id().cloned(); + let route = self.state.navigation.current(); + + // Best-effort sharable URL for the current page; some routes (e.g. local tables) can't be + // turned into a URL, in which case we leave it empty. + let url = ViewerOpenUrl::from_route(store_hub, route) + .and_then(|open_url| open_url.sharable_url(None)) + .unwrap_or_default(); + + let recordings = store_hub + .store_bundle() + .recordings() + .map(|db| { + let store_id = db.store_id(); + let timelines = db + .timelines() + .values() + .map(|timeline| { + let name = timeline.name(); + let range = db.time_range_for(name); + ViewerTimeline { + timeline: Some((*name).into()), + time_type: ProtoTimeType::from(timeline.typ()) as i32, + time_range: range.map(Into::into), + } + }) + .collect(); + + let current_time = self + .state + .time_control(store_id) + .map(|time_ctrl| TimeCursor { + timeline: Some((*time_ctrl.timeline_name()).into()), + time_type: time_ctrl.time_type().map(|t| ProtoTimeType::from(t) as i32), + time: time_ctrl.time_int().map(|t| t.as_i64().into()), + }); + + ViewerRecording { + store_id: Some(store_id.clone().into()), + timelines, + current_time, + } + }) + .collect(); + + GetViewerStateResponse { + url, + active_store_id: active_id.map(Into::into), + recordings, + } + } + + /// Resolve and apply a time-cursor move for `re_viewer_mcp`'s `SetTimeCursor`. + /// + /// Returns what was applied, or an error string if the recording or timeline could not + /// be resolved. + fn apply_set_time_cursor( + &self, + store_hub: &StoreHub, + store_id: Option, + timeline: Option<&str>, + time: i64, + play: bool, + egui_ctx: &egui::Context, + ) -> Result { + use re_sdk_types::blueprint::components::PlayState; + + let store_id = store_id + .or_else(|| self.state.active_recording_id().cloned()) + .ok_or_else(|| "no active recording to set the time for".to_owned())?; + + let db = store_hub + .entity_db(&store_id) + .ok_or_else(|| format!("recording {} is not open", store_id.recording_id().as_str()))?; + + let timelines = db.timelines(); + if timelines.is_empty() { + return Err(format!( + "recording {} has no timelines yet", + store_id.recording_id().as_str() + )); + } + + // Resolve the target timeline: explicit, else the active one, else the first. + let timeline_name = if let Some(tl) = timeline { + let name = TimelineName::try_new(tl).map_err(|err| err.to_string())?; + if !timelines.contains_key(&name) { + let available: Vec<&str> = timelines.keys().map(|n| n.as_str()).collect(); + return Err(format!( + "recording {} has no timeline {tl:?}; available: {available:?}", + store_id.recording_id().as_str() + )); + } + name + } else { + let active = self + .state + .time_control(&store_id) + .map(|tc| *tc.timeline_name()); + match active { + Some(name) if timelines.contains_key(&name) => name, + _ => *timelines.keys().next().expect("non-empty checked above"), + } + }; + + let time_type = timelines + .get(&timeline_name) + .map_or(TimeType::Sequence, |t| t.typ()); + + let play_state = if play { + PlayState::Playing + } else { + PlayState::Paused + }; + + // The order of these commands matters. + let time_commands = vec![ + TimeControlCommand::SetActiveTimeline(timeline_name), + TimeControlCommand::SetPlayState(play_state), + TimeControlCommand::SetTime(TimeReal::from(time)), + ]; + + self.command_sender + .send_system(SystemCommand::TimeControlCommands { + store_id: store_id.clone(), + time_commands, + }); + egui_ctx.request_repaint(); + + Ok(SetTimeCursorResponse { + store_id: Some(store_id.into()), + timeline: Some(timeline_name.into()), + time_type: ProtoTimeType::from(time_type) as i32, + time: Some(time.into()), + }) + } + + /// Receive in-transit chunks (previously prefetched): + fn receive_fetched_chunks(&self, store_hub: &mut StoreHub) { + re_tracing::profile_function!(); + + let store_ids: Vec<_> = store_hub + .store_bundle() + .recordings() + .map(|db| db.store_id().clone()) + .collect(); + + for store_id in store_ids { + let db = store_hub.entity_db_entry(&store_id); + + if cfg!(debug_assertions) && db.can_fetch_chunks_from_redap() { + re_tracing::profile_scope!("debug-sanity-check"); + let storage_engine = db.storage_engine(); + let store = storage_engine.store(); + + #[expect(clippy::iter_over_hash_type)] // sanity checks don't care about order + for missing_chunk_id in store.tracked_chunk_ids().missing_virtual { + let roots = store.find_root_chunks(&missing_chunk_id); + re_log::debug_assert!(!roots.is_empty(), "Missing chunk has no roots"); + + let all_roots_are_fully_loaded = roots.iter().all(|root_id| { + let root_info = db.rrd_manifest_index().root_chunk_info(root_id); + if let Some(root_info) = root_info { + root_info.is_fully_loaded() + } else { + re_log::debug_warn_once!("Failed to find root chunk"); + false + } + }); + + if all_roots_are_fully_loaded { + re_log::warn_once!( + "A chunk was reported missing, but all its roots are marked as fully loaded." + ); + re_log::debug_once!( + "Missing: {missing_chunk_id}, roots: {roots:?}, Chunk lineage: {}", + store.format_lineage(&missing_chunk_id) + ); + } + } + } + + if db.can_fetch_chunks_from_redap() { + re_tracing::profile_scope!("recording"); + + let mut store_events = Vec::new(); + for chunk in db + .rrd_manifest_index_mut() + .chunk_requests_mut() + .receive_finished(self.egui_ctx.time()) + { + match db.add_chunk(&std::sync::Arc::new(chunk)) { + Ok(events) => { + store_events.extend(events); + } + Err(err) => { + re_log::warn_once!("add_chunk failed: {err}"); + } + } + } + + self.process_store_events_for_db(store_hub, &store_id, &store_events); + + // Need to reborrow since we pass `&mut store_hub` above. + let db = store_hub.entity_db_entry(&store_id); + + // Note: some of the logic above is duplicated in `fn receive_log_msg`. + // Make sure they are kept in sync! + + // We cancel right after resoliving (above), so that + // we give each fetch as much time as possible to finish. + db.rrd_manifest_index_mut() + .cancel_outdated_requests(self.egui_ctx.time()); + + if db.rrd_manifest_index_mut().chunk_requests().has_pending() { + self.egui_ctx.request_repaint(); // check back for more + } + } + } + } + + /// Makes the given store active and request user attention if Rerun in the background. + pub(super) fn make_store_active_and_highlight( + &mut self, + store_hub: &mut StoreHub, + egui_ctx: &egui::Context, + store_id: &StoreId, + ) { + if store_id.is_blueprint() { + re_log::warn!( + "Can't make a blueprint active: {store_id:?}. This is likely a bug in Rerun." + ); + return; + } + + store_hub.set_opened(store_id, true); + store_hub.load_blueprint_and_caches(store_id, &self.view_class_registry); + // If this recording was streamed as a preview, fetch the blueprint we skipped back then. + self.fetch_pending_blueprint(store_hub, store_id); + self.state.navigation.replace(Route::LocalRecording { + recording_id: store_id.clone(), + }); + + // Also select the new recording: + self.command_sender + .send_system(SystemCommand::set_selection( + re_viewer_context::Item::StoreId(store_id.clone()), + )); + + // If the viewer is in the background, tell the user that it has received something new. + egui_ctx.send_viewport_cmd(egui::ViewportCommand::RequestUserAttention( + egui::UserAttentionType::Informational, + )); + } + + /// After loading some data; check if the loaded data makes sense. + fn validate_loaded_events(&self, store_events: &[re_chunk_store::ChunkStoreEvent]) { + re_tracing::profile_function!(); + + for event in store_events { + let Some(chunk) = event.delta_chunk() else { + continue; + }; + + // For speed, we don't care about the order of the following log statements, so we silence this warning + for component_descr in chunk.components().component_descriptors() { + if let Some(archetype_name) = component_descr.archetype { + if let Some(archetype) = self.reflection.archetypes.get(&archetype_name) { + for &view_type in archetype.view_types { + if !cfg!(feature = "map_view") && view_type == "MapView" { + re_log::warn_once!( + "Found map-related archetype, but viewer was not compiled with the `map_view` feature." + ); + } + } + } else { + re_log::trace_once!("Unknown archetype: {archetype_name}"); + } + } + } + } + } + + pub(super) fn purge_memory_if_needed(&mut self, store_hub: &mut StoreHub) { + re_tracing::profile_function!(); + + use re_format::format_bytes; + use re_memory::MemoryUse; + + let limit = self.app_options().memory_limit; + let mut mem_use_before = MemoryUse::capture(); + + let default_limit = re_memory::MemoryLimit::default_for_current_platform(); + + // If we are at the default limit, which is derived from system memory, + // we actually do want to count external to OOM. + let external_mem = if limit.as_bytes() >= default_limit.as_bytes() + || default_limit.is_exceeded_by(&mem_use_before).is_some() + { + 0 + } else { + let external_mem = self.external_memory_users.total_external_memory(); + + if let Some(counted) = &mut mem_use_before.counted { + *counted -= external_mem; + } + + if let Some(resident) = &mut mem_use_before.resident { + *resident -= external_mem; + } + + external_mem + }; + + if let Some(minimum_fraction_to_purge) = limit.is_exceeded_by(&mem_use_before) { + re_log::info_once!("Reached memory limit of {limit}. Freeing up data…"); + + let fraction_to_purge = (minimum_fraction_to_purge + 0.2).clamp(0.25, 1.0); + + re_log::trace!("RAM limit: {limit}"); + if let Some(resident) = mem_use_before.resident { + re_log::trace!("Resident: {}", format_bytes(resident as _),); + } + if let Some(counted) = mem_use_before.counted { + re_log::trace!("Counted: {}", format_bytes(counted as _)); + } + if external_mem > 0 { + re_log::trace!("External: {}", format_bytes(external_mem as _)); + } + + re_tracing::profile_scope!("pruning"); + if let Some(counted) = mem_use_before.counted { + re_log::trace!( + "Attempting to purge {:.1}% of used RAM ({})…", + 100.0 * fraction_to_purge, + format_bytes(counted as f64 * fraction_to_purge as f64) + ); + } + + store_hub.purge_fraction_of_ram( + fraction_to_purge, + self.active_recording_id(), + &|store_id| self.state.time_cursor_for(store_id).map(|t| t.time_cursor), + ); + self.state.app_caches.purge_memory(); + + let mem_use_after = MemoryUse::capture(); + + let freed_memory = mem_use_before - mem_use_after; + + if let (Some(counted_before), Some(counted_diff)) = + (mem_use_before.counted, freed_memory.counted) + && 0 < counted_diff + { + re_log::debug!( + "GC result: -{} (-{:.1}%).", + format_bytes(counted_diff as _), + 100.0 * counted_diff as f32 / counted_before as f32 + ); + } + + // Cache app overhead = total memory use minus all recording chunk data. + // This captures fonts, UI state, indices, and other unevictable memory. + if let Some(current_mem_use) = mem_use_after.counted.or(mem_use_after.resident) { + let total_chunk_bytes: u64 = store_hub + .store_bundle() + .recordings() + .map(|r| r.byte_size_of_physical_chunks()) + .sum(); + self.cached_app_overhead_bytes = + Some(current_mem_use.saturating_sub(total_chunk_bytes)); + } + + self.dev_panel.note_memory_purge(); + } + } + + /// Prefetch chunks for the open recording (stream from server) + /// + /// There is logic duplicated between this and [`Self::receive_log_msg`]. + /// Make sure they are kept in sync! + fn prefetch_chunks(&self, store_hub: &mut StoreHub) { + re_tracing::profile_function!(); + + use crate::prefetch_chunks::{RecordingOpenKind, RecordingPrefetchInfo}; + use re_entity_db::ChunkPrefetchOptions; + + let active_recording_id = self.active_recording_id(); + + // Fixed overhead for the app (fonts, icons, caches, etc.) that we cannot purge. + // We also want some headroom for spikes. + const APP_OVERHEAD_BYTES: u64 = 300_000_000; + + // When we have a measured overhead we need less extra headroom. + // When we don't, use a larger fraction to be safe. + const FIXED_FRACTION_OVERHEAD: f32 = 0.10; + const FALLBACK_FIXED_FRACTION_OVERHEAD: f32 = 0.20; + + let overhead = self.cached_app_overhead_bytes.unwrap_or(APP_OVERHEAD_BYTES); + let fixed_fraction_overhead = if self.cached_app_overhead_bytes.is_some() { + FIXED_FRACTION_OVERHEAD + } else { + FALLBACK_FIXED_FRACTION_OVERHEAD + }; + + let memory_limit = self + .app_options() + .memory_limit + .saturating_sub(overhead) + .split(fixed_fraction_overhead) + .1; + + if memory_limit == re_memory::MemoryLimit::ZERO { + re_log::warn_once!("Very little memory budget left for prefetching recordings."); + } + + let mut recordings_info: HashMap = HashMap::default(); + + for recording in store_hub.store_bundle().recordings() { + if !recording.can_fetch_chunks_from_redap() { + // Clear tracked chunk ids. + recording.storage_engine().store().take_tracked_chunk_ids(); + + continue; + } + if recording.is_downloading_first_part_of_manifest() { + // We need at least ONE part of the manifest before prefetching chunks. + continue; + } + + let is_active = Some(recording.store_id()) == active_recording_id; + let usage = store_hub.usage(recording.store_id()); + + let open_kind = if is_active { + RecordingOpenKind::Active + } else if usage.was_preview() { + RecordingOpenKind::Preview + } else if usage.opened { + RecordingOpenKind::Inactive + } else { + continue; + }; + + let time_cursor = match open_kind { + RecordingOpenKind::Active => self.state.time_cursor_for(recording.store_id()), + RecordingOpenKind::Preview => { + let timelines = recording.timelines(); + let timeline = + re_chunk::Timeline::pick_best_timeline(timelines.values(), |t| { + recording.num_temporal_rows_on_timeline(t.name()) + }); + + Some(re_entity_db::PrefetchTimeCursor { + time_cursor: re_log_types::TimelinePoint { + name: *timeline.name(), + typ: timeline.typ(), + // TODO(RR-4257): Don't hack mid-point time + time: recording + .rrd_manifest_index() + .timeline_range(timeline.name()) + .map(|r| r.center()) + .unwrap_or(re_chunk::TimeInt::ZERO), + }, + speed_if_unpaused: 1.0, + loop_range: None, + }) + } + RecordingOpenKind::Inactive => None, + }; + if let Some(redap_uri) = recording.redap_uri() { + let store_id = recording.store_id().clone(); + recordings_info.insert( + store_id.clone(), + RecordingPrefetchInfo { + store_id, + open_kind, + time_cursor, + origin: redap_uri.origin.clone(), + }, + ); + } + } + + let total_bytes_in_memory = memory_limit.at_least(100_000_000).as_bytes(); + + crate::prefetch_chunks::prefetch_chunks_for_recordings( + &self.egui_ctx, + store_hub.store_bundle_mut(), + &recordings_info, + total_bytes_in_memory, + self.connection_registry(), + &ChunkPrefetchOptions { + max_fetch_stage: self.app_options().max_fetch_stage, + ..ChunkPrefetchOptions::default() + }, + ); + } +} + +/// Handle a `egui_inspection` request. +fn serve_inspect_request( + egui_ctx: &egui::Context, + request: &[u8], + on_done: futures::channel::mpsc::UnboundedSender, InspectError>>, +) { + use egui_inspection::{InspectionPlugin, Request, protocol}; + + let req: Request = match protocol::decode_body(request) { + Ok(req) => req, + Err(err) => { + on_done + .unbounded_send(Err(InspectError::DecodeRequest(err.to_string()))) + .ok(); + return; + } + }; + + if egui_ctx.plugin_opt::().is_none() { + egui_ctx.add_plugin(InspectionPlugin::new(Some("rerun viewer".to_owned()))); + } + + egui_ctx.with_plugin::(|plugin| { + plugin.submit(req, move |resp| { + let encoded = protocol::encode_body(&resp) + .map_err(|err| InspectError::EncodeResponse(err.to_string())); + on_done.unbounded_send(encoded).ok(); + }); + }); + + egui_ctx.request_repaint(); +} diff --git a/crates/viewer/re_viewer/src/app/mod.rs b/crates/viewer/re_viewer/src/app/mod.rs new file mode 100644 index 000000000000..5182182d0557 --- /dev/null +++ b/crates/viewer/re_viewer/src/app/mod.rs @@ -0,0 +1,1779 @@ +use std::sync::Arc; + +use egui::{FocusDirection, Key}; +use re_auth::credentials::CredentialsProvider as _; +use re_build_info::CrateVersion; +use re_byte_size::{MemUsageTree, MemUsageTreeCapture}; +use re_capabilities::MainThreadToken; +use re_data_source::{AuthErrorHandler, FileContents, LogDataSource}; +use re_entity_db::InstancePath; +use re_entity_db::entity_db::EntityDb; +use re_log_channel::{LogReceiverSet, RecordingOpenBehavior, SaveScreenshotError}; +use re_log_types::{ApplicationId, FileSource, RecordingId, StoreId}; +use re_redap_client::ConnectionRegistryHandle; +use re_sdk_types::blueprint::components::PlayState; +use re_ui::{ContextExt as _, UICommand, UICommandSender as _, notifications}; +use re_viewer_context::open_url::{OpenUrlOptions, ViewerOpenUrl}; +use re_viewer_context::store_hub::{BlueprintPersistence, StoreHub}; +use re_viewer_context::{ + AppBlueprintCtx, AppOptions, AsyncRuntimeHandle, AuthContext, CommandReceiver, CommandSender, + ComponentUiRegistry, EditRedapServerModalCommand, FallbackProviderRegistry, Item, NeedsRepaint, + Route, SystemCommand, SystemCommandSender as _, TimeControlCommand, ViewClass, + ViewClassRegistry, ViewClassRegistryError, command_channel, +}; + +use crate::app_blueprint::{AppBlueprint, PanelStateOverrides}; +use crate::background_tasks::BackgroundTasks; +use crate::event::ViewerEventDispatcher; +use crate::latency_tracker::ServerLatencyTrackers; +use crate::startup_options::StartupOptions; +use crate::{AppState, command_palette::CommandPaletteAction}; + +mod add_data_source; +mod command_handling; +mod logic; +mod ui; + +// ---------------------------------------------------------------------------- + +/// Storage key used to store the last run Rerun version. +/// +/// This is then used to detect if the user has recently upgraded Rerun. +const RERUN_VERSION_KEY: &str = "rerun.version"; + +const REDAP_TOKEN_KEY: &str = "rerun.redap_token"; + +/// The egui temp-data key under which the `on_begin_pass` hook stashes the timeline +/// keyboard shortcut it consumed this frame. +/// +/// The hook (which only has an [`egui::Context`]) consumes these keys early, but `App::ui` +/// pairs the stashed command with the *live* active recording and dispatches it — so it can +/// never target a stale recording. +fn pending_timeline_shortcut_key() -> egui::Id { + egui::Id::new("rerun_pending_timeline_shortcut") +} + +#[cfg(target_arch = "wasm32")] +struct PendingFilePromise { + recommended_store_id: Option, + force_store_info: bool, + promise: poll_promise::Promise>, +} + +/// The Rerun Viewer as an [`eframe`] application. +pub struct App { + #[allow(clippy::allow_attributes, dead_code)] // Unused on wasm32 + main_thread_token: MainThreadToken, + build_info: re_build_info::BuildInfo, + + app_env: crate::AppEnvironment, + + startup_options: StartupOptions, + start_time: web_time::Instant, + ram_limit_warner: re_memory::RamLimitWarner, + pub(crate) egui_ctx: egui::Context, + screenshotter: crate::screenshotter::Screenshotter, + texture_readback: crate::texture_readback::TextureReadbacks, + + /// Notifiers waiting for a file-path screenshot to finish writing. + pending_screenshot_notifiers: std::collections::HashMap< + camino::Utf8PathBuf, + futures::channel::mpsc::UnboundedSender>, + >, + + #[cfg(target_arch = "wasm32")] + pub(crate) popstate_listener: Option, + + #[cfg(not(target_arch = "wasm32"))] + profiler: re_tracing::Profiler, + + /// Active in-memory profile capture, if any. + #[cfg(not(target_arch = "wasm32"))] + profile_capture: Option, + + /// Listens to the local text log stream + text_log_rx: crossbeam::channel::Receiver, + + component_ui_registry: ComponentUiRegistry, + component_fallback_registry: FallbackProviderRegistry, + + rx_log: LogReceiverSet, + + #[cfg(target_arch = "wasm32")] + open_files_promise: Option, + + /// What is serialized + pub(crate) state: AppState, + + /// Pending background tasks, e.g. files being saved. + pub(crate) background_tasks: BackgroundTasks, + + /// Interface for all recordings and blueprints + pub(crate) store_hub: Option, + + /// Notification panel. + pub(crate) notifications: notifications::NotificationUi, + + dev_panel: crate::dev_panel::DevPanel, + dev_panel_open: bool, + pub(crate) external_memory_users: crate::external_memory::ExternalMemoryUsers, + + /// Cached app overhead: total memory use minus sum of all recording chunk sizes. + /// Updated during GC when we have a fresh memory snapshot. + cached_app_overhead_bytes: Option, + + egui_debug_panel_open: bool, + + /// Last time the latency was deemed interesting. + /// + /// Note that initializing with an "old" `Instant` won't work reliably cross platform + /// since `Instant`'s counter may start at program start. + pub(crate) latest_latency_interest: Option, + + /// Measures how long a frame takes to paint + pub(crate) frame_time_history: egui::util::History, + + /// The last theme we pushed to the OS window (via [`egui::ViewportCommand::SetTheme`]). + last_window_theme: Option, + + /// Commands to run at the end of the frame. + pub command_sender: CommandSender, + command_receiver: CommandReceiver, + cmd_palette: re_ui::CommandPalette, + + /// All known view types. + view_class_registry: ViewClassRegistry, + + pub(crate) panel_state_overrides_active: bool, + pub(crate) panel_state_overrides: PanelStateOverrides, + + reflection: re_types_core::reflection::Reflection, + + /// External interactions with the Viewer host (JS, custom egui app, notebook, etc.). + pub event_dispatcher: Option, + + connection_registry: ConnectionRegistryHandle, + + pub(crate) server_latency_trackers: ServerLatencyTrackers, + + /// The async runtime that should be used for all asynchronous operations. + /// + /// Using the global tokio runtime should be avoided since: + /// * we don't have a tokio runtime on web + /// * we want the user to have full control over the runtime, + /// and not expect that a global runtime exists. + async_runtime: AsyncRuntimeHandle, +} + +impl App { + pub fn new( + main_thread_token: MainThreadToken, + build_info: re_build_info::BuildInfo, + app_env: crate::AppEnvironment, + startup_options: StartupOptions, + creation_context: &eframe::CreationContext<'_>, + connection_registry: Option, + tokio_runtime: AsyncRuntimeHandle, + ) -> Self { + Self::with_commands( + main_thread_token, + build_info, + app_env, + startup_options, + creation_context, + connection_registry, + tokio_runtime, + crate::register_text_log_receiver(), + command_channel(), + ) + } + + /// Create a viewer that receives new log messages over time + pub fn with_commands( + main_thread_token: MainThreadToken, + build_info: re_build_info::BuildInfo, + app_env: crate::AppEnvironment, + startup_options: StartupOptions, + creation_context: &eframe::CreationContext<'_>, + connection_registry: Option, + tokio_runtime: AsyncRuntimeHandle, + text_log_rx: crossbeam::channel::Receiver, + command_channel: (CommandSender, CommandReceiver), + ) -> Self { + re_tracing::profile_function!(); + + let is_test = app_env.is_test(); + + let connection_registry_was_provided = connection_registry.is_some(); + let connection_registry = connection_registry + .unwrap_or_else(re_redap_client::ConnectionRegistry::new_with_stored_credentials); + + // Only subscribe to auth changes and load credentials if we're supposed to use stored credentials. + // This prevents tests from being affected by stored credentials on the developer's machine. + if connection_registry.should_use_stored_credentials() { + let command_sender = command_channel.0.clone(); + re_auth::credentials::subscribe_auth_changes(move |user| { + command_sender.send_system(SystemCommand::OnAuthChanged(user.map(|user| { + AuthContext { + email: user.email, + org_name: user.org_name, + } + }))); + }); + + // Call get_token once so the auth state is initialized. + tokio_runtime.spawn_future(async move { + re_auth::credentials::CliCredentialsProvider::new() + .get_token() + .await + .ok(); + }); + } + + if connection_registry.should_use_stored_credentials() + && let Some(storage) = creation_context.storage + && let Some(tokens) = eframe::get_value(storage, REDAP_TOKEN_KEY) + { + connection_registry.load_tokens(tokens); + } + + let mut state: AppState = if startup_options.persist_state { + creation_context.storage + .and_then(|storage| { + // This re-implements: `eframe::get_value` so we can customize the warning message. + // TODO(#2849): More thorough error-handling. + let value = storage.get_string(eframe::APP_KEY)?; + match ron::from_str(&value) { + Ok(value) => Some(value), + Err(err) => { + re_log::warn!("Failed to restore application state. This is expected if you have just upgraded Rerun versions."); + re_log::debug!("Failed to decode RON for app state: {err}"); + None + } + } + }) + .unwrap_or_default() + } else { + AppState::default() + }; + + if startup_options.persist_state { + // Check if the user has recently upgraded Rerun. + if let Some(storage) = creation_context.storage { + let current_version = build_info.version; + let previous_version: Option = + storage.get_string(RERUN_VERSION_KEY).and_then(|version| { + // `CrateVersion::try_parse` is `const` (for good reasons), and needs a `&'static str`. + // In order to accomplish this, we need to leak the string here. + let version = Box::leak(version.into_boxed_str()); + CrateVersion::try_parse(version).ok() + }); + + if previous_version + .is_none_or(|previous_version| previous_version < CrateVersion::new(0, 24, 0)) + { + re_log::debug!( + "Upgrading from {} to {}.", + previous_version.map_or_else(|| "".to_owned(), |v| v.to_string()), + current_version + ); + // We used to have Dark as the hard-coded theme preference. Let's change that! + creation_context + .egui_ctx + .options_mut(|o| o.theme_preference = egui::ThemePreference::System); + } + } + } + + if let Some(video_decoder_hw_acceleration) = startup_options.video_decoder_hw_acceleration { + state.app_options.video.hw_acceleration = video_decoder_hw_acceleration; + } + + if is_test { + creation_context.egui_ctx.mark_as_test(); + state.app_options = AppOptions::test(); + } + + let connection_registry = { + if (!connection_registry_was_provided || cfg!(target_arch = "wasm32")) + && connection_registry.internal_origin().is_none() + { + #[cfg(not(target_arch = "wasm32"))] + let catalog = crate::internal_catalog::build(std::net::SocketAddr::from(( + std::net::Ipv4Addr::LOCALHOST, + re_uri::DEFAULT_PROXY_PORT, + ))); + #[cfg(target_arch = "wasm32")] + let catalog = crate::internal_catalog::build(); + + connection_registry.with_internal((catalog.origin, catalog.connection)) + } else { + connection_registry + } + }; + + let reflection = re_sdk_types::reflection::generate_reflection().unwrap_or_else(|err| { + re_log::error!( + "Failed to create list of serialized default values for components: {err}" + ); + Default::default() + }); + + let mut component_fallback_registry = + re_component_fallbacks::create_component_fallback_registry(); + + let view_class_registry = crate::default_views::create_view_class_registry( + &reflection, + &state.app_options, + &mut component_fallback_registry, + ) + .unwrap_or_else(|err| { + re_log::error!("Failed to create view class registry: {err}"); + Default::default() + }); + + #[allow(clippy::allow_attributes, unused_mut, clippy::needless_update)] + // false positive on web + let mut screenshotter = crate::screenshotter::Screenshotter::default(); + + #[cfg(not(target_arch = "wasm32"))] + if let Some(screenshot_path) = startup_options.screenshot_to_path_then_quit.clone() { + screenshotter.screenshot_to_path_then_quit(&creation_context.egui_ctx, screenshot_path); + } + + let (command_sender, command_receiver) = command_channel; + + let mut component_ui_registry = re_component_ui::create_component_ui_registry(); + re_data_ui::register_component_uis(&mut component_ui_registry); + + let (_adapter_backend, _device_tier) = creation_context.wgpu_render_state.as_ref().map_or( + ( + wgpu::Backend::Noop, + re_renderer::device_caps::DeviceCapabilityTier::Limited, + ), + |render_state| { + let egui_renderer = render_state.renderer.read(); + let render_ctx = egui_renderer + .callback_resources + .get::(); + + ( + render_state.adapter.get_info().backend, + render_ctx.map_or( + re_renderer::device_caps::DeviceCapabilityTier::Limited, + |ctx| ctx.device_caps().tier, + ), + ) + }, + ); + + #[cfg(feature = "analytics")] + if let Some(analytics) = re_analytics::Analytics::global_or_init() { + use crate::viewer_analytics::event; + + analytics.record(event::identify( + analytics.config(), + build_info.clone(), + &app_env, + )); + analytics.record(event::viewer_started( + &app_env, + &creation_context.egui_ctx, + _adapter_backend, + _device_tier, + )); + } + + let panel_state_overrides = startup_options.panel_state_overrides; + + let event_dispatcher = startup_options + .on_event + .clone() + .map(ViewerEventDispatcher::new); + + if !state.redap_servers.is_empty() { + command_sender.send_ui(UICommand::ExpandBlueprintPanel); + } + + creation_context.egui_ctx.on_end_pass( + "remove copied text formatting", + Arc::new(|ctx| { + ctx.output_mut(|o| { + for command in &mut o.commands { + if let egui::output::OutputCommand::CopyText(text) = command { + *text = re_format::remove_number_formatting(text); + } + } + }); + }), + ); + + { + // This is a workaround consuming the space and arrow keys so we can use them as timeline shortcuts. + // Egui's built in behavior is to interact with focus, and we don't want that. + // TODO(emilk/egui#7899): allow consuming events before egui uses them to move keyboard focus. + // TODO(emilk/egui#7659): allow disabling certain egui shortcuts. + creation_context.egui_ctx.on_begin_pass( + "rerun-kb-shortcuts", + Arc::new(move |ctx| { + // egui has already listened for arrow keys before this point, + // so in order for the arrow keys to NOT move the focus, we need to + // undo that focus change here: + let reset_focus_direction = ctx.input_mut(|i| { + i.key_pressed(Key::ArrowLeft) || i.key_pressed(Key::ArrowRight) + }); + + if reset_focus_direction { + ctx.memory_mut(|mem| { + mem.move_focus(FocusDirection::None); + }); + } + + // Consume the timeline shortcuts (space/arrows/home/end) here, before egui + // uses them for focus/scroll. We only stash which command was pressed; it is + // paired with the live active recording and dispatched later, in `App::ui`, so + // it can never target a stale recording. + if let Some(kind) = re_ui::consume_timeline_shortcut(ctx) { + ctx.data_mut(|data| { + data.insert_temp(pending_timeline_shortcut_key(), kind); + }); + } + }), + ); + } + + Self { + main_thread_token, + build_info, + app_env, + startup_options, + start_time: web_time::Instant::now(), + ram_limit_warner: re_memory::RamLimitWarner::warn_at_fraction_of_max(0.75), + egui_ctx: creation_context.egui_ctx.clone(), + screenshotter, + texture_readback: Default::default(), + pending_screenshot_notifiers: Default::default(), + + #[cfg(target_arch = "wasm32")] + popstate_listener: None, + + #[cfg(not(target_arch = "wasm32"))] + profiler: Default::default(), + + #[cfg(not(target_arch = "wasm32"))] + profile_capture: None, + + text_log_rx, + component_ui_registry, + component_fallback_registry, + rx_log: Default::default(), + + #[cfg(target_arch = "wasm32")] + open_files_promise: Default::default(), + + state, + background_tasks: Default::default(), + store_hub: Some(StoreHub::new( + if is_test { + noop_blueprint_loader() + } else { + blueprint_loader() + }, + &crate::app_blueprint::setup_welcome_screen_blueprint, + )), + notifications: notifications::NotificationUi::new(creation_context.egui_ctx.clone()), + + dev_panel: Default::default(), + dev_panel_open: false, + external_memory_users: crate::external_memory::ExternalMemoryUsers::default_users(), + cached_app_overhead_bytes: None, + + egui_debug_panel_open: false, + + latest_latency_interest: None, + + frame_time_history: egui::util::History::new(1..100, 0.5), + last_window_theme: None, + + command_sender, + command_receiver, + cmd_palette: Default::default(), + + view_class_registry, + + panel_state_overrides_active: true, + panel_state_overrides, + + reflection, + + event_dispatcher, + + connection_registry, + server_latency_trackers: ServerLatencyTrackers::default(), + async_runtime: tokio_runtime, + } + } + + #[cfg(not(target_arch = "wasm32"))] + pub fn set_profiler(&mut self, profiler: re_tracing::Profiler) { + self.profiler = profiler; + } + + pub fn connection_registry(&self) -> &ConnectionRegistryHandle { + &self.connection_registry + } + + pub fn set_examples_manifest_url(&mut self, url: String) { + re_log::info!("Using manifest_url={url:?}"); + self.state.set_examples_manifest_url(&self.egui_ctx, url); + } + + pub fn build_info(&self) -> &re_build_info::BuildInfo { + &self.build_info + } + + pub fn startup_options(&self) -> &StartupOptions { + &self.startup_options + } + + pub fn app_options(&self) -> &AppOptions { + self.state.app_options() + } + + pub fn reflection(&self) -> &re_types_core::reflection::Reflection { + &self.reflection + } + + pub fn app_options_mut(&mut self) -> &mut AppOptions { + self.state.app_options_mut() + } + + pub fn app_env(&self) -> &crate::AppEnvironment { + &self.app_env + } + + /// Whether we are responsible for painting a window frame. + /// + /// Not enabled on Windows ever since there the OS puts some margin & frame around the window content either way. + pub(crate) fn custom_window_frame(&self) -> bool { + self.custom_window_decorations() && !cfg!(target_os = "windows") + } + + /// The active recording [`StoreId`], if any, derived from the current [`Route`]. + pub fn active_recording_id(&self) -> Option<&StoreId> { + self.state.active_recording_id() + } + + /// Select `item` and navigate the viewer to it (if it maps to a route). + fn select_and_navigate_to(&self, item: &Item) { + self.command_sender + .send_system(SystemCommand::set_selection(item.clone())); + if let Some(route) = Route::from_item(item) { + self.command_sender + .send_system(SystemCommand::SetRoute(route)); + } + } + + /// Open a content URL in the viewer. + pub fn open_url_or_file(&self, url: &str) { + match ViewerOpenUrl::parse_with_options( + url, + &re_data_source::FromUriOptions { + accept_extensionless_http: true, + }, + ) { + Ok(url) => { + url.open( + &self.egui_ctx, + &OpenUrlOptions { + recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, + show_loader: true, + }, + &self.command_sender, + ); + } + Err(err) => { + if err.to_string().contains(url) { + re_log::error!("{err}"); + } else { + re_log::error!(?url, "Failed to open URL: {err}"); + } + } + } + } + + pub fn is_screenshotting(&self) -> bool { + self.screenshotter.is_screenshotting() + } + + /// Update the active [`re_viewer_context::TimeControl`]. And if the blueprint inspection + /// panel is open, also open that time control. + fn move_time(&mut self) { + let stable_dt = self.egui_ctx.input(|i| i.stable_dt); + + let Some(store_hub) = &self.store_hub else { + return; + }; + + if let Some(store_id) = self.active_recording_id() + && let Some(blueprint) = store_hub.active_blueprint_for_app(store_id.application_id()) + { + let default_blueprint = store_hub.default_blueprint_for_app(store_id.application_id()); + + let blueprint_query = self + .state + .get_blueprint_query_for_viewer(blueprint) + .unwrap_or_else(|| { + re_chunk::LatestAtQuery::latest(re_viewer_context::blueprint_timeline()) + }); + + let bp_ctx = AppBlueprintCtx { + command_sender: &self.command_sender, + current_blueprint: blueprint, + default_blueprint, + blueprint_query, + }; + + if let Some(recording) = store_hub.entity_db(store_id) { + // Are we still connected to the data source for the current store? + let more_data_is_streaming_in = + recording.data_source.as_ref().is_some_and(|store_source| { + self.rx_log + .sources() + .iter() + .any(|s| s.as_ref() == store_source) + }); + + let time_ctrl = self.state.time_control_mut(recording, &bp_ctx); + + // The state diffs are used to trigger callbacks if they are configured. + // If there's no active recording, we should not trigger any callbacks, but since there's an active recording here, + // we want to diff state changes. + let response = time_ctrl.update( + recording, + &re_viewer_context::TimeControlUpdateParams { + stable_dt, + more_data_is_streaming_in, + is_buffering: recording.is_buffering(), + should_diff_state: true, + }, + Some(&bp_ctx), + ); + + if response.needs_repaint == NeedsRepaint::Yes { + self.egui_ctx.request_repaint(); + } + + command_handling::handle_time_ctrl_event( + recording, + self.event_dispatcher.as_ref(), + &response, + ); + } + + if self.app_options().inspect_blueprint_timeline { + // We ignore most things from the time control response for the blueprint but still + // need to repaint if requested. + let re_viewer_context::TimeControlResponse { + needs_repaint, + playing_change: _, + timeline_change: _, + time_change: _, + } = self.state.blueprint_time_control.update( + blueprint, + &re_viewer_context::TimeControlUpdateParams { + stable_dt, + more_data_is_streaming_in: true, + is_buffering: false, + should_diff_state: false, + }, + None::<&AppBlueprintCtx<'_>>, + ); + + if needs_repaint == NeedsRepaint::Yes { + self.egui_ctx.request_repaint(); + } + + let undo_state = self + .state + .blueprint_undo_state + .entry(blueprint.store_id().clone()) + .or_default(); + // Apply changes to the blueprint time to the undo-state: + if self.state.blueprint_time_control.play_state() == PlayState::Following { + undo_state.redo_all(); + } else if let Some(time) = self.state.blueprint_time_control.time_int() { + undo_state.set_redo_time(time); + } + } + } + + // Tick time controls for preview recordings shown in grid or table cards. + // Runs even when there's no active recording. + if self + .state + .update_preview_time_controls(store_hub, stable_dt) + == re_viewer_context::NeedsRepaint::Yes + { + self.egui_ctx.request_repaint(); + } + } + + pub fn msg_receive_set(&self) -> &LogReceiverSet { + &self.rx_log + } + + /// The registry of component UIs used by the viewer. + pub fn component_ui_registry_mut(&mut self) -> &mut ComponentUiRegistry { + &mut self.component_ui_registry + } + + /// Registers runtime reflection metadata for a custom archetype. + pub fn add_archetype_reflection( + &mut self, + archetype_name: re_sdk_types::ArchetypeName, + archetype_reflection: re_sdk_types::reflection::ArchetypeReflection, + ) { + for field in &archetype_reflection.fields { + let descriptor = field.component_descriptor(archetype_name); + self.reflection + .component_identifiers + .insert(descriptor.component, descriptor); + } + + self.reflection + .archetypes + .insert(archetype_name, archetype_reflection); + } + + /// Adds a new view class to the viewer. + pub fn add_view_class( + &mut self, + ) -> Result<(), ViewClassRegistryError> { + self.view_class_registry.add_class::( + &self.reflection, + &self.state.app_options, + &mut self.component_fallback_registry, + ) + } + + /// Extends an already registered view class with additional systems (visualizers, context systems, fallbacks, etc.). + /// + /// **WARNING:** Many parts of the viewer assume that all views & visualizers are registered before the first frame is rendered. + /// Doing so later in the application life cycle may cause unexpected behavior. + pub fn extend_view_class( + &mut self, + view_class: re_sdk_types::ViewClassIdentifier, + register_fn: impl FnOnce( + &mut re_viewer_context::ViewSystemRegistrator<'_>, + ) -> Result<(), ViewClassRegistryError>, + ) -> Result<(), ViewClassRegistryError> { + self.view_class_registry.extend_class( + view_class, + &self.reflection, + &self.state.app_options, + &mut self.component_fallback_registry, + register_fn, + ) + } + + /// If we're on web and use web history this updates the + /// web address bar and updates history. + /// + /// Otherwise this updates the viewer tracked history. + fn update_history(&mut self, store_hub: &StoreHub) { + if self.startup_options().web_history_enabled() { + // We don't want to spam the web history API with changes, because + // otherwise it will start complaining about it being an insecure + // operation. + // + // This is a kind of hacky way to fix that: If there are currently any + // inputs, don't update the web address bar. This works for most cases + // because you need to hold down pointer to aggressively scrub, need to + // hold down key inputs to quickly step through the timeline. + #[cfg(target_arch = "wasm32")] + if !self.egui_ctx.egui_is_using_pointer() + && self + .egui_ctx + .input(|input| !input.any_touches() && input.keys_down.is_empty()) + { + self.update_web_history(store_hub); + } + } else { + self.update_viewer_history(store_hub); + } + } + + /// Updates the viewer tracked history + fn update_viewer_history(&mut self, store_hub: &StoreHub) { + let route = self.state.navigation.current(); + let time_ctrl = route + .recording_id() + .and_then(|id| self.state.time_control(id)); + + let selection = self.state.selection_state.selected_items(); + + let Ok(url) = ViewerOpenUrl::from_context_expanded(store_hub, route, time_ctrl, selection) + else { + return; + }; + + self.state.history.update_current_url(url); + } + + /// Updates the web address and web history. + #[cfg(target_arch = "wasm32")] + fn update_web_history(&self, store_hub: &StoreHub) { + let route = self.state.navigation.current(); + let time_ctrl = route + .recording_id() + .and_then(|id| self.state.time_control(id)); + let selection = self.state.selection_state.selected_items(); + + let Ok(url) = ViewerOpenUrl::from_context_expanded(store_hub, route, time_ctrl, selection) + .map(|mut url| { + // We don't want to update the url while playing, so we use the last paused time. + if let Some(fragment) = url.fragment_mut() { + fragment.when = time_ctrl.and_then(|time_ctrl| { + Some(( + *time_ctrl.timeline_name(), + re_log_types::TimeCell { + typ: time_ctrl.time_type()?, + value: time_ctrl.last_paused_time()?.floor().into(), + }, + )) + }); + } + + url + }) + // History entries expect the url parameter, not the full url, therefore don't pass a base url. + .and_then(|url| url.sharable_url(None)) + else { + return; + }; + + re_log::trace!("Updating navigation bar"); + + use crate::web_history::{HistoryEntry, HistoryExt as _, history}; + use crate::web_tools::JsResultExt as _; + + /// Returns the url without the fragment + fn strip_fragment(url: &str) -> &str { + // Split by url code for '#', which is used for fragments. + url.rsplit_once("%23").map_or(url, |(url, _)| url) + } + + if let Some(history) = history().ok_or_log_js_error() { + let current_entry = history.current_entry().ok_or_log_js_error().flatten(); + let new_entry = HistoryEntry::new(url); + if Some(&new_entry) != current_entry.as_ref() { + // If only the fragment has changed, we replace history instead of pushing it. + if current_entry + .and_then(|entry| { + Some(( + entry.to_query_string().ok_or_log_js_error()?, + new_entry.to_query_string().ok_or_log_js_error()?, + )) + }) + .is_some_and(|(current, new)| strip_fragment(¤t) == strip_fragment(&new)) + { + history.replace_entry(new_entry).ok_or_log_js_error(); + } else { + history.push_entry(new_entry).ok_or_log_js_error(); + } + } + } + } + + pub fn auth_error_handler(sender: CommandSender) -> AuthErrorHandler { + Arc::new(move |url, _err| { + sender.send_system(SystemCommand::EditRedapServerModal( + EditRedapServerModalCommand { + origin: url.origin.clone(), + open_on_success: Some(url.to_string()), + title: Some("Authenticate to see this recording".to_owned()), + }, + )); + }) + } + + /// Applies a fragment. + /// + /// Does *not* switch the active recording. + fn go_to_dataset_data(&self, store_id: StoreId, fragment: re_uri::Fragment) { + let time_commands = TimeControlCommand::from_url_fragment(&fragment); + + if let Some(selection) = fragment.selection { + let re_log_types::DataPath { + entity_path, + instance, + component, + } = selection; + + let item = if let Some(component) = component { + Item::from(re_log_types::ComponentPath::new(entity_path, component)) + } else if let Some(instance) = instance { + Item::from(InstancePath::instance(entity_path, instance)) + } else { + Item::from(entity_path) + }; + + self.command_sender + .send_system(SystemCommand::set_selection(item)); + } + + if !time_commands.is_empty() { + self.command_sender + .send_system(SystemCommand::TimeControlCommands { + store_id, + time_commands, + }); + } + } + + pub fn recording_db(&self) -> Option<&EntityDb> { + let store_hub = self.store_hub.as_ref()?; + let recording_id = self.active_recording_id()?; + store_hub.entity_db(recording_id) + } + + /// Returns a [`re_chunk_store::LatestAtQuery`] for the active recording's current timeline + /// position, suitable for querying frame data from [`Self::recording_db`]. + pub fn current_query(&self) -> Option { + let store_id = self.active_recording_id()?; + self.state + .time_controls + .get(store_id) + .map(|tc| tc.current_query()) + } + + // NOTE: Relying on `self` is dangerous, as this is called during a time where some internal + // fields may have been temporarily `take()`n out. Keep this a static method. + fn handle_dropping_files( + egui_ctx: &egui::Context, + command_sender: &CommandSender, + route: &Route, + ) { + #![allow(clippy::allow_attributes, clippy::needless_continue)] // false positive, depending on target_arch + + ui::preview_files_being_dropped(egui_ctx); + + let dropped_files = egui_ctx.input_mut(|i| std::mem::take(&mut i.raw.dropped_files)); + + if dropped_files.is_empty() { + return; + } + + egui_ctx.request_repaint(); + + let mut force_store_info = false; + + for file in dropped_files { + // egui gives an optional filesystem `path` plus a display `name` (only `name` is set on + // web); reconcile them once so the fallback isn't reproduced at each use below. + let file_path = file + .path + .clone() + .unwrap_or_else(|| std::path::PathBuf::from(&file.name)); + + let active_store_id = route + .recording_id() + .cloned() + // Don't redirect data to the welcome screen. + .filter(|store_id| store_id.application_id() != StoreHub::welcome_screen_app_id()) + .unwrap_or_else(|| { + // When we're on the welcome screen, there is no recording ID to recommend. + // But we want one, otherwise multiple things being dropped simultaneously on the + // welcome screen would end up in different recordings! + + // If we don't have any application ID to recommend (which means we are on the welcome screen), + // then we use the file path as the application ID or the file name if there is no path (on web builds). + let application_id = ApplicationId::from(file_path.display().to_string()); + + // NOTE: We don't override blueprints' store IDs anyhow, so it is sound to assume that + // this can only be a recording. + let recording_id = RecordingId::random(); + + // We're creating a recording just-in-time, directly from the viewer. + // We need those store infos or the data will just be silently ignored. + force_store_info = true; + + StoreId::recording(application_id, recording_id) + }); + + if let Some(bytes) = file.bytes { + // This is what we get on Web. + command_sender.send_system(SystemCommand::LoadDataSource( + LogDataSource::FileContents( + FileSource::DragAndDrop { + recommended_store_id: Some(active_store_id.clone()), + force_store_info, + }, + FileContents { + path: file_path, + bytes: bytes.clone(), + }, + ), + )); + + continue; + } + + #[cfg(not(target_arch = "wasm32"))] + if let Some(path) = file.path { + command_sender.send_system(SystemCommand::LoadDataSource( + LogDataSource::FilePath { + file_source: FileSource::DragAndDrop { + recommended_store_id: Some(active_store_id.clone()), + force_store_info, + }, + path, + }, + )); + } + } + } + + #[allow(clippy::allow_attributes, clippy::needless_pass_by_ref_mut)] // False positive on wasm + fn process_screenshot_result( + &mut self, + image: &Arc, + user_data: &egui::UserData, + ) { + use re_viewer_context::ScreenshotInfo; + + if let Some(info) = user_data + .data + .as_ref() + .and_then(|data| data.downcast_ref::()) + { + let ScreenshotInfo { + ui_rect, + pixels_per_point, + name, + target, + notify, + } = (*info).clone(); + + // Only used in the native `SaveToPath` branch below. + #[cfg(target_arch = "wasm32")] + let _ = notify; + + let rgba = if let Some(ui_rect) = ui_rect { + Arc::new(image.region(&ui_rect, Some(pixels_per_point))) + } else { + image.clone() + }; + + match target { + re_viewer_context::ScreenshotTarget::CopyToClipboard => { + self.egui_ctx.copy_image((*rgba).clone()); + } + + re_viewer_context::ScreenshotTarget::SaveToPathFromFileDialog => { + use image::ImageEncoder as _; + let mut png_bytes: Vec = Vec::new(); + if let Err(err) = image::codecs::png::PngEncoder::new(&mut png_bytes) + .write_image( + rgba.as_raw(), + rgba.width() as u32, + rgba.height() as u32, + image::ExtendedColorType::Rgba8, + ) + { + re_log::error!("Failed to encode screenshot as PNG: {err}"); + } else { + let file_name = format!("{name}.png"); + self.command_sender.save_file_dialog( + self.main_thread_token, + &file_name, + "Save screenshot".to_owned(), + png_bytes, + ); + } + } + + re_viewer_context::ScreenshotTarget::SaveToPath(file_path) => { + #[cfg(not(target_arch = "wasm32"))] + { + let rgba = rgba.clone(); + let notifier = self.pending_screenshot_notifiers.remove(&file_path); + let Some(rgba_image) = image::RgbaImage::from_vec( + rgba.width() as _, + rgba.height() as _, + bytemuck::pod_collect_to_vec(&rgba.pixels), + ) else { + re_log::error!("Failed to create image from screenshot data"); + if let Some(notifier) = notifier { + notifier + .unbounded_send(Err(SaveScreenshotError::InvalidImageData)) + .ok(); + } + return; + }; + + // Convert to RGB8 so it works with JPG and other formats that don't support alpha. + // (There's nothing interesting in the alpha channel anyways.) + let rgb_image = image::DynamicImage::ImageRgba8(rgba_image).to_rgb8(); + + let result = match rgb_image.save(&file_path) { + Ok(()) => { + // Only show a user-facing toast for user-initiated screenshots. + if notify { + re_log::info!("Saved screenshot to {file_path:?}"); + } else { + re_log::debug!("Saved screenshot to {file_path:?}"); + } + Ok(()) + } + Err(err) => { + re_log::error!(?file_path, "Failed to save screenshot: {err}"); + // Image library has the bad habit of creating the file even when it fails e.g. due to unsupported format. Remove it again. + std::fs::remove_file(&file_path).ok(); + Err(SaveScreenshotError::SaveToPathFailed { + path: file_path.to_string(), + reason: err.to_string(), + }) + } + }; + + if let Some(notifier) = notifier { + notifier.unbounded_send(result).ok(); + } + } + #[cfg(target_arch = "wasm32")] + { + re_log::error!( + "Saving screenshots to a path is not supported on web. Attempted to save to: {file_path:?}" + ); + } + } + } + } else { + #[cfg(not(target_arch = "wasm32"))] // no full-app screenshotting on web + if user_data + .data + .as_ref() + .is_some_and(|data| data.is::()) + { + self.screenshotter.save(&self.egui_ctx, image); + } + // Ignore any other screenshot requests + } + } +} + +impl eframe::App for App { + fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] { + if self.custom_window_decorations() { + [0.; 4] // transparent + } else if visuals.dark_mode { + [0., 0., 0., 1.] + } else { + [1., 1., 1., 1.] + } + } + + fn save(&mut self, storage: &mut dyn eframe::Storage) { + if !self.startup_options.persist_state { + return; + } + + re_tracing::profile_function!(); + + storage.set_string(RERUN_VERSION_KEY, self.build_info.version.to_string()); + + // Save the app state + eframe::set_value(storage, eframe::APP_KEY, &self.state); + eframe::set_value( + storage, + REDAP_TOKEN_KEY, + &self.connection_registry.dump_tokens(), + ); + + // Save the blueprints + // TODO(#2579): implement web-storage for blueprints as well + if let Some(hub) = &mut self.store_hub { + if self.state.app_options.blueprint_gc { + hub.gc_blueprints(&self.state.blueprint_undo_state); + } + + if let Err(err) = hub.save_app_blueprints() { + re_log::error!("Saving blueprints failed: {err}"); + } + } else { + re_log::error!("Could not save blueprints: the store hub is not available"); + } + } + + fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + self.logic_impl(ctx, frame); + } + + /// Called when application need to be repainted + fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { + #[cfg(all(not(target_arch = "wasm32"), feature = "perf_telemetry_tracy"))] + re_perf_telemetry::external::tracing_tracy::client::frame_mark(); + + #[cfg(not(target_arch = "wasm32"))] + if let Some(capture) = &self.profile_capture { + if capture.is_done() { + if let Some(capture) = self.profile_capture.take() + && let Err(err) = save_profile_trace(&capture.finish()) + { + re_log::error!("Failed to save profile trace: {err}"); + } + } else { + ui.ctx().request_repaint(); + } + } + + if let Some(seconds) = frame.info().cpu_usage { + self.frame_time_history.add(ui.input(|i| i.time), seconds); + } + + // NOTE: Memory stats can be very costly to compute, so only do so if the dev panel is opened. + let mem_usage_tree = self + .dev_panel_open + .then(|| re_byte_size::NamedMemUsageTree::new("App", self.capture_mem_usage_tree())); + + self.external_memory_users.update(); + + #[cfg(target_arch = "wasm32")] + if self.startup_options.enable_history { + // Handle pressing the back/forward mouse buttons explicitly, since eframe catches those. + let back_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Extra1)); + let fwd_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Extra2)); + + if back_pressed { + crate::web_history::go_back(); + } + if fwd_pressed { + crate::web_history::go_forward(); + } + } + + self.server_latency_trackers + .update(&self.connection_registry); + + // We move the time at the very start of the frame, + // so that we always show the latest data when we're in "follow" mode. + self.move_time(); + + // Temporarily take the `StoreHub` out of the Viewer so it doesn't interfere with mutability + let mut store_hub = self + .store_hub + .take() + .expect("Failed to take store hub from the Viewer"); + + // Update data source order so it's based on opening order. + store_hub.update_data_source_order(&self.rx_log.sources()); + + #[cfg(not(target_arch = "wasm32"))] + if let Some(resolution_in_points) = self.startup_options.resolution_in_points.take() { + ui.send_viewport_cmd(egui::ViewportCommand::InnerSize( + resolution_in_points.into(), + )); + } + + #[cfg(not(target_arch = "wasm32"))] + if self.screenshotter.update(ui).quit { + ui.send_viewport_cmd(egui::ViewportCommand::Close); + return; + } + + if self.app_options().memory_limit.is_unlimited() { + // we only warn about high memory usage if the user hasn't specified a limit + self.ram_limit_warner.update(); + } + + #[cfg(target_arch = "wasm32")] + if let Some(PendingFilePromise { + recommended_store_id, + force_store_info, + promise, + }) = &self.open_files_promise + && let Some(files) = promise.ready() + { + for file in files { + self.command_sender + .send_system(SystemCommand::LoadDataSource(LogDataSource::FileContents( + FileSource::FileDialog { + recommended_store_id: recommended_store_id.clone(), + force_store_info: *force_store_info, + }, + file.clone(), + ))); + } + self.open_files_promise = None; + } + + // NOTE: GPU resource stats are cheap to compute so we always do. + let gpu_resource_stats = { + re_tracing::profile_scope!("gpu_resource_stats"); + + let egui_renderer = frame + .wgpu_render_state() + .expect("Failed to get frame render state") + .renderer + .read(); + + let render_ctx = egui_renderer + .callback_resources + .get::() + .expect("Failed to get render context"); + + // Query statistics before begin_frame as this might be more accurate if there's resources that we recreate every frame. + render_ctx.gpu_resources.statistics() + }; + + // NOTE: Store and caching stats are very costly to compute: only do so if the dev panel + // is opened. + let store_stats = self.dev_panel_open.then(|| store_hub.stats()); + + // do early, before doing too many allocations + let store_bundle_for_streaming = self + .dev_panel_open + .then(|| store_hub.store_bundle() as &re_entity_db::StoreBundle); + self.dev_panel.update( + &gpu_resource_stats, + store_stats.as_ref(), + store_bundle_for_streaming, + ); + + self.purge_memory_if_needed(&mut store_hub); // Call BEFORE `begin_frame_caches` + + // In some (rare) circumstances we run two egui passes in a single frame. + // This happens on call to `egui::Context::request_discard`. + let is_start_of_new_frame = ui.current_pass_index() == 0; + if is_start_of_new_frame { + // IMPORTANT: only call this once per FRAME even if we run multiple passes. + // Otherwise we might incorrectly evict something that was invisible in the first (discarded) pass. + store_hub.begin_frame_caches(self.active_recording_id()); // Call AFTER `purge_memory_if_needed` + self.state.app_caches.begin_frame(); + } + + ui::file_saver_progress_ui(ui, &mut self.background_tasks); // toasts for background file saver + + // Make sure some app is active + // Must be called before `read_context` below. + if let Route::Loading(source) = self.state.navigation.current() { + if !self.msg_receive_set().contains(source) { + // The stream finished and may have produced a recording without triggering + // automatic navigation. So we try that before defaulting to showing the + // Welcome screen. + let loaded_recording = store_hub + .find_recording_store_by_source(source) + .map(|db| db.store_id().clone()); + + if let Some(store_id) = loaded_recording { + re_log::debug!("Stream completed, navigating to loaded recording {store_id:?}"); + store_hub.load_blueprint_and_caches(&store_id, &self.view_class_registry); + self.state.navigation.replace(Route::LocalRecording { + recording_id: store_id, + }); + } else if let Some(re_uri::RedapUri::DatasetData(uri)) = source.redap_uri() + && self.connection_registry.error_for_uri(uri).is_some() + { + // Do nothing, the loading screen will show the error and a button to go back to start screen. + } else { + re_log::debug!("No recording found from loading source, resetting navigation"); + self.state.navigation.reset(); + } + } + } else if !matches!( + self.state.navigation.current(), + Route::ChunkStoreBrowser { .. } + ) { + // If the current route points to a stale recording, find a new valid state. + let route_is_valid = self + .state + .navigation + .current() + .recording_id() + .is_none_or(|recording_id| store_hub.entity_db(recording_id).is_some()); + + if !route_is_valid { + let any_other_app_id: Option = store_hub + .store_bundle() + .entity_dbs() + .map(|db| db.application_id()) + .filter(|app_id| *app_id != StoreHub::welcome_screen_app_id()) + .min() + .cloned(); + if let Some(app_id) = any_other_app_id { + store_hub.load_persisted_blueprints_for_app(&app_id); + if let Some(recording_id) = store_hub.earliest_recording_for_app(&app_id) { + store_hub + .load_blueprint_and_caches(&recording_id, &self.view_class_registry); + self.state + .selection_state + .set_selection(Item::StoreId(recording_id.clone())); + self.state + .navigation + .replace(Route::LocalRecording { recording_id }); + } else { + self.state.navigation.reset(); + } + } else { + self.state.navigation.reset(); + } + } + } + + { + let active_route = self.state.navigation.current(); + + // Read-only copy of time control state (to avoid borrow checker issues with mutable state access). + let active_time_ctrl = active_route + .recording_id() + .and_then(|id| self.state.time_controls.get(id).cloned()) + .unwrap_or_default(); + + let (storage_context, store_context) = + store_hub.read_context(active_route, &active_time_ctrl); + + let blueprint = store_context.as_ref().map(|ctx| ctx.blueprint); + let blueprint_query = self.state.blueprint_query_for_viewer(blueprint); + + let app_blueprint = AppBlueprint::new( + blueprint, + &blueprint_query, + ui, + self.panel_state_overrides_active + .then_some(self.panel_state_overrides), + ); + + self.ui_impl( + ui, + frame, + &app_blueprint, + &gpu_resource_stats, + store_context.as_ref(), + &storage_context, + mem_usage_tree, + store_stats.as_ref(), + ); + + if self.custom_window_frame() { + ui::paint_custom_window_frame(ui); + } + + let selected_redap_server = if let Some(Item::RedapServer(origin)) = + self.state.selection_state.selected_items().single_item() + { + Some(origin.clone()) + } else { + None + }; + + let active_recording_id = store_context + .as_ref() + .map(|ctx| ctx.recording_store_id().clone()); + + // The Redap entry currently being viewed (if any), so its commands (e.g. refresh) + // are offered in the command palette. + let current_redap_entry = match self.state.navigation.current() { + Route::RedapEntry { origin, kind } => { + kind.entry_id().map(|entry_id| (origin.clone(), entry_id)) + } + _ => None, + }; + + let cmd_env = re_ui::CommandEnvironment { + recording: active_recording_id.clone(), + has_editable_redap_server: selected_redap_server + .as_ref() + .is_some_and(|origin| !self.state.redap_servers.is_internal_server(origin)), + redap_server: selected_redap_server, + redap_entry: current_redap_entry, + }; + + // Handle keyboard shortcuts, now that we have a live `CommandEnvironment`: + { + use re_ui::{ + RecordingCommandSender as _, RedapServerCommandSender as _, + TableCommandSender as _, + }; + + // Non-timeline shortcuts, resolved against the current environment: + if let Some(resolved) = re_ui::listen_for_kb_shortcuts(ui.ctx(), &cmd_env) { + match resolved { + re_ui::ResolvedCommand::Ui(cmd) => self.command_sender.send_ui(cmd), + re_ui::ResolvedCommand::Recording(cmd) => { + self.command_sender.send_recording_command(cmd); + } + re_ui::ResolvedCommand::RedapServer(cmd) => { + self.command_sender.send_redap_server_command(cmd); + } + re_ui::ResolvedCommand::Table(cmd) => { + self.command_sender.send_table_command(cmd); + } + } + } + + // Timeline shortcuts (space/arrows/home/end) were consumed early in + // `on_begin_pass` and stashed; pair them with the live recording here: + let pending_timeline = ui.ctx().data_mut(|data| { + let key = pending_timeline_shortcut_key(); + let kind = data.get_temp::(key); + data.remove::(key); + kind + }); + if let Some(cmd) = pending_timeline.and_then(|kind| kind.for_environment(&cmd_env)) + { + self.command_sender.send_recording_command(cmd); + } + } + + let mut cmd_palette_provider = crate::command_palette::CommandPaletteProviderImpl { + recording: store_context.as_ref().map(|ctx| ctx.recording()), + redap_servers: &self.state.redap_servers, + cmd_env, + }; + if let Some(cmd) = self.cmd_palette.show(ui.ctx(), &mut cmd_palette_provider) { + match cmd { + CommandPaletteAction::UiCommand(cmd) => { + self.command_sender.send_ui(cmd); + } + CommandPaletteAction::RecordingCommand(cmd) => { + use re_ui::RecordingCommandSender as _; + self.command_sender.send_recording_command(cmd); + } + CommandPaletteAction::RedapServerCommand(cmd) => { + use re_ui::RedapServerCommandSender as _; + self.command_sender.send_redap_server_command(cmd); + } + CommandPaletteAction::SelectEntityPath(entity_path) => { + self.command_sender + .send_system(SystemCommand::set_selection(Item::from( + entity_path.clone(), + ))); + self.command_sender + .send_system(SystemCommand::SetFocus(entity_path.into())); + } + CommandPaletteAction::SelectComponentPath(component_path) => { + let item = Item::from(component_path); + self.command_sender + .send_system(SystemCommand::set_selection(item.clone())); + self.command_sender + .send_system(SystemCommand::SetFocus(item.into())); + } + CommandPaletteAction::SelectRedapServer(origin) => { + self.select_and_navigate_to(&Item::RedapServer(origin)); + } + CommandPaletteAction::SelectRedapEntry { + origin, entry_id, .. + } => { + self.select_and_navigate_to(&Item::RedapEntry { + origin, + kind: re_viewer_context::RedapEntryKind::Entry(entry_id), + }); + } + CommandPaletteAction::TableCommand(cmd) => { + use re_ui::TableCommandSender as _; + self.command_sender.send_table_command(cmd); + } + CommandPaletteAction::OpenUrl(url) => { + match ViewerOpenUrl::parse_with_options( + url.as_str(), + &re_data_source::FromUriOptions { + accept_extensionless_http: true, + }, + ) { + Ok(url) => { + url.open( + ui, + &OpenUrlOptions { + recording_open_behavior: + RecordingOpenBehavior::OpenAndSelect, + show_loader: true, + }, + &self.command_sender, + ); + } + Err(err) => { + re_log::warn!("{err}"); + } + } + + // Note that we can't use `ui.open_url(egui::OpenUrl::same_tab(uri))` here because.. + // * the url redirect in `check_for_clicked_hyperlinks` wouldn't be hit + // * we don't actually want to open any URLs in the browser here ever, only ever into the current viewer + } + } + } + + let route = self.state.navigation.current().clone(); + Self::handle_dropping_files(ui, &self.command_sender, &route); + + // Run pending commands last (so we don't have to wait for a repaint before they are run): + self.run_pending_ui_commands( + ui, + &app_blueprint, + &storage_context, + store_context.as_ref(), + &route, + ); + self.run_pending_recording_commands( + ui, + &app_blueprint, + &storage_context, + store_context.as_ref(), + ); + } + self.run_pending_system_commands(&mut store_hub, ui); + + self.update_history(&store_hub); + + // Return the `StoreHub` to the Viewer so we have it on the next frame + self.store_hub = Some(store_hub); + + { + // Check for returned screenshots: + let screenshots: Vec<_> = ui.input(|i| { + i.raw + .events + .iter() + .filter_map(|event| { + if let egui::Event::Screenshot { + image, user_data, .. + } = event + { + Some((image.clone(), user_data.clone())) + } else { + None + } + }) + .collect() + }); + + for (image, user_data) in screenshots { + self.process_screenshot_result(&image, &user_data); + } + } + } + + #[cfg(target_arch = "wasm32")] + fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { + Some(&mut *self) + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn save_profile_trace(view: &re_tracing::reexports::puffin::FrameView) -> anyhow::Result<()> { + let Some(path) = rfd::FileDialog::new() + .set_file_name("rerun.puffin") + .set_title("Save profile trace") + .add_filter("Puffin profile", &["puffin"]) + .save_file() + else { + re_log::info!("Profile trace capture cancelled by user."); + return Ok(()); + }; + + let file = std::fs::File::create(&path)?; + let mut writer = std::io::BufWriter::new(file); + view.write(&mut writer)?; + + re_log::info!("Saved profile trace to {}", path.display()); + Ok(()) +} + +impl MemUsageTreeCapture for App { + fn capture_mem_usage_tree(&self) -> MemUsageTree { + re_tracing::profile_function!(); + let mut node = re_byte_size::MemUsageNode::default(); + node.add("state", self.state.capture_mem_usage_tree()); + node.add("rx_log", self.rx_log.capture_mem_usage_tree()); + node.add("store_hub", self.store_hub.capture_mem_usage_tree()); + node.add( + "store_subscribers", + re_chunk_store::ChunkStore::capture_all_subscribers_mem_usage_tree(), + ); + + let mut globals = re_byte_size::MemUsageNode::new(); + globals.add( + "forgiving_parse_cache", + re_log_types::forgiving_parse_cache_bytes_used(), + ); + globals.add("string_interner", re_string_interner::bytes_used() as u64); + node.add("globals", globals.into_tree()); + + node.into_tree() + } +} + +#[cfg(target_arch = "wasm32")] +fn blueprint_loader() -> BlueprintPersistence { + // TODO(#2579): implement persistence for web + noop_blueprint_loader() +} + +/// No-op blueprint persistence used on wasm. Also used in tests so that on-disk blueprints from +/// the developer's running viewer don't leak into the test environment. +fn noop_blueprint_loader() -> BlueprintPersistence { + BlueprintPersistence { + loader: None, + saver: None, + validator: Some(Box::new(crate::blueprint::is_valid_blueprint)), + deleter: None, + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn blueprint_loader() -> BlueprintPersistence { + use re_entity_db::{EntityDb, StoreBundle}; + use re_log_types::{ApplicationId, StoreKind}; + + fn load_blueprint_from_disk(app_id: &ApplicationId) -> anyhow::Result> { + let blueprint_path = crate::saving::default_blueprint_path(app_id)?; + if !blueprint_path.exists() { + return Ok(None); + } + + re_log::debug!("Trying to load blueprint for {app_id} from {blueprint_path:?}"); + + if let Some(bundle) = crate::loading::load_blueprint_file(&blueprint_path) { + for store in bundle.entity_dbs() { + if store.store_kind() == StoreKind::Blueprint + && !crate::blueprint::is_valid_blueprint(store) + { + re_log::warn_once!( + "Blueprint for {app_id} at {blueprint_path:?} appears invalid - will ignore. This is expected if you have just upgraded Rerun versions." + ); + return Ok(None); + } + } + Ok(Some(bundle)) + } else { + Ok(None) + } + } + + fn save_blueprint_to_disk(app_id: &ApplicationId, blueprint: &EntityDb) -> anyhow::Result<()> { + let blueprint_path = crate::saving::default_blueprint_path(app_id)?; + + let messages = blueprint.to_messages(None); + let rrd_version = blueprint + .store_info() + .and_then(|info| info.store_version) + .unwrap_or(re_build_info::CrateVersion::LOCAL); + + // TODO(jleibs): Should we push this into a background thread? Blueprints should generally + // be small & fast to save, but maybe not once we start adding big pieces of user data? + crate::saving::encode_to_file(rrd_version, &blueprint_path, messages)?; + + re_log::debug!("Saved blueprint for {app_id} to {blueprint_path:?}"); + + Ok(()) + } + + BlueprintPersistence { + loader: Some(Box::new(load_blueprint_from_disk)), + saver: Some(Box::new(save_blueprint_to_disk)), + validator: Some(Box::new(crate::blueprint::is_valid_blueprint)), + deleter: Some(Box::new(crate::saving::delete_blueprint)), + } +} diff --git a/crates/viewer/re_viewer/src/app/ui.rs b/crates/viewer/re_viewer/src/app/ui.rs new file mode 100644 index 000000000000..89cce1724149 --- /dev/null +++ b/crates/viewer/re_viewer/src/app/ui.rs @@ -0,0 +1,591 @@ +use re_byte_size::NamedMemUsageTree; +use re_entity_db::LogSource; +use re_renderer::WgpuResourcePoolStatistics; +use re_ui::{HasDesignTokens as _, UiExt as _, WindowFrameConfig}; +use re_viewer_context::{ActiveStoreContext, StorageContext, store_hub::StoreHubStats}; + +use crate::{ + app_blueprint::AppBlueprint, app_state::WelcomeScreenState, background_tasks::BackgroundTasks, +}; + +use super::App; + +impl App { + /// Top-level ui function. + /// + /// Shows the viewer ui. + pub(super) fn ui_impl( + &mut self, + ui: &mut egui::Ui, + frame: &eframe::Frame, + app_blueprint: &AppBlueprint<'_>, + gpu_resource_stats: &WgpuResourcePoolStatistics, + active_store_context: Option<&ActiveStoreContext<'_>>, + storage_context: &StorageContext<'_>, + mem_usage_tree: Option, + store_stats: Option<&StoreHubStats>, + ) { + let custom_window_decorations = self.custom_window_decorations(); + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + let id = egui::Id::new("custom_window_decorations_applied"); + let was_applied = ui.ctx().data_mut(|data| { + let was_applied = data.get_temp::(id); + data.insert_temp(id, custom_window_decorations); + was_applied + }); + + if let Some(was_applied) = was_applied + && was_applied != custom_window_decorations + { + ui.send_viewport_cmd(egui::ViewportCommand::Decorations( + !custom_window_decorations, + )); + ui.send_viewport_cmd(egui::ViewportCommand::Transparent( + custom_window_decorations, + )); + } + + // Apply windows undecorated shadow both on change and the first frame. + #[cfg(target_os = "windows")] + if was_applied != Some(custom_window_decorations) + && let Some(window) = frame.winit_window() + { + use winit::platform::windows::WindowExtWindows as _; + window.set_undecorated_shadow(custom_window_decorations); + } + } + + let mut main_panel_frame = egui::Frame::default(); + if self.custom_window_frame() { + // Add some margin so that we can later paint an outline around it all. + main_panel_frame.inner_margin = 1.0.into(); + } + + egui::CentralPanel::default() + .frame(main_panel_frame) + .show(ui, |ui| { + paint_background_fill(ui); + + crate::ui::mobile_warning_ui(ui, custom_window_decorations); + + if self.custom_window_frame() { + // The outer frame owns the rounded window background. Inner panels should not + // repaint opaque square corners over it. + ui.visuals_mut().panel_fill = egui::Color32::TRANSPARENT; + } + + crate::ui::top_panel( + frame, + self, + app_blueprint, + active_store_context, + storage_context.hub, + gpu_resource_stats, + ui, + ); + + self.dev_panel_ui( + ui, + gpu_resource_stats, + mem_usage_tree, + store_stats, + active_store_context, + storage_context, + ); + + self.egui_debug_panel_ui(ui); + + let egui_renderer = &mut frame + .wgpu_render_state() + .expect("Failed to get frame render state") + .renderer + .write(); + + if let Some(render_ctx) = egui_renderer + .callback_resources + .get_mut::() + { + render_ctx.begin_frame(); // This may actually be called multiple times per egui frame, if we have a multi-pass layout frame. + + // In some (rare) circumstances we run two egui passes in a single frame. + // This happens on call to `egui::Context::request_discard`. + let is_start_of_new_frame = ui.current_pass_index() == 0; + + if is_start_of_new_frame { + if let Some(origin) = self.connection_registry.internal_origin() { + self.state.redap_servers.add_internal_server( + origin.clone(), + &self.connection_registry, + &self.async_runtime, + &self.egui_ctx, + self.command_sender.clone(), + ); + if self.app_options().experimental.use_internal_catalog { + self.state.redap_servers.reveal_internal_catalog(); + } + } + + self.state.redap_servers.on_frame_start( + &self.connection_registry, + &self.async_runtime, + &self.egui_ctx, + self.startup_options.login_enabled(), + &self.command_sender, + ); + + // Install our url decorator so links render nicely. This is done every frame + // so the data stays up-to-date. + let url_name_lookup = + std::sync::Arc::new(self.state.redap_servers.build_url_lookup()); + re_ui::UrlDecorator::set( + &self.egui_ctx, + re_viewer_context::make_url_decorator( + url_name_lookup, + self.egui_ctx.theme(), + ), + ); + } + + self.texture_readback.poll_and_save_texture_readbacks( + render_ctx, + ui, + &self.command_sender, + &mut self.notifications, + ); + + self.state.show( + &self.app_env, + &self.startup_options, + app_blueprint, + ui, + render_ctx, + active_store_context, + storage_context, + &self.reflection, + &self.component_ui_registry, + &self.component_fallback_registry, + &self.view_class_registry, + &self.rx_log, + &self.command_sender, + &WelcomeScreenState { + hide_examples: self.startup_options.hide_welcome_screen, + opacity: self.welcome_screen_opacity(ui), + }, + self.event_dispatcher.as_ref(), + &self.connection_registry, + &self.async_runtime, + self.custom_window_frame(), + ); + render_ctx.before_submit(); + + self.show_text_logs_as_notifications(); + } + }); + + if custom_window_decorations { + custom_windows_decorations_resize_ui(ui); + } + + if self.app_options().show_notification_toasts { + self.notifications.show_toasts(ui); + } + } + + fn dev_panel_ui( + &mut self, + ui: &mut egui::Ui, + gpu_resource_stats: &WgpuResourcePoolStatistics, + mem_usage_tree: Option, + store_stats: Option<&StoreHubStats>, + store_context: Option<&ActiveStoreContext<'_>>, + storage_context: &re_viewer_context::StorageContext<'_>, + ) { + let window_frame = self.window_frame_config(ui.ctx()); + let frame = egui::Frame { + fill: ui.visuals().panel_fill, + ..ui.tokens().bottom_panel_frame(window_frame) + }; + + let external_trees = if self.dev_panel_open { + self.external_memory_users.captured_trees() + } else { + &[] + }; + + let mut dev_panel_open = self.dev_panel_open; + let mut close_requested = false; + egui::Panel::bottom("dev_panel") + .default_size(300.0) + .resizable(true) + .frame(frame) + .show_collapsible(ui, &mut dev_panel_open, |ui| { + let response = self.dev_panel.ui( + ui, + &self.state.app_options().memory_limit, + mem_usage_tree, + external_trees, + gpu_resource_stats, + store_stats, + store_context, + &self.state.time_controls, + storage_context, + ); + close_requested = response.close_requested; + if response.repaint_requested { + ui.request_repaint(); + } + }); + // `show_collapsible` flips `dev_panel_open` when the user drags the panel closed: + self.dev_panel_open = dev_panel_open && !close_requested; + } + + fn egui_debug_panel_ui(&mut self, ui: &mut egui::Ui) { + let egui_ctx = ui.ctx().clone(); + + let mut egui_debug_panel_open = self.egui_debug_panel_open; + egui::Panel::left("style_panel") + .default_size(300.0) + .resizable(true) + .frame( + ui.tokens() + .top_panel_frame(self.window_frame_config(ui.ctx())), + ) + .show_collapsible(ui, &mut egui_debug_panel_open, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + if ui + .button("request_discard") + .on_hover_text("Request a second layout pass. Just for testing.") + .clicked() + { + ui.request_discard("testing"); + } + + egui::CollapsingHeader::new("egui settings") + .default_open(false) + .show(ui, |ui| { + egui_ctx.settings_ui(ui); + }); + + egui::CollapsingHeader::new("egui inspection") + .default_open(false) + .show(ui, |ui| { + egui_ctx.inspection_ui(ui); + }); + }); + }); + // `show_collapsible` flips `egui_debug_panel_open` when the user drags the panel closed: + self.egui_debug_panel_open = egui_debug_panel_open; + } + + fn should_fade_in_welcome_screen(&self) -> bool { + if let Some(expect_data_soon) = self.startup_options.expect_data_soon { + return expect_data_soon; + } + + // The reason for the fade-in is to avoid the welcome screen + // flickering quickly before receiving some data. + // So: if we expect data very soon, we do a fade-in. + + for source in self.rx_log.sources() { + match &*source { + LogSource::File { .. } + | LogSource::HttpStream { .. } + | LogSource::RedapGrpcStream { .. } + | LogSource::Stdin + | LogSource::RrdWebEvent + | LogSource::Sdk + | LogSource::JsChannel { .. } => { + return true; // We expect data soon, so fade-in + } + + // We start a gRPC server by default in native rerun, i.e. when just running `rerun`, + // and in that case fading in the welcome screen would be slightly annoying. + // However, we also use the gRPC server for sending data from the logging SDKs + // when they call `spawn()`, and in that case we really want to fade in the welcome screen. + // Therefore `spawn()` uses the special `--expect-data-soon` flag + // (handled earlier in this function), so here we know we are in the other case: + // a user calling `rerun` in their terminal (don't fade in). + LogSource::MessageProxy { .. } => {} + } + } + + false // No special sources (or no sources at all), so don't fade in + } + + /// Handle fading in the welcome screen, if we should. + fn welcome_screen_opacity(&self, egui_ctx: &egui::Context) -> f32 { + if self.should_fade_in_welcome_screen() { + // The reason for this delay is to avoid the welcome screen + // flickering quickly before receiving some data. + // The only time it has for that is between the call to `spawn` and sending the recording info, + // which should happen _right away_, so we only need a small delay. + // Why not skip the wlecome screen completely when we expect the data? + // Because maybe the data never comes. + let sec_since_first_shown = self.start_time.elapsed().as_secs_f32(); + let opacity = egui::remap_clamp(sec_since_first_shown, 0.4..=0.6, 0.0..=1.0); + if opacity < 1.0 { + egui_ctx.request_repaint(); + } + opacity + } else { + 1.0 + } + } + + /// Show recent text log messages to the user as toast notifications. + fn show_text_logs_as_notifications(&mut self) { + re_tracing::profile_function!(); + + while let Ok(message) = self.text_log_rx.try_recv() { + self.notifications.add_log(message); + } + } + + #[cfg(any(target_os = "windows", target_os = "linux"))] + pub(crate) fn custom_window_decorations(&self) -> bool { + self.app_options().custom_window_decorations + && !self.is_screenshotting() + && !self.app_env().is_test() + } + + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + pub(crate) fn custom_window_decorations(&self) -> bool { + let _ = self; + false + } + + pub(crate) fn window_frame_config(&self, ctx: &egui::Context) -> WindowFrameConfig { + if self.custom_window_frame() { + WindowFrameConfig::custom(ctx) + } else { + WindowFrameConfig::Native + } + } +} + +/// Add invisible resize handles for the compact title bar. +/// +/// Disabling native decorations removes the OS-provided resize borders together +/// with the native title bar. This restores that interaction by placing thin +/// egui hit zones along the edges/corners and forwarding drag starts to winit +/// via [`egui::ViewportCommand::BeginResize`]. The actual resizing is still +/// performed by the windowing system. +#[cfg(not(target_arch = "wasm32"))] +fn custom_windows_decorations_resize_ui(ui: &egui::Ui) { + let fullscreen = ui.ctx().input(|i| i.viewport().fullscreen).unwrap_or(false); + let maximized = ui.ctx().input(|i| i.viewport().maximized).unwrap_or(false); + + if fullscreen || maximized { + return; + } + + let rect = ui.max_rect(); + let resize_margin = 6.0; + let corner_size = 16.0; + + // Corners get larger square hit zones so diagonal resizing is easy to grab. + // Edges get thin strips between the corners. + let resize_regions = [ + ( + egui::Rect::from_min_max( + rect.left_top(), + rect.left_top() + egui::vec2(corner_size, corner_size), + ), + egui::ResizeDirection::NorthWest, + egui::CursorIcon::ResizeNorthWest, + "compact_title_bar_resize_nw", + ), + ( + egui::Rect::from_min_max( + rect.right_top() - egui::vec2(corner_size, 0.0), + rect.right_top() + egui::vec2(0.0, corner_size), + ), + egui::ResizeDirection::NorthEast, + egui::CursorIcon::ResizeNorthEast, + "compact_title_bar_resize_ne", + ), + ( + egui::Rect::from_min_max( + rect.left_bottom() - egui::vec2(0.0, corner_size), + rect.left_bottom() + egui::vec2(corner_size, 0.0), + ), + egui::ResizeDirection::SouthWest, + egui::CursorIcon::ResizeSouthWest, + "compact_title_bar_resize_sw", + ), + ( + egui::Rect::from_min_max( + rect.right_bottom() - egui::vec2(corner_size, corner_size), + rect.right_bottom(), + ), + egui::ResizeDirection::SouthEast, + egui::CursorIcon::ResizeSouthEast, + "compact_title_bar_resize_se", + ), + ( + egui::Rect::from_min_max( + rect.left_top() + egui::vec2(corner_size, 0.0), + rect.right_top() + egui::vec2(-corner_size, resize_margin), + ), + egui::ResizeDirection::North, + egui::CursorIcon::ResizeNorth, + "compact_title_bar_resize_n", + ), + ( + egui::Rect::from_min_max( + rect.left_bottom() + egui::vec2(corner_size, -resize_margin), + rect.right_bottom() + egui::vec2(-corner_size, 0.0), + ), + egui::ResizeDirection::South, + egui::CursorIcon::ResizeSouth, + "compact_title_bar_resize_s", + ), + ( + egui::Rect::from_min_max( + rect.left_top() + egui::vec2(0.0, corner_size), + rect.left_bottom() + egui::vec2(resize_margin, -corner_size), + ), + egui::ResizeDirection::West, + egui::CursorIcon::ResizeWest, + "compact_title_bar_resize_w", + ), + ( + egui::Rect::from_min_max( + rect.right_top() + egui::vec2(-resize_margin, corner_size), + rect.right_bottom() + egui::vec2(0.0, -corner_size), + ), + egui::ResizeDirection::East, + egui::CursorIcon::ResizeEast, + "compact_title_bar_resize_e", + ), + ]; + + for (rect, direction, cursor_icon, id) in resize_regions { + let response = ui.interact(rect, ui.id().with(id), egui::Sense::click_and_drag()); + if response.hovered() || response.dragged() { + ui.ctx().set_cursor_icon(cursor_icon); + } + if response.drag_started_by(egui::PointerButton::Primary) { + ui.send_viewport_cmd(egui::ViewportCommand::BeginResize(direction)); + } + } +} + +fn paint_background_fill(ui: &egui::Ui) { + // This is required because the streams view (time panel) + // has rounded top corners, which leaves a gap. + // So we fill in that gap (and other) here. + // Of course this does some over-draw, but we have to live with that. + + let tokens = ui.tokens(); + let is_maximized = ui.ctx().input(|i| i.viewport().maximized == Some(true)); + + ui.painter().rect_filled( + ui.max_rect().expand(0.5), + tokens.native_window_corner_radius(is_maximized), + tokens.panel_bg_color, + ); +} + +#[cfg(target_arch = "wasm32")] +fn custom_windows_decorations_resize_ui(_ui: &egui::Ui) {} + +pub(super) fn paint_custom_window_frame(egui_ctx: &egui::Context) { + let tokens = egui_ctx.tokens(); + + let painter = egui::Painter::new( + egui_ctx.clone(), + egui::LayerId::new(egui::Order::TOP, egui::Id::new("native_window_frame")), + egui::Rect::EVERYTHING, + ); + + let is_maximized = egui_ctx.input(|i| i.viewport().maximized == Some(true)); + let corner_radius = tokens.native_window_corner_radius(is_maximized); + + painter.rect_stroke( + egui_ctx.content_rect(), + corner_radius, + egui_ctx.tokens().native_frame_stroke, + egui::StrokeKind::Inside, + ); +} + +pub(super) fn preview_files_being_dropped(egui_ctx: &egui::Context) { + use egui::{Align2, Id, LayerId, Order, TextStyle}; + + // Preview hovering files: + if !egui_ctx.input(|i| i.raw.hovered_files.is_empty()) { + use std::fmt::Write as _; + + let mut text = "Drop to load:\n".to_owned(); + egui_ctx.input(|input| { + for file in &input.raw.hovered_files { + if let Some(path) = &file.path { + write!(text, "\n{}", path.display()).ok(); + } else if !file.mime.is_empty() { + write!(text, "\n{}", file.mime).ok(); + } + } + }); + + let painter = + egui_ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target"))); + + let screen_rect = egui_ctx.content_rect(); + painter.rect_filled( + screen_rect, + 0.0, + egui_ctx + .global_style() + .visuals + .extreme_bg_color + .gamma_multiply_u8(192), + ); + painter.text( + screen_rect.center(), + Align2::CENTER_CENTER, + text, + TextStyle::Body.resolve(&egui_ctx.global_style()), + egui_ctx.global_style().visuals.strong_text_color(), + ); + } +} + +// ---------------------------------------------------------------------------- + +pub(super) fn file_saver_progress_ui( + egui_ctx: &egui::Context, + background_tasks: &mut BackgroundTasks, +) { + if background_tasks.is_file_save_in_progress() { + // There's already a file save running in the background. + + if let Some(res) = background_tasks.poll_file_saver_promise() { + // File save promise has returned. + match res { + Ok(path) => { + re_log::info!("File saved to {path:?}."); // this will also show a notification the user + } + Err(err) => { + re_log::error!("{err}"); // this will also show a notification the user + } + } + } else { + // File save promise is still running in the background. + + // NOTE: not a toast, want something a bit more discreet here. + egui::Window::new("file_saver_spin") + .anchor(egui::Align2::RIGHT_BOTTOM, egui::Vec2::ZERO) + .title_bar(false) + .enabled(false) + .auto_sized() + .show(egui_ctx, |ui| { + ui.horizontal(|ui| { + ui.loading_indicator("Writing file to disk"); + ui.label("Writing file to disk…"); + }) + }); + } + } +} diff --git a/crates/viewer/re_viewer/src/app_state.rs b/crates/viewer/re_viewer/src/app_state.rs index 83d8f267ac65..5beefea7cba7 100644 --- a/crates/viewer/re_viewer/src/app_state.rs +++ b/crates/viewer/re_viewer/src/app_state.rs @@ -4,23 +4,22 @@ use ahash::HashMap; use egui::Ui; use egui::text_edit::TextEditState; use egui::text_selection::LabelSelectionState; -use re_chunk::TimelineName; use re_chunk_store::LatestAtQuery; use re_entity_db::EntityDb; use re_log_channel::{LogReceiverSet, LogSource, RecordingOpenBehavior}; -use re_log_types::{AbsoluteTimeRangeF, StoreId, TableId}; +use re_log_types::StoreId; use re_redap_browser::RedapServers; use re_redap_client::ConnectionRegistryHandle; use re_sdk_types::blueprint::components::{PanelState, PlayState}; -use re_ui::{ContextExt as _, UiExt as _}; +use re_ui::{ContextExt as _, UiExt as _, WindowFrameConfig}; use re_viewer_context::open_url::{self, ViewerOpenUrl}; use re_viewer_context::{ ActiveStoreContext, AppBlueprintCtx, AppContext, AppOptions, ApplicationSelectionState, AsyncRuntimeHandle, AuthContext, BlueprintContext, BlueprintUndoState, CommandSender, - ComponentUiRegistry, DragAndDropManager, FallbackProviderRegistry, FocusTarget, Item, Route, - SelectionChange, StorageContext, StoreHub, StoreViewContext, SystemCommand, + ComponentUiRegistry, DragAndDropManager, FallbackProviderRegistry, FocusTarget, Item, + ItemCollection, Route, SelectionChange, StorageContext, StoreHub, SystemCommand, SystemCommandSender as _, TableStore, TimeControl, TimeControlCommand, ViewClassRegistry, - ViewStates, ViewerContext, blueprint_timeline, + ViewStates, ViewerContext, }; use re_viewport::ViewportUi; use re_viewport_blueprint::ViewportBlueprint; @@ -36,7 +35,10 @@ use crate::{StartupOptions, history}; const WATERMARK: bool = false; // Nice for recording media material #[cfg(feature = "testing")] -pub type TestHookFn = Box)>; +pub type TestHookRecordingFn = Box)>; + +#[cfg(feature = "testing")] +pub type TestHookAppFn = Box)>; // TODO(#11737): Remove the serde derives since almost everything is skipped. #[derive(serde::Deserialize, serde::Serialize)] @@ -45,9 +47,18 @@ pub struct AppState { /// Global options for the whole viewer. pub(crate) app_options: AppOptions, - /// Configuration for the current recording (found in [`EntityDb`]). + /// The time control for each recording (found in [`EntityDb`]). + /// + /// Created lazily on first use with a given store. #[serde(skip)] pub time_controls: HashMap, + + /// App-level caches for data that is not tied to any particular store. + /// + /// See [`AppContext::app_caches`]. + #[serde(skip)] + pub app_caches: re_viewer_context::AppCaches, + #[serde(skip)] pub blueprint_time_control: TimeControl, @@ -81,7 +92,13 @@ pub struct AppState { /// to interact with the `ViewerContext`. #[cfg(feature = "testing")] #[serde(skip)] - pub(crate) test_hook: Option, + pub(crate) test_hook_recording: Option, + + /// Test-only: single-shot callback to run at the end of the frame. Used in integration tests + /// to interact with the `AppContext`. + #[cfg(feature = "testing")] + #[serde(skip)] + pub(crate) test_hook_app: Option, /// A stack of [`Route`]s that represents tab-like navigation of the user. #[serde(skip)] @@ -99,14 +116,14 @@ pub struct AppState { /// This is stored here for simplicity. An exclusive reference for that is passed to the users, /// such as [`ViewportUi`] and [`re_selection_panel::SelectionPanel`]. #[serde(skip)] - view_states: ViewStates, + pub(crate) view_states: ViewStates, /// Selection & hovering state. /// /// Not serialized since on startup we have to typically discard it anyways since /// whatever data was selected before is no longer accessible. /// - /// For Data Platform use-cases this can even be rather irritating: + /// For catalog server use-cases this can even be rather irritating: /// if previously a server was selected, then starting with a URL should no longer select it. #[serde(skip)] pub selection_state: ApplicationSelectionState, @@ -128,6 +145,7 @@ impl Default for AppState { Self { app_options: Default::default(), time_controls: Default::default(), + app_caches: Default::default(), blueprint_undo_state: Default::default(), blueprint_time_control: Default::default(), selection_panel: Default::default(), @@ -148,7 +166,9 @@ impl Default for AppState { auth_state: Default::default(), #[cfg(feature = "testing")] - test_hook: None, + test_hook_recording: None, + #[cfg(feature = "testing")] + test_hook_app: None, } } } @@ -169,8 +189,7 @@ impl AppState { /// The current time cursor for a recording, if any. pub fn time_cursor_for(&self, store_id: &StoreId) -> Option { - let time_ctrl = self.time_controls.get(store_id)?; - time_ctrl.time_cursor() + self.time_controls.get(store_id)?.time_cursor() } pub fn set_examples_manifest_url(&mut self, egui_ctx: &egui::Context, url: String) { @@ -185,21 +204,7 @@ impl AppState { &mut self.app_options } - /// Currently selected section of time, if any. - pub fn loop_selection( - &self, - store_context: Option<&ActiveStoreContext<'_>>, - ) -> Option<(TimelineName, AbsoluteTimeRangeF)> { - let rec_id = store_context.as_ref()?.recording.store_id(); - let time_ctrl = self.time_controls.get(rec_id)?; - - // is there an active loop selection? - time_ctrl - .time_selection() - .map(|q| (*time_ctrl.timeline_name(), q)) - } - - #[expect(clippy::too_many_arguments)] + // TODO(andreas): Large route-dispatch match, one arm per `Route`. pub fn show( &mut self, app_env: &crate::AppEnvironment, @@ -207,9 +212,7 @@ impl AppState { app_blueprint: &AppBlueprint<'_>, ui: &mut egui::Ui, render_ctx: &re_renderer::RenderContext, - // TODO(RR-3033): the viewer should be robust to not having a `StoreContext`. - // We should only have one if we are currently looking at a blueprint. - store_context: &ActiveStoreContext<'_>, + active_store_context: Option<&ActiveStoreContext<'_>>, storage_context: &StorageContext<'_>, reflection: &re_types_core::reflection::Reflection, component_ui_registry: &ComponentUiRegistry, @@ -221,86 +224,128 @@ impl AppState { event_dispatcher: Option<&crate::event::ViewerEventDispatcher>, connection_registry: &ConnectionRegistryHandle, runtime: &AsyncRuntimeHandle, + custom_window_frame: bool, ) { re_tracing::profile_function!(); + let egui_ctx = ui.ctx().clone(); + + let blueprint_query = + self.blueprint_query_for_viewer(active_store_context.map(|ctx| ctx.blueprint)); + + let viewport_ui = active_store_context.map(|store_context| { + ViewportUi::new(ViewportBlueprint::from_db( + store_context.blueprint, + &blueprint_query, + )) + }); + let drag_and_drop_manager = if let Some(viewport_ui) = &viewport_ui { + // The root container cannot be dragged. + DragAndDropManager::new(Item::Container(viewport_ui.blueprint.root_container)) + } else { + DragAndDropManager::new(ItemCollection::default()) + }; + + let active_route = self.navigation.current().clone(); + + self.selection_on_frame_start( + storage_context, + event_dispatcher, + active_store_context, + &active_route, + viewport_ui.as_ref(), + ); + + // App-level context, available for all routes (also those without an active recording). + let app_ctx = AppContext { + is_test: app_env.is_test(), + + app_options: &self.app_options, + reflection, + + egui_ctx: &egui_ctx, + render_ctx, + command_sender, + + connection_registry, + storage_context, + active_store_context, + app_caches: &self.app_caches, + + component_ui_registry, + view_class_registry, + component_fallback_registry, + route: self.navigation.current(), + selection_state: &self.selection_state, + focused_item: &self.focused_item, + drag_and_drop_manager: &drag_and_drop_manager, + connected_receivers: rx_log, + auth_context: self.auth_state.as_ref(), + login_enabled: startup_options.login_enabled(), + login_signed_in_url: startup_options + .login + .as_ref() + .map(|l| l.signed_in_url.as_str()), + }; + // check state early, before the UI has a chance to close these popups let is_any_popup_open = egui::Popup::is_any_open(ui.ctx()); + let viewport_frame = egui::Frame { + fill: ui.style().visuals.panel_fill, + ..Default::default() + }; + + // Only the settings screen edits the app options, so we avoid cloning them every frame. + let mut new_app_options = None; + match self.navigation.current() { - Route::Settings { previous } => { + Route::Settings { + return_route: previous, + } => { let mut show_settings_ui = true; - settings_screen_ui(ui, &mut self.app_options, &mut show_settings_ui); + let app_options = new_app_options.insert(self.app_options.clone()); + settings_screen_ui(ui, app_options, &mut show_settings_ui); if !show_settings_ui { - self.navigation.replace((**previous).clone()); + command_sender.send_system(SystemCommand::SetRoute((**previous).clone())); } - - self.share_modal - .ui(None, ui, startup_options.web_viewer_base_url().as_ref()); } Route::ChunkStoreBrowser { store_id, selected_chunk, - previous, + return_route, } => { - let previous = previous.clone(); - let store_id = store_id.clone(); - let selected_chunk_before = *selected_chunk; - - let result = self.datastore_ui.ui( - store_context, - storage_context, + self.datastore_ui.ui( ui, + storage_context, self.app_options.timestamp_format, - selected_chunk_before, + store_id.as_ref(), + selected_chunk.as_ref(), + return_route.as_ref(), + command_sender, ); - // Only reflect store/chunk changes back into the route when - // the route started with an explicit store_id. Otherwise the - // empty fallback recording would be written into the route, - // polluting the navigation history. - let result_store_id = store_id.as_ref().map(|_| result.recording_id.clone()); - if !result.keep_open { - self.navigation.replace((*previous).clone()); - } else if result_store_id != store_id - || result.selected_chunk != selected_chunk_before - { - self.navigation.replace(Route::ChunkStoreBrowser { - store_id: result_store_id.clone(), - selected_chunk: if result_store_id == store_id { - result.selected_chunk - } else { - None - }, - previous, - }); - } - - self.share_modal - .ui(None, ui, startup_options.web_viewer_base_url().as_ref()); } - // TODO(RR-3033): This needs to be further cleaned up and split into separately handled routes. - _ => { - let blueprint_query = - self.blueprint_query_for_viewer(Some(store_context.blueprint)); - let route = self.navigation.current(); + Route::LocalRecording { recording_id: _ } => { + // `viewport_ui` is `Some` iff `active_store_context` is `Some`. + let (Some(store_context), Some(viewport_ui)) = (active_store_context, viewport_ui) + else { + re_log::error_once!( + "No active store context for route {active_route:?}. This is likely a bug in the viewer state management." + ); + return; + }; let Self { - app_options, - time_controls, blueprint_undo_state, blueprint_time_control, selection_panel, time_panel, blueprint_time_panel, blueprint_tree, - welcome_screen, redap_servers, view_states, - selection_state, - focused_item, - auth_state, .. } = self; @@ -309,10 +354,6 @@ impl AppState { .or_default() .update(ui.ctx(), store_context.blueprint); - let viewport_blueprint = - ViewportBlueprint::from_db(store_context.blueprint, &blueprint_query); - let viewport_ui = ViewportUi::new(viewport_blueprint); - // If the blueprint is invalid, reset it. if viewport_ui.blueprint.is_invalid() { re_log::warn!("Incompatible blueprint detected. Resetting to default."); @@ -327,34 +368,6 @@ impl AppState { return; } - let selection_change = selection_state.on_frame_start( - |item| { - if let Item::StoreId(store_id) = item - && store_id.is_empty_recording() - { - return false; - } - - item.is_compatible_with_route(route) - && viewport_ui.blueprint.is_item_valid(storage_context, item) - }, - route.item(), - ); - - if let SelectionChange::SelectionChanged(selection) = selection_change - && let Some(event_dispatcher) = event_dispatcher - { - event_dispatcher.on_selection_change( - store_context.recording, - selection, - &viewport_ui.blueprint, - ); - } - - // The root container cannot be dragged. - let drag_and_drop_manager = - DragAndDropManager::new(Item::Container(viewport_ui.blueprint.root_container)); - let recording = store_context.recording; let visualizable_entities_per_visualizer = store_context @@ -369,8 +382,7 @@ impl AppState { default_blueprint: store_context.default_blueprint, blueprint_query: blueprint_query.clone(), }; - let time_ctrl = - create_time_control_for(time_controls, recording, &app_blueprint_ctx); + let time_ctrl = store_context.time_ctrl; let active_timeline = time_ctrl.timeline(); // Execute the queries for every `View` @@ -418,46 +430,15 @@ impl AppState { &query_range, &visualizable_entities_per_visualizer, &indicated_entities_per_visualizer, - app_options, + app_ctx.app_options, ), ) }) .collect::<_>() }; - let egui_ctx = ui.ctx().clone(); let ctx = ViewerContext { - app_ctx: AppContext { - is_test: app_env.is_test(), - - app_options, - reflection, - - egui_ctx: &egui_ctx, - render_ctx, - command_sender, - - connection_registry, - storage_context, - active_store_context: Some(store_context), - - component_ui_registry, - view_class_registry, - component_fallback_registry, - route, - selection_state, - focused_item, - drag_and_drop_manager: &drag_and_drop_manager, - active_time_ctrl: Some(time_ctrl), - connected_receivers: rx_log, - auth_context: auth_state.as_ref(), - login_enabled: startup_options.login_enabled(), - login_signed_in_url: startup_options - .login - .as_ref() - .map(|l| l.signed_in_url.as_str()), - }, - connected_receivers: rx_log, + app_ctx: app_ctx.clone(), store_context, visualizable_entities_per_visualizer: &visualizable_entities_per_visualizer, indicated_entities_per_visualizer: &indicated_entities_per_visualizer, @@ -477,13 +458,14 @@ impl AppState { // Update the viewport. May spawn new views and handle queued requests (like screenshots). viewport_ui.on_frame_start(&ctx); - // - // Blueprint time panel - // + let window_frame = if custom_window_frame { + WindowFrameConfig::custom(ui.ctx()) + } else { + WindowFrameConfig::Native + }; - if app_options.inspect_blueprint_timeline - && matches!(route, Route::LocalRecording { .. }) - { + let was_open = app_ctx.app_options.inspect_blueprint_timeline; + if was_open { let blueprint_db = ctx.store_context.blueprint; let undo_state = self @@ -512,293 +494,316 @@ impl AppState { }); } } - - blueprint_time_panel.show_panel( - &ctx, - &ctx.blueprint_store_view_ctx(), - &viewport_ui.blueprint, - ui, - PanelState::Expanded, - // Give the blueprint time panel a distinct color from the normal time panel: - ui.tokens() - .bottom_panel_frame() - .fill(ui.tokens().blueprint_time_panel_bg_fill), - ); } - // TODO(grtlr): We override the app blueprint, until we have proper blueprint support for tables. - let app_blueprint = if matches!(route, Route::LocalTable(..)) { - &AppBlueprint::new( - None, - &LatestAtQuery::latest(blueprint_timeline()), - &egui_ctx, - None, - ) - } else { - app_blueprint - }; - - // - // Time panel - // - - if route.has_time_panel() { - time_panel.show_panel( - &ctx, - &ctx.active_recording_store_view_context(), - &viewport_ui.blueprint, - ui, - app_blueprint.time_panel_state(), - ui.tokens().bottom_panel_frame(), - ); + // The blueprint inspection panel has no collapsed state: it is either + // fully expanded or completely hidden. Dragging it closed hides it. + let mut inspect_blueprint_timeline = was_open; + blueprint_time_panel.show_panel( + &ctx, + &ctx.blueprint_store_view_ctx(), + &viewport_ui.blueprint, + ui, + PanelState::Expanded, + &mut inspect_blueprint_timeline, + // Give the blueprint time panel a distinct color from the normal time panel: + ui.tokens() + .bottom_panel_frame(window_frame) + .fill(ui.tokens().blueprint_time_panel_bg_fill), + // No collapsed bar: dragging the panel closed hides it entirely. + false, + ); + #[cfg(debug_assertions)] + if inspect_blueprint_timeline != was_open { + // The user dragged the panel closed: + command_sender.send_system(SystemCommand::EnableInspectBlueprintTimeline( + inspect_blueprint_timeline, + )); } - // - // Selection Panel - // - - if route.has_selection_panel() { - selection_panel.show_panel( - &ctx, - &viewport_ui.blueprint, - view_states, - ui, - app_blueprint.selection_panel_state().is_expanded(), - ); + let time_was_expanded = app_blueprint.time_panel_state().is_expanded(); + let mut time_expanded = time_was_expanded; + time_panel.show_panel( + &ctx, + &ctx.active_recording_store_view_context(), + &viewport_ui.blueprint, + ui, + app_blueprint.time_panel_state(), + &mut time_expanded, + ui.tokens().bottom_panel_frame(window_frame), + true, + ); + if time_expanded != time_was_expanded { + // The user dragged the resize handle past the panel's limits to collapse/expand it: + app_blueprint.toggle_time_panel(command_sender); } - // - // Left panel (recordings and blueprint) - // - - let left_panel = egui::Panel::left("blueprint_panel") - .resizable(true) - .frame(egui::Frame { - fill: ui.visuals().panel_fill, - ..Default::default() - }) - .min_size(120.0) - .default_size(default_blueprint_panel_width(ui.content_rect().width())); - - let left_panel_response = left_panel.show_animated_inside( + let selection_was_expanded = app_blueprint.selection_panel_state().is_expanded(); + let mut selection_expanded = selection_was_expanded; + selection_panel.show_panel( + &ctx, + &viewport_ui.blueprint, + view_states, ui, - app_blueprint.blueprint_panel_state().is_expanded(), - |ui: &mut egui::Ui| { - // ListItem don't need vertical spacing so we disable it, but restore it - // before drawing the blueprint panel. - ui.spacing_mut().item_spacing.y = 0.0; - - match route { - Route::LocalRecording { .. } - | Route::LocalTable(..) - | Route::RedapEntry { .. } - | Route::RedapServer(..) - | Route::Loading(..) => { - let show_blueprints = matches!(route, Route::LocalRecording { .. }); - let resizable = show_blueprints; - if resizable { - // Ensure Blueprint panel has at least 150px minimum height, because now it doesn't autogrow (as it does without resizing=active) - let blueprint_min_height = 150.0; - let recordings_min_height = 104.0; // Minimum for recordings panel = top panel + 1 opened recording + extra space before bluprint - let available_height = ui.available_height(); - - // Calculate the maximum height for recordings panel - // Allow full space usage minus the blueprint minimum height, so that the blueprint panel can grow below existing content - let max_recordings_height = (available_height - - blueprint_min_height) - .max(recordings_min_height); - - egui::Panel::top("recording_panel") - .frame(egui::Frame::new()) - .resizable(resizable) - .show_separator_line(false) - .min_size(recordings_min_height) - .max_size(max_recordings_height) - .default_size(160.0_f32.max(recordings_min_height)) - .show_inside(ui, |ui| { - self.recording_panel.show_panel( - &ctx, - ui, - redap_servers, - welcome_screen_state.hide_examples, - ); - }); - } else { - self.recording_panel.show_panel( - &ctx, - ui, - redap_servers, - welcome_screen_state.hide_examples, - ); - } - - if show_blueprints { - blueprint_tree.show( - &ctx, - &viewport_ui.blueprint, - ui, - view_states, - ); - } - } - - Route::ChunkStoreBrowser { .. } | Route::Settings { .. } => {} // handled above - } - }, + &mut selection_expanded, ); - if let Some(left_panel_response) = left_panel_response { - left_panel_response.response.widget_info(|| { - egui::WidgetInfo::labeled(egui::WidgetType::Panel, true, "blueprint_panel") - }); + if selection_expanded != selection_was_expanded { + // The user dragged the resize handle past the panel's limits to collapse/expand it: + app_blueprint.toggle_selection_panel(command_sender); } - // - // Viewport - // + // If we are here and the "default" app id is selected, + // we should instead switch to the welcome screen. + if ctx.store_context.application_id() == StoreHub::welcome_screen_app_id() { + ctx.command_sender() + .send_system(SystemCommand::SetRoute(Route::welcome_page())); + } - let viewport_frame = egui::Frame { - fill: ui.style().visuals.panel_fill, - ..Default::default() - }; + Self::left_panel_ui( + &mut self.recording_panel, + blueprint_tree, + redap_servers, + view_states, + ui, + app_blueprint, + welcome_screen_state, + &ctx.app_ctx, + Some((&viewport_ui, &ctx)), + ); egui::CentralPanel::default() .frame(viewport_frame) - .show_inside(ui, |ui| { - match route { - Route::LocalTable(table_id) => { - if let Some(store) = ctx.table_stores().get(table_id) { - table_ui( - &ctx.active_recording_store_view_context(), - runtime, - ui, - table_id, - store, - ); - } else { - re_log::error_once!( - "Could not find batch store for table id {}", - table_id - ); - } - } - - Route::LocalRecording { .. } => { - // If we are here and the "default" app id is selected, - // we should instead switch to the welcome screen. - if ctx.store_context.application_id() - == StoreHub::welcome_screen_app_id() - { - ctx.command_sender().send_system(SystemCommand::SetRoute( - Route::welcome_page(), - )); - } - viewport_ui.viewport_ui(ui, &ctx, view_states); - } - - Route::RedapEntry { - kind: re_viewer_context::RedapEntryKind::Entry(entry_id), - .. - } => { - redap_servers.entry_ui( - &ctx.active_recording_store_view_context(), // TODO(RR-1127): this makes no sense - ui, - *entry_id, - ); - } - - Route::RedapServer(origin) => { - if origin == &*re_redap_browser::EXAMPLES_ORIGIN { - let origin = redap_servers - .iter_servers() - .find(|s| !s.origin().is_localhost()) - .map(|s| s.origin()) - .cloned(); - - let email = auth_state.as_ref().map(|auth| auth.email.clone()); - let origin_token = origin - .as_ref() - .map(|o| redap_servers.is_authenticated(o)) - .unwrap_or(false); - - let login_state = if origin_token || email.is_some() { - LoginState::Auth { email } - } else { - LoginState::NoAuth - }; - - let login_state = CloudState { - login: login_state, - has_server: origin, - }; - welcome_screen.ui( - ui, - &ctx.app_ctx, - welcome_screen_state, - &rx_log.sources(), - &login_state, - ); - } else { - redap_servers.server_central_panel_ui( - &ctx.active_recording_store_view_context(), // TODO(RR-3033): server_central_panel_ui should not know about any recording/blueprint - ui, - origin, - ); - } - } - - Route::RedapEntry { - origin, - kind: re_viewer_context::RedapEntryKind::Folder(path_prefix), - } => { - redap_servers.folder_central_panel_ui( - &ctx.active_recording_store_view_context(), - ui, - origin, - path_prefix, - ); - } - - Route::Loading(source) => { - let source = if let Ok(url) = - ViewerOpenUrl::from_data_source(source) - { - Cow::Owned(ViewerOpenUrlDescription::from_url(&url).to_string()) - } else { - // In practice this shouldn't happen. - Cow::Borrowed("") - }; - ui.loading_screen("Loading data source:", &*source); - } - - Route::ChunkStoreBrowser { .. } | Route::Settings { .. } => {} // Handled above - } + .show(ui, |ui| { + viewport_ui.viewport_ui(ui, &ctx, view_states); }); add_view_or_container_modal_ui(&ctx, &viewport_ui.blueprint, ui); - drag_and_drop_manager.payload_cursor_ui(ctx.egui_ctx()); // Process deferred layout operations and apply updates back to blueprint: viewport_ui.save_to_blueprint_store(&ctx); - self.redap_servers.modals_ui(&ctx.app_ctx, ui); - self.open_url_modal.ui(ui); + // NOTE: `redap_servers.modals_ui` is called once for all other routes after the match. self.share_modal.ui( Some(&ctx), ui, startup_options.web_viewer_base_url().as_ref(), ); - // Only in integration tests: call the test hook if any. #[cfg(feature = "testing")] - if let Some(test_hook) = self.test_hook.take() { + if let Some(test_hook) = self.test_hook_recording.take() { test_hook(&ctx); } } + + Route::Loading(log_source) => { + Self::left_panel_ui( + &mut self.recording_panel, + &mut self.blueprint_tree, + &self.redap_servers, + &self.view_states, + ui, + app_blueprint, + welcome_screen_state, + &app_ctx, + None, + ); + + let source_name = if let Ok(url) = ViewerOpenUrl::from_data_source(log_source) { + Cow::Owned(ViewerOpenUrlDescription::from_url(&url).to_string()) + } else { + // In practice this shouldn't happen. + Cow::Borrowed("") + }; + + egui::CentralPanel::default() + .frame(viewport_frame) + .show(ui, |ui| { + if let Some(re_uri::RedapUri::DatasetData(uri)) = log_source.redap_uri() + && let Some(err) = app_ctx.connection_registry.error_for_uri(uri) + { + ui.center("loading error", |ui| { + ui.set_max_width(ui.available_width() * 0.75); + ui.vertical_centered(|ui| { + ui.error_label(format!("Failed to load {source_name}: {err}")); + + if ui.button("Go Back").clicked() { + command_sender.send_system(SystemCommand::ResetRoute); + } + }) + }); + } else { + ui.loading_screen("Loading data source:", &*source_name); + } + }); + } + + Route::LocalTable(table_id) => { + Self::left_panel_ui( + &mut self.recording_panel, + &mut self.blueprint_tree, + &self.redap_servers, + &self.view_states, + ui, + app_blueprint, + welcome_screen_state, + &app_ctx, + None, + ); + + egui::CentralPanel::default() + .frame(viewport_frame) + .show(ui, |ui| { + if let Some(store) = app_ctx.table_stores().get(table_id) { + re_dataframe_ui::DataFusionTableWidget::new( + store.session_context(), + TableStore::TABLE_NAME, + ) + .table_id(table_id.clone()) + .title(table_id.as_str()) + .show( + &app_ctx, + runtime, + ui, + &mut self.view_states, + ); + } else { + re_log::error_once!( + "Could not find batch store for table id {}", + table_id + ); + } + }); + } + + Route::RedapServer(origin) => { + Self::left_panel_ui( + &mut self.recording_panel, + &mut self.blueprint_tree, + &self.redap_servers, + &self.view_states, + ui, + app_blueprint, + welcome_screen_state, + &app_ctx, + None, + ); + + egui::CentralPanel::default() + .frame(viewport_frame) + .show(ui, |ui| { + if origin == &*re_redap_browser::EXAMPLES_ORIGIN { + let origin = self + .redap_servers + .iter_servers() + .find(|s| !s.origin().is_localhost()) + .map(|s| s.origin()) + .cloned(); + + let email = self.auth_state.as_ref().map(|auth| auth.email.clone()); + let origin_token = origin + .as_ref() + .map(|o| self.redap_servers.is_authenticated(o)) + .unwrap_or(false); + + let login_state = if origin_token || email.is_some() { + LoginState::Auth { email } + } else { + LoginState::NoAuth + }; + + let login_state = CloudState { + login: login_state, + has_server: origin, + }; + self.welcome_screen.ui( + ui, + &app_ctx, + welcome_screen_state, + &rx_log.sources(), + &login_state, + ); + } else { + self.redap_servers.server_central_panel_ui( + &app_ctx, + ui, + origin, + &mut self.view_states, + ); + } + }); + } + + Route::RedapEntry { + kind: re_viewer_context::RedapEntryKind::Entry(entry_id), + .. + } => { + Self::left_panel_ui( + &mut self.recording_panel, + &mut self.blueprint_tree, + &self.redap_servers, + &self.view_states, + ui, + app_blueprint, + welcome_screen_state, + &app_ctx, + None, + ); + + egui::CentralPanel::default() + .frame(viewport_frame) + .show(ui, |ui| { + self.redap_servers + .entry_ui(&app_ctx, ui, *entry_id, &mut self.view_states); + }); + } + + Route::RedapEntry { + origin, + kind: re_viewer_context::RedapEntryKind::Folder(path_prefix), + } => { + Self::left_panel_ui( + &mut self.recording_panel, + &mut self.blueprint_tree, + &self.redap_servers, + &self.view_states, + ui, + app_blueprint, + welcome_screen_state, + &app_ctx, + None, + ); + + egui::CentralPanel::default() + .frame(viewport_frame) + .show(ui, |ui| { + self.redap_servers.folder_central_panel_ui( + &app_ctx, + ui, + origin, + path_prefix, + ); + }); + } + } + + // The `LocalRecording` arm shows the share modal itself with a `ViewerContext`. + if !matches!(active_route, Route::LocalRecording { .. }) { + self.share_modal + .ui(None, ui, startup_options.web_viewer_base_url().as_ref()); } + self.redap_servers.modals_ui(&app_ctx, ui); + self.open_url_modal.ui(ui); + + drag_and_drop_manager.payload_cursor_ui(&egui_ctx); - // - // Other UI things - // + #[cfg(feature = "testing")] + if let Some(app_test_hook) = self.test_hook_app.take() { + app_test_hook(&app_ctx); + } + + if let Some(new_app_options) = new_app_options { + self.app_options = new_app_options; + } if WATERMARK { ui.paint_watermark(); @@ -835,6 +840,163 @@ impl AppState { self.focused_item = None; } + fn selection_on_frame_start( + &mut self, + storage_context: &StorageContext<'_>, + event_dispatcher: Option<&crate::event::ViewerEventDispatcher>, + active_store_context: Option<&ActiveStoreContext<'_>>, + route: &Route, + viewport_ui: Option<&ViewportUi>, + ) { + let selection_change = self.selection_state.on_frame_start( + |item| { + if let Item::StoreId(store_id) = item + && store_id.is_empty_recording() + { + return None; + } + + if !item.is_compatible_with_route(route) { + return None; + } + + if let Some(viewport_ui) = viewport_ui { + if viewport_ui.blueprint.is_item_valid(storage_context, item) { + return Some(item.clone()); + } + + // A data result whose view still exists but whose entity isn't actually + // part of that view: fall back to selecting the entity itself rather than + // dropping the selection entirely. + if let Item::DataResult(data_result) = item + && viewport_ui.blueprint.view(&data_result.view_id).is_some() + { + return Some(Item::InstancePath(data_result.instance_path.clone())); + } + + None + } else if item.requires_blueprint() { + // This item is invalid without an active blueprint + None + } else { + // This item doesn't depend on any blueprint, lets keep it valid + Some(item.clone()) + } + }, + route.item(), + ); + + if let SelectionChange::SelectionChanged(selection) = selection_change + && let Some(event_dispatcher) = event_dispatcher + && let Some(active_store_context) = active_store_context + && let Some(viewport_ui) = viewport_ui + { + event_dispatcher.on_selection_change( + active_store_context.recording, + selection, + &viewport_ui.blueprint, + ); + } + } + + /// Left panel (recordings and blueprint) + /// + /// Takes the individual fields rather than `&mut self` so that it can be called with an + /// [`AppContext`] (or [`ViewerContext`]) borrowed from `self` alive at the call site. + #[expect(clippy::too_many_arguments)] + fn left_panel_ui( + recording_panel: &mut re_recording_panel::RecordingPanel, + blueprint_tree: &mut re_blueprint_tree::BlueprintTree, + redap_servers: &RedapServers, + view_states: &ViewStates, + ui: &mut Ui, + app_blueprint: &AppBlueprint<'_>, + welcome_screen_state: &WelcomeScreenState, + app_ctx: &AppContext<'_>, + // Present only for `Route::LocalRecording`: the blueprint tree is shown below the + // recordings panel and needs a full `ViewerContext`. + viewport: Option<(&ViewportUi, &ViewerContext<'_>)>, + ) { + let route = app_ctx.route(); + let left_panel = egui::Panel::left("blueprint_panel") + .resizable(true) + .frame(egui::Frame { + fill: ui.visuals().panel_fill, + ..Default::default() + }) + .min_size(120.0) + .default_size(default_blueprint_panel_width(ui.content_rect().width())); + + let was_expanded = app_blueprint.blueprint_panel_state().is_expanded(); + let mut blueprint_panel_expanded = was_expanded; + let left_panel_response = + left_panel.show_collapsible(ui, &mut blueprint_panel_expanded, |ui: &mut egui::Ui| { + // ListItem don't need vertical spacing so we disable it, but restore it + // before drawing the blueprint panel. + ui.spacing_mut().item_spacing.y = 0.0; + + match route { + Route::LocalRecording { .. } + | Route::LocalTable(..) + | Route::RedapEntry { .. } + | Route::RedapServer(..) + | Route::Loading(..) => { + let resizable = viewport.is_some(); + if resizable { + // Ensure Blueprint panel has at least 150px minimum height, because now it doesn't autogrow (as it does without resizing=active) + let blueprint_min_height = 150.0; + let recordings_min_height = 104.0; // Minimum for recordings panel = top panel + 1 opened recording + extra space before bluprint + let available_height = ui.available_height(); + + // Calculate the maximum height for recordings panel + // Allow full space usage minus the blueprint minimum height, so that the blueprint panel can grow below existing content + let max_recordings_height = (available_height - blueprint_min_height) + .max(recordings_min_height); + + egui::Panel::top("recording_panel") + .frame(egui::Frame::new()) + .resizable(resizable) + .show_separator_line(false) + .min_size(recordings_min_height) + .max_size(max_recordings_height) + .default_size(160.0_f32.max(recordings_min_height)) + .show(ui, |ui| { + recording_panel.show_panel( + app_ctx, + ui, + redap_servers, + welcome_screen_state.hide_examples, + ); + }); + } else { + recording_panel.show_panel( + app_ctx, + ui, + redap_servers, + welcome_screen_state.hide_examples, + ); + } + + if let Some((viewport_ui, ctx)) = viewport { + blueprint_tree.show(ctx, &viewport_ui.blueprint, ui, view_states); + } + } + + Route::ChunkStoreBrowser { .. } | Route::Settings { .. } => {} // handled above + } + }); + if let Some(left_panel_response) = left_panel_response { + left_panel_response.response.widget_info(|| { + egui::WidgetInfo::labeled(egui::WidgetType::Panel, true, "blueprint_panel") + }); + } + + // The user dragged the resize handle past the panel's limits to collapse/expand it: + if blueprint_panel_expanded != was_expanded { + app_blueprint.toggle_blueprint_panel(app_ctx.command_sender()); + } + } + pub fn time_control(&self, rec_id: &StoreId) -> Option<&TimeControl> { self.time_controls.get(rec_id) } @@ -847,6 +1009,24 @@ impl AppState { create_time_control_for(&mut self.time_controls, entity_db, blueprint_ctx) } + /// Tick time controls for all preview recordings shown in grid cards. + /// + /// All previews share a single playback clock in raw timeline units, so + /// nanoseconds for timestamp timelines and frame numbers for sequence timelines. + /// Shorter clips hold at their last frame while the longest one finishes, then + /// everything loops together. + pub fn update_preview_time_controls( + &mut self, + store_hub: &StoreHub, + stable_dt: f32, + ) -> re_viewer_context::NeedsRepaint { + if let Some(preview_state) = &mut self.view_states.preview_state { + preview_state.tick(|id| store_hub.entity_db(id), stable_dt) + } else { + re_viewer_context::NeedsRepaint::No + } + } + /// Remove dangling state pub fn cleanup(&mut self, store_hub: &StoreHub) { re_tracing::profile_function!(); @@ -856,6 +1036,10 @@ impl AppState { self.blueprint_undo_state .retain(|store_id, _| store_hub.store_bundle().contains(store_id)); + + if let Some(preview_state) = &mut self.view_states.preview_state { + preview_state.cleanup_recordings(|id| store_hub.store_bundle().contains(id)); + } } /// Returns the blueprint query that should be used for generating the current @@ -908,18 +1092,6 @@ impl AppState { } } -fn table_ui( - ctx: &StoreViewContext<'_>, - runtime: &AsyncRuntimeHandle, - ui: &mut Ui, - table_id: &TableId, - store: &TableStore, -) { - re_dataframe_ui::DataFusionTableWidget::new(store.session_context(), TableStore::TABLE_NAME) - .title(table_id.as_str()) - .show(ctx, runtime, ui); -} - pub(crate) fn create_time_control_for<'cfgs>( configs: &'cfgs mut HashMap, entity_db: &'_ EntityDb, @@ -931,11 +1103,11 @@ pub(crate) fn create_time_control_for<'cfgs>( ) -> TimeControl { let follow = if let Some(data_source) = &entity_db.data_source { match data_source { - // Potentially live data: - LogSource::File { follow, .. } | LogSource::HttpStream { follow, .. } => *follow, - // Not live data: - LogSource::RedapGrpcStream { .. } | LogSource::RrdWebEvent => false, + LogSource::File { .. } + | LogSource::HttpStream { .. } + | LogSource::RedapGrpcStream { .. } + | LogSource::RrdWebEvent => false, // Live data: LogSource::Sdk @@ -954,11 +1126,14 @@ pub(crate) fn create_time_control_for<'cfgs>( PlayState::Playing }; - let mut time_ctrl = TimeControl::from_blueprint(blueprint_ctx); - - time_ctrl.set_play_state(Some(entity_db), play_state, Some(blueprint_ctx)); - - time_ctrl + // Apply the data-source-derived default only if the blueprint did not + // already specify a `play_state`. Otherwise we'd clobber the user's + // setting (and write the clobbered value back to the blueprint). + TimeControl::from_blueprint_with_fallback_play_state( + blueprint_ctx, + Some(entity_db), + play_state, + ) } configs @@ -979,13 +1154,11 @@ fn check_for_clicked_hyperlinks(egui_ctx: &egui::Context, command_sender: &Comma &open_url.url, &re_data_source::FromUriOptions { accept_extensionless_http: false, - ..Default::default() }, ) { url.open( egui_ctx, &open_url::OpenUrlOptions { - follow: false, recording_open_behavior: if open_url.new_tab { RecordingOpenBehavior::Open } else { @@ -1021,7 +1194,10 @@ impl re_byte_size::MemUsageTreeCapture for AppState { "blueprint_undo_state", self.blueprint_undo_state.total_size_bytes(), ); - tree.add("view_states", self.view_states.total_size_bytes()); + tree.add( + "view_states", + re_byte_size::MemUsageTreeCapture::capture_mem_usage_tree(&self.view_states), + ); tree.into_tree() } } diff --git a/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs b/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs index af6f3504e9b1..470f9ba4b52b 100644 --- a/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs +++ b/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs @@ -12,6 +12,7 @@ pub use re_sdk_types::blueprint::components::AutoLayout; pub use re_sdk_types::blueprint::components::AutoScroll; pub use re_sdk_types::blueprint::components::AutoViews; pub use re_sdk_types::blueprint::components::BackgroundKind; +pub use re_sdk_types::blueprint::components::ColumnName; pub use re_sdk_types::blueprint::components::ColumnOrder; pub use re_sdk_types::blueprint::components::ColumnShare; pub use re_sdk_types::blueprint::components::ComponentColumnSelector; @@ -70,6 +71,7 @@ pub fn is_valid_blueprint(blueprint: &EntityDb) -> bool { && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) + && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) diff --git a/crates/viewer/re_viewer/src/command_palette.rs b/crates/viewer/re_viewer/src/command_palette.rs new file mode 100644 index 000000000000..f79849ca52dd --- /dev/null +++ b/crates/viewer/re_viewer/src/command_palette.rs @@ -0,0 +1,427 @@ +//! The viewer's command palette: a fuzzy-searchable list of commands +//! ([`UICommand`]s, commands acting on the active recording, and commands acting on the +//! selected Redap server), entity and component paths in the active recording, Redap servers +//! and their entries (datasets and tables) known to the viewer, and a fallback for opening any +//! URL or file path the user pastes. + +use std::task::Poll; + +use re_entity_db::EntityDb; +use re_log_types::{ComponentPath, EntityPath, EntryId}; +use re_redap_browser::RedapServers; +use re_ui::{ + CmdRow, CommandEnvironment, CommandPaletteProvider, FuzzyMatch, FuzzyQuery, MatchGroup, + MatchedCmd, RecordingCommand, RecordingCommandKind, RedapServerCommand, + SyntaxHighlighting as _, TableCommand, TableCommandKind, UICommand, +}; +use re_viewer_context::open_url::ViewerOpenUrl; + +use crate::open_url_description::ViewerOpenUrlDescription; + +/// Something the user can pick in the command palette. +#[derive(Clone, Debug)] +pub enum CommandPaletteAction { + /// Run a UI command. + UiCommand(UICommand), + + /// Run a command on a specific recording. + RecordingCommand(RecordingCommand), + + /// Run a command on the currently selected Redap server. + RedapServerCommand(RedapServerCommand), + + /// Select and focus an entity in the active recording. + SelectEntityPath(EntityPath), + + /// Select and focus a component of an entity in the active recording. + SelectComponentPath(ComponentPath), + + /// Select a Redap server known to the viewer. + SelectRedapServer(re_uri::Origin), + + /// Select an entry (dataset or table) on a Redap server known to the viewer. + SelectRedapEntry { + origin: re_uri::Origin, + entry_id: EntryId, + + /// The viewer is connected to more than one server, so the row should also show the + /// server this entry belongs to. + show_server: bool, + }, + + /// Run a command on the Redap entry (dataset or table) currently being viewed. + TableCommand(TableCommand), + + /// Open a URL (or file path). + /// + /// URL opening is the fallback for the command palette and needs some special treatment since + /// ui commands usually don't have arbitrary state. We keep the raw query string and let the + /// handler re-parse it, so this also covers file paths and schemeless URLs. + OpenUrl(String), +} + +impl CommandPaletteAction { + fn tooltip(&self) -> &'static str { + match self { + Self::UiCommand(command) => command.tooltip(), + Self::RecordingCommand(command) => command.kind.tooltip(), + Self::RedapServerCommand(command) => command.tooltip(), + Self::SelectEntityPath(_) => "Select and focus on this entity", + Self::SelectComponentPath(_) => "Select and focus on this component", + Self::SelectRedapServer(_) => "Select and navigate to this Redap server", + Self::SelectRedapEntry { .. } => "Select and navigate to this entry", + Self::TableCommand(command) => command.tooltip(), + Self::OpenUrl(_) => { + "Try to open this URL in the viewer. If the contents are already loaded, this will select them." + } + } + } +} + +/// Feeds the viewer's commands into the [`re_ui::CommandPalette`]. +pub struct CommandPaletteProviderImpl<'a> { + /// The active recording, if any. Provides entity-path completion. + pub recording: Option<&'a EntityDb>, + + /// All Redap servers known to the viewer. Provides server- and entry-name completion. + pub redap_servers: &'a RedapServers, + + /// Determines which commands are currently available. + pub cmd_env: CommandEnvironment, +} + +impl CommandPaletteProvider for CommandPaletteProviderImpl<'_> { + fn initial_hint_ui(&mut self, ui: &mut egui::Ui) { + if self.recording.is_some() { + ui.weak( + "Find a command, search for an entity, dataset or table, or enter a URL to open", + ); + } else { + ui.weak( + "Find a command, search for a server, dataset or table, or enter a URL to open", + ); + } + ui.add_space(4.0); + } + + fn all_matching(&mut self, query: &FuzzyQuery) -> Vec> { + re_tracing::profile_function!(); + use strum::IntoEnumIterator as _; + + let ui_cmd_group = if query.raw_query().starts_with('/') { + vec![] // The user is looking for an entity path. + } else { + let cmd_env = &self.cmd_env; + + // Helper to match a command against the query: + let match_command = |target_text: &str, enabled: bool, command| { + if query.is_empty() { + // Nothing entered yet: show all commands. + Some(MatchedCmd { + command, + fuzzy_match: FuzzyMatch::lowest(target_text.to_owned()), + enabled, + }) + } else { + query + .try_match(target_text.to_owned()) + .map(|fuzzy_match| MatchedCmd { + command, + fuzzy_match, + enabled, + }) + } + }; + + let mut matches: Vec<_> = UICommand::iter() + .filter_map(|command| { + match_command( + command.text(), + true, + CommandPaletteAction::UiCommand(command), + ) + }) + .collect(); + + // Commands acting on the active recording, if any: + if let Some(recording_id) = &cmd_env.recording { + for command in RecordingCommand::all_for_recording(recording_id) { + // `PlaybackSpeed` is a chord (type e.g. `5` then `0`), not a single + // action — as a palette entry it would just reset the speed to 1x. + if matches!(command.kind, RecordingCommandKind::PlaybackSpeed(_)) { + continue; + } + matches.extend(match_command( + command.kind.text(), + true, + CommandPaletteAction::RecordingCommand(command), + )); + } + } + + // Commands acting on the selected Redap server, if any: + if let Some(origin) = &cmd_env.redap_server { + for command in RedapServerCommand::all_for_server(origin) { + let enabled = + !command.requires_editable_server() || cmd_env.has_editable_redap_server; + matches.extend(match_command( + command.text(), + enabled, + CommandPaletteAction::RedapServerCommand(command), + )); + } + } + + // Commands acting on the Redap entry currently being viewed, if any: + for kind in TableCommandKind::iter() { + if let Some(command) = kind.for_environment(cmd_env) { + matches.extend(match_command( + command.text(), + true, + CommandPaletteAction::TableCommand(command), + )); + } + } + + matches + }; + + let entity_group = if query.is_empty() { + vec![] // Nothing entered yet: only show commands, no entities. + } else if let Some(recording) = self.recording { + let engine = recording.storage_engine(); + let schema = engine.store().schema(); + + // We fuzzy-match against the same (unescaped, syntax-highlight) text that + // `cmd_row` renders, so `FuzzyMatch::highlight_matching_text` lines up. + // The style doesn't affect the resulting text, so a default one is fine. + let style = egui::Style::default(); + + let mut matches = Vec::new(); + for entity_path in recording.sorted_entity_paths() { + if let Some(fuzzy_match) = + query.try_match(entity_path.syntax_highlighted(&style).text) + { + matches.push(MatchedCmd { + command: CommandPaletteAction::SelectEntityPath(entity_path.clone()), + fuzzy_match, + enabled: true, + }); + } + + // Also offer each component ever logged to this entity: + if let Some(components) = schema.all_components_for_entity(entity_path) { + for &component in components { + let component_path = ComponentPath::new(entity_path.clone(), component); + if let Some(fuzzy_match) = + query.try_match(component_path.syntax_highlighted(&style).text) + { + matches.push(MatchedCmd { + command: CommandPaletteAction::SelectComponentPath(component_path), + fuzzy_match, + enabled: true, + }); + } + } + } + } + matches + } else { + vec![] + }; + + // Redap servers and entries (datasets and tables) known to the viewer. + // Entries are grouped per server; if a server is selected, only its entries are offered. + // Skip when the user is clearly typing an entity path (leading `/`). + let (server_group, entry_groups) = if query.is_empty() || query.raw_query().starts_with('/') + { + (vec![], vec![]) + } else { + let selected_server = self.cmd_env.redap_server.as_ref(); + + // When entries from more than one server can show up, + // show which server each entry belongs to. + let show_server = + selected_server.is_none() && 1 < self.redap_servers.iter_servers().count(); + + let mut server_matches = Vec::new(); + let mut entry_groups: Vec> = Vec::new(); + for server in self.redap_servers.iter_servers() { + let origin = server.origin(); + if let Some(fuzzy_match) = query.try_match(origin.host.to_string()) { + server_matches.push(MatchedCmd { + command: CommandPaletteAction::SelectRedapServer(origin.clone()), + fuzzy_match, + enabled: true, + }); + } + + // If a server is selected, only offer entries from that server: + if selected_server.is_some_and(|selected| selected != origin) { + continue; + } + + if let Poll::Ready(Ok(entries)) = server.entries().state() { + // Offer every entry (datasets and tables) by name. + let mut entries: Vec<_> = entries.values().collect(); + entries.sort_by_key(|entry| entry.id()); + + let mut group = Vec::new(); + for entry in entries { + if let Some(fuzzy_match) = query.try_match(entry.name().to_string()) { + group.push(MatchedCmd { + command: CommandPaletteAction::SelectRedapEntry { + origin: origin.clone(), + entry_id: entry.id(), + show_server, + }, + fuzzy_match, + enabled: true, + }); + } + } + if !group.is_empty() { + entry_groups.push(group); + } + } + } + + (server_matches, entry_groups) + }; + + let raw_url = query.raw_query().trim(); + let url_group = if let Ok(open_url) = ViewerOpenUrl::parse_with_options( + raw_url, + &re_data_source::FromUriOptions { + accept_extensionless_http: true, + }, + ) { + // The user entered something openable (URL, file path, …). Offer to open it! + let command_text = format!("Open {}", ViewerOpenUrlDescription::from_url(&open_url)); + vec![MatchedCmd { + fuzzy_match: FuzzyMatch::highest(command_text), + command: CommandPaletteAction::OpenUrl(raw_url.to_owned()), + enabled: true, + }] + } else { + vec![] + }; + + itertools::chain!( + [ui_cmd_group, entity_group, server_group], + entry_groups, + [url_group], + ) + .collect() + } + + fn cmd_row( + &self, + ui: &egui::Ui, + matched: &MatchedCmd, + selected: bool, + ) -> CmdRow { + let kb_shortcut = match &matched.command { + CommandPaletteAction::UiCommand(command) => { + command.formatted_kb_shortcut(ui.ctx()).unwrap_or_default() + } + CommandPaletteAction::RecordingCommand(command) => command + .kind + .formatted_kb_shortcut(ui.ctx()) + .unwrap_or_default(), + CommandPaletteAction::RedapServerCommand(command) => command + .kind + .formatted_kb_shortcut(ui.ctx()) + .unwrap_or_default(), + CommandPaletteAction::TableCommand(command) => command + .kind + .formatted_kb_shortcut(ui.ctx()) + .unwrap_or_default(), + CommandPaletteAction::SelectEntityPath(_) + | CommandPaletteAction::SelectComponentPath(_) + | CommandPaletteAction::SelectRedapServer(_) + | CommandPaletteAction::SelectRedapEntry { .. } + | CommandPaletteAction::OpenUrl(_) => String::new(), + }; + + let text_color = if !matched.enabled { + ui.visuals().weak_text_color() + } else if selected { + ui.visuals().selection.stroke.color + } else { + ui.visuals().widgets.inactive.fg_stroke.color + }; + + // On the selected row the syntax colors clash with the selection background, + // so recolor the whole (syntax-highlighted) path to the selection text color. + // We keep the syntax-highlighted job either way, so the font/size stays the same. + let recolor_if_selected = |mut job: egui::text::LayoutJob| { + if selected { + for section in &mut job.sections { + section.format.color = text_color; + } + } + job + }; + + let job = match &matched.command { + CommandPaletteAction::SelectEntityPath(entity_path) => { + recolor_if_selected(entity_path.syntax_highlighted(ui.style())) + } + CommandPaletteAction::SelectComponentPath(component_path) => { + recolor_if_selected(component_path.syntax_highlighted(ui.style())) + } + CommandPaletteAction::UiCommand(_) + | CommandPaletteAction::RecordingCommand(_) + | CommandPaletteAction::RedapServerCommand(_) + | CommandPaletteAction::SelectRedapServer(_) + | CommandPaletteAction::SelectRedapEntry { .. } + | CommandPaletteAction::TableCommand(_) + | CommandPaletteAction::OpenUrl(_) => egui::text::LayoutJob::simple( + matched.fuzzy_match.target().to_owned(), + egui::TextStyle::Button.resolve(ui.style()), + text_color, + f32::INFINITY, + ), + }; + + let mut job = if matched.enabled { + // Only highlight the matched characters on available commands; + // unavailable ones stay uniformly grayed out. + // Otherwise the user may confusingly think the underlined command is the one that will be executed when they hit enter. + matched + .fuzzy_match + .highlight_matching_text(ui.style(), &job, selected) + } else { + job + }; + + // When connected to multiple servers, append the entry's server in weak text so it + // doesn't distract from (or fuzzy-match against) the entry name. + if let CommandPaletteAction::SelectRedapEntry { + origin, + show_server: true, + .. + } = &matched.command + { + job.append( + &format!(" {}", origin.host), + 0.0, + egui::TextFormat::simple( + egui::TextStyle::Button.resolve(ui.style()), + if selected { + text_color + } else { + ui.visuals().weak_text_color() + }, + ), + ); + } + + CmdRow { + job, + kb_shortcut, + tooltip: Some(matched.command.tooltip().to_owned()), + } + } +} diff --git a/crates/viewer/re_viewer/src/default_views.rs b/crates/viewer/re_viewer/src/default_views.rs index b7ca47cefa4b..8e9af0dbeee1 100644 --- a/crates/viewer/re_viewer/src/default_views.rs +++ b/crates/viewer/re_viewer/src/default_views.rs @@ -77,13 +77,11 @@ fn populate_view_class_registry_with_builtin( app_options, fallback_registry, )?; - if app_options.experimental.enable_status_view { - view_class_registry.add_class::( - reflection, - app_options, - fallback_registry, - )?; - } + view_class_registry.add_class::( + reflection, + app_options, + fallback_registry, + )?; Ok(()) } diff --git a/crates/viewer/re_viewer/src/event.rs b/crates/viewer/re_viewer/src/event.rs index 6a5970ce7891..7ae20a578868 100644 --- a/crates/viewer/re_viewer/src/event.rs +++ b/crates/viewer/re_viewer/src/event.rs @@ -13,6 +13,7 @@ use std::rc::Rc; use re_entity_db::EntityDb; use re_log_channel::LogSource; use re_log_types::{ApplicationId, RecordingId, TimeReal, Timeline, TimelineName}; +use re_sdk_types::SegmentId; use re_viewer_context::{ContainerId, Item, ItemCollection, ItemContext, ViewId}; use re_viewport_blueprint::ViewportBlueprint; @@ -26,7 +27,7 @@ pub struct ViewerEvent { #[serde(with = "serde::recording_id")] pub recording_id: RecordingId, - pub segment_id: Option, + pub segment_id: Option, #[serde(flatten)] pub kind: ViewerEventKind, diff --git a/crates/viewer/re_viewer/src/external_memory.rs b/crates/viewer/re_viewer/src/external_memory.rs new file mode 100644 index 000000000000..675b99c3fc23 --- /dev/null +++ b/crates/viewer/re_viewer/src/external_memory.rs @@ -0,0 +1,75 @@ +//! External users of memory, i.e things that allocate memory but +//! aren't accessible in this crate. Which is used for GC and +//! memory usage information. + +use re_byte_size::NamedMemUsageTree; + +/// Something contributing to application memory that is not visible in this +/// crate. +pub trait ExternalMemoryUser { + /// Will return `Some` while the memory user exists. After which + /// it will always return `None`. + fn capture(&mut self) -> Option; +} + +#[derive(Default)] +pub struct ExternalMemoryUsers { + users: Vec>, + + latest_capture: Vec, + total_external_memory: u64, +} + +impl ExternalMemoryUsers { + pub fn default_users() -> Self { + let mut this = Self::default(); + + struct AllocatorTrackingOverhead; + + impl ExternalMemoryUser for AllocatorTrackingOverhead { + fn capture(&mut self) -> Option { + re_memory::accounting_allocator::tracking_stats().map(|tracking_stats| { + NamedMemUsageTree::new( + "Allocator tracking", + tracking_stats.overhead.size as u64, + ) + }) + } + } + + this.add(Box::new(AllocatorTrackingOverhead)); + + this + } + + pub fn captured_trees(&self) -> &[NamedMemUsageTree] { + &self.latest_capture + } + + pub fn total_external_memory(&self) -> u64 { + self.total_external_memory + } + + /// Capture memory usage trees for all registered external memory users. + pub fn update(&mut self) { + re_tracing::profile_function!(); + + self.latest_capture.clear(); + + self.users.retain_mut(|user| { + if let Some(tree) = user.capture() { + self.latest_capture.push(tree); + + true + } else { + false + } + }); + + self.total_external_memory = self.latest_capture.iter().map(|t| t.size_bytes()).sum(); + } + + pub fn add(&mut self, user: Box) { + self.users.push(user); + } +} diff --git a/crates/viewer/re_viewer/src/headless.rs b/crates/viewer/re_viewer/src/headless.rs new file mode 100644 index 000000000000..79090b33cf33 --- /dev/null +++ b/crates/viewer/re_viewer/src/headless.rs @@ -0,0 +1,142 @@ +//! Headless viewer driven by [`egui_kittest`] instead of a real eframe window. +//! +//! Used for things like CI screenshot generation via `ViewerClient::save_screenshot`. + +use std::sync::Arc; +use std::time::Duration; + +use parking_lot::{Condvar, Mutex}; + +use crate::App; + +type AppCreator = Box) -> App>; + +/// Default headless viewport size (logical points). +const DEFAULT_HEADLESS_SIZE: (f32, f32) = (1920.0, 1080.0); + +/// Run the viewer in headless mode. +/// +/// Instead of opening a real OS window via `eframe::run_native`, this drives the +/// viewer through an `egui_kittest` harness backed by `wgpu`, repeatedly calling +/// `step()`. The gRPC server keeps running in the background just like in the +/// normal viewer, so SDK clients (including `save_screenshot`) work the same way. +/// +/// Blocks until the process is killed. +pub fn run_headless_app( + app_creator: AppCreator, + force_wgpu_backend: Option<&str>, + initial_size: Option, +) -> eframe::Result { + let size = initial_size + .unwrap_or_else(|| egui::vec2(DEFAULT_HEADLESS_SIZE.0, DEFAULT_HEADLESS_SIZE.1)); + + let wgpu_setup = crate::wgpu_options(force_wgpu_backend).wgpu_setup; + + // Signal flipped to `true` whenever something calls `ctx.request_repaint()`. + // The headless loop uses this to wake up early instead of waiting the full + // 1s idle tick — keeps animations and incoming gRPC data feeling snappy + // while still letting an idle viewer sleep most of the time. + let repaint_signal: Arc<(Mutex, Condvar)> = Arc::new((Mutex::new(false), Condvar::new())); + + let mut init_result = Ok(()); + let init_result_mut = &mut init_result; + + let mut harness = { + let repaint_signal = repaint_signal.clone(); + egui_kittest::Harness::::builder() + .with_size(size) + .wgpu_setup(wgpu_setup) + .build_eframe(move |cc| { + let repaint_signal = repaint_signal.clone(); + cc.egui_ctx.set_request_repaint_callback(move |_info| { + let (lock, cvar) = &*repaint_signal; + *lock.lock() = true; + cvar.notify_all(); + }); + *init_result_mut = crate::customize_eframe_and_setup_renderer(cc); + app_creator(cc) + }) + }; + + init_result.map_err(|err| eframe::Error::AppCreation(Box::new(err)))?; + + re_log::info!("Headless viewer running at {}x{}.", size.x, size.y); + + let idle_timeout = Duration::from_secs(1); + loop { + harness.step(); + handle_pending_screenshots(&mut harness); + + if has_pending_close(&harness) { + re_log::info!("Headless viewer received close request, shutting down."); + return Ok(()); + } + + let (lock, cvar) = &*repaint_signal; + let mut signaled = lock.lock(); + if !*signaled { + cvar.wait_for(&mut signaled, idle_timeout); + } + *signaled = false; + } +} + +/// Detect `ViewportCommand::Close` in this frame's viewport output. +/// +/// `UICommand::Quit` (and the Ctrl-C handler) ultimately send +/// `ViewportCommand::Close`. In a normal `eframe::run_native` setup the +/// windowing backend consumes that and exits the event loop. `kittest` +/// ignores viewport commands, so we have to detect `Close` here and break +/// out of the headless loop ourselves. +fn has_pending_close(harness: &egui_kittest::Harness<'_, App>) -> bool { + harness + .output() + .viewport_output + .values() + .flat_map(|v| v.commands.iter()) + .any(|cmd| matches!(cmd, egui::ViewportCommand::Close)) +} + +/// Bridge [`egui::ViewportCommand::Screenshot`] requests through `kittest`'s +/// offscreen renderer. +/// +/// In a normal `eframe::run_native` setup, the windowing backend captures the +/// framebuffer after a screenshot command and emits an +/// [`egui::Event::Screenshot`] that the viewer's `App` listens for. `kittest` +/// doesn't process viewport commands itself, so we have to do that translation +/// here, otherwise `save_screenshot` requests would be silently dropped. +fn handle_pending_screenshots(harness: &mut egui_kittest::Harness<'_, App>) { + let pending: Vec = harness + .output() + .viewport_output + .values() + .flat_map(|v| v.commands.iter()) + .filter_map(|cmd| match cmd { + egui::ViewportCommand::Screenshot(user_data) => Some(user_data.clone()), + _ => None, + }) + .collect(); + + if pending.is_empty() { + return; + } + + let rgba = match harness.render() { + Ok(rgba) => rgba, + Err(err) => { + re_log::error!("Failed to render headless screenshot: {err}"); + return; + } + }; + let size = [rgba.width() as usize, rgba.height() as usize]; + let pixels = rgba.into_raw(); + let color_image = Arc::new(egui::ColorImage::from_rgba_premultiplied(size, &pixels)); + + for user_data in pending { + harness.event(egui::Event::Screenshot { + viewport_id: egui::ViewportId::ROOT, + user_data, + image: color_image.clone(), + }); + } +} diff --git a/crates/viewer/re_viewer/src/history.rs b/crates/viewer/re_viewer/src/history.rs index c2751e549699..c22c25b4de23 100644 --- a/crates/viewer/re_viewer/src/history.rs +++ b/crates/viewer/re_viewer/src/history.rs @@ -33,6 +33,13 @@ impl History { } } + /// The closest earlier entry satisfying `predicate`. + pub fn find_back(&self, predicate: impl Fn(&ViewerOpenUrl) -> bool) -> Option<&ViewerOpenUrl> { + self.entries[..self.current_entry] + .iter() + .rfind(|url| predicate(url)) + } + /// Goes back in history, returning the new url to open. pub fn go_back(&mut self) -> Option<&ViewerOpenUrl> { self.current_entry = self.current_entry.checked_sub(1)?; diff --git a/crates/viewer/re_viewer/src/internal_catalog.rs b/crates/viewer/re_viewer/src/internal_catalog.rs new file mode 100644 index 000000000000..e6ca89c89f39 --- /dev/null +++ b/crates/viewer/re_viewer/src/internal_catalog.rs @@ -0,0 +1,78 @@ +//! The in-process "internal catalog" [`re_server`]. +//! +//! The app hosts a single in-process [`re_server`] (the "internal catalog"). +//! The viewer then loads local resources by registering them with that catalog and opening the +//! resulting redap segment URI, instead of importing them directly. +//! +//! The viewer talks to the catalog in-process via [`InternalCatalog::connection`]. +//! On native, the same handler is also served on the proxy server's port (see +//! [`InternalCatalog::grpc_service`]) so that other local processes can reach it. +//! The served endpoint is restricted to connections from the local machine. + +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; + +use re_redap_client::Connection; +use re_server::RerunCloudHandlerBuilder; + +#[cfg(not(target_arch = "wasm32"))] +use { + re_protos::cloud::v1alpha1::rerun_cloud_service_server::RerunCloudServiceServer, + re_server::RerunCloudHandler, +}; + +/// The in-process internal catalog. +pub struct InternalCatalog { + /// The origin under which the catalog is registered. + pub origin: re_uri::Origin, + + /// The in-process connection the viewer uses to talk to the catalog. + pub connection: Connection, + + /// The single handler shared between [`Self::connection`] and [`Self::grpc_service`]. + #[cfg(not(target_arch = "wasm32"))] + handler: Arc, +} + +impl InternalCatalog { + /// The catalog as a gRPC service, to be served (loopback-only) on the proxy server's port. + #[cfg(not(target_arch = "wasm32"))] + pub fn grpc_service(&self) -> RerunCloudServiceServer { + RerunCloudServiceServer::from_arc(self.handler.clone()) + .max_decoding_message_size(re_redap_client::MAX_DECODING_MESSAGE_SIZE) + } +} + +/// Build the in-process internal catalog, addressed at the proxy server's port. +#[cfg(not(target_arch = "wasm32"))] +pub fn build(proxy_addr: SocketAddr) -> InternalCatalog { + let origin = re_uri::Origin::from_scheme_and_socket_addr( + re_uri::Scheme::RerunHttp, + SocketAddr::from((Ipv4Addr::LOCALHOST, proxy_addr.port())), + ); + + let handler = Arc::new(RerunCloudHandlerBuilder::new().build()); + let connection = Connection::from_service(handler.clone()); + + InternalCatalog { + origin, + connection, + handler, + } +} + +/// Build the in-process internal catalog. +#[cfg(target_arch = "wasm32")] +pub fn build() -> InternalCatalog { + let handler = Arc::new(RerunCloudHandlerBuilder::new().build()); + let connection = Connection::from_service(handler); + + // The Wasm catalog lives purely in-process; the loopback address is a stable identity for the + // in-memory handler (matching the native construction), not a reachable endpoint. + let origin = re_uri::Origin::from_scheme_and_socket_addr( + re_uri::Scheme::RerunHttp, + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + ); + + InternalCatalog { origin, connection } +} diff --git a/crates/viewer/re_viewer/src/latency_tracker.rs b/crates/viewer/re_viewer/src/latency_tracker.rs index 938ec12c11b0..263f5265d9b6 100644 --- a/crates/viewer/re_viewer/src/latency_tracker.rs +++ b/crates/viewer/re_viewer/src/latency_tracker.rs @@ -19,30 +19,22 @@ pub enum LatencyResult { MostRecent(web_time::Duration), } -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] struct InnerState { accessed: bool, update_in_progress: bool, has_error: bool, last_latency: Option, + + #[size_bytes(ignore)] // No size bytes impl. last_update_time: Option, } -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] struct LatencyTracker { pub inner: Mutex, } -impl re_byte_size::SizeBytes for LatencyTracker { - fn heap_size_bytes(&self) -> u64 { - 0 - } - - fn is_pod() -> bool { - true - } -} - impl LatencyTracker { fn should_update(&self) -> bool { let lock = self.inner.lock(); diff --git a/crates/viewer/re_viewer/src/lib.rs b/crates/viewer/re_viewer/src/lib.rs index 1dacec0b394a..6fa0d22d02cc 100644 --- a/crates/viewer/re_viewer/src/lib.rs +++ b/crates/viewer/re_viewer/src/lib.rs @@ -31,10 +31,12 @@ mod app; mod app_blueprint; mod app_state; mod background_tasks; +mod command_palette; mod default_views; mod docker_detection; pub mod env_vars; pub mod event; +mod external_memory; mod history; mod latency_tracker; mod navigation; @@ -53,6 +55,8 @@ mod viewer_analytics; #[cfg(not(target_arch = "wasm32"))] pub mod viewer_test_utils; +pub mod internal_catalog; + #[cfg(not(target_arch = "wasm32"))] mod loading; @@ -66,13 +70,15 @@ pub mod blueprint; pub use app::App; pub(crate) use app_state::AppState; pub use event::{SelectionChangeItem, ViewerEvent, ViewerEventKind}; +pub use external_memory::ExternalMemoryUser; pub use re_capabilities::MainThreadToken; pub use re_viewer_context::{ AsyncRuntimeHandle, CommandReceiver, CommandSender, SystemCommand, SystemCommandSender, command_channel, }; pub use startup_options::{LoginOptions, StartupOptions}; -pub(crate) use ui::memory_panel; +pub use ui::about_rerun_ui; +pub(crate) use ui::dev_panel; pub mod external { pub use re_chunk::external::*; @@ -82,7 +88,7 @@ pub mod external { pub use { eframe, egui, parking_lot, re_chunk, re_chunk_store, re_data_ui, re_entity_db, re_log, re_log_channel, re_log_types, re_memory, re_renderer, re_sdk_types, re_ui, re_view, - re_view_spatial, re_viewer_context, re_viewport, + re_view_spatial, re_viewer_context, re_viewport, re_viewport_blueprint, }; } @@ -94,6 +100,11 @@ pub mod native; #[cfg(not(target_arch = "wasm32"))] pub use native::run_native_app; +#[cfg(not(target_arch = "wasm32"))] +pub mod headless; +#[cfg(not(target_arch = "wasm32"))] +pub use headless::run_headless_app; + // ---------------------------------------------------------------------------- // When compiling for web: @@ -229,6 +240,15 @@ pub(crate) fn wgpu_options(force_wgpu_backend: Option<&str>) -> egui_wgpu::WgpuC ..egui_wgpu::WgpuSetupCreateNew::without_display_handle() }), + + surface: egui_wgpu::SurfaceConfig { + // Explicitly stick with wgpu's latency default which is more optimized for high throughput than + // what egui may have in mind. + desired_maximum_frame_latency: None, + + ..egui_wgpu::SurfaceConfig::HIGH_THROUGHPUT + }, + ..Default::default() } } @@ -324,10 +344,5 @@ pub fn reset_viewer_persistence() -> anyhow::Result<()> { /// Hook into [`re_log`] to receive copies of text log messages on a channel, /// which we will then show in the notification panel. pub fn register_text_log_receiver() -> crossbeam::channel::Receiver { - let (logger, text_log_rx) = re_log::ChannelLogger::new(re_log::LevelFilter::Info); - if re_log::add_boxed_logger(Box::new(logger)).is_err() { - // This can happen when users wrap re_viewer in their own eframe app. - re_log::info!("re_log not initialized. You won't see log messages as GUI notifications."); - } - text_log_rx + re_log::add_log_msg_receiver(re_log::LevelFilter::INFO) } diff --git a/crates/viewer/re_viewer/src/loading.rs b/crates/viewer/re_viewer/src/loading.rs index 96faf0c2d829..89ab5cf63649 100644 --- a/crates/viewer/re_viewer/src/loading.rs +++ b/crates/viewer/re_viewer/src/loading.rs @@ -19,10 +19,7 @@ pub fn load_blueprint_file(path: &std::path::Path) -> Option { let file = std::fs::File::open(path)?; let reader = std::io::BufReader::new(file); - let data_source = re_log_channel::LogSource::File { - path: path.into(), - follow: false, - }; + let data_source = re_log_channel::LogSource::File { path: path.into() }; Ok(StoreBundle::from_rrd(reader, &data_source)?) } diff --git a/crates/viewer/re_viewer/src/native.rs b/crates/viewer/re_viewer/src/native.rs index 24d8efb13489..a90e86f46652 100644 --- a/crates/viewer/re_viewer/src/native.rs +++ b/crates/viewer/re_viewer/src/native.rs @@ -35,18 +35,19 @@ pub fn run_native_app( pub fn eframe_options(force_wgpu_backend: Option<&str>) -> eframe::NativeOptions { re_tracing::profile_function!(); let os = egui::os::OperatingSystem::default(); + let custom_window_decorations = re_ui::supports_custom_decorations(os); eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_app_id(APP_ID) // Controls where on disk the app state is persisted - .with_decorations(!re_ui::CUSTOM_WINDOW_DECORATIONS) // Maybe hide the OS-specific "chrome" around the window + .with_decorations(!custom_window_decorations) // Maybe hide the OS-specific "chrome" around the window .with_fullsize_content_view(re_ui::fullsize_content(os)) .with_icon(icon_data()) .with_inner_size([1600.0, 1200.0]) .with_min_inner_size([320.0, 450.0]) // Should be high enough to fit the rerun menu .with_title_shown(!re_ui::fullsize_content(os)) - .with_titlebar_buttons_shown(!re_ui::CUSTOM_WINDOW_DECORATIONS) + .with_titlebar_buttons_shown(!custom_window_decorations) .with_titlebar_shown(!re_ui::fullsize_content(os)) - .with_transparent(re_ui::CUSTOM_WINDOW_DECORATIONS), // To have rounded corners without decorations we need transparency + .with_transparent(custom_window_decorations), // To have rounded corners without decorations we need transparency on Linux. On Windows this mostly affects resizing which looks a bit better with this. renderer: eframe::Renderer::Wgpu, wgpu_options: crate::wgpu_options(force_wgpu_backend), @@ -60,16 +61,18 @@ pub fn eframe_options(force_wgpu_backend: Option<&str>) -> eframe::NativeOptions fn icon_data() -> egui::IconData { re_tracing::profile_function!(); - cfg_if::cfg_if! { - if #[cfg(target_os = "macos")] { + cfg_select! { + target_os = "macos" => { let app_icon_png_bytes = include_bytes!("../data/app_icon_mac.png"); - } else if #[cfg(target_os = "windows")] { - let app_icon_png_bytes = include_bytes!("../data/app_icon_windows.png"); - } else { + } + target_os = "windows" => { + let app_icon_png_bytes = include_bytes!("../data/app_icon.png"); + } + _ => { // Use the same icon for X11 as for Windows, at least for now. - let app_icon_png_bytes = include_bytes!("../data/app_icon_windows.png"); + let app_icon_png_bytes = include_bytes!("../data/app_icon.png"); } - }; + } // We include the .png with `include_bytes`. If that fails, things are extremely broken. match eframe::icon_data::from_png_bytes(app_icon_png_bytes) { diff --git a/crates/viewer/re_viewer/src/open_url_description.rs b/crates/viewer/re_viewer/src/open_url_description.rs index 79a0d80d90b8..e5fd23601d71 100644 --- a/crates/viewer/re_viewer/src/open_url_description.rs +++ b/crates/viewer/re_viewer/src/open_url_description.rs @@ -1,4 +1,3 @@ -use re_ui::CommandPaletteUrl; use re_viewer_context::open_url::ViewerOpenUrl; /// A description of what happens when opening a [`ViewerOpenUrl`]. @@ -35,7 +34,7 @@ impl ViewerOpenUrlDescription { let rrd_file_name = path.split('/').next_back().map(|s| s.to_owned()); Self { - category: "From http link", + category: "HTTP url", target_short: rrd_file_name, } } @@ -48,7 +47,7 @@ impl ViewerOpenUrlDescription { ViewerOpenUrl::RedapDatasetSegment(uri) => Self { category: "Segment", - target_short: Some(uri.segment_id.clone()), + target_short: Some(uri.segment_id.to_string()), }, ViewerOpenUrl::RedapProxy(_) => Self { @@ -62,7 +61,7 @@ impl ViewerOpenUrlDescription { }, ViewerOpenUrl::RedapEntry(uri) => Self { - category: "Redap Entry", + category: "Redap entry", target_short: Some(uri.entry_id.to_string()), }, @@ -99,19 +98,3 @@ impl ViewerOpenUrlDescription { } } } - -pub fn command_palette_parse_url(url: &str) -> Option { - let open_url = ViewerOpenUrl::parse_with_options( - url, - &re_data_source::FromUriOptions { - accept_extensionless_http: true, - ..Default::default() - }, - ) - .ok()?; - - Some(CommandPaletteUrl { - url: url.to_owned(), - command_text: format!("Open {}", ViewerOpenUrlDescription::from_url(&open_url)), - }) -} diff --git a/crates/viewer/re_viewer/src/prefetch_chunks.rs b/crates/viewer/re_viewer/src/prefetch_chunks.rs index a12192584aec..f78a40573e4f 100644 --- a/crates/viewer/re_viewer/src/prefetch_chunks.rs +++ b/crates/viewer/re_viewer/src/prefetch_chunks.rs @@ -213,19 +213,16 @@ fn make_load_fn<'a>( let fut = async move { let mut client = connection_registry.client(origin).await.map_err(|err| { - re_log::warn_once!("Failed to connect to remote: {err}"); + re_log::warn_once!("Failed to connect to server: {err}"); })?; load_chunks(&mut client, &rb).await.map_err(|err| { re_log::warn_once!("{err}"); }) }; - cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - poll_promise::Promise::spawn_local(fut) - } else { - poll_promise::Promise::spawn_async(fut) - } + cfg_select! { + target_arch = "wasm32" => poll_promise::Promise::spawn_local(fut), + _ => poll_promise::Promise::spawn_async(fut), } } } diff --git a/crates/viewer/re_viewer/src/screenshotter.rs b/crates/viewer/re_viewer/src/screenshotter.rs index 3d5e5ed07a04..98bf5b09c8e9 100644 --- a/crates/viewer/re_viewer/src/screenshotter.rs +++ b/crates/viewer/re_viewer/src/screenshotter.rs @@ -1,6 +1,12 @@ //! Screenshotting not implemented on web yet because we //! haven't implemented "copy image to clipboard" there. +/// Marker attached as [`egui::UserData`] to the full-app screenshot request, so we can identify +/// the resulting [`egui::Event::Screenshot`] as ours. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FullAppScreenshot; + /// Helper for screenshotting the entire app #[cfg(not(target_arch = "wasm32"))] #[derive(Default)] @@ -57,7 +63,9 @@ impl Screenshotter { // is done and transferred to ram. // Obviously we want to send the command this command only once, so we keep counting down // to negatives until we get a call to `save` which then disables the counter. - egui_ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(Default::default())); + egui_ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::new( + FullAppScreenshot, + ))); } *countdown -= 1; diff --git a/crates/viewer/re_viewer/src/startup_options.rs b/crates/viewer/re_viewer/src/startup_options.rs index b33a5442f76d..87a4fc047ef6 100644 --- a/crates/viewer/re_viewer/src/startup_options.rs +++ b/crates/viewer/re_viewer/src/startup_options.rs @@ -106,9 +106,6 @@ pub struct StartupOptions { /// * notebooks & native: use rerun.io/viewer with the crate's last known stable version /// * web viewers: use the url of the page it is embedded in pub viewer_base_url: Option, - - /// Enable the experimental Status view. - pub enable_experimental_status_view: bool, } impl StartupOptions { @@ -141,7 +138,7 @@ impl StartupOptions { /// The url to use for the web viewer when sharing links. #[allow(clippy::allow_attributes, clippy::unused_self)] // Only used on web. pub fn web_viewer_base_url(&self) -> Option { - // TODO(RR-1878): Would be great to grab this from the Data Platform when available. + // TODO(RR-1878): Would be great to grab this from the catalog server when available. if let Some(url) = &self.viewer_base_url && let Ok(url) = url.parse::() @@ -204,8 +201,6 @@ impl Default for StartupOptions { login: None, viewer_base_url: None, - - enable_experimental_status_view: false, } } } diff --git a/crates/viewer/re_viewer/src/texture_readback.rs b/crates/viewer/re_viewer/src/texture_readback.rs index ba8ced71f52b..a6e2cedc86b3 100644 --- a/crates/viewer/re_viewer/src/texture_readback.rs +++ b/crates/viewer/re_viewer/src/texture_readback.rs @@ -2,19 +2,19 @@ use image::{ExtendedColorType, ImageEncoder as _}; use re_renderer::{external::wgpu::TextureFormat, texture_readback::TextureReadbackId}; -use re_viewer_context::CommandSender; +use re_viewer_context::{CommandSender, DownloadAction}; /// Keeps track of textures we're reading back, and prompts the user to save them /// when they're done. #[derive(Default)] pub struct TextureReadbacks { - active_readbacks: Vec, + active_readbacks: Vec<(TextureReadbackId, DownloadAction)>, } impl TextureReadbacks { /// Push a new texture readback to keep track of. - pub fn push(&mut self, id: TextureReadbackId) { - self.active_readbacks.push(id); + pub fn push(&mut self, id: TextureReadbackId, action: DownloadAction) { + self.active_readbacks.push((id, action)); } /// Polls if there are any readback textures done, and if so prompt the user to save them. @@ -23,28 +23,49 @@ impl TextureReadbacks { render_ctx: &re_renderer::RenderContext, ui: &egui::Ui, command_sender: &CommandSender, + notifications: &mut re_ui::notifications::NotificationUi, ) { - self.active_readbacks.retain(|id| { + self.active_readbacks.retain(|(id, action)| { if let Some(readback) = re_renderer::poll_read_texture(render_ctx, *id) { let Some(color_type) = texture_format_to_color_type(readback.format) else { re_log::warn!("Can't download texture with format {:?}", readback.format); return false; }; - let mut png_bytes = Vec::new(); - if let Err(err) = image::codecs::png::PngEncoder::new(&mut png_bytes).write_image( - &readback.data, - readback.extent.width, - readback.extent.height, - color_type, - ) { - re_log::error!("Failed to encode preview image as PNG: {err}"); - } else { - command_sender.save_file_dialog( - re_capabilities::MainThreadToken::from_egui_ui(ui), - "preview.png", - "Preview Image".to_owned(), - png_bytes, - ); + match action { + DownloadAction::CopyToClipboard => { + let size = [ + readback.extent.width as usize, + readback.extent.height as usize, + ]; + let data = &readback.data; + + let Some(image) = to_color_image(color_type, size, data) else { + return false; + }; + + ui.copy_image(image); + notifications.success("Copied image to clipboard"); + } + DownloadAction::Save => { + let mut png_bytes = Vec::new(); + if let Err(err) = image::codecs::png::PngEncoder::new(&mut png_bytes) + .write_image( + &readback.data, + readback.extent.width, + readback.extent.height, + color_type, + ) + { + re_log::error!("Failed to encode preview image as PNG: {err}"); + } else { + command_sender.save_file_dialog( + re_capabilities::MainThreadToken::from_egui_ui(ui), + "preview.png", + "Preview Image".to_owned(), + png_bytes, + ); + } + } } false @@ -60,6 +81,117 @@ impl TextureReadbacks { } } +/// Convert a raw image to a [`egui::ColorImage`]. +/// +/// As [`egui::ColorImage`] is always 8 bit channels this sometimes looses +/// precision and warns in those cases. +fn to_color_image( + color_type: ExtendedColorType, + size: [usize; 2], + data: &[u8], +) -> Option { + Some(match color_type { + ExtendedColorType::A8 | ExtendedColorType::L8 => egui::ColorImage::from_gray(size, data), + + ExtendedColorType::L16 => { + re_log::warn!("16 bit image copied as 8 bit image, some precision was lost."); + + egui::ColorImage::from_gray_iter( + size, + data.chunks_exact(2).map(|slice| { + let pixel = u16::from_ne_bytes(slice.try_into().expect("we use chunks_exact")); + + // Divide by 2 ^ 8 to convert 16 bit to 8 bit. + // + // Which means we lose some detail when copying. + (pixel >> 8) as u8 + }), + ) + } + + ExtendedColorType::Rgb8 => egui::ColorImage::from_rgb(size, data), + ExtendedColorType::Rgba8 => egui::ColorImage::from_rgba_unmultiplied(size, data), + + ExtendedColorType::Bgr8 => egui::ColorImage::from_rgb( + size, + &data + .chunks_exact(3) + .flat_map(|slice| [slice[2], slice[1], slice[0]]) + .collect::>(), + ), + + ExtendedColorType::Bgra8 => egui::ColorImage::from_rgb( + size, + &data + .chunks_exact(4) + .flat_map(|slice| [slice[2], slice[1], slice[0], slice[3]]) + .collect::>(), + ), + + ExtendedColorType::Rgb16 => { + re_log::warn!("16 bit image copied as 8 bit image, some precision was lost."); + + egui::ColorImage::from_rgb( + size, + &data + .chunks_exact(6) + .flat_map(|slice| { + let r = u16::from_ne_bytes( + slice[0..2].try_into().expect("we use chunks_exact"), + ); + let g = u16::from_ne_bytes( + slice[2..4].try_into().expect("we use chunks_exact"), + ); + let b = u16::from_ne_bytes( + slice[4..6].try_into().expect("we use chunks_exact"), + ); + + // Divide by 2 ^ 8 to convert 16 bit to 8 bit. + // + // Which means we lose some detail when copying. + [r, g, b].map(|e| (e >> 8) as u8) + }) + .collect::>(), + ) + } + + ExtendedColorType::Rgba16 => { + re_log::warn!("16 bit image copied as 8 bit image, some precision was lost."); + + egui::ColorImage::from_rgb( + size, + &data + .chunks_exact(8) + .flat_map(|slice| { + let r = u16::from_ne_bytes( + slice[0..2].try_into().expect("we use chunks_exact"), + ); + let g = u16::from_ne_bytes( + slice[2..4].try_into().expect("we use chunks_exact"), + ); + let b = u16::from_ne_bytes( + slice[4..6].try_into().expect("we use chunks_exact"), + ); + let a = u16::from_ne_bytes( + slice[6..8].try_into().expect("we use chunks_exact"), + ); + + // Divide by 2 ^ 8 to convert 16 bit to 8 bit. + // + // Which means we lose some detail when copying. + [r, g, b, a].map(|e| (e >> 8) as u8) + }) + .collect::>(), + ) + } + + _ => { + re_log::error!("Can't copy textures with color type `{color_type:?}`"); + return None; + } + }) +} + fn texture_format_to_color_type( format: re_renderer::external::wgpu::TextureFormat, ) -> Option { diff --git a/crates/viewer/re_viewer/src/ui/memory_panel/chunk_event_stats.rs b/crates/viewer/re_viewer/src/ui/dev_panel/chunk_event_stats.rs similarity index 100% rename from crates/viewer/re_viewer/src/ui/memory_panel/chunk_event_stats.rs rename to crates/viewer/re_viewer/src/ui/dev_panel/chunk_event_stats.rs diff --git a/crates/viewer/re_viewer/src/ui/memory_panel/memory_history.rs b/crates/viewer/re_viewer/src/ui/dev_panel/memory_history.rs similarity index 99% rename from crates/viewer/re_viewer/src/ui/memory_panel/memory_history.rs rename to crates/viewer/re_viewer/src/ui/dev_panel/memory_history.rs index 6b6690a85381..18d3f8af602c 100644 --- a/crates/viewer/re_viewer/src/ui/memory_panel/memory_history.rs +++ b/crates/viewer/re_viewer/src/ui/dev_panel/memory_history.rs @@ -96,6 +96,7 @@ impl MemoryHistory { for (store_id, stats) in store_stats { let StoreStats { + store_source: _, store_config: _, store_stats, query_cache_stats, diff --git a/crates/viewer/re_viewer/src/ui/memory_panel/mod.rs b/crates/viewer/re_viewer/src/ui/dev_panel/mod.rs similarity index 83% rename from crates/viewer/re_viewer/src/ui/memory_panel/mod.rs rename to crates/viewer/re_viewer/src/ui/dev_panel/mod.rs index e960bb45861d..fb3e4b6113c1 100644 --- a/crates/viewer/re_viewer/src/ui/memory_panel/mod.rs +++ b/crates/viewer/re_viewer/src/ui/dev_panel/mod.rs @@ -3,18 +3,22 @@ mod memory_history; mod plot_utils; mod server_streaming_tab; mod streaming_history; +mod transform_cache_ui; +use ahash::HashMap; +use egui_plot::HoverPosition; use plot_utils::history_to_plot; use re_chunk_store::{ChunkStoreChunkStats, ChunkStoreConfig, ChunkStoreStats}; use re_entity_db::StoreBundle; use re_format::{format_bytes, format_uint}; +use re_log_types::StoreId; use re_memory::MemoryLimit; use re_memory::util::sec_since_start; use re_query::{QueryCacheStats, QueryCachesStats}; use re_renderer::WgpuResourcePoolStatistics; use re_ui::UiExt as _; -use re_viewer_context::StorageContext; use re_viewer_context::store_hub::StoreHubStats; +use re_viewer_context::{ActiveStoreContext, StorageContext, TimeControl}; use crate::env_vars::RERUN_TRACK_ALLOCATIONS; use memory_history::MemoryHistory; @@ -22,9 +26,9 @@ use streaming_history::StreamingHistory; // ---------------------------------------------------------------------------- -/// Which view to show in the memory panel. +/// Which view to show in the dev panel. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, strum_macros::EnumIter)] -enum MemoryViewTab { +enum DevPanelTab { #[default] Flamegraph, @@ -37,9 +41,11 @@ enum MemoryViewTab { AllocationTracking, Gpu, + + TransformCache, } -impl MemoryViewTab { +impl DevPanelTab { fn label(&self) -> &'static str { match self { Self::Flamegraph => "Flamegraph", @@ -48,20 +54,28 @@ impl MemoryViewTab { Self::Streaming => "Server streaming", Self::AllocationTracking => "Allocation tracking", Self::Gpu => "GPU", + Self::TransformCache => "Transform cache", } } } #[derive(Default)] -pub struct MemoryPanel { +pub struct DevPanel { history: MemoryHistory, streaming_history: StreamingHistory, memory_purge_times: Vec, - selected_tab: MemoryViewTab, + selected_tab: DevPanelTab, include_rss_in_flamegraph: bool, + transform_cache_state: transform_cache_ui::TransformCacheUiState, +} + +#[derive(Default)] +pub struct DevPanelResponse { + pub close_requested: bool, + pub repaint_requested: bool, } -impl MemoryPanel { +impl DevPanel { /// Call once per frame pub fn update( &mut self, @@ -91,57 +105,79 @@ impl MemoryPanel { ui: &mut egui::Ui, limit: &MemoryLimit, mem_usage_tree: Option, + external_trees: &[re_byte_size::NamedMemUsageTree], gpu_resource_stats: &WgpuResourcePoolStatistics, store_stats: Option<&StoreHubStats>, + store_context: Option<&ActiveStoreContext<'_>>, + time_controls: &HashMap, storage_context: &StorageContext<'_>, - ) { + ) -> DevPanelResponse { re_tracing::profile_function!(); // We show realtime stats, so keep showing the latest! - ui.request_repaint(); + // Specific dev panel tabs can opt-out of this below, for resource efficiency if it's not necessary. + let mut request_repaint = true; ui.add_space(4.0); // Tab selector at the top - ui.horizontal_wrapped(|ui| { - use strum::IntoEnumIterator as _; - for tab in MemoryViewTab::iter() { - ui.selectable_value(&mut self.selected_tab, tab, tab.label()); - } - }); + let ((), close_clicked) = egui::Sides::new().shrink_left().show( + ui, + |ui| { + use strum::IntoEnumIterator as _; + for tab in DevPanelTab::iter() { + ui.selectable_value(&mut self.selected_tab, tab, tab.label()); + } + }, + |ui| { + ui.small_icon_button(&re_ui::icons::CLOSE, "Close dev panel") + .clicked() + }, + ); ui.separator(); match self.selected_tab { - MemoryViewTab::Flamegraph => { - memory_tree_ui(ui, mem_usage_tree, &mut self.include_rss_in_flamegraph); + DevPanelTab::Flamegraph => { + memory_tree_ui( + ui, + mem_usage_tree, + external_trees, + &mut self.include_rss_in_flamegraph, + ); } - MemoryViewTab::TimeGraph => { + DevPanelTab::TimeGraph => { ui.label("🗠 Rerun Viewer memory use over time"); self.plot(ui, limit); } - MemoryViewTab::Stores => { + DevPanelTab::Stores => { egui::ScrollArea::vertical() .auto_shrink(false) .show(ui, |ui| { Self::store_stats_ui(ui, store_stats); }); } - MemoryViewTab::Streaming => { + DevPanelTab::TransformCache => { + self.transform_cache_ui(ui, store_context, time_controls, storage_context); + // Normal repaint behavior of the viewer is enough for the transform debugger, + // no need to constantly trigger an expensive repaint. + request_repaint = false; + } + DevPanelTab::Streaming => { server_streaming_tab::server_streaming_tab_ui( ui, storage_context, &self.streaming_history, ); } - MemoryViewTab::AllocationTracking => { + DevPanelTab::AllocationTracking => { egui::ScrollArea::vertical() .auto_shrink(false) .show(ui, |ui| { Self::allocation_tracking_ui(ui); }); } - MemoryViewTab::Gpu => { + DevPanelTab::Gpu => { egui::ScrollArea::vertical() .auto_shrink(false) .show(ui, |ui| { @@ -149,6 +185,45 @@ impl MemoryPanel { }); } } + + DevPanelResponse { + close_requested: close_clicked, + repaint_requested: request_repaint, + } + } + + fn transform_cache_ui( + &mut self, + ui: &mut egui::Ui, + store_context: Option<&ActiveStoreContext<'_>>, + time_controls: &HashMap, + storage_context: &StorageContext<'_>, + ) { + // Keep the tab from reporting a tiny content height when it only has a warning label, + // without imposing a fixed minimum size on the resizable dev panel. + ui.set_min_height(ui.available_height()); + + let Some(store_context) = store_context else { + ui.warning_label("No active recording selected for the transform cache."); + return; + }; + + let query = time_controls + .get(store_context.recording.store_id()) + .and_then(|time_ctrl| { + // Pending timelines do not have resolved timeline metadata yet, so avoid issuing a + // query that cannot match the viewer's current timepoint. + time_ctrl.timeline()?; + Some(time_ctrl.current_query()) + }); + + transform_cache_ui::ui( + ui, + store_context.recording, + storage_context, + query, + &mut self.transform_cache_state, + ); } fn store_stats_ui(ui: &mut egui::Ui, store_stats: Option<&StoreHubStats>) { @@ -156,6 +231,11 @@ impl MemoryPanel { for (store_id, store_stats) in &store_stats.store_stats { let title = format!("{} {}", store_id.kind(), store_id.recording_id()); ui.collapsing_header(&title, false, |ui| { + if let Some(data_source) = &store_stats.store_source { + ui.weak(format!("Source: {data_source}")); + + ui.separator(); + } ui.collapsing("Datastore Resources", |ui| { Self::chunk_store_stats( ui, @@ -470,7 +550,14 @@ impl MemoryPanel { egui_plot::Plot::new("mem_history_plot") .min_size(egui::Vec2::splat(200.0)) - .label_formatter(|name, value| format!("{name}: {}", format_bytes(value.y))) + .label_formatter(|hover_position| match hover_position { + HoverPosition::NearDataPoint { + plot_name, + position, + .. + } => Some(format!("{plot_name}: {}", format_bytes(position.y))), + HoverPosition::Elsewhere { position } => Some(format_bytes(position.y)), + }) .x_axis_formatter(|time, _| format!("{} s", time.value)) .y_axis_formatter(|bytes, _| format_bytes(bytes.value)) .show_x(false) @@ -559,6 +646,7 @@ fn summarize_callstack(callstack: &str) -> String { pub fn memory_tree_ui( ui: &mut egui::Ui, tree: Option, + external_trees: &[re_byte_size::NamedMemUsageTree], include_rss: &mut bool, ) { // Add explanation at the top @@ -592,12 +680,13 @@ pub fn memory_tree_ui( let include_counted = true; // What our allocator counts. Perfectly accurate. if include_counted && let Some(counted) = counted { - tree = re_byte_size::NamedMemUsageTree::new( - "counted", - re_byte_size::MemUsageNode::new() - .with_named_child(tree) - .with_total_size_bytes(counted), - ); + let mut node = re_byte_size::MemUsageNode::new().with_named_child(tree); + + for tree in external_trees { + node = node.with_named_child(tree.clone()); + } + + tree = re_byte_size::NamedMemUsageTree::new("counted", node.with_total_size_bytes(counted)); } if *include_rss && let Some(resident) = resident { diff --git a/crates/viewer/re_viewer/src/ui/memory_panel/plot_utils.rs b/crates/viewer/re_viewer/src/ui/dev_panel/plot_utils.rs similarity index 100% rename from crates/viewer/re_viewer/src/ui/memory_panel/plot_utils.rs rename to crates/viewer/re_viewer/src/ui/dev_panel/plot_utils.rs diff --git a/crates/viewer/re_viewer/src/ui/memory_panel/server_streaming_tab.rs b/crates/viewer/re_viewer/src/ui/dev_panel/server_streaming_tab.rs similarity index 94% rename from crates/viewer/re_viewer/src/ui/memory_panel/server_streaming_tab.rs rename to crates/viewer/re_viewer/src/ui/dev_panel/server_streaming_tab.rs index a09dd563c883..65e7059f3a24 100644 --- a/crates/viewer/re_viewer/src/ui/memory_panel/server_streaming_tab.rs +++ b/crates/viewer/re_viewer/src/ui/dev_panel/server_streaming_tab.rs @@ -1,5 +1,4 @@ -use std::collections::BTreeSet; - +use egui_plot::HoverPosition; use re_byte_size::SizeBytes as _; use re_chunk_store::Chunk; use re_format::{format_bytes, format_uint}; @@ -7,6 +6,7 @@ use re_log_types::EntityPath; use re_ui::UiExt as _; use re_ui::list_item; use re_viewer_context::StorageContext; +use std::collections::BTreeSet; use super::plot_utils::history_to_plot; use super::streaming_history::StreamingHistory; @@ -529,7 +529,14 @@ fn streaming_plots(ui: &mut egui::Ui, history: &StreamingHistory) { columns[0].label("Progress"); show_plot( base_plot("streaming_progress", axis_group, cursor_group) - .label_formatter(|name, value| format!("{name}: {}", format_bytes(value.y))) + .label_formatter(|hover_position| match hover_position { + HoverPosition::NearDataPoint { + plot_name, + position, + .. + } => Some(format!("{plot_name}: {}", format_bytes(position.y))), + HoverPosition::Elsewhere { position } => Some(format_bytes(position.y)), + }) .y_axis_formatter(|bytes, _| format_bytes(bytes.value)), &mut columns[0], &mut following, @@ -547,7 +554,14 @@ fn streaming_plots(ui: &mut egui::Ui, history: &StreamingHistory) { columns[1].label("Throughput"); show_plot( base_plot("streaming_throughput", axis_group, cursor_group) - .label_formatter(|name, value| format!("{name}: {}", format_bytes(value.y))) + .label_formatter(|hover_position| match hover_position { + HoverPosition::NearDataPoint { + plot_name, + position, + .. + } => Some(format!("{plot_name}: {}", format_bytes(position.y))), + HoverPosition::Elsewhere { position } => Some(format_bytes(position.y)), + }) .y_axis_formatter(|bytes, _| format_bytes(bytes.value)), &mut columns[1], &mut following, @@ -562,8 +576,16 @@ fn streaming_plots(ui: &mut egui::Ui, history: &StreamingHistory) { columns[2].label("Counts"); show_plot( - base_plot("streaming_counts", axis_group, cursor_group) - .label_formatter(|name, value| format!("{name}: {:.0}", value.y)), + base_plot("streaming_counts", axis_group, cursor_group).label_formatter( + |hover_position| match hover_position { + HoverPosition::NearDataPoint { + plot_name, + position, + .. + } => Some(format!("{plot_name}: {:.0}", position.y)), + HoverPosition::Elsewhere { position } => Some(format!("{:.0}", position.y)), + }, + ), &mut columns[2], &mut following, now, diff --git a/crates/viewer/re_viewer/src/ui/memory_panel/streaming_history.rs b/crates/viewer/re_viewer/src/ui/dev_panel/streaming_history.rs similarity index 98% rename from crates/viewer/re_viewer/src/ui/memory_panel/streaming_history.rs rename to crates/viewer/re_viewer/src/ui/dev_panel/streaming_history.rs index e43d33b75235..b7c220302d6b 100644 --- a/crates/viewer/re_viewer/src/ui/memory_panel/streaming_history.rs +++ b/crates/viewer/re_viewer/src/ui/dev_panel/streaming_history.rs @@ -5,7 +5,7 @@ use super::chunk_event_stats::ChunkEventStats; /// Tracks server streaming metrics over time. /// -/// Only updated while the memory panel is open. +/// Only updated while the dev panel is open. pub struct StreamingHistory { pub bandwidth_bytes_per_sec: History, pub pending_bytes: History, diff --git a/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/layout.rs b/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/layout.rs new file mode 100644 index 000000000000..d0b64e785634 --- /dev/null +++ b/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/layout.rs @@ -0,0 +1,215 @@ +use ahash::{HashMap, HashSet}; +use re_sdk_types::TransformFrameIdHash; + +use super::{LayoutDirection, Model}; + +/// Simple tidy-tree style layout for the transform-cache graph. +/// +/// The recursive pass places leaves on a monotonically increasing sibling axis and centers each +/// parent over the span of its laid-out children. +/// +/// This uses the core idea from Reingold and Tilford's +/// [Tidier Drawings of Trees](https://doi.org/10.1109/TSE.1981.234519), but deliberately omits +/// contour-based subtree compaction. Fixed spacing keeps node labels and right-angled edge routing +/// predictable for this UI. +/// +/// Layout is computed in orientation-independent coordinates: +/// +/// * `depth` is the axis that moves away from the root through child transforms. +/// * `cross` is the sibling axis, perpendicular to `depth`, used to spread leaves and disjoint +/// trees. +pub(super) struct Layout<'a> { + model: &'a Model, + pub(super) direction: LayoutDirection, + pub(super) positions: HashMap, + visited: HashSet, + + // Next available coordinate on the `cross` axis for a leaf or disconnected tree. + next_cross: f32, + + // Total offset between adjacent node slots, including the node size. + node_offset: egui::Vec2, + + pub(super) margin: f32, +} + +impl<'a> Layout<'a> { + const START_CROSS_OFFSET: f32 = 30.0; + const MARGIN: f32 = 30.0; + + /// Computes screen-space node positions for the current filtered transform-cache model. + /// + /// The same algorithm is used for horizontal and vertical output; only the final mapping of + /// depth/cross coordinates to x/y changes. + pub(super) fn compute( + model: &'a Model, + direction: LayoutDirection, + node_size: egui::Vec2, + ) -> Self { + let mut layout = Self::new(model, direction, node_size); + + // Start with true roots so disconnected components are laid out independently. + let mut roots = model + .snapshot + .frames + .iter() + .filter(|node| !model.edge_indices_by_child.contains_key(&node.id)) + .map(|node| node.id) + .collect::>(); + roots.sort_by_key(|id| model.sort_key(*id)); + + for root in roots { + layout.layout_tree(root, 0); + layout.finish_tree(); + } + + // Any nodes not reached from a root are still useful to show: they are disconnected or cyclic. + // Treat each as a separate tree instead of hiding broken transform data. + for node in &model.snapshot.frames { + if !layout.visited.contains(&node.id) { + layout.layout_tree(node.id, 0); + layout.finish_tree(); + } + } + + layout + } + + /// Returns the full scene-space bounds needed to contain every laid-out node. + pub(super) fn content_rect(&self, node_size: egui::Vec2) -> egui::Rect { + let max_x = self.positions.values().map(|pos| pos.x).fold(0.0, f32::max) + + node_size.x + + self.margin; + let max_y = self.positions.values().map(|pos| pos.y).fold(0.0, f32::max) + + node_size.y + + self.margin; + egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(max_x, max_y).max(egui::vec2(1.0, 1.0)), + ) + .expand(self.margin) + } + + /// Returns the midpoint between a parent node and child node on the depth axis. + /// + /// Shared fork edges use this coordinate for the bus line that fans out to siblings. + pub(super) fn fork_depth_coordinate( + &self, + parent_pos: egui::Pos2, + child_pos: egui::Pos2, + node_size: egui::Vec2, + ) -> f32 { + match self.direction { + LayoutDirection::Horizontal => (parent_pos.x + node_size.x + child_pos.x) / 2.0, + LayoutDirection::Vertical => (parent_pos.y + node_size.y + child_pos.y) / 2.0, + } + } + + /// Creates an empty layout accumulator with spacing tuned for the requested direction. + fn new(model: &'a Model, direction: LayoutDirection, node_size: egui::Vec2) -> Self { + Self { + model, + direction, + positions: Default::default(), + visited: Default::default(), + next_cross: Self::START_CROSS_OFFSET, + node_offset: direction.node_offset(node_size), + margin: Self::MARGIN, + } + } + + /// Recursively lays out one tree and returns the node's coordinate on the sibling axis. + fn layout_tree(&mut self, frame: TransformFrameIdHash, depth: usize) -> f32 { + if !self.visited.insert(frame) { + // Cycles should not happen for a valid transform graph, but the view should stay + // responsive if bad data sneaks in. + return self + .positions + .get(&frame) + .map_or(self.next_cross, |pos| self.cross_coordinate(*pos)); + } + + let children = self + .model + .edge_indices_by_parent + .get(&frame) + .cloned() + .unwrap_or_default(); + let child_cross_coordinates = children + .into_iter() + .map(|edge_index| { + self.layout_tree(self.model.snapshot.edges[edge_index].child, depth + 1) + }) + .collect::>(); + + let cross = if child_cross_coordinates.is_empty() { + // Leaves claim the next available slot on the sibling axis. + let cross = self.next_cross; + self.next_cross += self.cross_spacing(); + cross + } else { + // Parent nodes are centered over the span occupied by their visible children. + child_cross_coordinates + .first() + .copied() + .unwrap_or(self.next_cross) + .midpoint( + child_cross_coordinates + .last() + .copied() + .unwrap_or(self.next_cross), + ) + }; + self.positions + .insert(frame, self.node_position(depth, cross)); + cross + } + + /// Adds spacing between independent components in the same model. + fn finish_tree(&mut self) { + self.next_cross += self.cross_spacing(); + } + + /// Converts algorithm coordinates (`depth`, `cross`) into scene-space node positions. + fn node_position(&self, depth: usize, cross: f32) -> egui::Pos2 { + let depth_coordinate = self.margin + depth as f32 * self.depth_spacing(); + // The tree algorithm is orientation-independent: `depth` flows away from the root, + // while `cross` spreads siblings. The final position maps those axes to x/y. + match self.direction { + LayoutDirection::Horizontal => egui::pos2(depth_coordinate, cross), + LayoutDirection::Vertical => egui::pos2(cross, depth_coordinate), + } + } + + /// Extracts the sibling-axis coordinate from a scene-space position. + fn cross_coordinate(&self, pos: egui::Pos2) -> f32 { + match self.direction { + LayoutDirection::Horizontal => pos.y, + LayoutDirection::Vertical => pos.x, + } + } + + fn cross_spacing(&self) -> f32 { + match self.direction { + LayoutDirection::Horizontal => self.node_offset.y, + LayoutDirection::Vertical => self.node_offset.x, + } + } + + fn depth_spacing(&self) -> f32 { + match self.direction { + LayoutDirection::Horizontal => self.node_offset.x, + LayoutDirection::Vertical => self.node_offset.y, + } + } +} + +impl LayoutDirection { + /// Total offset between nodes in one tree step, including the node size. + fn node_offset(self, node_size: egui::Vec2) -> egui::Vec2 { + match self { + Self::Horizontal => node_size + egui::vec2(65.0, 20.0), + Self::Vertical => node_size + egui::vec2(50.0, 60.0), + } + } +} diff --git a/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/mod.rs b/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/mod.rs new file mode 100644 index 000000000000..3493bdf26944 --- /dev/null +++ b/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/mod.rs @@ -0,0 +1,252 @@ +mod layout; +mod model; +mod paint; + +use self::layout::Layout; +use self::model::{Model, ModelFilter, build_transform_cache_model}; +use self::paint::{draw_transform_cache_contents, scene_legend_ui}; + +use re_chunk_store::LatestAtQuery; +use re_ui::UiExt as _; +use re_viewer_context::external::re_entity_db::EntityDb; +use re_viewer_context::external::re_tf::transform_cache_snapshot; +use re_viewer_context::{StorageContext, TransformDatabaseStoreCache}; + +const NODE_SIZE: egui::Vec2 = egui::Vec2 { x: 220.0, y: 52.0 }; + +// Zooming in too far can cause blurred text, see: https://github.com/emilk/egui/issues/5691 +const SCENE_MAX_ZOOM: f32 = 1.0; +const SCENE_MIN_ZOOM: f32 = 0.05; + +/// Compared to [`transform_cache_snapshot::FrameFilter`], +/// this adds a category for unlinked named frames. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +enum FrameVisibilityFilter { + Implicit, + #[default] + Named, + Unlinked, + All, +} + +/// Orientation of the rendered transform-cache graph. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +enum LayoutDirection { + Horizontal, + #[default] + Vertical, +} + +/// Persistent UI state for the transform-cache dev panel. +#[derive(Default)] +pub(super) struct TransformCacheUiState { + frame_filter: FrameVisibilityFilter, + edge_filter: transform_cache_snapshot::EdgeFilter, + layout_direction: LayoutDirection, + scene_rect: Option, + user_interacted: bool, +} + +/// Draws the transform-cache dev-panel tab for the active recording and latest-at time. +pub(super) fn ui( + ui: &mut egui::Ui, + recording: &EntityDb, + storage_context: &StorageContext<'_>, + query: Option, + state: &mut TransformCacheUiState, +) { + re_tracing::profile_function!(); + + let Some(query) = query else { + ui.warning_label("No active timeline selected for the transform cache."); + return; + }; + + let Some(caches) = storage_context.hub.store_caches(recording.store_id()) else { + ui.label("No transform cache is available for this recording yet."); + return; + }; + + let (model, settings_changed) = ui + .allocate_ui_with_layout( + egui::vec2(ui.available_width(), ui.spacing().interact_size.y), + egui::Layout::left_to_right(egui::Align::TOP), + |ui| { + let previous_settings = ( + state.frame_filter, + state.edge_filter, + state.layout_direction, + ); + + frame_filter_ui(ui, state); + ui.separator(); + + edge_filter_ui(ui, state); + ui.separator(); + + layout_direction_ui(ui, state); + ui.separator(); + + let filter = ModelFilter { + frame_filter: state.frame_filter, + edge_filter: state.edge_filter, + }; + let current_model = caches.memoizer(|cache: &mut TransformDatabaseStoreCache| { + build_transform_cache_model(recording, cache, &query, filter) + }); + + if current_model.snapshot.frames.is_empty() { + ui.warning_label("No transform frames match the current filter."); + } else { + ui.info_label(format!( + "{}, {}, {}", + re_format::format_plural_s(current_model.num_trees(), "tree"), + re_format::format_plural_s(current_model.snapshot.frames.len(), "frame"), + re_format::format_plural_s(current_model.snapshot.edges.len(), "transform") + )); + } + ui.separator(); + + // Center the recording name label horizontally to align it with the widgets' text. + ui.horizontal_centered(|ui| { + let store_id = recording.store_id(); + ui.label(format!( + "{} ({})", + store_id.application_id(), + store_id.recording_id() + )); + + if current_model.any_missing_chunks { + ui.warning_label("Some chunks are missing"); + } + }); + + let settings_changed = previous_settings + != ( + state.frame_filter, + state.edge_filter, + state.layout_direction, + ); + (current_model, settings_changed) + }, + ) + .inner; + + draw_transform_cache(ui, &model, state, settings_changed); +} + +/// Draws the implicit/named frame filter controls. +fn frame_filter_ui(ui: &mut egui::Ui, state: &mut TransformCacheUiState) { + ui.selectable_toggle(|ui| { + ui.selectable_value( + &mut state.frame_filter, + FrameVisibilityFilter::Implicit, + "Implicit", + ) + .on_hover_text("Show only tf# frames derived from entity paths"); + ui.selectable_value( + &mut state.frame_filter, + FrameVisibilityFilter::Named, + "Named", + ) + .on_hover_text("Show explicitly named frames with transforms"); + ui.selectable_value( + &mut state.frame_filter, + FrameVisibilityFilter::Unlinked, + "Unlinked", + ) + .on_hover_text("Show named coordinate frames without transforms"); + ui.selectable_value(&mut state.frame_filter, FrameVisibilityFilter::All, "All") + .on_hover_text("Show implicit, named, and unlinked frames"); + }); +} + +/// Draws the static/temporal edge filter controls. +fn edge_filter_ui(ui: &mut egui::Ui, state: &mut TransformCacheUiState) { + ui.selectable_toggle(|ui| { + ui.selectable_value( + &mut state.edge_filter, + transform_cache_snapshot::EdgeFilter::Static, + "Static", + ) + .on_hover_text("Show only static transforms"); + ui.selectable_value( + &mut state.edge_filter, + transform_cache_snapshot::EdgeFilter::Temporal, + "Temporal", + ) + .on_hover_text("Show only temporal transforms"); + ui.selectable_value( + &mut state.edge_filter, + transform_cache_snapshot::EdgeFilter::All, + "All", + ) + .on_hover_text("Show static and temporal transforms"); + }); +} + +/// Draws the horizontal/vertical layout controls. +fn layout_direction_ui(ui: &mut egui::Ui, state: &mut TransformCacheUiState) { + ui.horizontal_centered(|ui| { + ui.label("Layout:"); + ui.selectable_toggle(|ui| { + ui.selectable_value( + &mut state.layout_direction, + LayoutDirection::Horizontal, + "▶", + ) + .on_hover_text("Lay out transform frames horizontally"); + ui.selectable_value(&mut state.layout_direction, LayoutDirection::Vertical, "▼") + .on_hover_text("Lay out transform frames vertically"); + }) + }); +} + +/// Draws the zoomable transform-cache scene and its fixed overlay legend. +fn draw_transform_cache( + ui: &mut egui::Ui, + model: &Model, + state: &mut TransformCacheUiState, + settings_changed: bool, +) { + let node_size = NODE_SIZE; + let layout = (!model.snapshot.frames.is_empty()) + .then(|| Layout::compute(model, state.layout_direction, node_size)); + let content_rect = if let Some(layout) = &layout { + layout.content_rect(node_size) + } else { + // Empty models still need scene bounds; use whatever space the panel currently has. + ui.available_rect_before_wrap() + }; + + // Auto-adapt the rect to fit the content, unless the user has panned/zoomed. + // Always redraw if the settings changed. + if state.scene_rect.is_none() || settings_changed || !state.user_interacted { + state.scene_rect = Some(content_rect); + } + let scene_rect = state.scene_rect.get_or_insert(content_rect); + let frame_output = egui::Frame::new() + .fill(ui.tokens().faint_bg_color) + .show(ui, |ui| { + egui::Scene::new() + .zoom_range(SCENE_MIN_ZOOM..=SCENE_MAX_ZOOM) + .show(ui, scene_rect, |ui| { + if let Some(layout) = &layout { + draw_transform_cache_contents(ui, model, layout, node_size, content_rect); + } + }) + .response + }); + let response = frame_output.inner; + scene_legend_ui(ui, frame_output.response.rect); + + if response.changed() { + state.user_interacted = true; + } + + // Reset scene rect on double-click. + if response.double_clicked() { + state.scene_rect = Some(content_rect); + state.user_interacted = false; + } +} diff --git a/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/model.rs b/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/model.rs new file mode 100644 index 000000000000..5bfafd6a314b --- /dev/null +++ b/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/model.rs @@ -0,0 +1,229 @@ +use ahash::{HashMap, HashSet}; +use re_chunk_store::{LatestAtQuery, MissingChunkReporter}; +use re_sdk_types::TransformFrameIdHash; +use re_viewer_context::TransformDatabaseStoreCache; +use re_viewer_context::external::re_entity_db::EntityDb; +use re_viewer_context::external::re_tf::transform_cache_snapshot; + +use super::FrameVisibilityFilter; +pub(super) use re_viewer_context::external::re_tf::transform_cache_snapshot::{ + Edge, Frame as Node, SubspaceKind, +}; + +/// Thin UI wrapper around a filtered transform-cache snapshot. +/// +/// The wrapper adds only the derived graph lookups needed for layout, hover highlighting, and +/// tooltip labels. +#[derive(Debug, Clone)] +pub(super) struct Model { + /// Visible transform-cache snapshot. + pub(super) snapshot: transform_cache_snapshot::Snapshot, + + /// Edge indices grouped by parent frame for layout and shared-fork drawing. + pub(super) edge_indices_by_parent: HashMap>, + + /// Edge indices grouped by child frame for root detection and ancestor traversal. + pub(super) edge_indices_by_child: HashMap>, + + /// Node indices keyed by frame id for label lookups without duplicating labels. + node_indices_by_id: HashMap, + + pub(super) any_missing_chunks: bool, +} + +/// Returns whether a frame is derived from an entity path. +pub(super) fn is_implicit_frame(frame: &Node) -> bool { + frame.kind == transform_cache_snapshot::FrameKind::EntityPath +} + +/// Filters used while building a transform-cache display model. +#[derive(Debug, Clone, Copy)] +pub(super) struct ModelFilter { + pub(super) frame_filter: FrameVisibilityFilter, + pub(super) edge_filter: transform_cache_snapshot::EdgeFilter, +} + +impl ModelFilter { + fn snapshot_filter(self) -> transform_cache_snapshot::SnapshotFilter { + transform_cache_snapshot::SnapshotFilter { + frames: match self.frame_filter { + FrameVisibilityFilter::All => transform_cache_snapshot::FrameFilter::All, + FrameVisibilityFilter::Implicit => { + transform_cache_snapshot::FrameFilter::EntityPath + } + FrameVisibilityFilter::Named | FrameVisibilityFilter::Unlinked => { + transform_cache_snapshot::FrameFilter::Named + } + }, + edges: self.edge_filter, + } + } + + fn shows_node(self, node: &Node) -> bool { + let is_implicit = is_implicit_frame(node); + match self.frame_filter { + FrameVisibilityFilter::All => true, + FrameVisibilityFilter::Implicit => is_implicit, + FrameVisibilityFilter::Named => !is_implicit && node.has_transform, + FrameVisibilityFilter::Unlinked => !is_implicit && !node.has_transform, + } + } +} + +/// Builds a display model from the transform cache at one latest-at time. +pub(super) fn build_transform_cache_model( + recording: &EntityDb, + cache: &mut TransformDatabaseStoreCache, + query: &LatestAtQuery, + filter: ModelFilter, +) -> Model { + let missing_chunk_reporter = MissingChunkReporter::default(); + let mut snapshot = cache.latest_at_transform_cache_snapshot( + recording, + &missing_chunk_reporter, + query, + filter.snapshot_filter(), + ); + + let connected_frames = snapshot + .edges + .iter() + .flat_map(|edge| [edge.parent, edge.child]) + .collect::>(); + + snapshot.frames.retain(|frame| { + (connected_frames.contains(&frame.id) + || (!is_implicit_frame(frame) && !frame.has_transform)) + && filter.shows_node(frame) + }); + snapshot + .frames + .sort_by(|a, b| a.label.as_str().cmp(b.label.as_str())); + + let visible_frames = snapshot + .frames + .iter() + .map(|frame| frame.id) + .collect::>(); + snapshot.edges.retain(|edge| { + visible_frames.contains(&edge.parent) && visible_frames.contains(&edge.child) + }); + let frame_labels = snapshot + .frames + .iter() + .map(|frame| (frame.id, frame.label.to_string())) + .collect::>(); + snapshot.edges.sort_by_key(|edge| { + ( + frame_labels + .get(&edge.parent) + .cloned() + .unwrap_or_else(|| format!("{:?}", edge.parent)), + frame_labels + .get(&edge.child) + .cloned() + .unwrap_or_else(|| format!("{:?}", edge.child)), + ) + }); + + Model::new(snapshot, missing_chunk_reporter.any_missing()) +} + +impl Model { + fn new(snapshot: transform_cache_snapshot::Snapshot, any_missing_chunks: bool) -> Self { + let mut edge_indices_by_parent: HashMap> = + Default::default(); + let mut edge_indices_by_child: HashMap> = + Default::default(); + + for (edge_index, edge) in snapshot.edges.iter().enumerate() { + edge_indices_by_parent + .entry(edge.parent) + .or_default() + .push(edge_index); + edge_indices_by_child + .entry(edge.child) + .or_default() + .push(edge_index); + } + + let node_indices_by_id = snapshot + .frames + .iter() + .enumerate() + .map(|(node_index, node)| (node.id, node_index)) + .collect(); + + Self { + snapshot, + edge_indices_by_parent, + edge_indices_by_child, + node_indices_by_id, + any_missing_chunks, + } + } + + /// Returns the user-facing label for a frame id. + pub(super) fn frame_label(&self, frame: TransformFrameIdHash) -> &str { + self.node_indices_by_id + .get(&frame) + .and_then(|&node_index| self.snapshot.frames.get(node_index)) + .map_or("", |node| node.label.as_str()) + } + + /// Returns a stable sort key for root ordering. + pub(super) fn sort_key(&self, frame: TransformFrameIdHash) -> &str { + self.node_indices_by_id + .get(&frame) + .and_then(|&node_index| self.snapshot.frames.get(node_index)) + .map_or("", |node| node.label.as_str()) + } + + /// Returns true when an edge leaves a parent through a shared fork path. + pub(super) fn edge_starts_at_shared_fork(&self, edge: &Edge) -> bool { + self.edge_indices_by_parent + .get(&edge.parent) + .is_some_and(|children| children.len() > 1) + } + + /// Returns the number of visible child transforms for a frame. + pub(super) fn num_children(&self, frame: TransformFrameIdHash) -> usize { + self.edge_indices_by_parent.get(&frame).map_or(0, Vec::len) + } + + /// Collects all visible ancestors of a frame and the edges on the path to them. + pub(super) fn path_to_roots( + &self, + frame: TransformFrameIdHash, + ) -> (HashSet, HashSet) { + let mut ancestors = HashSet::default(); + let mut edge_indices = HashSet::default(); + self.collect_path_to_roots(frame, &mut ancestors, &mut edge_indices); + (ancestors, edge_indices) + } + + /// Counts visible root components in the graph. + pub(super) fn num_trees(&self) -> usize { + self.snapshot + .frames + .iter() + .filter(|node| !self.edge_indices_by_child.contains_key(&node.id)) + .count() + } + + /// Recursively collects root paths while protecting against cycles. + fn collect_path_to_roots( + &self, + frame: TransformFrameIdHash, + ancestors: &mut HashSet, + edge_indices: &mut HashSet, + ) { + for &edge_index in self.edge_indices_by_child.get(&frame).into_iter().flatten() { + let edge = &self.snapshot.edges[edge_index]; + edge_indices.insert(edge_index); + if ancestors.insert(edge.parent) { + self.collect_path_to_roots(edge.parent, ancestors, edge_indices); + } + } + } +} diff --git a/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/paint.rs b/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/paint.rs new file mode 100644 index 000000000000..90a8522b1242 --- /dev/null +++ b/crates/viewer/re_viewer/src/ui/dev_panel/transform_cache_ui/paint.rs @@ -0,0 +1,853 @@ +use ahash::HashSet; +use egui::{FontSelection, TextWrapMode, WidgetText}; +use re_log_types::TimeInt; +use re_sdk_types::TransformFrameIdHash; +use re_ui::UiExt as _; +use re_ui::list_item; +use re_viewer_context::external::re_tf::transform_cache_snapshot::EdgeSource; + +use super::LayoutDirection; +use super::layout::Layout; +use super::model::{Edge, Model, Node, SubspaceKind, is_implicit_frame}; + +const EDGE_STROKE_WIDTH: f32 = 1.5; +const EDGE_HIGHLIGHT_STROKE_WIDTH: f32 = EDGE_STROKE_WIDTH * 2.; +const EDGE_HIT_RADIUS: f32 = EDGE_STROKE_WIDTH * 4.; +const FRAME_PROPERTY_MIN_WIDTH: f32 = 300.0; +const LEGEND_FILL_OPACITY: f32 = 0.5; +const SCENE_ICON_SCALE: f32 = 1.25; + +/// Thing currently hovered in the transform-cache scene. +#[derive(Debug, Clone, Copy)] +enum HoveredTransformItem { + Node(TransformFrameIdHash), + Edge(usize), + SharedFork(TransformFrameIdHash), +} + +/// Frames and transforms highlighted for the current hover state. +#[derive(Default)] +struct HighlightedTransformPath { + nodes: HashSet, + edges: HashSet, +} + +/// Orientation-independent coordinates for a shared fork path. +struct ForkGeometry { + segments: [[egui::Pos2; 2]; 2], + joints: Vec, +} + +/// Orientation-independent coordinates for the shared part of one edge's fork path. +struct ForkEdgePath { + segments: [[egui::Pos2; 2]; 2], + joints: [egui::Pos2; 2], +} + +impl HighlightedTransformPath { + fn new(hovered_item: Option, model: &Model) -> Self { + let mut highlighted_path = Self::default(); + + match hovered_item { + Some(HoveredTransformItem::Node(frame)) => { + highlighted_path.nodes.insert(frame); + let (ancestors, edge_indices) = model.path_to_roots(frame); + highlighted_path.nodes.extend(ancestors); + highlighted_path.edges = edge_indices; + } + Some(HoveredTransformItem::Edge(edge_index)) => { + if let Some(edge) = model.snapshot.edges.get(edge_index) { + highlighted_path.nodes.insert(edge.parent); + highlighted_path.nodes.insert(edge.child); + highlighted_path.edges.insert(edge_index); + } + } + Some(HoveredTransformItem::SharedFork(parent)) => { + highlighted_path.nodes.insert(parent); + highlighted_path.edges.extend( + model + .edge_indices_by_parent + .get(&parent) + .into_iter() + .flatten() + .copied(), + ); + } + None => {} + } + + highlighted_path + } +} + +impl ForkGeometry { + fn new( + parent: TransformFrameIdHash, + model: &Model, + layout: &Layout<'_>, + node_size: egui::Vec2, + ) -> Option { + let child_edge_indices = model.edge_indices_by_parent.get(&parent)?; + if child_edge_indices.len() <= 1 { + return None; + } + + let parent_pos = layout.positions.get(&parent).copied()?; + let child_positions = child_edge_indices + .iter() + .filter_map(|&edge_index| { + layout + .positions + .get(&model.snapshot.edges[edge_index].child) + .copied() + }) + .collect::>(); + let first_child_pos = child_positions.first().copied()?; + + let fork_depth = layout.fork_depth_coordinate(parent_pos, first_child_pos, node_size); + let parent_exit = node_exit(parent_pos, layout.direction, node_size); + let parent_fork = pos_from_depth_cross( + fork_depth, + cross_coordinate(parent_exit, layout.direction), + layout.direction, + ); + let child_forks = child_positions + .iter() + .map(|&child_pos| { + let child_entry = node_entry(child_pos, layout.direction, node_size); + pos_from_depth_cross( + fork_depth, + cross_coordinate(child_entry, layout.direction), + layout.direction, + ) + }) + .collect::>(); + let first_child_fork = child_forks.first().copied()?; + let (min_child_cross, max_child_cross) = child_forks.iter().skip(1).fold( + ( + cross_coordinate(first_child_fork, layout.direction), + cross_coordinate(first_child_fork, layout.direction), + ), + |(min_cross, max_cross), child_fork| { + let cross = cross_coordinate(*child_fork, layout.direction); + (min_cross.min(cross), max_cross.max(cross)) + }, + ); + let bus_start = pos_from_depth_cross(fork_depth, min_child_cross, layout.direction); + let bus_end = pos_from_depth_cross(fork_depth, max_child_cross, layout.direction); + + let mut joints = Vec::with_capacity(child_forks.len() + 1); + joints.push(parent_fork); + joints.extend(child_forks); + + Some(Self { + segments: [[parent_exit, parent_fork], [bus_start, bus_end]], + joints, + }) + } +} + +impl ForkEdgePath { + fn new(edge: &Edge, layout: &Layout<'_>, node_size: egui::Vec2) -> Option { + let parent_pos = layout.positions.get(&edge.parent).copied()?; + let child_pos = layout.positions.get(&edge.child).copied()?; + let fork_depth = layout.fork_depth_coordinate(parent_pos, child_pos, node_size); + let parent_exit = node_exit(parent_pos, layout.direction, node_size); + let parent_fork = pos_from_depth_cross( + fork_depth, + cross_coordinate(parent_exit, layout.direction), + layout.direction, + ); + let child_entry = node_entry(child_pos, layout.direction, node_size); + let child_fork = pos_from_depth_cross( + fork_depth, + cross_coordinate(child_entry, layout.direction), + layout.direction, + ); + + Some(Self { + segments: [[parent_exit, parent_fork], [parent_fork, child_fork]], + joints: [parent_fork, child_fork], + }) + } +} + +/// Draws the legend as a fixed overlay over the scene widget. +pub(super) fn scene_legend_ui(ui: &egui::Ui, scene_ui_rect: egui::Rect) { + let tokens = ui.tokens(); + // Hide legend overlay if we don't have enough vertical space. + let min_scene_height_for_legend = ui.spacing().interact_size.y * 2.0 + + ui.spacing().item_spacing.y + + tokens.text_to_icon_padding() * 2.0; + if scene_ui_rect.height() < min_scene_height_for_legend { + return; + } + + let legend_inset = f32::from(tokens.view_padding()); + let anchor_offset = egui::vec2( + scene_ui_rect.right() - ui.ctx().content_rect().right() - legend_inset, + scene_ui_rect.top() - ui.ctx().content_rect().top() + legend_inset, + ); + + egui::Area::new(ui.id().with("transform_cache_legend")) + .order(egui::Order::Foreground) + .anchor(egui::Align2::RIGHT_TOP, anchor_offset) + .show(ui.ctx(), |ui| { + let tokens = ui.tokens(); + egui::Frame::new() + .fill(tokens.panel_bg_color.gamma_multiply(LEGEND_FILL_OPACITY)) + .stroke(egui::Stroke::new( + 1.0, + tokens.widget_noninteractive_bg_stroke, + )) + .corner_radius(tokens.small_corner_radius()) + .inner_margin(tokens.text_to_icon_padding()) + .show(ui, |ui| { + // The legend is anchored to the scene UI, not the zoomable content, so it + // remains readable while panning and zooming the model. + legend_ui(ui); + }); + }); +} + +/// Draws transform-cache geometry, hover regions, and hover tooltips inside the scene. +pub(super) fn draw_transform_cache_contents( + ui: &mut egui::Ui, + model: &Model, + layout: &Layout<'_>, + node_size: egui::Vec2, + content_rect: egui::Rect, +) { + let mut hovered_item = None; + let scene_response = ui.allocate_rect(content_rect, egui::Sense::hover()); + + // Allocate node hover regions before drawing so nodes take precedence over nearby edges. + for node in &model.snapshot.frames { + let Some(pos) = layout.positions.get(&node.id) else { + continue; + }; + let response = ui.allocate_rect( + egui::Rect::from_min_size(*pos, node_size), + egui::Sense::hover(), + ); + if response.hovered() { + hovered_item = Some(HoveredTransformItem::Node(node.id)); + } + response.on_hover_ui(|ui| node_tooltip_ui(ui, model, node)); + } + + if hovered_item.is_none() + && let Some(hovered_edge_index) = scene_response.hover_pos().and_then(|pos| { + nearest_edge( + pos, + model, + layout, + node_size, + scene_icon_size(ui), + EDGE_HIT_RADIUS, + ) + }) + { + hovered_item = Some(HoveredTransformItem::Edge(hovered_edge_index)); + egui::Tooltip::always_open( + ui.ctx().clone(), + ui.layer_id(), + ui.id().with("transform_edge_tooltip"), + egui::PopupAnchor::Pointer, + ) + .at_pointer() + .show(|ui| edge_tooltip_ui(ui, model, &model.snapshot.edges[hovered_edge_index])); + } + + if hovered_item.is_none() + && let Some(hovered_parent) = scene_response + .hover_pos() + .and_then(|pos| nearest_shared_fork(pos, model, layout, node_size, EDGE_HIT_RADIUS)) + { + hovered_item = Some(HoveredTransformItem::SharedFork(hovered_parent)); + egui::Tooltip::always_open( + ui.ctx().clone(), + ui.layer_id(), + ui.id().with("transform_shared_fork_tooltip"), + egui::PopupAnchor::Pointer, + ) + .at_pointer() + .show(|ui| shared_fork_tooltip_ui(ui, model, hovered_parent)); + } + let highlighted_path = HighlightedTransformPath::new(hovered_item, model); + + let painter = ui.painter(); + painter.rect_filled(content_rect, 0.0, ui.tokens().faint_bg_color); + + // Sibling edges share a neutral fork path so only the terminal segment carries per-transform + // interaction and time-kind styling. + draw_shared_fork_segments(painter, model, layout, node_size, ui); + + // Draw highlighted ancestry through shared fork segments, not just through the terminal edge. + for (edge_index, edge) in model.snapshot.edges.iter().enumerate() { + let highlighted = highlighted_path.edges.contains(&edge_index); + if highlighted && model.edge_starts_at_shared_fork(edge) { + draw_shared_fork_path( + painter, + edge, + layout, + node_size, + egui::Stroke::new(EDGE_HIGHLIGHT_STROKE_WIDTH, edge_color(ui, highlighted)), + ); + } + } + + // Draw transform-specific edge terminals above the shared fork segments, but below nodes. + for (edge_index, edge) in model.snapshot.edges.iter().enumerate() { + let Some((start, end)) = edge_unique_segment(edge, model, layout, node_size) else { + continue; + }; + let highlighted = highlighted_path.edges.contains(&edge_index); + let color = edge_color(ui, highlighted); + draw_edge_line( + painter, + start, + end, + egui::Stroke::new( + if highlighted { + EDGE_HIGHLIGHT_STROKE_WIDTH + } else { + EDGE_STROKE_WIDTH + }, + color, + ), + ); + edge_time_icon(edge.time) + .as_image() + .tint(color) + .paint_at(ui, edge_time_icon_rect(start, end, scene_icon_size(ui))); + } + + for node in &model.snapshot.frames { + let Some(pos) = layout.positions.get(&node.id) else { + continue; + }; + let tokens = ui.tokens(); + let highlighted = highlighted_path.nodes.contains(&node.id); + let rect = egui::Rect::from_min_size(*pos, node_size); + painter.rect( + rect, + tokens.small_corner_radius(), + if highlighted { + tokens.highlight_color + } else { + tokens.panel_bg_color + }, + if highlighted { + tokens.focus_outline_stroke + } else { + egui::Stroke::new(2.0, tokens.widget_noninteractive_bg_stroke) + }, + egui::StrokeKind::Inside, + ); + + let icon_size = scene_icon_size(ui); + let icon_inset = tokens.text_to_icon_padding(); + let mut text_rect = rect.shrink2(egui::vec2(2.5 * icon_inset, 1.25 * icon_inset)); + if !node.has_transform { + text_rect.min.x += icon_size.x + icon_inset; + } + text_rect.max.x -= icon_size.x + icon_inset; + let galley = WidgetText::from(node.label.as_str()).into_galley( + ui, + Some(TextWrapMode::Wrap), + text_rect.width(), + FontSelection::Style(egui::TextStyle::Body), + ); + let text_pos = egui::pos2( + text_rect.center().x - galley.size().x.min(text_rect.width()) / 2.0, + text_rect.center().y - galley.size().y.min(text_rect.height()) / 2.0, + ); + painter + .with_clip_rect(text_rect) + .galley(text_pos, galley, tokens.text_default); + + let icon_rect = egui::Rect::from_min_size( + egui::pos2( + rect.right() - icon_inset - icon_size.x, + rect.top() + icon_inset, + ), + icon_size, + ); + subspace_icon(node.subspace_kind) + .as_image() + .tint(tokens.text_subdued) + .paint_at(ui, icon_rect); + + if !node.has_transform { + let warning_icon_rect = egui::Rect::from_min_size( + egui::pos2(rect.left() + icon_inset, rect.top() + icon_inset), + icon_size, + ); + re_ui::icons::WARNING + .as_image() + .tint(tokens.alert_warning.icon) + .paint_at(ui, warning_icon_rect); + } + } +} + +/// Draws the static/temporal edge-style legend contents. +fn legend_ui(ui: &mut egui::Ui) { + ui.vertical(|ui| { + legend_item_ui(ui, "static", edge_color(ui, false), TimeInt::STATIC); + legend_item_ui( + ui, + "temporal", + edge_color(ui, false), + TimeInt::new_temporal(0), + ); + }); +} + +/// Draws one edge time-kind icon and label in the legend. +fn legend_item_ui(ui: &mut egui::Ui, label: &str, color: egui::Color32, time: TimeInt) { + ui.horizontal(|ui| { + let (rect, _) = ui.allocate_exact_size(ui.tokens().small_icon_size, egui::Sense::hover()); + edge_time_icon(time) + .as_image() + .tint(color) + .paint_at(ui, rect); + ui.label(label); + }); +} + +/// Returns the color used for transform edges. +pub(super) fn edge_color(ui: &egui::Ui, highlighted: bool) -> egui::Color32 { + if highlighted { + // Highlight color has alpha, make it opaque to avoid blending at crossing edges. + ui.tokens().highlight_color.to_opaque() + } else { + ui.tokens().text_subdued + } +} + +/// Returns the icon size used for zoomable scene contents. +fn scene_icon_size(ui: &egui::Ui) -> egui::Vec2 { + ui.tokens().small_icon_size * SCENE_ICON_SCALE +} + +/// Draws one transform edge segment. +pub(super) fn draw_edge_line( + painter: &egui::Painter, + start: egui::Pos2, + end: egui::Pos2, + stroke: egui::Stroke, +) { + painter.line_segment([start, end], stroke); +} + +/// Returns the transform-specific terminal segment for an edge. +fn edge_unique_segment( + edge: &Edge, + model: &Model, + layout: &Layout<'_>, + node_size: egui::Vec2, +) -> Option<(egui::Pos2, egui::Pos2)> { + let child_pos = layout.positions.get(&edge.child).copied()?; + let parent_pos = layout.positions.get(&edge.parent).copied()?; + let child_entry = node_entry(child_pos, layout.direction, node_size); + + let start = if model.edge_starts_at_shared_fork(edge) { + let fork_depth = layout.fork_depth_coordinate(parent_pos, child_pos, node_size); + pos_from_depth_cross( + fork_depth, + cross_coordinate(child_entry, layout.direction), + layout.direction, + ) + } else { + let parent_exit = node_exit(parent_pos, layout.direction, node_size); + pos_from_depth_cross( + depth_coordinate(parent_exit, layout.direction), + cross_coordinate(child_entry, layout.direction), + layout.direction, + ) + }; + + Some((start, child_entry)) +} + +/// Returns the point where outgoing transform edges leave a node. +fn node_exit( + node_pos: egui::Pos2, + direction: LayoutDirection, + node_size: egui::Vec2, +) -> egui::Pos2 { + match direction { + LayoutDirection::Horizontal => { + egui::pos2(node_pos.x + node_size.x, node_pos.y + node_size.y / 2.0) + } + LayoutDirection::Vertical => { + egui::pos2(node_pos.x + node_size.x / 2.0, node_pos.y + node_size.y) + } + } +} + +/// Returns the point where incoming transform edges enter a node. +fn node_entry( + node_pos: egui::Pos2, + direction: LayoutDirection, + node_size: egui::Vec2, +) -> egui::Pos2 { + match direction { + LayoutDirection::Horizontal => egui::pos2(node_pos.x, node_pos.y + node_size.y / 2.0), + LayoutDirection::Vertical => egui::pos2(node_pos.x + node_size.x / 2.0, node_pos.y), + } +} + +/// Converts depth and cross-axis coordinates into scene-space coordinates. +fn pos_from_depth_cross(depth: f32, cross: f32, direction: LayoutDirection) -> egui::Pos2 { + match direction { + LayoutDirection::Horizontal => egui::pos2(depth, cross), + LayoutDirection::Vertical => egui::pos2(cross, depth), + } +} + +/// Extracts the depth-axis coordinate from a scene-space position. +fn depth_coordinate(pos: egui::Pos2, direction: LayoutDirection) -> f32 { + match direction { + LayoutDirection::Horizontal => pos.x, + LayoutDirection::Vertical => pos.y, + } +} + +/// Extracts the cross-axis coordinate from a scene-space position. +fn cross_coordinate(pos: egui::Pos2, direction: LayoutDirection) -> f32 { + match direction { + LayoutDirection::Horizontal => pos.y, + LayoutDirection::Vertical => pos.x, + } +} + +/// Returns the icon used to distinguish static and temporal transforms. +fn edge_time_icon(time: TimeInt) -> &'static re_ui::Icon { + if time.is_static() { + &re_ui::icons::COMPONENT_STATIC + } else { + &re_ui::icons::COMPONENT_TEMPORAL + } +} + +/// Places the time-kind icon beside a terminal edge segment. +fn edge_time_icon_rect(start: egui::Pos2, end: egui::Pos2, icon_size: egui::Vec2) -> egui::Rect { + let segment = end - start; + let center = start.lerp(end, 0.5); + let icon_offset = re_ui::DesignTokens::menu_button_padding(); + let offset = if segment.length_sq() <= f32::EPSILON { + egui::vec2(0.0, -(icon_size.y / 2.0 + icon_offset)) + } else { + let direction = segment.normalized(); + egui::vec2(direction.y, -direction.x) * (icon_size.y / 2.0 + icon_offset) + }; + + egui::Rect::from_center_size(center + offset, icon_size) +} + +/// Draws the shared fork paths used by parents with multiple children. +fn draw_shared_fork_segments( + painter: &egui::Painter, + model: &Model, + layout: &Layout<'_>, + node_size: egui::Vec2, + ui: &egui::Ui, +) { + let color = edge_color(ui, false); + let stroke = egui::Stroke::new(EDGE_STROKE_WIDTH, color); + // These tiny disks hide rasterization gaps where perpendicular line caps meet. + let intersection_radius = stroke.width / 2.0; + + for parent in shared_fork_parents(model) { + let Some(geometry) = ForkGeometry::new(parent, model, layout, node_size) else { + continue; + }; + for [start, end] in geometry.segments { + painter.line_segment([start, end], stroke); + } + for joint in geometry.joints { + painter.circle_filled(joint, intersection_radius, color); + } + } +} + +/// Draws the shared part of a highlighted path back to the root. +fn draw_shared_fork_path( + painter: &egui::Painter, + edge: &Edge, + layout: &Layout<'_>, + node_size: egui::Vec2, + stroke: egui::Stroke, +) { + let Some(path) = ForkEdgePath::new(edge, layout, node_size) else { + return; + }; + for [start, end] in path.segments { + draw_edge_line(painter, start, end, stroke); + } + for joint in path.joints { + painter.circle_filled(joint, stroke.width / 2.0, stroke.color); + } +} + +/// Computes the shortest distance from a point to a line segment. +fn distance_to_segment(point: egui::Pos2, start: egui::Pos2, end: egui::Pos2) -> f32 { + let segment = end - start; + let length_squared = segment.length_sq(); + if length_squared <= f32::EPSILON { + return point.distance(start); + } + + let t = ((point - start).dot(segment) / length_squared).clamp(0.0, 1.0); + point.distance(start + t * segment) +} + +/// Finds the closest terminal transform edge under the pointer. +fn nearest_edge( + point: egui::Pos2, + model: &Model, + layout: &Layout<'_>, + node_size: egui::Vec2, + icon_size: egui::Vec2, + max_distance: f32, +) -> Option { + // Hit-test only the transform-specific terminal segment; shared fork segments are decorative. + model + .snapshot + .edges + .iter() + .enumerate() + .filter_map(|(edge_index, edge)| { + let (start, end) = edge_unique_segment(edge, model, layout, node_size)?; + let icon_rect = edge_time_icon_rect(start, end, icon_size); + let distance = if icon_rect.expand(max_distance).contains(point) { + 0.0 + } else { + distance_to_segment(point, start, end) + }; + Some((edge_index, distance)) + }) + .filter(|(_, distance)| *distance <= max_distance) + .min_by(|(_, a), (_, b)| a.total_cmp(b)) + .map(|(edge_index, _)| edge_index) +} + +/// Finds the closest shared fork path under the pointer. +fn nearest_shared_fork( + point: egui::Pos2, + model: &Model, + layout: &Layout<'_>, + node_size: egui::Vec2, + max_distance: f32, +) -> Option { + shared_fork_parents(model) + .into_iter() + .filter_map(|parent| { + let min_distance = ForkGeometry::new(parent, model, layout, node_size)? + .segments + .into_iter() + .map(|[start, end]| distance_to_segment(point, start, end)) + .min_by(f32::total_cmp)?; + (min_distance <= max_distance).then_some((parent, min_distance)) + }) + .min_by(|(_, a), (_, b)| a.total_cmp(b)) + .map(|(parent, _)| parent) +} + +/// Returns parents with visible shared forks in deterministic edge order. +fn shared_fork_parents(model: &Model) -> Vec { + let mut seen = HashSet::default(); + model + .snapshot + .edges + .iter() + .filter_map(|edge| { + (model.edge_starts_at_shared_fork(edge) && seen.insert(edge.parent)) + .then_some(edge.parent) + }) + .collect() +} + +/// Draws the node hover tooltip. +fn node_tooltip_ui(ui: &mut egui::Ui, model: &Model, node: &Node) { + list_item::list_item_scope(ui, "transform_node_hover", |ui| { + ui.list_item_flat_noninteractive(list_item::PropertyContent::new("Subspace").value_fn( + |ui, _| { + ui.horizontal(|ui| { + ui.small_icon( + subspace_icon(node.subspace_kind), + Some(ui.tokens().text_subdued), + ); + ui.label(subspace_kind_label(node.subspace_kind)); + }); + }, + )); + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("Frame") + .min_desired_width(FRAME_PROPERTY_MIN_WIDTH) + .value_text(node.label.as_str()), + ); + ui.list_item_flat_noninteractive(list_item::PropertyContent::new("Kind").value_text( + if is_implicit_frame(node) { + "implicit" + } else { + "named" + }, + )); + if !node.has_transform { + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("Warning").value_text("No transforms"), + ); + } + + let ancestors = model.path_to_roots(node.id).0.len(); + let children = model.num_children(node.id); + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("Ancestors").value_text(ancestors.to_string()), + ); + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("Children").value_text(children.to_string()), + ); + }); +} + +/// Returns the icon used for a subspace kind. +fn subspace_icon(subspace_kind: SubspaceKind) -> &'static re_ui::Icon { + match subspace_kind { + SubspaceKind::TwoD => &re_ui::icons::VIEW_2D, + SubspaceKind::ThreeD => &re_ui::icons::VIEW_3D, + } +} + +/// Returns the short label used for a subspace kind. +fn subspace_kind_label(subspace_kind: SubspaceKind) -> &'static str { + match subspace_kind { + SubspaceKind::TwoD => "2D", + SubspaceKind::ThreeD => "3D", + } +} + +/// Draws the edge hover tooltip for an individual transform. +fn edge_tooltip_ui(ui: &mut egui::Ui, model: &Model, edge: &Edge) { + list_item::list_item_scope(ui, "transform_edge_hover", |ui| { + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("Parent") + .min_desired_width(FRAME_PROPERTY_MIN_WIDTH) + .value_text(model.frame_label(edge.parent)), + ); + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("Child") + .min_desired_width(FRAME_PROPERTY_MIN_WIDTH) + .value_text(model.frame_label(edge.child)), + ); + ui.list_item_flat_noninteractive(list_item::PropertyContent::new("Time").value_fn( + |ui, _| { + ui.horizontal(|ui| { + ui.small_icon(edge_time_icon(edge.time), Some(ui.tokens().text_subdued)); + ui.label(edge_time_label(edge.time)); + }); + }, + )); + match &edge.source { + EdgeSource::ImplicitHierarchy => {} + EdgeSource::Transform { + entity_path, + transform, + } => { + edge_source_tooltip_ui( + ui, + entity_path, + Some(transform.transform.translation), + transform.transform.matrix3.to_cols_array(), + ); + } + EdgeSource::Pinhole { + entity_path, + pinhole, + } => { + edge_source_tooltip_ui( + ui, + entity_path, + None, + pinhole.image_from_camera.0.0.map(f64::from), + ); + } + } + }); +} + +/// Draws transform source details in an edge hover tooltip. +fn edge_source_tooltip_ui( + ui: &mut egui::Ui, + entity_path: &re_log_types::EntityPath, + translation: Option, + matrix_cols: [f64; 9], +) { + ui.separator(); + ui.list_item_flat_noninteractive( + list_item::PropertyContent::new("Entity").value_text(entity_path.ui_string()), + ); + if let Some(translation) = translation { + ui.list_item_flat_noninteractive(list_item::PropertyContent::new("Translation").value_fn( + |ui, _| { + ui.monospace(format_dvec3(translation)); + }, + )); + } + ui.list_item_flat_noninteractive(list_item::PropertyContent::new("Matrix 3x3").value_fn( + |ui, _| { + matrix3x3_ui(ui, matrix_cols); + }, + )); +} + +/// Draws the hover tooltip for a shared fork path. +fn shared_fork_tooltip_ui(ui: &mut egui::Ui, model: &Model, parent: TransformFrameIdHash) { + let num_transforms = model.num_children(parent); + ui.label(format!( + "{} transform{}", + num_transforms, + if num_transforms == 1 { "" } else { "s" } + )); + ui.colored_label( + ui.tokens().text_subdued, + "Hover a terminal segment or icon to inspect an individual transform.", + ); +} + +/// Formats a translation vector for tooltip display. +fn format_dvec3(vec: glam::DVec3) -> String { + format!( + "[{}, {}, {}]", + re_format::format_f64(vec.x), + re_format::format_f64(vec.y), + re_format::format_f64(vec.z) + ) +} + +/// Draws a compact 3x3 matrix in the edge hover tooltip. +fn matrix3x3_ui(ui: &mut egui::Ui, matrix_cols: [f64; 9]) { + egui::Grid::new("transform_edge_matrix3x3") + .num_columns(3) + .spacing(egui::vec2(8.0, 2.0)) + .show(ui, |ui| { + for row in 0..3 { + for col in 0..3 { + ui.monospace(re_format::format_f64(matrix_cols[row + col * 3])); + } + ui.end_row(); + } + }); +} + +/// Returns the short label used for an edge time. +fn edge_time_label(time: TimeInt) -> &'static str { + if time.is_static() { + "static" + } else { + "temporal" + } +} diff --git a/crates/viewer/re_viewer/src/ui/mobile_warning_ui.rs b/crates/viewer/re_viewer/src/ui/mobile_warning_ui.rs index 9273657efc31..ea25a47a65f3 100644 --- a/crates/viewer/re_viewer/src/ui/mobile_warning_ui.rs +++ b/crates/viewer/re_viewer/src/ui/mobile_warning_ui.rs @@ -1,19 +1,24 @@ -use re_ui::{ContextExt as _, UiExt as _}; +use re_ui::{ContextExt as _, UiExt as _, WindowFrameConfig}; -pub fn mobile_warning_ui(ui: &mut egui::Ui) { +pub fn mobile_warning_ui(ui: &mut egui::Ui, custom_window_decorations: bool) { // We have not yet optimized the UI experience for mobile. Show a warning banner // with a link to the tracking issue. if ui.os() == egui::os::OperatingSystem::IOS || ui.os() == egui::os::OperatingSystem::Android { + let window_frame = if custom_window_decorations { + WindowFrameConfig::custom(ui.ctx()) + } else { + WindowFrameConfig::Native + }; let frame = egui::Frame { fill: ui.visuals().panel_fill, - ..ui.tokens().bottom_panel_frame() + ..ui.tokens().bottom_panel_frame(window_frame) }; egui::Panel::bottom("warning_panel") .resizable(false) .frame(frame) - .show_inside(ui, |ui| { + .show(ui, |ui| { ui.centered_and_justified(|ui| { let text = ui .ctx() diff --git a/crates/viewer/re_viewer/src/ui/mod.rs b/crates/viewer/re_viewer/src/ui/mod.rs index 0374aedf46dd..6dcd31bdc0f0 100644 --- a/crates/viewer/re_viewer/src/ui/mod.rs +++ b/crates/viewer/re_viewer/src/ui/mod.rs @@ -5,11 +5,13 @@ mod share_modal; mod top_panel; mod welcome_screen; -pub(crate) mod memory_panel; +pub(crate) mod dev_panel; mod settings_screen; // ---- +pub use rerun_menu::about_rerun_ui; + pub(crate) use open_url_modal::OpenUrlModal; pub(crate) use settings_screen::settings_screen_ui; pub(crate) use share_modal::ShareModal; diff --git a/crates/viewer/re_viewer/src/ui/open_url_modal.rs b/crates/viewer/re_viewer/src/ui/open_url_modal.rs index 7db2661ddc48..a33042499f7a 100644 --- a/crates/viewer/re_viewer/src/ui/open_url_modal.rs +++ b/crates/viewer/re_viewer/src/ui/open_url_modal.rs @@ -57,7 +57,6 @@ impl OpenUrlModal { &self.url, &re_data_source::FromUriOptions { accept_extensionless_http: true, - ..Default::default() }, ); let can_import = match &open_url { diff --git a/crates/viewer/re_viewer/src/ui/rerun_menu.rs b/crates/viewer/re_viewer/src/ui/rerun_menu.rs index f2ec64ad43a8..c30dc2ebbdb4 100644 --- a/crates/viewer/re_viewer/src/ui/rerun_menu.rs +++ b/crates/viewer/re_viewer/src/ui/rerun_menu.rs @@ -2,12 +2,14 @@ use std::fmt::Write as _; +use egui::ScrollArea; #[cfg(debug_assertions)] use egui::containers::menu; use egui::containers::menu::{MenuButton, MenuConfig}; -use egui::{Button, NumExt as _, ScrollArea}; use re_ui::menu::menu_style; -use re_ui::{UICommand, UICommandSender as _, UiExt as _}; +use re_ui::{ + RecordingCommand, RecordingCommandKind, UICommand, UICommandSender as _, UiExt as _, icons, +}; use re_viewer_context::ActiveStoreContext; use crate::App; @@ -21,23 +23,14 @@ impl App { _store_context: Option<&ActiveStoreContext<'_>>, ui: &mut egui::Ui, ) { - let desired_icon_height = if ui.max_rect().height() <= 24.0 { - // This is a bit of a hack to produce a sharp logo on mac on low-DPI screens. - // At a 16x16 size, the Rerun logo SVG just happens to have all its vertical - // lines at even pixel positions, making it look sharp and nice. - 16.0 - } else { - ui.max_rect().height() - 4.0 - }; - let desired_icon_height = desired_icon_height.at_most(28.0); - - let image = re_ui::icons::RERUN_MENU + let icon_tint = ui.tokens().strong_fg_color; + let image = re_ui::icons::RERUN_WORDMARK .as_image() - .max_height(desired_icon_height) - .tint(ui.tokens().strong_fg_color) + .max_height(12.0) + .tint(icon_tint) .alt_text("Menu"); - MenuButton::from_button(Button::image(image)) + MenuButton::new((image, icons::DROPDOWN_ARROW.as_image().tint(icon_tint))) .config(MenuConfig::new().style(menu_style())) .ui(ui, |ui| { ui.set_max_height(ui.content_rect().height()); @@ -85,12 +78,17 @@ impl App { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // no wrapping: make as wide as needed - ui.menu_button("About", |ui| self.about_rerun_ui(ui, render_state)); + let build_info = self.build_info(); + ui.menu_button("About", |ui| { + about_rerun_ui(ui, build_info, render_state); + }); ui.add_space(SPACING); - UICommand::Undo.menu_button_ui(ui, &self.command_sender); // TODO(emilk): only enabled if there is something to undo - UICommand::Redo.menu_button_ui(ui, &self.command_sender); // TODO(emilk): only enabled if there is something to redo + let recording_id = _store_context.map(|ctx| ctx.recording_store_id()); + + RecordingCommandKind::Undo.menu_button_ui(ui, recording_id, &self.command_sender); // TODO(emilk): only enabled if there is something to undo + RecordingCommandKind::Redo.menu_button_ui(ui, recording_id, &self.command_sender); // TODO(emilk): only enabled if there is something to redo UICommand::ToggleCommandPalette.menu_button_ui(ui, &self.command_sender); @@ -103,12 +101,8 @@ impl App { self.save_buttons_ui(ui, _store_context); - UICommand::SaveBlueprint.menu_button_ui(ui, &self.command_sender); - - let has_recording = _store_context.is_some(); - ui.add_enabled_ui(has_recording, |ui| { - UICommand::CloseCurrentRecording.menu_button_ui(ui, &self.command_sender); - }); + RecordingCommandKind::SaveBlueprint.menu_button_ui(ui, recording_id, &self.command_sender); + RecordingCommandKind::Close.menu_button_ui(ui, recording_id, &self.command_sender); ui.add_space(SPACING); @@ -139,8 +133,12 @@ impl App { #[cfg(not(target_arch = "wasm32"))] UICommand::OpenProfiler.menu_button_ui(ui, &self.command_sender); - UICommand::ToggleMemoryPanel.menu_button_ui(ui, &self.command_sender); - UICommand::ToggleChunkStoreBrowser.menu_button_ui(ui, &self.command_sender); + UICommand::ToggleDevPanel.menu_button_ui(ui, &self.command_sender); + RecordingCommandKind::ToggleChunkStoreBrowser.menu_button_ui( + ui, + recording_id, + &self.command_sender, + ); #[cfg(debug_assertions)] UICommand::ToggleEguiDebugPanel.menu_button_ui(ui, &self.command_sender); @@ -162,7 +160,12 @@ impl App { ) .ui(ui, |ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - debug_menu_options_ui(ui, &mut self.state.app_options, &self.command_sender); + debug_menu_options_ui( + ui, + &mut self.state.app_options, + recording_id, + &self.command_sender, + ); ui.label("egui debug options:"); ui.weak(format!("pixels_per_point: {:?}", ui.pixels_per_point())); @@ -171,6 +174,7 @@ impl App { ui.add_space(SPACING); + UICommand::OpenWebsite.menu_button_ui(ui, &self.command_sender); UICommand::OpenWebHelp.menu_button_ui(ui, &self.command_sender); UICommand::OpenRerunDiscord.menu_button_ui(ui, &self.command_sender); @@ -181,68 +185,13 @@ impl App { } } - fn about_rerun_ui(&self, ui: &mut egui::Ui, render_state: Option<&egui_wgpu::RenderState>) { - let re_build_info::BuildInfo { - crate_name, - features, - version, - rustc_version, - llvm_version, - git_hash, - git_branch: _, - is_in_rerun_workspace: _, - target_triple, - datetime, - is_debug_build, - } = self.build_info(); - - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - - let git_hash_suffix = if git_hash.is_empty() { - String::new() - } else { - let short_git_hash = &git_hash[..std::cmp::min(git_hash.len(), 7)]; - format!("({short_git_hash})") - }; - - let debug_label = if *is_debug_build { " (debug)" } else { "" }; - - let mut label = format!( - "{crate_name} {version} {git_hash_suffix}{debug_label}\n\ - {target_triple}" - ); - - // It is really the features of `rerun-cli` (the `rerun` binary) that are interesting. - // For the web-viewer we get `crate_name: "re_viewer"` here, which is much less interesting. - if crate_name == "rerun-cli" && !features.is_empty() { - write!(label, "\n{crate_name} features: {features}").ok(); - } - - if !rustc_version.is_empty() { - write!(label, "\nrustc {rustc_version}").ok(); - if !llvm_version.is_empty() { - write!(label, ", LLVM {llvm_version}").ok(); - } - } - - if !datetime.is_empty() { - write!(label, "\nbuilt {datetime}").ok(); - } - - ui.label(label); - - if let Some(render_state) = render_state { - render_state_ui(ui, render_state); - } - } - fn save_buttons_ui(&self, ui: &mut egui::Ui, store_ctx: Option<&ActiveStoreContext<'_>>) { - use re_ui::UICommandSender as _; + use re_ui::RecordingCommandSender as _; let file_save_in_progress = self.background_tasks.is_file_save_in_progress(); - let save_recording_button = UICommand::SaveRecording.menu_button(ui.ctx()); - let save_selection_button = UICommand::SaveRecordingSelection.menu_button(ui.ctx()); + let save_recording_button = RecordingCommandKind::Save.menu_button(ui.ctx()); + let save_selection_button = RecordingCommandKind::SaveTimeSelection.menu_button(ui.ctx()); if file_save_in_progress { ui.add_enabled_ui(false, |ui| { @@ -256,23 +205,29 @@ impl App { }); }); } else { - let entity_db_is_nonempty = - store_ctx.is_some_and(|ctx| 0 < ctx.recording.num_physical_chunks()); - ui.add_enabled_ui(entity_db_is_nonempty, |ui| { + let recording_id = store_ctx + .filter(|ctx| 0 < ctx.recording.num_physical_chunks()) + .map(|ctx| ctx.recording.store_id()); + ui.add_enabled_ui(recording_id.is_some(), |ui| { if ui .add(save_recording_button) .on_hover_text("Save all data to a Rerun data file (.rrd)") .clicked() + && let Some(recording_id) = recording_id { ui.close(); - self.command_sender.send_ui(UICommand::SaveRecording); + self.command_sender + .send_recording_command(RecordingCommand { + recording_id: recording_id.clone(), + kind: RecordingCommandKind::Save, + }); } // We need to know the loop selection _before_ we can even display the // button, as this will determine whether its grayed out or not! // TODO(cmc): In practice the loop (green) selection is always there // at the moment so… - let loop_selection = self.state.loop_selection(store_ctx); + let loop_selection = store_ctx.and_then(|ctx| ctx.loop_selection()); if ui .add_enabled(loop_selection.is_some(), save_selection_button) @@ -280,16 +235,134 @@ impl App { "Save data for the current loop selection to a Rerun data file (.rrd)", ) .clicked() + && let Some(recording_id) = recording_id { ui.close(); self.command_sender - .send_ui(UICommand::SaveRecordingSelection); + .send_recording_command(RecordingCommand { + recording_id: recording_id.clone(), + kind: RecordingCommandKind::SaveTimeSelection, + }); } }); } } } +/// The about-menu serves two purposes: +/// +/// A) Tell users about what Rerun is, in case they just stumbled upon it online. +/// B) Show detailed build information, that can be used when reporting bugs. +pub fn about_rerun_ui( + ui: &mut egui::Ui, + build_info: &re_build_info::BuildInfo, + render_state: Option<&egui_wgpu::RenderState>, +) { + let re_build_info::BuildInfo { + crate_name, + features, + version, + rustc_version, + llvm_version, + git_hash, + git_branch: _, + is_in_rerun_workspace: _, + target_triple, + datetime, + is_debug_build, + } = build_info; + + ui.set_max_width(400.0); + + let logo_size = 68.0; + + ui.horizontal(|ui|{ + ui.add( + re_ui::icons::RERUN_LOGO + .as_image() + .fit_to_exact_size(egui::Vec2::splat(logo_size)) + .corner_radius(4.0) + .alt_text("Rerun"), + ); + + ui.vertical(|ui|{ + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap); + ui.label( + "Rerun is a toolchain for robotics and physical AI that makes it easy to log, query, visualize, and train on multi-rate, multimodal data.", + ); + + ui.add_space(4.0); + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("Learn more at "); + ui.hyperlink_to("rerun.io", "https://rerun.io/"); + ui.label("."); + }); + }); + }); + + ui.add_space(SPACING); + + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + let version = { + let git_hash_suffix = if git_hash.is_empty() { + String::new() + } else { + let short_git_hash = &git_hash[..std::cmp::min(git_hash.len(), 7)]; + format!(" ({short_git_hash})") + }; + + let debug_label = if *is_debug_build { " (debug)" } else { "" }; + + format!("{version}{git_hash_suffix}{debug_label}") + }; + + egui::Grid::new("build_info").num_columns(2).show(ui, |ui| { + ui.label("Crate"); + ui.label(crate_name.as_ref()); + ui.end_row(); + + ui.label("Version"); + ui.label(version); + ui.end_row(); + + if !datetime.is_empty() { + ui.label("Built"); + ui.label(datetime.as_ref()); + ui.end_row(); + } + + // It is really the features of `rerun-cli` (the `rerun` binary) that are interesting. + // For the web-viewer (`crate_name: "re_viewer"`) it is much less interesting. + if crate_name == "rerun-cli" && !features.is_empty() { + ui.label("Features"); + ui.label(features.as_ref()); + ui.end_row(); + } + + ui.label("Platform"); + ui.label(target_triple.as_ref()); + ui.end_row(); + + if !rustc_version.is_empty() { + ui.label("Compiler"); + let mut compiler = format!("rustc {rustc_version}"); + if !llvm_version.is_empty() { + write!(compiler, ", LLVM {llvm_version}").ok(); + } + ui.label(compiler); + ui.end_row(); + } + }); + + if let Some(render_state) = render_state { + ui.add_space(SPACING); + render_state_ui(ui, render_state); + } +} + fn render_state_ui(ui: &mut egui::Ui, render_state: &egui_wgpu::RenderState) { let wgpu_adapter_details_ui = |ui: &mut egui::Ui, adapter: &eframe::wgpu::Adapter| { let info = &adapter.get_info(); @@ -360,13 +433,13 @@ fn render_state_ui(ui: &mut egui::Ui, render_state: &egui_wgpu::RenderState) { }; egui::Grid::new("wgpu_info").num_columns(2).show(ui, |ui| { - ui.label("Rendering backend:"); + ui.label("Rendering backend"); wgpu_adapter_ui(ui, &render_state.adapter); ui.end_row(); #[cfg(not(target_arch = "wasm32"))] if 1 < render_state.available_adapters.len() { - ui.label("Other rendering backends:"); + ui.label("Other rendering backends"); ui.vertical(|ui| { for adapter in &*render_state.available_adapters { if adapter.get_info() != render_state.adapter.get_info() { @@ -441,6 +514,7 @@ use re_viewer_context::CommandSender; fn debug_menu_options_ui( ui: &mut egui::Ui, app_options: &mut re_viewer_context::AppOptions, + active_recording_id: Option<&re_log_types::StoreId>, command_sender: &CommandSender, ) { use re_ui::UiExt as _; @@ -465,7 +539,11 @@ fn debug_menu_options_ui( re_log::info!("Logging some info"); } - UICommand::ToggleBlueprintInspectionPanel.menu_button_ui(ui, command_sender); + RecordingCommandKind::ToggleBlueprintInspectionPanel.menu_button_ui( + ui, + active_recording_id, + command_sender, + ); ui.horizontal(|ui| { ui.label("Blueprint GC:"); diff --git a/crates/viewer/re_viewer/src/ui/settings_screen.rs b/crates/viewer/re_viewer/src/ui/settings_screen.rs index 6009a3d2344b..424130546a86 100644 --- a/crates/viewer/re_viewer/src/ui/settings_screen.rs +++ b/crates/viewer/re_viewer/src/ui/settings_screen.rs @@ -81,6 +81,7 @@ fn settings_screen_ui_impl(ui: &mut egui::Ui, app_options: &mut AppOptions, keep warn_e2e_latency: _, // not yet exposed show_metrics, show_notification_toasts, + custom_window_decorations, include_rerun_examples_button_in_recordings_panel, show_picking_debug_overlay: _, // not yet exposed inspect_blueprint_timeline: _, // not yet exposed @@ -126,12 +127,6 @@ fn settings_screen_ui_impl(ui: &mut egui::Ui, app_options: &mut AppOptions, keep "Show 'Rerun examples' button", ); - ui.re_checkbox(show_metrics, "Show performance metrics") - .on_hover_text("Show metrics for milliseconds/frame and RAM usage in the top bar"); - - ui.re_checkbox(show_notification_toasts, "Show notification toasts") - .on_hover_text("Show toasts for log messages and other notifications"); - ui.re_checkbox( visualizer_limits_enabled, "Limit number of primitives in a view", @@ -143,11 +138,27 @@ fn settings_screen_ui_impl(ui: &mut egui::Ui, app_options: &mut AppOptions, keep with very large data sets.", ); - separator_with_some_space(ui); ui.collapsing_header("Timestamp format", false, |ui| { time_format_section_ui(ui, timestamp_format); }); + separator_with_some_space(ui); + ui.strong("Title bar"); + + if re_ui::supports_custom_decorations(ui.os()) { + ui.re_checkbox(custom_window_decorations, "Use custom window decorations") + .on_hover_text( + "Hide the native title bar and draw Rerun's top bar as the window frame.\n\n\ + Opt out of this if you experience any issues with the window's behavior.", + ); + } + + ui.re_checkbox(show_metrics, "Show performance metrics") + .on_hover_text("Show metrics for milliseconds/frame and RAM usage in the top bar"); + + ui.re_checkbox(show_notification_toasts, "Show notification toasts") + .on_hover_text("Show toasts for log messages and other notifications"); + separator_with_some_space(ui); ui.strong("Map view"); map_view_section_ui(ui, mapbox_access_token); @@ -158,20 +169,41 @@ fn settings_screen_ui_impl(ui: &mut egui::Ui, app_options: &mut AppOptions, keep { let ExperimentalAppOptions { - enable_status_view, - table_grid_view, + table_cards_and_blueprints, + gamepad_navigation, + point_cloud_transparency, + use_internal_catalog, } = experimental; separator_with_some_space(ui); ui.strong("Experimental"); - ui.re_checkbox(enable_status_view, "Enable Status view (requires restart)") + ui.re_checkbox(table_cards_and_blueprints, "Table cards and blueprints") .on_hover_text( - "Show the experimental Status view for visualizing status transitions over time.", + "Enable registered table blueprints, plus grid view mode for server supplied tables.\n\n\ + When enabled, tables can use registered view definitions for segment previews, and a list/grid toggle appears in the table title bar.", ); - ui.re_checkbox(table_grid_view, "Table grid view") + ui.re_checkbox(point_cloud_transparency, "Point cloud transparency") .on_hover_text( - "Enable grid view mode for server supplied tables.\n\n\ - When enabled, a list/grid toggle appears in the table title bar.", + "Alpha-blend semi-transparent point clouds, sorting them back-to-front.\n\n\ + Sorting happens on the CPU every frame, so this is very slow for large point clouds.", ); + ui.re_checkbox(use_internal_catalog, "Load files via Viewer catalog") + .on_hover_text( + "Load .rrd files through the Viewer catalog instead of importing them as a live \ + recording. Takes effect for files opened after enabling.", + ); + #[cfg(not(target_arch = "wasm32"))] + { + let gamepad_navigation_response = ui + .re_checkbox(gamepad_navigation, "Gamepad navigation") + .on_hover_text("Enable gamepad navigation in 3D spatial views."); + if gamepad_navigation_response.changed() && !*gamepad_navigation { + re_gamepad::clear_event_waker(); + } + } + #[cfg(target_arch = "wasm32")] + { + let _ = gamepad_navigation; + } } } @@ -216,7 +248,7 @@ fn memory_budget_section_ui(ui: &mut Ui, memory_limit: &mut MemoryLimit) { fn prefetch_stage_combo_box_ui(ui: &mut Ui, max_fetch_stage: &mut FetchStage) { fn label(stage: FetchStage) -> &'static str { match stage { - FetchStage::Required => "Required", + FetchStage::Required | FetchStage::Indicated => "Required", FetchStage::Similar(_) => "Similar", FetchStage::Everything => "Everything", } @@ -226,7 +258,7 @@ fn prefetch_stage_combo_box_ui(ui: &mut Ui, max_fetch_stage: &mut FetchStage) { .selected_text(label(*max_fetch_stage)) .show_ui(ui, |ui| { for stage in [ - FetchStage::Required, + FetchStage::Indicated, FetchStage::default(), FetchStage::Everything, ] { @@ -277,7 +309,7 @@ fn prefetch_stage_combo_box_ui(ui: &mut Ui, max_fetch_stage: &mut FetchStage) { ui.label(label); } - FetchStage::Required | FetchStage::Everything => {} + FetchStage::Required | FetchStage::Indicated | FetchStage::Everything => {} } } diff --git a/crates/viewer/re_viewer/src/ui/share_modal.rs b/crates/viewer/re_viewer/src/ui/share_modal.rs index 7976b69ddee5..d729dbe7bcf9 100644 --- a/crates/viewer/re_viewer/src/ui/share_modal.rs +++ b/crates/viewer/re_viewer/src/ui/share_modal.rs @@ -419,7 +419,7 @@ mod tests { re_uri::DatasetSegmentUri { origin: origin.clone(), dataset_id, - segment_id: "segment_id".to_owned(), + segment_id: "segment_id".into(), fragment: re_uri::Fragment::default(), }, )); @@ -432,7 +432,9 @@ mod tests { [ TimeControlCommand::SetActiveTimeline(*timeline.name()), TimeControlCommand::SetTime(re_chunk::TimeInt::ZERO.into()), - TimeControlCommand::SetTimeSelection(AbsoluteTimeRangeF::new(0.0, 1000.0).to_int()), + TimeControlCommand::SetTimeSelectionClamped( + AbsoluteTimeRangeF::new(0.0, 1000.0).to_int(), + ), ], ); @@ -442,7 +444,7 @@ mod tests { re_uri::DatasetSegmentUri { origin: origin.clone(), dataset_id, - segment_id: "segment_id".to_owned(), + segment_id: "segment_id".into(), fragment: re_uri::Fragment { selection: selection.to_data_path(), when: Some((*timeline.name(), TimeCell::new(timeline.typ(), 234))), diff --git a/crates/viewer/re_viewer/src/ui/top_panel.rs b/crates/viewer/re_viewer/src/ui/top_panel.rs index 26494c0e9781..87fa59a789c7 100644 --- a/crates/viewer/re_viewer/src/ui/top_panel.rs +++ b/crates/viewer/re_viewer/src/ui/top_panel.rs @@ -1,6 +1,5 @@ use egui::{ - Align, Atom, Button, Color32, Id, Image, Layout, NumExt as _, Popup, RichText, Sense, - include_image, + Align, Atom, Button, Color32, Id, Image, Layout, Popup, RichText, Sense, include_image, }; use emath::{Rect, RectAlign, Vec2}; use re_format::format_uint; @@ -25,13 +24,21 @@ pub fn top_panel( re_tracing::profile_function!(); let style_like_web = app.is_screenshotting() || app.app_env().is_test(); + let native_window_bar = !re_ui::fullsize_content(ui.os()) && !app.custom_window_decorations(); let top_bar_style = ui.top_bar_style(frame, style_like_web); - let top_panel_frame = ui.tokens().top_panel_frame(); + let window_frame = app.window_frame_config(ui.ctx()); + let mut top_panel_frame = ui.tokens().top_panel_frame(window_frame); + + if app.custom_window_decorations() { + // Keep the custom window buttons flush with the right edge. `custom_window_frame` is false + // on Windows, but we still draw custom caption buttons there. + top_panel_frame.inner_margin.right = 0; + } let mut content = |ui: &mut egui::Ui, show_content: bool| { // React to dragging and double-clicking the top bar: #[cfg(not(target_arch = "wasm32"))] - if !re_ui::native_window_bar(ui.os()) { + if !native_window_bar { // Interact with background first, so that buttons in the top bar gets input priority // (last added widget has priority for input). let title_bar_response = ui.interact( @@ -74,10 +81,11 @@ pub fn top_panel( // On MacOS, we show the close/minimize/maximize buttons in the top panel. // We _always_ want to show the top panel in that case, and only hide its content. - if re_ui::native_window_bar(ui.os()) { - panel.show_animated_inside(ui, is_expanded, |ui| content(ui, is_expanded)); + if native_window_bar { + let mut panel_expanded = is_expanded; // Note: can't resize top panel, or drag-to-close it. + panel.show_collapsible(ui, &mut panel_expanded, |ui| content(ui, is_expanded)); } else { - panel.show_inside(ui, |ui| content(ui, is_expanded)); + panel.show(ui, |ui| content(ui, is_expanded)); } } @@ -92,9 +100,6 @@ fn top_bar_ui( ) { app.rerun_menu_button_ui(frame.wgpu_render_state(), store_context, ui); - ui.add_space(12.0); - website_link_ui(ui); - if !app.startup_options().web_history_enabled() { ui.add_space(12.0); app.navigation_buttons(ui); @@ -113,7 +118,7 @@ fn top_bar_ui( ui.spacing_mut().item_spacing.x = 12.0; // Varying widths: - memory_use_label_ui(ui, gpu_resource_stats); + memory_use_label_ui(ui, gpu_resource_stats, &app.external_memory_users); frame_time_label_ui(ui, app); fps_ui(ui, app); @@ -153,8 +158,7 @@ fn top_bar_ui( } ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if re_ui::CUSTOM_WINDOW_DECORATIONS && !cfg!(target_arch = "wasm32") { - ui.add_space(8.0); + if app.custom_window_decorations() && !cfg!(target_arch = "wasm32") { #[cfg(not(target_arch = "wasm32"))] ui.native_window_buttons_ui(); ui.separator(); @@ -406,7 +410,7 @@ fn panel_buttons_r2l( "Time panel toggle", &mut app_blueprint.time_panel_state().is_expanded(), ) - .on_hover_ui(|ui| UICommand::ToggleTimePanel.tooltip_ui(ui)) + .on_hover_ui(|ui| re_ui::RecordingCommandKind::ToggleTimePanel.tooltip_ui(ui)) .clicked() { app_blueprint.toggle_time_panel(&app.command_sender); @@ -513,29 +517,6 @@ fn user_icon(email: &str, rect: Rect, ui: &egui::Ui, corner_radius: f32, tint: u ); } -/// Shows clickable website link as an image (text doesn't look as nice) -fn website_link_ui(ui: &mut egui::Ui) { - let desired_height = ui.max_rect().height(); - let desired_height = desired_height.at_most(20.0); - - let image = re_ui::icons::RERUN_IO_TEXT - .as_image() - .fit_to_original_size(2.0) // hack, because the original SVG is very small - .max_height(desired_height) - .tint(ui.tokens().strong_fg_color); - - let url = "https://rerun.io/"; - let response = ui - .add(egui::Button::image(image)) - .on_hover_cursor(egui::CursorIcon::PointingHand); - if response.clicked() { - ui.open_url(egui::output::OpenUrl { - url: url.to_owned(), - new_tab: true, - }); - } -} - fn frame_time_label_ui(ui: &mut egui::Ui, app: &App) { if let Some(frame_time) = app.frame_time_history.average() { let ms = frame_time * 1e3; @@ -588,7 +569,11 @@ fn fps_ui(ui: &mut egui::Ui, app: &App) { } } -fn memory_use_label_ui(ui: &mut egui::Ui, gpu_resource_stats: &WgpuResourcePoolStatistics) { +fn memory_use_label_ui( + ui: &mut egui::Ui, + gpu_resource_stats: &WgpuResourcePoolStatistics, + external_usage: &crate::external_memory::ExternalMemoryUsers, +) { const CODE: &str = "use re_memory::AccountingAllocator;\n\ #[global_allocator]\n\ static GLOBAL: AccountingAllocator =\n \ @@ -620,22 +605,52 @@ fn memory_use_label_ui(ui: &mut egui::Ui, gpu_resource_stats: &WgpuResourcePoolS if let Some(count) = re_memory::accounting_allocator::global_allocs() { // we use monospace so the width doesn't fluctuate as the numbers change. - let bytes_used_text = re_format::format_bytes(count.size as _); + ui.label( egui::RichText::new(&bytes_used_text) .monospace() .color(ui.visuals().weak_text_color()), ) - .on_hover_text(format!( - "Rerun Viewer is using {} of RAM in {} separate allocations,\n\ - plus {} of GPU memory in {} textures and {} buffers.", - bytes_used_text, - format_uint(count.count), - re_format::format_bytes(gpu_resource_stats.total_bytes() as _), - format_uint(gpu_resource_stats.num_textures), - format_uint(gpu_resource_stats.num_buffers), - )); + .on_hover_ui(|ui| { + egui::Grid::new("memory usage hover") + .num_columns(2) + .show(ui, |ui| { + let global_mem = count.size; + let external_mem = external_usage.total_external_memory(); + let viewer_mem = global_mem as u64 - external_mem; + + ui.label("Viewer"); + ui.monospace(re_format::format_bytes(viewer_mem as _)); + ui.end_row(); + + if external_mem > 0 { + ui.label("External"); + ui.monospace(re_format::format_bytes(external_mem as _)); + ui.end_row(); + } + + ui.label("Allocations"); + ui.monospace(format_uint(count.count)); + ui.end_row(); + + ui.label("GPU"); + ui.monospace(re_format::format_bytes( + gpu_resource_stats.total_bytes() as _ + )); + ui.end_row(); + + ui.label("GPU textures"); + ui.monospace(format_uint(gpu_resource_stats.num_textures)); + ui.end_row(); + + ui.label("GPU buffers"); + ui.monospace(format_uint(gpu_resource_stats.num_buffers)); + ui.end_row(); + }); + + ui.weak("See dev panel for more info"); + }); } else if let Some(rss) = mem.resident { let bytes_used_text = re_format::format_bytes(rss as _); click_to_copy(ui, &bytes_used_text, |ui| { @@ -702,7 +717,7 @@ fn latency_details_ui(ui: &mut egui::Ui, latency: re_entity_db::LatencySnapshot) let e2e_hover_text = "End-to-end latency from when the data was logged by the SDK to when it is shown in the viewer.\n\ This includes time for encoding, network latency, and decoding.\n\ - It is also affected by the framerate of the viewer.\n\ + It is also affected by the frame rate of the viewer.\n\ This latency is inaccurate if the logging was done on a different machine, since it is clock-based."; let re_entity_db::LatencySnapshot { secs_since_log } = latency; diff --git a/crates/viewer/re_viewer/src/ui/welcome_screen/intro_section.rs b/crates/viewer/re_viewer/src/ui/welcome_screen/intro_section.rs index 2811ec237de0..22483d1bee22 100644 --- a/crates/viewer/re_viewer/src/ui/welcome_screen/intro_section.rs +++ b/crates/viewer/re_viewer/src/ui/welcome_screen/intro_section.rs @@ -43,17 +43,17 @@ impl IntroItem { Self::DocItem { title: "Send data in", url: "https://rerun.io/docs/getting-started/data-in", - body: "Send data to Rerun from your running applications or existing files.", + body: "Ingest multi-rate, multimodal data from robot logs, sensors, simulation, or video.", }, Self::DocItem { title: "Explore data", url: "https://rerun.io/docs/getting-started/configure-the-viewer", - body: "Familiarize yourself with the basics of using the Rerun Viewer.", + body: "Visualize and explore multi-rate, multimodal data across every stage of the pipeline.", }, Self::DocItem { title: "Query data out", url: "https://rerun.io/docs/getting-started/data-out", - body: "Perform analysis and send back the results to the original recording.", + body: "Query raw, intermediate, and derived data with dataframes or SQL, and stream to training.", }, ]; if login_enabled { @@ -120,7 +120,7 @@ impl IntroItem { }; ui.set_style(ui.style_of(opposite_theme)); - ui.heading(RichText::new("Rerun Data Platform").strong()); + ui.heading(RichText::new("Rerun Hub").strong()); ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; @@ -141,9 +141,9 @@ impl IntroItem { ui.style_mut().text_styles.get_mut(&TextStyle::Body).expect("Should always have body text style").size = label_size; ui.label( - "Iterate faster on robotics learning with unified infrastructure. Interested? Read more " + "The production backend for the Rerun data layer — turn your object stores into a queryable, streamable foundation. " ); - link(ui, "here", "https://rerun.io/#rerun-data-platform"); + link(ui, "Learn more", "https://rerun.io/#rerun-data-platform"); ui.label(" or "); link(ui, "book a demo", "https://calendly.com/d/ctht-4kp-qnt/rerun-demo-meeting"); ui.label("."); @@ -188,7 +188,7 @@ impl IntroItem { } ui.spacing_mut().item_spacing.x = 0.0; ui.weak("for address "); - ui.strong(format!("{}", &origin.host)); + ui.strong(format!("{}", origin.host)); }); } CloudState { has_server: Some(origin), login: LoginState::Auth { .. } } => { @@ -209,7 +209,7 @@ pub fn intro_section(ui: &mut egui::Ui, ctx: &AppContext<'_>, cloud_state: &Clou ui.add_space(32.0); if let Some(auth) = ctx.auth_context { - ui.strong(RichText::new(format!("Hi, {}!", &auth.email)).size(15.0)); + ui.strong(RichText::new(format!("Hi, {}!", auth.email)).size(15.0)); if ui.add(Button::new("Log out").secondary().small()).clicked() { ctx.command_sender.send_system(SystemCommand::Logout); diff --git a/crates/viewer/re_viewer/src/ui/welcome_screen/welcome_section.rs b/crates/viewer/re_viewer/src/ui/welcome_screen/welcome_section.rs index 56e4c3ecbeb4..1f7563af9519 100644 --- a/crates/viewer/re_viewer/src/ui/welcome_screen/welcome_section.rs +++ b/crates/viewer/re_viewer/src/ui/welcome_screen/welcome_section.rs @@ -1,11 +1,11 @@ use re_ui::DesignTokens; pub(super) const DOCS_URL: &str = "https://www.rerun.io/docs"; -pub(super) const WELCOME_SCREEN_TITLE: &str = "Welcome to Rerun"; +pub(super) const WELCOME_SCREEN_TITLE: &str = "The data layer for physical AI"; pub(super) const WELCOME_SCREEN_BULLET_TEXT: &[&str] = &[ - "Log data with the Rerun SDK in C++, Python, or Rust", - "Visualize and explore live or recorded data", - "Configure the viewer interactively or through code", + "Log multi-rate, multimodal data with the Rerun SDK in C++, Python, or Rust", + "Visualize and explore live or recorded data across the pipeline", + "Query with dataframes or SQL, and stream directly to training", ]; /// Show the welcome section. diff --git a/crates/viewer/re_viewer/src/viewer_test_utils/app_testing_ext.rs b/crates/viewer/re_viewer/src/viewer_test_utils/app_testing_ext.rs index 1f030139dc50..89c87a67d8cd 100644 --- a/crates/viewer/re_viewer/src/viewer_test_utils/app_testing_ext.rs +++ b/crates/viewer/re_viewer/src/viewer_test_utils/app_testing_ext.rs @@ -1,4 +1,5 @@ #![cfg(feature = "testing")] +use re_ui::notifications::NotificationUi; use re_viewer_context::{Route, StoreHub}; use crate::App; @@ -6,7 +7,9 @@ use crate::App; pub trait AppTestingExt { fn testonly_get_store_hub(&mut self) -> &mut StoreHub; fn testonly_get_route(&self) -> &Route; - fn testonly_set_test_hook(&mut self, func: crate::app_state::TestHookFn); + fn testonly_set_recording_test_hook(&mut self, func: crate::app_state::TestHookRecordingFn); + fn testonly_set_app_test_hook(&mut self, func: crate::app_state::TestHookAppFn); + fn testonly_get_notifications(&self) -> &NotificationUi; } impl AppTestingExt for App { @@ -20,7 +23,15 @@ impl AppTestingExt for App { self.state.navigation.current() } - fn testonly_set_test_hook(&mut self, func: crate::app_state::TestHookFn) { - self.state.test_hook = Some(func); + fn testonly_set_recording_test_hook(&mut self, func: crate::app_state::TestHookRecordingFn) { + self.state.test_hook_recording = Some(func); + } + + fn testonly_set_app_test_hook(&mut self, func: crate::app_state::TestHookAppFn) { + self.state.test_hook_app = Some(func); + } + + fn testonly_get_notifications(&self) -> &NotificationUi { + &self.notifications } } diff --git a/crates/viewer/re_viewer/src/viewer_test_utils/mod.rs b/crates/viewer/re_viewer/src/viewer_test_utils/mod.rs index 11438e1cd972..100d0a963f78 100644 --- a/crates/viewer/re_viewer/src/viewer_test_utils/mod.rs +++ b/crates/viewer/re_viewer/src/viewer_test_utils/mod.rs @@ -9,6 +9,7 @@ pub use app_testing_ext::AppTestingExt; use egui_kittest::Harness; use re_build_info::build_info; use re_viewer_context::AppOptions; +use re_viewer_context::external::re_log_types::DateVisibility; pub type AppOptionsEditor = Box; @@ -19,7 +20,9 @@ pub struct HarnessOptions { pub step_dt: Option, pub startup_url: Option, pub enable_component_mapping: bool, - pub enable_experimental_status_view: bool, + + /// Allows tests to emulate platform-specific UI behavior. + pub os: Option, /// Allows the test to set `AppOptions` at start. pub app_options_editor: Option, @@ -37,10 +40,20 @@ pub fn viewer_harness(options: &HarnessOptions) -> Harness<'static, App> { if let Some(step_dt) = options.step_dt { harness_builder = harness_builder.with_step_dt(step_dt); } + if let Some(os) = options.os { + harness_builder = harness_builder.with_os(os); + } harness_builder.build_eframe(|cc| { - cc.egui_ctx.set_os(egui::os::OperatingSystem::Nix); customize_eframe_and_setup_renderer(cc).expect("Failed to customize eframe"); + let connection_registry = + re_redap_client::ConnectionRegistry::new_without_stored_credentials(); + // Tests don't spawn a proxy server, so the catalog is only reached in-process; the + // origin's port is just a label here. + let addr = + std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, re_uri::DEFAULT_PROXY_PORT)); + let catalog = crate::internal_catalog::build(addr); + connection_registry.set_internal((catalog.origin, catalog.connection)); let mut app = App::new( MainThreadToken::i_promise_i_am_only_using_this_for_a_test(), build_info!(), @@ -49,11 +62,10 @@ pub fn viewer_harness(options: &HarnessOptions) -> Harness<'static, App> { // Don't show the welcome / example screen in tests. // See also: https://github.com/rerun-io/rerun/issues/10989 hide_welcome_screen: true, - enable_experimental_status_view: options.enable_experimental_status_view, ..Default::default() }, cc, - Some(re_redap_client::ConnectionRegistry::new_without_stored_credentials()), + Some(connection_registry), AsyncRuntimeHandle::from_current_tokio_runtime_or_wasmbindgen() .expect("Failed to create AsyncRuntimeHandle"), ); @@ -61,8 +73,18 @@ pub fn viewer_harness(options: &HarnessOptions) -> Harness<'static, App> { app.app_options_mut().video.ffmpeg_path = "/fake/ffmpeg/path".to_owned(); app.app_options_mut().video.override_ffmpeg_path = true; - // Enable experimental grid view in tests. - app.app_options_mut().experimental.table_grid_view = true; + // Enable table cards and blueprints in tests. + app.app_options_mut() + .experimental + .table_cards_and_blueprints = true; + + // Always show the full date so timestamps render as `YYYY-MM-DD HH:MM:SS` + // regardless of when the test runs. The default `HideDateToday` would + // silently break snapshots once the calendar day rolls over. + app.app_options_mut().timestamp_format = app + .app_options() + .timestamp_format + .with_date_visibility(DateVisibility::ShowDate); if let Some(editor) = &options.app_options_editor { editor(app.app_options_mut()); @@ -88,7 +110,7 @@ pub fn step_until<'app, 'harness, Predicate>( step_duration: std::time::Duration, max_duration: std::time::Duration, ) where - Predicate: for<'a> FnMut(&'a egui_kittest::Harness<'app, App>) -> bool, + Predicate: for<'a> FnMut(&'a mut egui_kittest::Harness<'app, App>) -> bool, { let start_time = std::time::Instant::now(); let mut success = predicate(harness); diff --git a/crates/viewer/re_viewer/src/web.rs b/crates/viewer/re_viewer/src/web.rs index a0e8469731a9..0fc8d1f9c978 100644 --- a/crates/viewer/re_viewer/src/web.rs +++ b/crates/viewer/re_viewer/src/web.rs @@ -7,9 +7,10 @@ use std::str::FromStr as _; use ahash::HashMap; use arrow::array::RecordBatch; +use itertools::Itertools as _; use re_log::ResultExt as _; use re_log_channel::{LogSender, RecordingOpenBehavior}; -use re_log_types::{TableId, TableMsg}; +use re_log_types::{TableId, TableMsg, TimelineName}; use re_memory::AccountingAllocator; use re_sdk_types::blueprint::components::PlayState; use re_viewer_context::{ @@ -191,15 +192,11 @@ impl WebHandle { /// Add a new receiver streaming data from the given url. /// - /// If `follow` is `true`, and the url is an HTTP source or file path, - /// the viewer will open the stream - /// in `Following` mode rather than `Playing` mode. - /// /// Websocket streams are always opened in `Following` mode. /// /// It is an error to open a channel twice with the same id. #[wasm_bindgen] - pub fn add_receiver(&self, url: &str, follow: Option) { + pub fn add_receiver(&self, url: &str) { let Some(app) = self.runner.app_mut::() else { return; }; @@ -209,8 +206,6 @@ impl WebHandle { url.open( &app.egui_ctx, &open_url::OpenUrlOptions { - // TODO(andreas): should follow be part of the fragments? - follow: follow.unwrap_or(false), recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, show_loader: true, }, @@ -348,7 +343,7 @@ impl WebHandle { } }; - let mut batches = match stream_reader.collect::, _>>() { + let mut batches: Vec<_> = match stream_reader.try_collect() { Ok(batches) => batches, Err(err) => { re_log::error_once!("Could not read from IPC stream: {err}"); @@ -392,7 +387,7 @@ impl WebHandle { Some(recording.store_id().recording_id().to_string()) } - //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) + //TODO(#10737): we should refer to logical recordings using store id (recording id is ambiguous) #[wasm_bindgen] pub fn set_active_recording_id(&self, recording_id: &str) { let Some(mut app) = self.runner.app_mut::() else { @@ -418,28 +413,21 @@ impl WebHandle { app.egui_ctx.request_repaint(); } - //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) + //TODO(#10737): we should refer to logical recordings using store id (recording id is ambiguous) #[wasm_bindgen] pub fn get_active_timeline(&self, recording_id: &str) -> Option { - let mut app = self.runner.app_mut::()?; - let crate::App { - store_hub: Some(hub), - state, - .. - } = &mut *app - else { - return None; - }; + let app = self.runner.app_mut::()?; + let hub = app.store_hub.as_ref()?; let store_id = store_id_from_recording_id(hub, recording_id)?; - let time_ctrl = state.time_control(&store_id)?; + let time_ctrl = app.state.time_control(&store_id)?; Some(time_ctrl.timeline_name().as_str().to_owned()) } /// Set the active timeline. /// /// This does nothing if the timeline can't be found. - //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) + //TODO(#10737): we should refer to logical recordings using store id (recording id is ambiguous) #[wasm_bindgen] pub fn set_active_timeline(&self, recording_id: &str, timeline_name: &str) { let Some(app) = self.runner.app_mut::() else { @@ -454,29 +442,37 @@ impl WebHandle { return; }; + let Some(timeline_name) = TimelineName::try_new(timeline_name).ok_or_log_error_once() + else { + return; + }; + app.command_sender .send_system(SystemCommand::TimeControlCommands { store_id: recording_id, - time_commands: vec![TimeControlCommand::SetActiveTimeline(timeline_name.into())], + time_commands: vec![TimeControlCommand::SetActiveTimeline(timeline_name)], }); app.egui_ctx.request_repaint(); } - //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) + //TODO(#10737): we should refer to logical recordings using store id (recording id is ambiguous) #[wasm_bindgen] pub fn get_time_for_timeline(&self, recording_id: &str, timeline_name: &str) -> Option { let app = self.runner.app_mut::()?; - let store_id = store_id_from_recording_id(app.store_hub.as_ref()?, recording_id)?; + let hub = app.store_hub.as_ref()?; + let store_id = store_id_from_recording_id(hub, recording_id)?; let time_ctrl = app.state.time_control(&store_id)?; + let timeline_name = TimelineName::try_new(timeline_name).ok_or_log_error_once()?; + time_ctrl - .time_for_timeline(timeline_name.into()) + .time_for_timeline(timeline_name) .map(|v| v.as_f64()) } - //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) + //TODO(#10737): we should refer to logical recordings using store id (recording id is ambiguous) #[wasm_bindgen] pub fn set_time_for_timeline(&self, recording_id: &str, timeline_name: &str, time: f64) { let Some(app) = self.runner.app_mut::() else { @@ -491,11 +487,16 @@ impl WebHandle { return; }; + let Some(timeline_name) = TimelineName::try_new(timeline_name).ok_or_log_error_once() + else { + return; + }; + app.command_sender .send_system(SystemCommand::TimeControlCommands { store_id: recording_id, time_commands: vec![ - TimeControlCommand::SetActiveTimeline(timeline_name.into()), + TimeControlCommand::SetActiveTimeline(timeline_name), TimeControlCommand::SetTime(time.into()), ], }); @@ -503,7 +504,7 @@ impl WebHandle { app.egui_ctx.request_repaint(); } - //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) + //TODO(#10737): we should refer to logical recordings using store id (recording id is ambiguous) #[wasm_bindgen] pub fn get_timeline_time_range(&self, recording_id: &str, timeline_name: &str) -> JsValue { let Some(app) = self.runner.app_mut::() else { @@ -524,7 +525,11 @@ impl WebHandle { return JsValue::null(); }; - let Some(time_range) = recording.time_range_for(&timeline_name.into()) else { + let Some(timeline_name) = TimelineName::try_new(timeline_name).ok_or_log_error_once() + else { + return JsValue::null(); + }; + let Some(time_range) = recording.time_range_for(&timeline_name) else { return JsValue::null(); }; @@ -538,29 +543,22 @@ impl WebHandle { JsValue::from(obj) } - //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) + //TODO(#10737): we should refer to logical recordings using store id (recording id is ambiguous) #[wasm_bindgen] pub fn get_playing(&self, recording_id: &str) -> Option { let app = self.runner.app_mut::()?; - let crate::App { - store_hub: Some(hub), - state, - .. - } = &*app - else { - return None; - }; + let hub = app.store_hub.as_ref()?; let store_id = store_id_from_recording_id(hub, recording_id)?; if !hub.store_bundle().contains(&store_id) { return None; } - let time_ctrl = state.time_control(&store_id)?; + let time_ctrl = app.state.time_control(&store_id)?; Some(time_ctrl.play_state() == PlayState::Playing) } - //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) + //TODO(#10737): we should refer to logical recordings using store id (recording id is ambiguous) #[wasm_bindgen] pub fn set_playing(&self, recording_id: &str, value: bool) { let Some(mut app) = self.runner.app_mut::() else { @@ -786,12 +784,11 @@ fn create_app( enable_history, viewer_base_url, login, - enable_experimental_status_view: false, }; crate::customize_eframe_and_setup_renderer(cc)?; if let Some(theme) = theme { - match theme.as_str() { + match theme.to_ascii_lowercase().as_str() { "dark" => cc .egui_ctx .options_mut(|o| o.theme_preference = egui::ThemePreference::Dark), @@ -802,7 +799,9 @@ fn create_app( .egui_ctx .options_mut(|o| o.theme_preference = egui::ThemePreference::System), _ => { - // Don't touch egui's settings, might be loaded from previous user interaction. + re_log::warn!( + "Ignoring unknown `theme` value {theme:?}; expected `dark`, `light`, or `system`." + ); } } } @@ -832,7 +831,6 @@ fn create_app( url.open( &app.egui_ctx, &open_url::OpenUrlOptions { - follow: false, recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, show_loader: true, }, diff --git a/crates/viewer/re_viewer/src/web_history.rs b/crates/viewer/re_viewer/src/web_history.rs index 6d61fd2377d6..a0dea509bfee 100644 --- a/crates/viewer/re_viewer/src/web_history.rs +++ b/crates/viewer/re_viewer/src/web_history.rs @@ -163,7 +163,6 @@ fn handle_popstate( url.open( egui_ctx, &open_url::OpenUrlOptions { - follow: false, recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, show_loader: true, }, diff --git a/crates/viewer/re_viewer/tests/app_kittest.rs b/crates/viewer/re_viewer/tests/app_kittest.rs index 4280e074771b..0eeee6a24a98 100644 --- a/crates/viewer/re_viewer/tests/app_kittest.rs +++ b/crates/viewer/re_viewer/tests/app_kittest.rs @@ -3,6 +3,8 @@ use std::time::Duration; use egui::accesskit::Role; +use egui::os::OperatingSystem; +use egui_kittest::SnapshotResults; use egui_kittest::kittest::Queryable as _; use re_sdk_types::ColormapSelection; use re_sdk_types::components::Colormap; @@ -10,6 +12,17 @@ use re_test_context::TestContext; use re_viewer::viewer_test_utils::{self, HarnessOptions}; use re_viewer_context::MaybeMutRef; +fn os_snapshot_suffix(os: OperatingSystem) -> &'static str { + match os { + OperatingSystem::Nix => "linux", + OperatingSystem::Mac => "mac", + OperatingSystem::Windows => "windows", + OperatingSystem::Unknown => "unknown", + OperatingSystem::Android => "android", + OperatingSystem::IOS => "ios", + } +} + /// Navigates from welcome to settings screen and snapshots it. #[tokio::test] async fn settings_screen() { @@ -20,28 +33,80 @@ async fn settings_screen() { std::env::set_var("TZ", "Europe/Stockholm"); } - let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { - window_size: Some(egui::vec2(1024.0, 1080.0)), // Settings screen can be a bit tall - ..Default::default() - }); - harness.get_by_label("Menu").click(); - harness.run_ok(); - harness.get_by_label_contains("Settings…").click(); - // Wait for the FFmpeg-check loading indicator to disappear. - viewer_test_utils::step_until( - "Settings screen shows up with FFMpeg binary not found error", - &mut harness, - |harness| { - harness - .query_by_label_contains( - "The specified FFmpeg binary path does not exist or is not a file.", - ) - .is_some() + let mut snapshot_results = SnapshotResults::new(); + + for os in [ + OperatingSystem::Nix, + OperatingSystem::Mac, + OperatingSystem::Windows, + ] { + let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { + window_size: Some(egui::vec2(1024.0, 1080.0)), // Settings screen can be a bit tall + os: Some(os), + ..Default::default() + }); + harness.get_by_label("Menu").click(); + harness.run_ok(); + harness.get_by_label_contains("Settings…").click(); + // Wait for the FFmpeg-check loading indicator to disappear. + viewer_test_utils::step_until( + "Settings screen shows up with FFMpeg binary not found error", + &mut harness, + |harness| { + harness + .query_by_label_contains( + "The specified FFmpeg binary path does not exist or is not a file.", + ) + .is_some() + }, + Duration::from_millis(100), + Duration::from_secs(5), + ); + snapshot_results + .add(harness.try_snapshot(format!("settings_screen_{}", os_snapshot_suffix(os)))); + } +} + +/// Snapshots the "About Rerun" menu content with a fixed, realistic `BuildInfo`. +#[test] +fn about_rerun() { + let test_context = TestContext::new(); + + let build_info = re_build_info::BuildInfo { + crate_name: "rerun-cli".into(), + features: "default analytics map_view nasm".into(), + version: re_build_info::CrateVersion { + major: 0, + minor: 33, + patch: 0, + meta: None, }, - Duration::from_millis(100), - Duration::from_secs(5), - ); - harness.snapshot("settings_screen"); + rustc_version: "1.84.0 (9fc6b4312 2025-01-07)".into(), + llvm_version: "19.1.5".into(), + git_hash: "abc1234deadbeefcafebabe00000000000000".into(), + git_branch: "main".into(), + is_in_rerun_workspace: true, + target_triple: "aarch64-apple-darwin".into(), + datetime: "2026-05-25T12:34:56Z".into(), + is_debug_build: false, + }; + + let harness = test_context + .setup_kittest_for_rendering_ui([460.0, 360.0]) + .with_theme(egui::Theme::Light); + + // let render_state = test_context.egui_render_state.lock().clone(); + let render_state = None; // Otherwise we get different results on different platforms + + let mut harness = harness.build_ui(|ui| { + re_ui::apply_style_and_install_loaders(ui.ctx()); + egui::containers::menu::menu_style(ui.style_mut()); // The about-dialog is in a menu + re_viewer::about_rerun_ui(ui, &build_info, render_state.as_ref()); + }); + + harness.run(); + harness.fit_contents(); + harness.snapshot("about_rerun"); } /// Opens the Rerun menu without an active recording and snapshots the app. diff --git a/crates/viewer/re_viewer/tests/blueprint_test.rs b/crates/viewer/re_viewer/tests/blueprint_test.rs index 1615391055de..7bd8db091a21 100644 --- a/crates/viewer/re_viewer/tests/blueprint_test.rs +++ b/crates/viewer/re_viewer/tests/blueprint_test.rs @@ -67,10 +67,7 @@ fn save_blueprint_to_file(test_context: &TestContext, path: &Path) { fn load_blueprint_from_file(test_context: &mut TestContext, path: &Path) { let file = std::fs::File::open(path).expect("Failed to open blueprint file."); let reader = std::io::BufReader::new(file); - let data_source = re_log_channel::LogSource::File { - path: path.into(), - follow: false, - }; + let data_source = re_log_channel::LogSource::File { path: path.into() }; let rbl_store = re_entity_db::StoreBundle::from_rrd(reader, &data_source) .expect("Failed to load blueprint store"); { diff --git a/crates/viewer/re_viewer/tests/snapshots/about_rerun.png b/crates/viewer/re_viewer/tests/snapshots/about_rerun.png new file mode 100644 index 000000000000..7df1b1202118 --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/about_rerun.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6872f8693c2ac6aaf590bc22f1fe2b2cc28521f92797dacc21f4cdce90edc0f +size 54077 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Ellipses2D.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Ellipses2D.snap new file mode 100644 index 000000000000..28fed6e49c3e --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Ellipses2D.snap @@ -0,0 +1,12 @@ +--- +source: crates/viewer/re_viewer/tests/all_component_fallbacks.rs +expression: arch_display +--- +half_sizes: [[1.0, 1.0]] +centers: [[0.0, 0.0]] +colors: [3161148927] +line_radii: [-1.5] +labels: [] +show_labels: [true] +draw_order: [10.0] +class_ids: [0] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.EncodedDepthImage.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.EncodedDepthImage.snap index 034a85c964ee..f53167ca5f5b 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.EncodedDepthImage.snap +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.EncodedDepthImage.snap @@ -4,9 +4,9 @@ expression: arch_display --- blob: [[0]] media_type: [application/octet-stream] -meter: [1.0] +meter: [1000.0] colormap: [5] -depth_range: [[0.0, 1.7976931348623157e308]] +depth_range: [[0.0, 65535.0]] point_fill_ratio: [1.0] draw_order: [-20.0] magnification_filter: [1] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Pinhole.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Pinhole.snap index 601a7a568827..fe7d6a9409bb 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Pinhole.snap +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Pinhole.snap @@ -1,5 +1,6 @@ --- source: crates/viewer/re_viewer/tests/all_component_fallbacks.rs +assertion_line: 55 expression: arch_display --- image_from_camera: [[100.0, 0.0, 0.0, 0.0, 100.0, 0.0, 100.0, 100.0, 1.0]] @@ -8,5 +9,5 @@ camera_xyz: [[3, 2, 5]] child_frame: [tf#/stockholm/södermalm/slussen] parent_frame: [tf#/stockholm/södermalm] image_plane_distance: [1.0] -color: [2392761855] +color: [2678038527] line_width: [-1.0] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Points3D.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Points3D.snap index 3efc334fd67f..7213f1c6e365 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Points3D.snap +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Points3D.snap @@ -7,5 +7,6 @@ radii: [-1.5] colors: [3161148927] labels: [] show_labels: [true] +point_shading: [1] class_ids: [0] keypoint_ids: [0] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Status.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.StateChange.snap similarity index 89% rename from crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Status.snap rename to crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.StateChange.snap index eec8168d2cab..ea554bac13f5 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.Status.snap +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.StateChange.snap @@ -2,4 +2,4 @@ source: crates/viewer/re_viewer/tests/all_component_fallbacks.rs expression: arch_display --- -status: [] +state: [] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.StateConfiguration.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.StateConfiguration.snap new file mode 100644 index 000000000000..76fcb020dee5 --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.StateConfiguration.snap @@ -0,0 +1,8 @@ +--- +source: crates/viewer/re_viewer/tests/all_component_fallbacks.rs +expression: arch_display +--- +values: [] +labels: [] +colors: [3161148927] +visible: [true] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.VideoStream.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.VideoStream.snap index 44e4e4460632..332dd4ccd95b 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.VideoStream.snap +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.VideoStream.snap @@ -4,5 +4,6 @@ expression: arch_display --- codec: [0] sample: [[0]] +is_keyframe: [false] opacity: [1.0] draw_order: [-15.0] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.VoxelGridMap.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.VoxelGridMap.snap new file mode 100644 index 000000000000..a7daf73c7546 --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.archetypes.VoxelGridMap.snap @@ -0,0 +1,15 @@ +--- +source: crates/viewer/re_viewer/tests/all_component_fallbacks.rs +assertion_line: 55 +expression: arch_display +--- +voxel_indices: [[0, 0, 0]] +voxel_size: [[0.01, 0.01, 0.01]] +values: [0.0] +colors: [3161148927] +translation: [[0.0, 0.0, 0.0]] +rotation_axis_angle: [{axis: [1.0, 0.0, 0.0], angle: 0.0}] +quaternion: [[0.0, 0.0, 0.0, 1.0]] +opacity: [1.0] +value_range: [[0.0, 1.0]] +colormap: [5] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.blueprint.archetypes.TableBlueprint.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.blueprint.archetypes.TableBlueprint.snap new file mode 100644 index 000000000000..54d8913c38ff --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.blueprint.archetypes.TableBlueprint.snap @@ -0,0 +1,8 @@ +--- +source: crates/viewer/re_viewer/tests/all_component_fallbacks.rs +expression: arch_display +--- +segment_preview_column: [] +flag_column: [] +grid_view_card_title: [] +url_column: [] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.blueprint.archetypes.TextDocumentFormat.snap b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.blueprint.archetypes.TextDocumentFormat.snap new file mode 100644 index 000000000000..7ef57cf26845 --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/all_component_fallbacks__arch_fallback_rerun.blueprint.archetypes.TextDocumentFormat.snap @@ -0,0 +1,7 @@ +--- +source: crates/viewer/re_viewer/tests/all_component_fallbacks.rs +assertion_line: 55 +expression: arch_display +--- +monospace: [false] +word_wrap: [false] diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/2D.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/2D.png index 155114c9d575..8ca8ca33d8df 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/2D.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/2D.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e35dac090f9efc8c9af1f9e478588c6fcb6611611cc19602e4b749c918117741 -size 22255 +oid sha256:d773f67954948d755db73c843be9bcc51f0ea348917e321f7615f4c141ddcaa2 +size 36269 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/3D.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/3D.png index 52769ab25dbe..625418e6e8d2 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/3D.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/3D.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93854a35f692c53cc331d1b0de198bc250996017449003a7931e6adaa98b1c8c -size 71512 +oid sha256:d88aedb796d1a858f0d1c85edafca1475b914d3a20694409006fe3ab0d8a26c1 +size 71615 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/BarChart.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/BarChart.png index a9f387dcedfe..2eabec5665f8 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/BarChart.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/BarChart.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f61193ed8c7bea4a64fbb44e8ec7455919b703ba4256574ebb79dbf4f0f12f3 -size 17286 +oid sha256:bf57e09159cc2976cf5cb67d7ce428677e54a50be7a2a5a8b7f89eb74017ceb8 +size 17409 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Dataframe.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Dataframe.png index 9980929708f7..ba1c72e28fbb 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Dataframe.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Dataframe.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec68c359a878e24773c88f09b1db7dfab88eba128853cb55bbddcde43fe531ce -size 31870 +oid sha256:3f55a87b4f4d319c7299e43ebdcf0d17c0f3734e13a2d0f4f55cc2b4a65cd5fa +size 32199 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Graph.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Graph.png index b2aecf813d2a..7c554a2abf50 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Graph.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Graph.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c442fe21b54ce74f1cd7f4be6bbc1acc32fda11578a434b62b9da0e24c7bb7ef -size 28339 +oid sha256:4e823a6dff331c97c24a2cae43e1ee26baa8b6b1d8ffeb8b22243b5f6e62fb3d +size 28458 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Map.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Map.png index e4a91eca3d29..dc5f9e3a6b19 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Map.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Map.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85947691c7133fa1e55700888066f1dbc8c993b226f32a1ba5183cc991a8b60d -size 16098 +oid sha256:307ecd8f8e2304db7852c98173104bdc90616cdf6bb95d96d7864ad7f74a4338 +size 16144 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/StateTimeline.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/StateTimeline.png new file mode 100644 index 000000000000..91d4e87878f6 --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/StateTimeline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f676d97a0d42fd7dad7f8c52101cbf257e4bfa17ee4e1b07cbec6e1c1675c89 +size 11802 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Tensor.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Tensor.png index f1ae95bd339e..acada6cfcee9 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Tensor.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Tensor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f7b7713d44ee4087f437f03d5ecad24714bea1d638ad28c1c4d121de2a28d40 -size 22885 +oid sha256:e1fd49129b77fe57eccd737460a65befe0524537b22df7b76c255b5a1c33eb71 +size 22891 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TextDocument.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TextDocument.png index a931a6475205..a1e6092c8936 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TextDocument.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TextDocument.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9446581293741073066f36e8d173f7e2b9af91ba08961b712dc4f3f40bb6d5b4 -size 14708 +oid sha256:f90d6250a0150e117ed170b133ecc3d28fbdf130ebfe205db628e1c7e63ae571 +size 13808 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TextLog.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TextLog.png index e5be9ef8a32b..acb37b6d6a15 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TextLog.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TextLog.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:662e49e005d0390245ecb411ce7dd7a7f263c357d26ae9f4715e83355426bce8 -size 24076 +oid sha256:99043563be28bfb0f23bc2c66466a1cdc135644906f8cb71ecffee0d6603c61f +size 24058 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TimeSeries.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TimeSeries.png index c9d5612339e4..b67d1e333458 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TimeSeries.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/TimeSeries.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84db26b939f412d8d930de8a4314bd62629caaad18af568549050c5d9327aaa4 -size 32142 +oid sha256:64f13001e47658a25dd188d7c1eb77b87949bcf6c413ccf52005c0e0d57ada35 +size 32358 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/2D.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/2D.png index 77f3e7e80060..d117f044393b 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/2D.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/2D.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:591d42df1683a062c1c9b033ebf7d3940b1ec2afc511805717940ac106f119ef -size 22039 +oid sha256:e5ddd57295fe34b3056e72ce7e46efe86e802bed0a0be5dd273e18a0da834a76 +size 35516 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/3D.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/3D.png index 6e3589d3ad9c..b9bdbdc9fba3 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/3D.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/3D.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e60cd88d82ec3088b10aa89ffba5ec51b406f59d30ee13c21df1956f7577b48 -size 71278 +oid sha256:02a86d8d917c3b324703d81b5abebf01566dd17ef050ee6d721dfe34e5967f36 +size 70758 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/BarChart.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/BarChart.png index 1b54961c58b4..8ff43be34b81 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/BarChart.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/BarChart.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41497fae5b51488e81603bf842d647ba79ec76ad9364167bd92a8e1334561365 -size 17200 +oid sha256:82fce45da35689cd7acd9f380c2d00d755a74caf5b16fdd27ee9b2bbb0f87338 +size 17106 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Dataframe.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Dataframe.png index e5d2095e3e5f..c8f8ba2db91f 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Dataframe.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Dataframe.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8e20463b80918eec2bd8e18e5ccaa8bb740b642c1675a978cdf63cda88e8e37 -size 31685 +oid sha256:6162bf441774b410b6c8a2d6f24fe375951be68c7b8be8818ce234097340fccc +size 31805 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Graph.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Graph.png index cfac60d8cf77..9b534739dc94 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Graph.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Graph.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbe6b4ce2ed2e6bb18d314c9591d81fd4c1a837c82edd2b9e6ed119f8256f0ab -size 27802 +oid sha256:68b536e59bc15b64169b1b95bf570f4e878f6a88e56fae979935a3852d7093c6 +size 27573 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Map.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Map.png index 6266f4d3b80e..15d77ca37c0c 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Map.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Map.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd744306e008d079b15ed9e248e68acc29525472476871348f12c1a0c7e591bd -size 16043 +oid sha256:6fa5184d905f663e3b35c12788931b22ca196db184aaad287b0d814db6467094 +size 15961 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/StateTimeline.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/StateTimeline.png new file mode 100644 index 000000000000..7efdb13caa39 --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/StateTimeline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f624080977e496f5c2a7c15020167181860970b1294ad823d705120701487de2 +size 11802 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Tensor.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Tensor.png index 25e8a0e8fe09..01c41a1704bb 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Tensor.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Tensor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:018902c6bd01459db6b4121665ce8b39dfaf200cebeccb7fc7c274e7074c6f89 -size 22736 +oid sha256:8f87e89c7d515241d3e8b8be8744345ccf7b06185d2c0dedc2085299c0073e42 +size 22528 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TextDocument.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TextDocument.png index 7ad9235ab2f7..3a7f54830517 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TextDocument.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TextDocument.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d50f65039c72cb77587c03178b83ff491da791d912c0914d45bb44d800b3ce09 -size 14749 +oid sha256:b273a3c64263aa065b8a4980b675ac3d84650b0f8e93a7c5fa32f40fb3ae9942 +size 13524 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TextLog.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TextLog.png index cd8cfb58a328..7cddfd0a5dad 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TextLog.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TextLog.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c12de387766fb54ed1d6b29fb6f64bc32fa3ca868de01bcee65d0f3d46b7380 -size 23746 +oid sha256:569c21abe913a91abc4307ec3152f5b38d3e3f5e41c6dc29bb13f25c3fdbe225 +size 23607 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TimeSeries.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TimeSeries.png index 4d5f40518d25..1122728d0927 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TimeSeries.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/TimeSeries.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39aa61d898419bc785b6e1db0b4f9958f33890b39c88a246606855ae011c224a -size 31845 +oid sha256:6ce0173b95c43985e58f8e5529828efaa41eaa76c208973a019b649aa2135bfa +size 31894 diff --git a/crates/viewer/re_viewer/tests/snapshots/blueprint_change_and_restore.png b/crates/viewer/re_viewer/tests/snapshots/blueprint_change_and_restore.png index a7edebf96713..03dfbd30de80 100644 --- a/crates/viewer/re_viewer/tests/snapshots/blueprint_change_and_restore.png +++ b/crates/viewer/re_viewer/tests/snapshots/blueprint_change_and_restore.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:876a6affab77f9a3e71208f9abee7968d457bb38bbe3a030b6b2a00b1d74d012 -size 28551 +oid sha256:8ffb910668a968aa65a60ac609468c05f44567fc9814d1658800699abfbea8d5 +size 28541 diff --git a/crates/viewer/re_viewer/tests/snapshots/blueprint_load_into_new_context_1.png b/crates/viewer/re_viewer/tests/snapshots/blueprint_load_into_new_context_1.png index 9ecbae829ace..4577d791ab59 100644 --- a/crates/viewer/re_viewer/tests/snapshots/blueprint_load_into_new_context_1.png +++ b/crates/viewer/re_viewer/tests/snapshots/blueprint_load_into_new_context_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb2571d088b53d902d0dbdf698ce2ad67af7e1d50ef556981f45fae9fc828ce6 -size 24780 +oid sha256:e3a27de75aadc54de25f229ceeee1efe4baf8894b09522b55790eebc2af4530f +size 24824 diff --git a/crates/viewer/re_viewer/tests/snapshots/blueprint_load_into_new_context_2.png b/crates/viewer/re_viewer/tests/snapshots/blueprint_load_into_new_context_2.png index caa9d0cfd3f0..b815e4f9ea93 100644 --- a/crates/viewer/re_viewer/tests/snapshots/blueprint_load_into_new_context_2.png +++ b/crates/viewer/re_viewer/tests/snapshots/blueprint_load_into_new_context_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2b642cb67926f85b5594e9db73f30bc681f8dabc9796f57ca8e7a4951d25ecf -size 28358 +oid sha256:8c824b10b421d608753cecc060962d8081d26f4b624e3003988bb5204c65bfc4 +size 28255 diff --git a/crates/viewer/re_viewer/tests/snapshots/colormap_selector_closed.png b/crates/viewer/re_viewer/tests/snapshots/colormap_selector_closed.png index 13227a451a1e..77b9f5eb6eaa 100644 --- a/crates/viewer/re_viewer/tests/snapshots/colormap_selector_closed.png +++ b/crates/viewer/re_viewer/tests/snapshots/colormap_selector_closed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92e1aade23f33e1cbcf1a5977de4aa879fc00276f76bb0c9feb71a243f5b2bd6 -size 3134 +oid sha256:5993d3885a926d8340b133b2f42605b2c908e7fabf6ca3b3b1d0c4a24804a21c +size 3150 diff --git a/crates/viewer/re_viewer/tests/snapshots/colormap_selector_open.png b/crates/viewer/re_viewer/tests/snapshots/colormap_selector_open.png index 0b42f2646604..e4176f84f356 100644 --- a/crates/viewer/re_viewer/tests/snapshots/colormap_selector_open.png +++ b/crates/viewer/re_viewer/tests/snapshots/colormap_selector_open.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76a1ee366a4907edcd8e292cae0d89c3666f4311cded8cf04c340171da671b54 -size 27072 +oid sha256:1f156c04a4515a28e2e9c21b1a84b839258c395af953e5626aa0c2c49900007f +size 27433 diff --git a/crates/viewer/re_viewer/tests/snapshots/menu_without_recording.png b/crates/viewer/re_viewer/tests/snapshots/menu_without_recording.png index ba421e01b07a..97340df4b1b8 100644 --- a/crates/viewer/re_viewer/tests/snapshots/menu_without_recording.png +++ b/crates/viewer/re_viewer/tests/snapshots/menu_without_recording.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a85a3ac0621c1df0e24eb6ff67228cf64f6e3712c0e472d55ebb587589df867b -size 122528 +oid sha256:00ed25fbedbd1ff769f6e1234fd231ebc4e61fed25ae276cd5d88da879e380c7 +size 135394 diff --git a/crates/viewer/re_viewer/tests/snapshots/open_url_modal__invalid_url.png b/crates/viewer/re_viewer/tests/snapshots/open_url_modal__invalid_url.png index a399f6e844a3..d71f901b91c8 100644 --- a/crates/viewer/re_viewer/tests/snapshots/open_url_modal__invalid_url.png +++ b/crates/viewer/re_viewer/tests/snapshots/open_url_modal__invalid_url.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19eeb7485364d8a4b2e3b9919e7db0e77d89258a32ed65f778bd8f32b938e512 -size 21200 +oid sha256:0be7e10db1a87aecfe1b9a7851322fa39b2d40f0a05b527c62cf643a3821eb33 +size 21141 diff --git a/crates/viewer/re_viewer/tests/snapshots/open_url_modal__no_url.png b/crates/viewer/re_viewer/tests/snapshots/open_url_modal__no_url.png index 235bed65935e..306f619795dc 100644 --- a/crates/viewer/re_viewer/tests/snapshots/open_url_modal__no_url.png +++ b/crates/viewer/re_viewer/tests/snapshots/open_url_modal__no_url.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc848c48b272ab41648a453e7a7fdd3f820034bed66c28fc0c295cbb20725953 -size 12414 +oid sha256:97006a821e45884369599716fe161682841f39018f83bdd4bdb9d33ccc6c88b7 +size 12402 diff --git a/crates/viewer/re_viewer/tests/snapshots/open_url_modal__valid_url.png b/crates/viewer/re_viewer/tests/snapshots/open_url_modal__valid_url.png index a20589362766..cf52eb817789 100644 --- a/crates/viewer/re_viewer/tests/snapshots/open_url_modal__valid_url.png +++ b/crates/viewer/re_viewer/tests/snapshots/open_url_modal__valid_url.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db0e27978ce3f56057720a4d4dea5ce4884882242486a820180cf953916df26b -size 25484 +oid sha256:83ebfe9b1869a2dc873591e7e89835ac720fc0414281fa3cec3a95f17a55aed6 +size 25540 diff --git a/crates/viewer/re_viewer/tests/snapshots/settings_screen.png b/crates/viewer/re_viewer/tests/snapshots/settings_screen.png deleted file mode 100644 index a12a73a2f38c..000000000000 --- a/crates/viewer/re_viewer/tests/snapshots/settings_screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8dd7d5ad26ebc9a2b386cb39a031047ad17a4f0b07ef7f67100591b62f35d095 -size 92598 diff --git a/crates/viewer/re_viewer/tests/snapshots/settings_screen_linux.png b/crates/viewer/re_viewer/tests/snapshots/settings_screen_linux.png new file mode 100644 index 000000000000..415b00a76831 --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/settings_screen_linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f68a231118208c60946c8ba91a5c08eae0088ed4fa460c378180bfbd83a9e41a +size 105903 diff --git a/crates/viewer/re_viewer/tests/snapshots/settings_screen_mac.png b/crates/viewer/re_viewer/tests/snapshots/settings_screen_mac.png new file mode 100644 index 000000000000..d3d2a8d0954f --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/settings_screen_mac.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b20499d3204bddc55dab57e9858d6f62be87e36d2fa9111c1f937962106f793 +size 101626 diff --git a/crates/viewer/re_viewer/tests/snapshots/settings_screen_windows.png b/crates/viewer/re_viewer/tests/snapshots/settings_screen_windows.png new file mode 100644 index 000000000000..415b00a76831 --- /dev/null +++ b/crates/viewer/re_viewer/tests/snapshots/settings_screen_windows.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f68a231118208c60946c8ba91a5c08eae0088ed4fa460c378180bfbd83a9e41a +size 105903 diff --git a/crates/viewer/re_viewer/tests/snapshots/share_modal__dataset_segment_url.png b/crates/viewer/re_viewer/tests/snapshots/share_modal__dataset_segment_url.png index 3f1ca00be224..fe69e616e263 100644 --- a/crates/viewer/re_viewer/tests/snapshots/share_modal__dataset_segment_url.png +++ b/crates/viewer/re_viewer/tests/snapshots/share_modal__dataset_segment_url.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bb7578b80c0ac7247d98b0fd6069ed2f8b73a50396de70be6c6bde672f70816 -size 30989 +oid sha256:192ae6536f9aafcc6814d812424a47f4ec5cd1dda1eb9b56c56347bcb8a43f32 +size 30973 diff --git a/crates/viewer/re_viewer/tests/snapshots/share_modal__dataset_segment_url_with_time_range.png b/crates/viewer/re_viewer/tests/snapshots/share_modal__dataset_segment_url_with_time_range.png index 5e0f87dc728d..50f1f673ce77 100644 --- a/crates/viewer/re_viewer/tests/snapshots/share_modal__dataset_segment_url_with_time_range.png +++ b/crates/viewer/re_viewer/tests/snapshots/share_modal__dataset_segment_url_with_time_range.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ab6a529c4900339c8a2334ded493603abdc6546643acba9a67dbdbb549e6794 -size 34300 +oid sha256:6c9a2615b496409bbe5c76da51bf93c41071e3096c22d375b3f69416130fc1d4 +size 34426 diff --git a/crates/viewer/re_viewer/tests/snapshots/share_modal__server_url.png b/crates/viewer/re_viewer/tests/snapshots/share_modal__server_url.png index 4fd1682a34a3..a8a26c191207 100644 --- a/crates/viewer/re_viewer/tests/snapshots/share_modal__server_url.png +++ b/crates/viewer/re_viewer/tests/snapshots/share_modal__server_url.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9014813fe1f0994bb7a62ddb733b29be1c2f0a49c8e913927d705deb02b2740b -size 17358 +oid sha256:be3fbc745116c3e62ddad1c3b0406d8244e0f8607bf565bd5fa797894fb5e2ff +size 17343 diff --git a/crates/viewer/re_viewer/tests/time_control_blueprint_test.rs b/crates/viewer/re_viewer/tests/time_control_blueprint_test.rs new file mode 100644 index 000000000000..e4477a99f22b --- /dev/null +++ b/crates/viewer/re_viewer/tests/time_control_blueprint_test.rs @@ -0,0 +1,135 @@ +//! Verifies that `TimeControl::from_blueprint_with_fallback_play_state` respects +//! a `PlayState` set in the blueprint and only applies the fallback when the +//! blueprint did not specify one. +//! +//! Regression test for . + +use re_log_channel::RecordingOpenBehavior; +use re_log_types::{RecordingId, TimeReal}; +use re_sdk_types::blueprint::components::PlayState; +use re_test_context::TestContext; +use re_viewer_context::open_url::{OpenUrlOptions, ViewerOpenUrl}; +use re_viewer_context::{TimeControl, TimeControlCommand}; + +#[test] +fn empty_blueprint_applies_fallback_play_state() { + let test_context = TestContext::new(); + + let result = test_context.with_blueprint_ctx(|blueprint_ctx, _| { + TimeControl::from_blueprint_with_fallback_play_state( + &blueprint_ctx, + None, + PlayState::Playing, + ) + .play_state() + }); + + assert_eq!(result, PlayState::Playing); +} + +#[test] +fn blueprint_play_state_overrides_fallback() { + let test_context = TestContext::new(); + + // Pin `PlayState::Paused` into the blueprint via the normal command path. + test_context.send_time_commands( + test_context.active_store_id(), + [TimeControlCommand::SetPlayState(PlayState::Paused)], + ); + test_context.handle_system_commands(&egui::Context::default()); + + let result = test_context.with_blueprint_ctx(|blueprint_ctx, _| { + // Fallback says `Playing`, but the blueprint already has `Paused` — it + // should win. + TimeControl::from_blueprint_with_fallback_play_state( + &blueprint_ctx, + None, + PlayState::Playing, + ) + .play_state() + }); + + assert_eq!(result, PlayState::Paused); +} + +#[test] +fn opening_url_with_temporal_anchor_pauses_playing_recording() { + let test_context = TestContext::new(); + + // Create a store. + let dataset_id = re_log_types::external::re_tuid::Tuid::new(); + let segment_id = RecordingId::random(); + let url: ViewerOpenUrl = format!( + "rerun+http://localhost:51234/dataset/{dataset_id}?segment_id={segment_id}#when=stable_time@+3.990s" + ) + .parse() + .expect("test URL should parse"); + let store_id = match &url { + ViewerOpenUrl::RedapDatasetSegment(uri) => uri.store_id(), + _ => unreachable!("test URL should be a dataset segment"), + }; + test_context.store_hub.lock().entity_db_entry(&store_id); + + // Set things to playing. + test_context.send_time_commands( + store_id.clone(), + [TimeControlCommand::SetPlayState(PlayState::Playing)], + ); + test_context.handle_system_commands(&egui::Context::default()); + assert_eq!( + test_context.time_ctrl.read().play_state(), + PlayState::Playing + ); + + // Open the URL with a `when=…` fragment, which should pin the time to a specific point and pause playback. + test_context.run_once_in_egui_central_panel(|ctx, ui| { + url.open( + ui.ctx(), + &OpenUrlOptions { + recording_open_behavior: RecordingOpenBehavior::OpenAndSelect, + show_loader: false, + }, + ctx.command_sender(), + ); + }); + test_context.handle_system_commands(&egui::Context::default()); + + assert_eq!( + test_context.time_ctrl.read().play_state(), + PlayState::Paused, + "opening a URL with a `when=…` fragment should pause an already-playing recording" + ); +} + +/// Regression test for the cursor-drag-resumes-playback symptom of rerun#12773. +/// +/// `TimeControl::default()` starts with `following: true`. When the blueprint pins +/// `PlayState::Paused`, the resulting state must clear `following` — otherwise a +/// subsequent `SetTime` (cursor drag) routes through `exit_follow_mode`, which +/// flips the state to `Playing` and clobbers the blueprint. +#[test] +fn dragging_cursor_does_not_resume_playback_after_blueprint_pause() { + let test_context = TestContext::new(); + let store_id = test_context.active_store_id(); + + test_context.send_time_commands( + store_id.clone(), + [TimeControlCommand::SetPlayState(PlayState::Paused)], + ); + test_context.handle_system_commands(&egui::Context::default()); + assert_eq!( + test_context.time_ctrl.read().play_state(), + PlayState::Paused + ); + + // Simulate dragging the time cursor; the specific target time doesn't matter. + let drag_target = TimeReal::from(5_i64); + test_context.send_time_commands(store_id, [TimeControlCommand::SetTimeClamped(drag_target)]); + test_context.handle_system_commands(&egui::Context::default()); + + assert_eq!( + test_context.time_ctrl.read().play_state(), + PlayState::Paused, + "dragging the cursor must not resume playback when paused via blueprint" + ); +} diff --git a/crates/viewer/re_viewer_context/Cargo.toml b/crates/viewer/re_viewer_context/Cargo.toml index c233c4e5bd6b..c9fa0d3a3040 100644 --- a/crates/viewer/re_viewer_context/Cargo.toml +++ b/crates/viewer/re_viewer_context/Cargo.toml @@ -28,20 +28,20 @@ re_capabilities.workspace = true re_chunk_store.workspace = true re_chunk.workspace = true re_data_source.workspace = true -re_rvl.workspace = true re_entity_db.workspace = true re_error.workspace = true re_format.workspace = true re_log_channel.workspace = true re_log_encoding = { workspace = true, features = ["decoder", "encoder", "stream_from_http"] } -re_log_types = { workspace = true, features = ["serde"] } +re_log_types.workspace = true re_log.workspace = true re_memory.workspace = true re_mutex.workspace = true re_query.workspace = true re_quota_channel.workspace = true re_redap_client.workspace = true -re_renderer = { workspace = true, features = ["serde"] } +re_renderer.workspace = true +re_rvl.workspace = true re_string_interner.workspace = true re_tf.workspace = true re_tracing.workspace = true @@ -49,7 +49,7 @@ re_sdk_types = { workspace = true, features = ["ecolor", "glam", "image"] } re_types_core.workspace = true re_ui.workspace = true re_uri.workspace = true -re_video = { workspace = true, features = ["serde"] } +re_video.workspace = true ahash.workspace = true anyhow.workspace = true @@ -97,4 +97,7 @@ wasm-bindgen-futures.workspace = true web-sys = { workspace = true, features = ["Window"] } [dev-dependencies] +re_ui = { workspace = true, features = ["testing"] } + +egui_kittest.workspace = true rand = { workspace = true, features = ["std_rng"] } diff --git a/crates/viewer/re_viewer_context/src/active_store_context.rs b/crates/viewer/re_viewer_context/src/active_store_context.rs index 48d5f9881006..2aa2f1540e1b 100644 --- a/crates/viewer/re_viewer_context/src/active_store_context.rs +++ b/crates/viewer/re_viewer_context/src/active_store_context.rs @@ -1,11 +1,11 @@ -use std::sync::LazyLock; - use re_entity_db::EntityDb; use re_log_types::{ApplicationId, StoreId}; -use crate::{Cache, StoreCache, ViewClassRegistry}; +use crate::{Cache, CacheEntryAccess, StoreCache, StoreHub, TimeControl}; -/// The current Blueprint and Recording being displayed by the viewer +/// The current Blueprint and Recording being displayed by the viewer. +/// +/// This is only constructed when the viewer is currently displaying a recording. pub struct ActiveStoreContext<'a> { /// The current active blueprint. pub blueprint: &'a EntityDb, @@ -21,6 +21,11 @@ pub struct ActiveStoreContext<'a> { /// Per-recording caches. pub caches: &'a StoreCache, + /// The time control for the active recording. + /// + /// If none was created yet (or none is active), this points to a default time control. + pub time_ctrl: &'a TimeControl, + /// Should we enable the heuristics during this frame? pub should_enable_heuristics: bool, } @@ -31,47 +36,59 @@ impl ActiveStoreContext<'_> { } pub fn application_id(&self) -> &ApplicationId { + re_log::debug_assert!( + self.recording.application_id() != StoreHub::welcome_screen_app_id(), + "Bug: we should not be treating the welcome screen as a recording" + ); self.recording.application_id() } pub fn recording_store_id(&self) -> &StoreId { + re_log::debug_assert!(self.recording.store_id() != &StoreId::empty_recording()); self.recording.store_id() } + /// The active recording + pub fn recording(&self) -> &EntityDb { + re_log::debug_assert!(self.recording.store_id() != &StoreId::empty_recording()); + self.recording + } + + /// Currently selected section of time, if any. + pub fn loop_selection( + &self, + ) -> Option<(re_log_types::TimelineName, re_log_types::AbsoluteTimeRangeF)> { + self.time_ctrl + .time_selection() + .map(|q| (*self.time_ctrl.timeline_name(), q)) + } + /// Accesses a memoization cache for reading and writing. /// /// Shorthand for `self.caches.memoizer(f)`. pub fn memoizer(&self, f: impl FnOnce(&mut C) -> R) -> R { self.caches.memoizer(f) } -} -impl ActiveStoreContext<'static> { - /// A sentinel "empty" store context, backed by static empty stores. + /// Accesses an existing memoization cache for reading. /// - /// Useful as a last-resort fallback for code paths that require a - /// non-optional [`ActiveStoreContext`] but can be reached while no - /// recording/blueprint is active (e.g. Redap catalog browsing). Prefer - /// propagating `Option` upwards when possible. - // TODO(RR-3033): should not be needed, instead we the application should handle absence of an active store context explicitly. - pub fn empty() -> Self { - static EMPTY_RECORDING: LazyLock = - LazyLock::new(|| EntityDb::new(StoreId::empty_recording())); - static EMPTY_BLUEPRINT: LazyLock = LazyLock::new(|| { - EntityDb::new(StoreId::default_blueprint( - StoreId::empty_recording().application_id().clone(), - )) - }); - static EMPTY_CACHES: LazyLock = LazyLock::new(|| { - StoreCache::empty(&ViewClassRegistry::default(), StoreId::empty_recording()) - }); + /// Shorthand for `self.caches.memoizer_read(f)`. + pub fn memoizer_read(&self, f: impl FnOnce(&C) -> R) -> Option { + self.caches.memoizer_read(f) + } - Self { - blueprint: &EMPTY_BLUEPRINT, - default_blueprint: None, - recording: &EMPTY_RECORDING, - caches: &EMPTY_CACHES, - should_enable_heuristics: false, - } + /// Tries to read an existing memoization cache entry, then computes it through mutable access on miss. + /// + /// Use this if you're working with init-only cache entries, expect your cache entry to be usually present + /// and want to avoid the overhead of a write lock. + /// Note that this _adds_ overhead for the miss path compared to `memoizer`, so don't use this if you expect many misses! + /// (UI code typically doesn't need to care about this optimization, since it's usually single-threaded already.) + /// + /// Shorthand for `self.caches.memoizer_read_or_compute(key)`. + pub fn memoizer_read_or_compute(&self, key: &Key) -> Value + where + C: CacheEntryAccess + Default, + { + self.caches.memoizer_read_or_compute::(key) } } diff --git a/crates/viewer/re_viewer_context/src/annotations.rs b/crates/viewer/re_viewer_context/src/annotations.rs index 3cec01dbf8d2..90d1d45e8ca7 100644 --- a/crates/viewer/re_viewer_context/src/annotations.rs +++ b/crates/viewer/re_viewer_context/src/annotations.rs @@ -131,7 +131,7 @@ impl ResolvedClassDescription<'_> { // ---------------------------------------------------------------------------- -#[derive(Clone, Default)] +#[derive(Clone, Default, re_byte_size::SizeBytes)] pub struct ResolvedAnnotationInfo { pub class_id: Option, pub annotation_info: Option, @@ -179,21 +179,11 @@ impl ResolvedAnnotationInfo { } } -impl re_byte_size::SizeBytes for ResolvedAnnotationInfo { - #[inline] - fn heap_size_bytes(&self) -> u64 { - let Self { - class_id, - annotation_info, - } = self; - class_id.heap_size_bytes() + annotation_info.heap_size_bytes() - } -} - // ---------------------------------------------------------------------------- /// Many [`ResolvedAnnotationInfo`], with optimization /// for a common case where they are all the same. +#[derive(re_byte_size::SizeBytes)] pub enum ResolvedAnnotationInfos { /// All the same Same(usize, ResolvedAnnotationInfo), @@ -202,16 +192,6 @@ pub enum ResolvedAnnotationInfos { Many(Vec), } -impl re_byte_size::SizeBytes for ResolvedAnnotationInfos { - #[inline] - fn heap_size_bytes(&self) -> u64 { - match self { - Self::Same(_count, info) => info.heap_size_bytes(), - Self::Many(infos) => infos.heap_size_bytes(), - } - } -} - impl ResolvedAnnotationInfos { pub fn iter(&self) -> impl Iterator { use itertools::Either; diff --git a/crates/viewer/re_viewer_context/src/app_context.rs b/crates/viewer/re_viewer_context/src/app_context.rs index 40d93b422c5f..a9494c5cb2ba 100644 --- a/crates/viewer/re_viewer_context/src/app_context.rs +++ b/crates/viewer/re_viewer_context/src/app_context.rs @@ -6,14 +6,16 @@ use re_entity_db::EntityDb; use re_log_types::{EntityPath, StoreId, TimePoint}; use re_sdk_types::ComponentDescriptor; use re_ui::ContextExt as _; +use re_ui::list_item::ListItem; +use crate::command_sender::{SelectionSource, SetSelection}; use crate::drag_and_drop::DragAndDropPayload; use crate::time_control::TimeControlCommand; use crate::{ - ActiveStoreContext, AppOptions, ApplicationSelectionState, CommandSender, ComponentUiRegistry, - DragAndDropManager, FallbackProviderRegistry, FocusTarget, Item, ItemCollection, Route, - StorageContext, StoreHub, SystemCommand, SystemCommandSender as _, TableStores, TimeControl, - ViewClassRegistry, + ActiveStoreContext, AppCaches, AppOptions, ApplicationSelectionState, CommandSender, + ComponentUiRegistry, DragAndDropManager, FallbackProviderRegistry, FocusTarget, Item, + ItemCollection, Route, StorageContext, StoreHub, SystemCommand, SystemCommandSender as _, + TableStores, TimeControl, ViewClassRegistry, }; /// Application context that is shared across all parts of the viewer. @@ -21,6 +23,7 @@ use crate::{ /// This context, in difference to [`crate::ViewerContext`] can exist for /// any arbitrary state of the viewer. And not only when there is an open /// recording. +#[derive(Clone)] pub struct AppContext<'a> { /// Set during tests (e.g. snapshot tests). /// @@ -53,6 +56,9 @@ pub struct AppContext<'a> { /// This is `None` if the current [`Route`] is not pointing to a recording. pub active_store_context: Option<&'a ActiveStoreContext<'a>>, + /// App-level caches for data that is not tied to any particular store. + pub app_caches: &'a AppCaches, + /// How to display components. pub component_ui_registry: &'a ComponentUiRegistry, @@ -77,13 +83,10 @@ pub struct AppContext<'a> { /// Helper object to manage drag-and-drop operations. pub drag_and_drop_manager: &'a DragAndDropManager, - /// The time control for the active recording, if any. - pub active_time_ctrl: Option<&'a TimeControl>, - /// Where we are getting our data from. pub connected_receivers: &'a re_log_channel::LogReceiverSet, - /// Are we logged in to rerun cloud? + /// Are we logged in to Rerun Hub? pub auth_context: Option<&'a AuthContext>, /// Whether `OAuth` login is enabled in this viewer instance. @@ -109,6 +112,16 @@ impl AppContext<'_> { self.selection_state } + /// Interface for sending commands back to the app. + pub fn command_sender(&self) -> &CommandSender { + self.command_sender + } + + /// The current route of the viewer. + pub fn route(&self) -> &Route { + self.route + } + /// Returns the current selection. pub fn selection(&self) -> &ItemCollection { self.selection_state.selected_items() @@ -202,7 +215,7 @@ impl AppContext<'_> { /// The time control for the active recording, if any. pub fn active_time_ctrl(&self) -> Option<&TimeControl> { - self.active_time_ctrl + self.active_store_context.map(|ctx| ctx.time_ctrl) } /// Helper function to send [`TimeControlCommand`]s for the active recording. @@ -233,11 +246,11 @@ impl AppContext<'_> { ) { let mut interacted_items = interacted_items.into(); - if let Some(store_ctx) = self.active_store_context - && let Some(time_ctrl) = self.active_time_ctrl - { - interacted_items = interacted_items - .into_mono_instance_path_items(store_ctx.recording, &time_ctrl.current_query()); + if let Some(store_ctx) = self.active_store_context { + interacted_items = interacted_items.into_mono_instance_path_items( + store_ctx.recording, + &store_ctx.time_ctrl.current_query(), + ); } let selection_state = self.selection_state(); @@ -351,6 +364,59 @@ impl AppContext<'_> { } } + /// Helper to synchronize item selection with egui focus. + /// + /// Call if _this_ is where the user would expect keyboard focus to be + /// when the item is selected (e.g. blueprint tree for views, recording panel for recordings). + pub fn handle_select_focus_sync( + &self, + response: &egui::Response, + interacted_items: impl Into, + ) { + let mut interacted_items = interacted_items.into(); + + // If we have an active recording, resolve to mono-instance paths so selection matches + // what the rest of the viewer selects. + if let Some(store_ctx) = self.active_store_context { + interacted_items = interacted_items.into_mono_instance_path_items( + store_ctx.recording, + &store_ctx.time_ctrl.current_query(), + ); + } + + // Focus -> Selection + + // We want the item to be selected if it was selected with arrow keys (in list_item) + // but not when focused using e.g. the tab key. + if ListItem::gained_focus_via_arrow_key(&response.ctx, response.id) { + self.command_sender.send_system(SystemCommand::SetSelection( + SetSelection::new(interacted_items.clone()) + .with_source(SelectionSource::ListItemNavigation), + )); + } + + // Selection -> Focus + + let single_selected = self.selection().single_item() == interacted_items.single_item(); + if single_selected { + // If selection changes, and a single item is selected, the selected item should + // receive egui focus. + // We don't do this if selection happened due to list item navigation to avoid + // a feedback loop. + let selection_changed = self + .selection_state() + .selection_changed() + .is_some_and(|source| source != SelectionSource::ListItemNavigation); + + // If there is a single selected item and nothing is focused, focus that item. + let nothing_focused = response.ctx.memory(|mem| mem.focused().is_none()); + + if selection_changed || nothing_focused { + response.request_focus(); + } + } + } + /// Reverts to the default route. pub fn revert_to_default_route(&self) { self.command_sender.send_system(SystemCommand::ResetRoute); diff --git a/crates/viewer/re_viewer_context/src/app_options.rs b/crates/viewer/re_viewer_context/src/app_options.rs index 4d070daa5fbc..7342323b94b6 100644 --- a/crates/viewer/re_viewer_context/src/app_options.rs +++ b/crates/viewer/re_viewer_context/src/app_options.rs @@ -6,7 +6,7 @@ use re_video::{DecodeHardwareAcceleration, DecodeSettings}; const MAPBOX_ACCESS_TOKEN_ENV_VAR: &str = "RERUN_MAPBOX_ACCESS_TOKEN"; /// Global options for the viewer. -#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize, Clone)] #[serde(default)] pub struct AppOptions { /// Experimental feature flags. @@ -23,6 +23,9 @@ pub struct AppOptions { /// If false, you can still view them in the notifications panel. pub show_notification_toasts: bool, + /// Use Rerun's custom window decorations instead of the native OS decorations. + pub custom_window_decorations: bool, + /// Include the "Welcome screen" application in the recordings panel? #[serde(alias = "include_welcome_screen_button_in_recordings_panel")] pub include_rerun_examples_button_in_recordings_panel: bool, @@ -86,6 +89,8 @@ impl Default for AppOptions { show_notification_toasts: true, + custom_window_decorations: re_ui::custom_window_decorations_default(), + include_rerun_examples_button_in_recordings_panel: true, show_picking_debug_overlay: false, @@ -102,12 +107,7 @@ impl Default for AppOptions { mapbox_access_token: String::new(), - memory_limit: if cfg!(target_arch = "wasm32") { - // On wasm32 we only have 4GB of memory to play around with. - re_memory::MemoryLimit::from_bytes(2_500_000_000) - } else { - MemoryLimit::from_fraction_of_total(0.75) - }, + memory_limit: MemoryLimit::default_for_current_platform(), max_fetch_stage: FetchStage::default(), @@ -122,6 +122,8 @@ impl AppOptions { Self { memory_limit: MemoryLimit::UNLIMITED, show_metrics: false, // flaky in snapshot tests + #[cfg(any(target_os = "windows", target_os = "linux"))] + custom_window_decorations: false, ..Default::default() } } @@ -164,7 +166,7 @@ impl AppOptions { } } -#[derive(Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)] #[serde(default)] pub struct VideoOptions { /// Preferred method for video decoding on web. @@ -187,15 +189,29 @@ pub struct VideoOptions { pub ffmpeg_path: String, } -#[derive(Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)] #[serde(default)] pub struct ExperimentalAppOptions { - /// Enable the experimental Status view. - pub enable_status_view: bool, + /// Enable table cards and blueprints. + /// + /// This enables registered table blueprints, + /// plus the table/grid view toggle for card-based table layouts. + pub table_cards_and_blueprints: bool, + + /// Enable gamepad navigation in 3D spatial views. + pub gamepad_navigation: bool, + + /// Enable alpha-blending of semi-transparent point clouds. + /// + /// Off by default: transparent point clouds are sorted on the CPU every frame, which is + /// slow for large clouds. Opaque point clouds render much faster. + pub point_cloud_transparency: bool, - /// Enable grid view mode for data tables. + /// Load `.rrd` files through the Viewer's in-process catalog instead of importing them + /// as a live recording. /// - /// When enabled, a list/grid toggle appears in the table title bar, - /// allowing users to switch between the traditional table and a card-based grid layout. - pub table_grid_view: bool, + /// When enabled, opened `.rrd` files are registered with the catalog and surfaced as redap + /// datasets under an internal server in the recording panel. When disabled, files are imported + /// directly into the viewer as plain recordings. + pub use_internal_catalog: bool, } diff --git a/crates/viewer/re_viewer_context/src/blueprint_helpers.rs b/crates/viewer/re_viewer_context/src/blueprint_helpers.rs index f3147788eac7..9c7055b7a3ba 100644 --- a/crates/viewer/re_viewer_context/src/blueprint_helpers.rs +++ b/crates/viewer/re_viewer_context/src/blueprint_helpers.rs @@ -11,7 +11,7 @@ use crate::{ #[inline] pub fn blueprint_timeline() -> TimelineName { - TimelineName::new("blueprint") + re_string_interner::intern_static!(TimelineName, "blueprint") } /// The timepoint to use when writing an update to the blueprint. diff --git a/crates/viewer/re_viewer_context/src/blueprint_id.rs b/crates/viewer/re_viewer_context/src/blueprint_id.rs index 1173becdbfe4..40e45b9bea19 100644 --- a/crates/viewer/re_viewer_context/src/blueprint_id.rs +++ b/crates/viewer/re_viewer_context/src/blueprint_id.rs @@ -18,13 +18,11 @@ pub struct BlueprintId { } impl re_byte_size::SizeBytes for BlueprintId { + const IS_POD: bool = true; + fn heap_size_bytes(&self) -> u64 { 0 } - - fn is_pod() -> bool { - true - } } impl BlueprintId { @@ -93,11 +91,11 @@ impl BlueprintId { #[inline] pub fn as_entity_path(&self) -> EntityPath { - T::registry_path() - .iter() - .cloned() - .chain(std::iter::once(EntityPathPart::new(self.id.to_string()))) - .collect() + std::iter::chain( + T::registry_path().iter().cloned(), + std::iter::once(EntityPathPart::new(self.id.to_string())), + ) + .collect() } #[inline] @@ -194,6 +192,13 @@ macro_rules! define_blueprint_id_type { define_blueprint_id_type!(ViewId, ViewIdRegistry, "view"); define_blueprint_id_type!(ContainerId, ContainerIdRegistry, "container"); +impl ViewId { + /// Returns the renderer identity corresponding to this blueprint view. + pub fn render_view_id(self) -> re_renderer::ViewBuilderId { + re_renderer::ViewBuilderId::new(re_log_types::hash::Hash64::hash(self.id).hash64()) + } +} + // ---------------------------------------------------------------------------- // Builtin `ViewId`s. diff --git a/crates/viewer/re_viewer_context/src/cache/app_caches.rs b/crates/viewer/re_viewer_context/src/cache/app_caches.rs new file mode 100644 index 000000000000..b05da1fdfa44 --- /dev/null +++ b/crates/viewer/re_viewer_context/src/cache/app_caches.rs @@ -0,0 +1,43 @@ +use re_mutex::RwLock; + +use crate::{Cache as _, ImageDecodeCache, ImageStatsCache}; + +/// App-level caches for data that is not tied to any particular store. +/// +/// Unlike per-store caches ([`crate::StoreCache`]), these receive no store events +/// and are not dropped together with a store. +/// Therefore, only caches whose keys are globally unique (e.g. content-addressed by row id) +/// and whose entries expire on their own (see [`crate::Cache::begin_frame`]) belong here. +#[derive(Default)] +pub struct AppCaches { + pub image_decode: RwLock, + pub image_stats: RwLock, +} + +impl AppCaches { + /// Call once per frame to potentially flush the caches. + pub fn begin_frame(&self) { + re_tracing::profile_function!(); + + let Self { + image_decode, + image_stats, + } = self; + image_decode.write().begin_frame(); + image_stats.write().begin_frame(); + } + + /// Attempt to free up memory. + /// + /// Called BEFORE `begin_frame` (if at all). + pub fn purge_memory(&mut self) { + re_tracing::profile_function!(); + + let Self { + image_decode, + image_stats, + } = self; + image_decode.get_mut().purge_memory(); + image_stats.get_mut().purge_memory(); + } +} diff --git a/crates/viewer/re_viewer_context/src/cache/cache_trait.rs b/crates/viewer/re_viewer_context/src/cache/cache_trait.rs index dbc9f1a0ca7b..4ee2405d1fe1 100644 --- a/crates/viewer/re_viewer_context/src/cache/cache_trait.rs +++ b/crates/viewer/re_viewer_context/src/cache/cache_trait.rs @@ -4,6 +4,15 @@ use re_entity_db::EntityDb; /// A cache for memoizing things in order to speed up immediate mode UI & other immediate mode style things. /// +/// Caches are stored in [`crate::Memoizers`], and each [`crate::Memoizers`] instance belongs to a +/// single [`re_log_types::StoreId`]. This means cache implementations are already scoped to one +/// store (recording or blueprint) and must not include the store id in their internal keys. +/// +/// Cache implementations may still need finer-grained keys, such as [`crate::ViewId`], entity paths, +/// timelines, or query ranges. In particular, view-related caches should explicitly decide whether +/// their data is truly per-view (`ViewId` key needed) or can be shared across all views of the same +/// store (`ViewId` key not needed). +/// /// See also egus's cache system, in [`egui::cache`] (). pub trait Cache: std::any::Any + Send + Sync + re_byte_size::MemUsageTreeCapture { fn name(&self) -> &'static str; @@ -36,3 +45,19 @@ pub trait Cache: std::any::Any + Send + Sync + re_byte_size::MemUsageTreeCapture _ = entity_db; } } + +/// Trait for [`Cache`]es that are internally a list of key-value pairs that are computed once +/// and can be trivially returned without holding the lock. +/// +/// Implementing this is required for [`crate::Memoizers::read_or_compute`]. +pub trait CacheEntryAccess: Cache { + /// Reads the cache entry for the given key, if it exists. + fn read(&self, key: &Key) -> Option; + + /// Computes the cache entry for the given key and returns it. + /// + /// While we generally expect this to be called only ever once for a given key, + /// in high contended situations it may be called repeatedly for the same key. + /// Implementations *have* to handle this gracefully. + fn compute(&mut self, key: &Key) -> Value; +} diff --git a/crates/viewer/re_viewer_context/src/cache/encoded_depth_image_stats_cache.rs b/crates/viewer/re_viewer_context/src/cache/encoded_depth_image_stats_cache.rs new file mode 100644 index 000000000000..2c6cdefb3f13 --- /dev/null +++ b/crates/viewer/re_viewer_context/src/cache/encoded_depth_image_stats_cache.rs @@ -0,0 +1,211 @@ +use ahash::HashMap; + +use re_byte_size::SizeBytes as _; +use re_chunk::RowId; +use re_chunk_store::ChunkStoreEvent; +use re_entity_db::EntityDb; +use re_log_types::hash::Hash64; +use re_sdk_types::ComponentIdentifier; +use re_sdk_types::components::MediaType; +use re_sdk_types::image::{ImageKind, ImageLoadError}; + +use crate::cache::filter_blob_removed_events; +use crate::image_info::StoredBlobCacheKey; +use crate::{Cache, ImageInfo, ImageStats}; + +/// Caches the [`ImageStats`] of encoded depth images +/// ([`re_sdk_types::archetypes::EncodedDepthImage`]), e.g. to derive a depth range. +/// +/// The image is decoded transiently. Only the stats are retained, not the decoded pixels. +/// Blobs that failed to decode are cached as `None`, so decoding is not retried every frame. +// TODO(RR-4570): Ideally the stats would be derived from the frames the video player +// decodes anyway, instead of decoding a second time here. +#[derive(Default)] +pub struct EncodedDepthImageStatsCache( + // The inner key is the hash of the media type, + // since a media type logged later can change how the same blob is decoded. + HashMap>>, +); + +impl EncodedDepthImageStatsCache { + /// Decode some depth image data (e.g. 16-bit PNG or RVL) and compute & cache its stats. + /// + /// NOTE: images are never batched atm (they are mono-archetypes), + /// so we don't need the instance id here. + pub fn entry( + &mut self, + blob_row_id: RowId, + blob_component: ComponentIdentifier, + image_bytes: &[u8], + media_type: Option<&MediaType>, + ) -> Option { + re_tracing::profile_function!(); + + *self + .0 + .entry(StoredBlobCacheKey::new(blob_row_id, blob_component)) + .or_default() + .entry(Hash64::hash(media_type)) + .or_insert_with(|| { + match decode_depth_image(blob_row_id, blob_component, image_bytes, media_type) { + Ok(image) => Some(ImageStats::from_image(&image)), + Err(err) => { + re_log::warn_once!("Failed to decode depth image: {err}"); + None + } + } + }) + } +} + +fn decode_depth_image( + blob_row_id: RowId, + blob_component: ComponentIdentifier, + image_bytes: &[u8], + media_type: Option<&MediaType>, +) -> Result { + re_tracing::profile_function!(); + + let Some(media_type) = media_type + .cloned() + .or_else(|| MediaType::guess_from_data(image_bytes)) + else { + return Err(ImageLoadError::UnrecognizedMimeType); + }; + + if media_type.as_str() == MediaType::RVL { + let metadata = re_rvl::RosRvlMetadata::parse(image_bytes) + .map_err(|err| ImageLoadError::DecodeError(err.to_string()))?; + let depths = re_rvl::decode_rvl_with_quantization(image_bytes, &metadata) + .map_err(|err| ImageLoadError::DecodeError(err.to_string()))?; + + let format = re_sdk_types::datatypes::ImageFormat::depth( + [metadata.width, metadata.height], + re_sdk_types::datatypes::ChannelDatatype::F32, + ); + + return Ok(ImageInfo::from_stored_blob( + blob_row_id, + blob_component, + arrow::buffer::Buffer::from_vec(depths).into(), + format, + ImageKind::Depth, + )); + } + + super::image_decode_cache::decode_image( + blob_row_id, + blob_component, + image_bytes, + media_type.as_str(), + ImageKind::Depth, + ) +} + +impl Cache for EncodedDepthImageStatsCache { + fn name(&self) -> &'static str { + "EncodedDepthImageStatsCache" + } + + fn purge_memory(&mut self) { + self.0.clear(); + } + + fn on_store_events(&mut self, events: &[&ChunkStoreEvent], _entity_db: &EntityDb) { + re_tracing::profile_function!(); + + let cache_key_removed = filter_blob_removed_events(events); + self.0 + .retain(|cache_key, _per_media_type| !cache_key_removed.contains(cache_key)); + } +} + +impl re_byte_size::MemUsageTreeCapture for EncodedDepthImageStatsCache { + fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree { + re_byte_size::MemUsageTree::Bytes(self.0.total_size_bytes()) + } +} + +#[cfg(test)] +mod tests { + use re_sdk_types::archetypes::EncodedDepthImage; + + use super::*; + + fn blob_component() -> ComponentIdentifier { + EncodedDepthImage::descriptor_blob().component + } + + fn encode_l16_png(values: &[u16], width: u32, height: u32) -> Vec { + let mut buf = Vec::new(); + let encoder = image::codecs::png::PngEncoder::new(&mut buf); + image::ImageEncoder::write_image( + encoder, + bytemuck::cast_slice(values), + width, + height, + image::ColorType::L16.into(), + ) + .unwrap(); + buf + } + + /// Regression test for RR-5172: the stats used by the depth range fallback must + /// be derived from the decoded pixel values, not from the encoded bit depth + /// (which would put the maximum at 65535 for a 16-bit PNG). + #[test] + fn stats_come_from_decoded_png_pixels() { + let png = encode_l16_png(&[0, 1000, 4000, 2500], 2, 2); + let mut cache = EncodedDepthImageStatsCache::default(); + + let stats = cache + .entry( + RowId::new(), + blob_component(), + &png, + Some(&MediaType::png()), + ) + .expect("16-bit grayscale PNG should decode"); + + assert_eq!(stats.finite_range, (0.0, 4000.0)); + } + + /// Without an explicit media type, the media type is guessed from the blob contents. + #[test] + fn guesses_media_type_from_blob() { + let png = encode_l16_png(&[0, 4000], 2, 1); + let mut cache = EncodedDepthImageStatsCache::default(); + + let stats = cache + .entry(RowId::new(), blob_component(), &png, None) + .expect("PNG should be recognized without an explicit media type"); + + assert_eq!(stats.finite_range, (0.0, 4000.0)); + } + + /// A failed decode under one media type must not shadow a later successful decode + /// of the same blob under a corrected media type. + #[test] + fn failures_are_cached_per_media_type() { + let png = encode_l16_png(&[0, 4000], 2, 1); + let row_id = RowId::new(); + let mut cache = EncodedDepthImageStatsCache::default(); + + // PNG bytes declared as RVL fail to decode… + assert!( + cache + .entry(row_id, blob_component(), &png, Some(&MediaType::rvl())) + .is_none() + ); + // …and the failure itself is memoized… + let per_media_type = &cache.0[&StoredBlobCacheKey::new(row_id, blob_component())]; + assert_eq!(per_media_type.len(), 1); + assert!(per_media_type.values().all(Option::is_none)); + // …but the same blob decodes fine once the media type is corrected. + assert!( + cache + .entry(row_id, blob_component(), &png, Some(&MediaType::png())) + .is_some() + ); + } +} diff --git a/crates/viewer/re_viewer_context/src/cache/image_decode_cache.rs b/crates/viewer/re_viewer_context/src/cache/image_decode_cache.rs index 0d611caf1f5a..81dd4d787a98 100644 --- a/crates/viewer/re_viewer_context/src/cache/image_decode_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/image_decode_cache.rs @@ -4,10 +4,8 @@ use re_chunk::RowId; use re_chunk_store::ChunkStoreEvent; use re_entity_db::EntityDb; use re_log_types::hash::Hash64; -use re_rvl::{RosRvlMetadata, decode_rvl_with_quantization}; use re_sdk_types::ComponentIdentifier; -use re_sdk_types::components::{ImageBuffer, ImageFormat as ImageFormatComponent, MediaType}; -use re_sdk_types::datatypes::{Blob, ChannelDatatype, ColorModel, ImageFormat}; +use re_sdk_types::components::{ImageBuffer, MediaType}; use re_sdk_types::image::{ImageKind, ImageLoadError}; use crate::cache::filter_blob_removed_events; @@ -34,7 +32,7 @@ pub struct ImageDecodeCache { } impl ImageDecodeCache { - // TODO(isse): Remove this, we want to use the video player instead of this + // TODO(RR-4570): Remove this, we want to use the video player instead of this // but hard to do for the only remaining usage in `redap_thumbnail`. #[deprecated = "Use video stream cache instead if possible."] /// Decode some image data and cache the result. @@ -70,39 +68,6 @@ impl ImageDecodeCache { }) } - /// Decode some depth image data and cache the result. - /// - /// The `RowId`, if available, may be used to generate the cache key. - /// NOTE: depth images are never batched atm (they are mono-archetypes), - /// so we don't need the instance id here. - pub fn entry_encoded_depth( - &mut self, - blob_row_id: RowId, - blob_component: ComponentIdentifier, - image_bytes: &[u8], - media_type: Option<&MediaType>, - ) -> Result { - re_tracing::profile_function!(); - - let Some(media_type) = media_type - .cloned() - .or_else(|| MediaType::guess_from_data(image_bytes)) - else { - return Err(ImageLoadError::UnrecognizedMimeType); - }; - - let inner_key = Hash64::hash(&media_type); - - self.cache_lookup_or_decode(blob_row_id, blob_component, inner_key, || { - decode_encoded_depth( - blob_row_id, - blob_component, - image_bytes, - media_type.as_str(), - ) - }) - } - fn cache_lookup_or_decode( &mut self, blob_row_id: RowId, @@ -147,115 +112,43 @@ fn decode_color_image( image_bytes: &[u8], media_type: &str, ) -> Result { - re_tracing::profile_function!(media_type); - - let mut reader = image::ImageReader::new(std::io::Cursor::new(image_bytes)); - - if let Some(format) = image::ImageFormat::from_mime_type(media_type) { - reader.set_format(format); - } else { - return Err(ImageLoadError::UnsupportedMimeType(media_type.to_owned())); - } - - let dynamic_image = reader.decode()?; - - let (buffer, format) = ImageBuffer::from_dynamic_image(dynamic_image)?; - - Ok(ImageInfo::from_stored_blob( + decode_image( blob_row_id, blob_component, - buffer.0, - format.0, + image_bytes, + media_type, ImageKind::Color, - )) + ) } -fn decode_encoded_depth( +/// Decode image data supported by the `image` crate (e.g. PNG or JPEG) into an [`ImageInfo`]. +pub(crate) fn decode_image( blob_row_id: RowId, blob_component: ComponentIdentifier, image_bytes: &[u8], media_type: &str, + kind: ImageKind, ) -> Result { - match media_type { - MediaType::PNG => decode_png_depth(blob_row_id, blob_component, image_bytes), - MediaType::RVL => decode_rvl_depth(blob_row_id, blob_component, image_bytes), - other => Err(ImageLoadError::UnsupportedMimeType(other.to_owned())), - } -} - -fn decode_png_depth( - blob_row_id: RowId, - blob_component: ComponentIdentifier, - image_bytes: &[u8], -) -> Result { - re_tracing::profile_function!(); + re_tracing::profile_function!(media_type); let mut reader = image::ImageReader::new(std::io::Cursor::new(image_bytes)); - reader.set_format(image::ImageFormat::Png); - - let dynamic_image = reader.decode()?; - let (buffer, mut format) = ImageBuffer::from_dynamic_image(dynamic_image)?; - if format.color_model != Some(ColorModel::L) { - return Err(ImageLoadError::DecodeError(format!( - "Encoded depth PNG must be single-channel (L); got {:?}", - format.color_model - ))); - } - // .. but in our semantics we treat depth as `None` color model since there _is_ no color. (see `ImageKind::Depth`) - format.color_model = None; - - let expected_num_bytes = format.num_bytes(); - let ImageBuffer(blob) = buffer; - let actual_num_bytes = blob.len(); - if actual_num_bytes != expected_num_bytes { - return Err(ImageLoadError::DecodeError(format!( - "Encoded depth PNG payload is {actual_num_bytes} B, but {format:?} requires {expected_num_bytes} B", - ))); + if let Some(format) = image::ImageFormat::from_mime_type(media_type) { + reader.set_format(format); + } else { + return Err(ImageLoadError::UnsupportedMimeType(media_type.to_owned())); } - Ok(ImageInfo::from_stored_blob( - blob_row_id, - blob_component, - blob, - *format, - ImageKind::Depth, - )) -} - -fn decode_rvl_depth( - blob_row_id: RowId, - blob_component: ComponentIdentifier, - image_bytes: &[u8], -) -> Result { - let metadata = RosRvlMetadata::parse(image_bytes) - .map_err(|err| ImageLoadError::DecodeError(err.to_string()))?; - - let format = ImageFormatComponent::from(ImageFormat::depth( - [metadata.width, metadata.height], - ChannelDatatype::F32, // We always use the quantization information from the metadata to convert to f32. - )); + let dynamic_image = reader.decode()?; - let buffer: Vec = decode_rvl_with_quantization(image_bytes, &metadata) - .map(|v| { - bytemuck::try_cast_vec(v).unwrap_or_else(|(_err, v)| bytemuck::cast_slice(&v).to_vec()) - }) - .map_err(|err| ImageLoadError::DecodeError(err.to_string()))?; - - let expected_num_bytes = format.num_bytes(); - let actual_num_bytes = buffer.len(); - if actual_num_bytes != expected_num_bytes { - return Err(ImageLoadError::DecodeError(format!( - "RVL payload decoded to {actual_num_bytes} B, but {format:?} requires {expected_num_bytes} B" - ))); - } + let (buffer, format) = ImageBuffer::from_dynamic_image(dynamic_image)?; Ok(ImageInfo::from_stored_blob( blob_row_id, blob_component, - Blob::from(buffer), + buffer.0, format.0, - ImageKind::Depth, + kind, )) } @@ -338,81 +231,3 @@ impl re_byte_size::MemUsageTreeCapture for ImageDecodeCache { node.into_tree() } } - -#[cfg(test)] -mod tests { - use super::*; - - use image::{ColorType, ImageEncoder as _, codecs::png::PngEncoder}; - - #[test] - fn entry_encoded_depth_guesses_png_media_type() { - let width = 2; - let height = 2; - let depth_values: [u16; 4] = [0, 1, 2, 3]; - - let mut encoded_png = Vec::new(); - { - let encoder = PngEncoder::new(&mut encoded_png); - encoder - .write_image( - bytemuck::cast_slice(&depth_values), - width, - height, - ColorType::L16.into(), - ) - .expect("encoding png failed"); - } - - let mut cache = ImageDecodeCache::default(); - - let image_info = cache - .entry_encoded_depth( - RowId::ZERO, - ComponentIdentifier::from("test"), - &encoded_png, - None, - ) - .expect("decoding encoded depth image failed"); - - assert_eq!(image_info.kind, ImageKind::Depth); - assert_eq!( - image_info.format, - ImageFormat::depth([width, height], ChannelDatatype::U16) - ); - } - - #[test] - fn decoding_png_depth_works() { - let width = 2; - let height = 2; - let depth_values: [u16; 4] = [0, 1, 2, 3]; - - let mut encoded_png = Vec::new(); - { - let encoder = PngEncoder::new(&mut encoded_png); - encoder - .write_image( - bytemuck::cast_slice(&depth_values), - width, - height, - ColorType::L16.into(), - ) - .expect("encoding png failed"); - } - - let image_info = - decode_png_depth(RowId::ZERO, ComponentIdentifier::from("test"), &encoded_png) - .expect("decoding png depth failed"); - - assert_eq!(image_info.kind, ImageKind::Depth); - assert_eq!( - image_info.format, - ImageFormat::depth([width, height], ChannelDatatype::U16) - ); - assert_eq!( - image_info.buffer.len(), - depth_values.len() * std::mem::size_of::() - ); - } -} diff --git a/crates/viewer/re_viewer_context/src/cache/image_histogram_cache.rs b/crates/viewer/re_viewer_context/src/cache/image_histogram_cache.rs new file mode 100644 index 000000000000..9bc1601b85be --- /dev/null +++ b/crates/viewer/re_viewer_context/src/cache/image_histogram_cache.rs @@ -0,0 +1,149 @@ +use crate::cache::filter_blob_removed_events; +use crate::image_info::StoredBlobCacheKey; +use crate::{Cache, CacheEntryAccess, ImageInfo}; +use ahash::HashMap; +use re_byte_size::SizeBytes as _; +use re_byte_size::{MemUsageTree, MemUsageTreeCapture}; +use re_chunk_store::ChunkStoreEvent; +use re_entity_db::EntityDb; +use std::sync::Arc; + +/// Per-channel histogram of an 8-bit `RGB` image. +/// +/// Each channel has 256 bins. +#[derive(Clone, Debug, re_byte_size::SizeBytes)] +pub struct Rgb8Histogram { + /// One 256-bin histogram per channel (R, G, B). + pub bins: [[u64; 256]; 3], +} + +impl Rgb8Histogram { + /// Compute the per-channel histogram of an 8-bit `RGB` buffer. + pub fn from_rgb8(rgb: &[u8]) -> Self { + re_tracing::profile_function!(); + + let mut bins_r = [0_u64; 256]; + let mut bins_g = [0_u64; 256]; + let mut bins_b = [0_u64; 256]; + + let (chunks, _remainder) = rgb.as_chunks::<3>(); + for &[r, g, b] in chunks { + bins_r[r as usize] += 1; + bins_g[g as usize] += 1; + bins_b[b as usize] += 1; + } + + Self { + bins: [bins_r, bins_g, bins_b], + } + } +} + +/// Caches per-channel histograms for 8-bit RGB images, keyed by image content. +#[derive(Default)] +pub struct ImageHistogramCache(HashMap>); + +impl ImageHistogramCache { + /// Get the histogram for the given 8-bit `RGB` image, computing and caching it on first access. + /// + /// The caller is responsible for only passing in 8-bit RGB images. + pub fn entry(&mut self, image: &ImageInfo) -> Arc { + self.0 + .entry(image.buffer_content_hash) + .or_insert_with(|| Arc::new(Rgb8Histogram::from_rgb8(&image.buffer))) + .clone() + } +} + +impl CacheEntryAccess> for ImageHistogramCache { + fn read(&self, image: &ImageInfo) -> Option> { + self.0.get(&image.buffer_content_hash).cloned() + } + + fn compute(&mut self, image: &ImageInfo) -> Arc { + self.entry(image) + } +} + +impl Cache for ImageHistogramCache { + fn name(&self) -> &'static str { + "ImageHistogramCache" + } + + fn purge_memory(&mut self) { + // [[u64; 256]; 3] ≈ 6 KiB per cached image — small enough that we + // leave it to store-event invalidation rather than periodic purging. + } + + fn on_store_events(&mut self, events: &[&ChunkStoreEvent], _entity_db: &EntityDb) { + let removed = filter_blob_removed_events(events); + if removed.is_empty() { + return; + } + self.0.retain(|key, _| !removed.contains(key)); + } +} + +impl MemUsageTreeCapture for ImageHistogramCache { + fn capture_mem_usage_tree(&self) -> MemUsageTree { + MemUsageTree::Bytes(self.0.total_size_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_buffer_yields_zero_bins() { + let hist = Rgb8Histogram::from_rgb8(&[]); + for channel in &hist.bins { + assert!(channel.iter().all(|&c| c == 0)); + } + } + + #[test] + fn single_pixel_increments_one_bin_per_channel() { + let hist = Rgb8Histogram::from_rgb8(&[10, 20, 30]); + assert_eq!(hist.bins[0][10], 1); + assert_eq!(hist.bins[1][20], 1); + assert_eq!(hist.bins[2][30], 1); + let total: u64 = hist.bins.iter().flatten().sum(); + assert_eq!(total, 3); + } + + #[test] + fn trailing_bytes_are_ignored() { + // 4 trailing bytes; only the first complete pixel should contribute. + let hist = Rgb8Histogram::from_rgb8(&[1, 2, 3, 99]); + assert_eq!(hist.bins[0][1], 1); + assert_eq!(hist.bins[1][2], 1); + assert_eq!(hist.bins[2][3], 1); + assert_eq!(hist.bins[0][99], 0); + assert_eq!(hist.bins[1][99], 0); + assert_eq!(hist.bins[2][99], 0); + } + + #[test] + fn many_pixels_count_correctly() { + // 100 pixels, all (5, 6, 7). + let buffer: Vec = (0..100).flat_map(|_| [5, 6, 7]).collect(); + let hist = Rgb8Histogram::from_rgb8(&buffer); + assert_eq!(hist.bins[0][5], 100); + assert_eq!(hist.bins[1][6], 100); + assert_eq!(hist.bins[2][7], 100); + // No other bins should be set. + for channel in 0..3 { + for bin in 0..256 { + let expected = match (channel, bin) { + (0, 5) | (1, 6) | (2, 7) => 100, + _ => 0, + }; + assert_eq!( + hist.bins[channel][bin], expected, + "channel {channel} bin {bin}" + ); + } + } + } +} diff --git a/crates/viewer/re_viewer_context/src/cache/image_stats_cache.rs b/crates/viewer/re_viewer_context/src/cache/image_stats_cache.rs index 51165c193f82..7eacc0ce5831 100644 --- a/crates/viewer/re_viewer_context/src/cache/image_stats_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/image_stats_cache.rs @@ -6,7 +6,7 @@ use re_sdk_types::image::ImageKind; use re_sdk_types::{Component as _, components}; use crate::image_info::StoredBlobCacheKey; -use crate::{Cache, ImageInfo, ImageStats}; +use crate::{Cache, CacheEntryAccess, ImageInfo, ImageStats}; // Caches image stats (use e.g. `RowId` to generate cache key). #[derive(Default)] @@ -21,13 +21,25 @@ impl ImageStatsCache { } } +impl CacheEntryAccess for ImageStatsCache { + fn read(&self, image: &ImageInfo) -> Option { + self.0 + .get(&(image.buffer_content_hash, image.kind)) + .copied() + } + + fn compute(&mut self, image: &ImageInfo) -> ImageStats { + self.entry(image) + } +} + impl Cache for ImageStatsCache { fn name(&self) -> &'static str { "ImageStatsCache" } fn purge_memory(&mut self) { - // Purging the image stats is not worth it - these are very small objects! + self.0.clear(); } fn on_store_events(&mut self, events: &[&ChunkStoreEvent], _entity_db: &EntityDb) { diff --git a/crates/viewer/re_viewer_context/src/cache/memoizers.rs b/crates/viewer/re_viewer_context/src/cache/memoizers.rs index b957b4e6b26a..f8a5c8d2b404 100644 --- a/crates/viewer/re_viewer_context/src/cache/memoizers.rs +++ b/crates/viewer/re_viewer_context/src/cache/memoizers.rs @@ -6,9 +6,9 @@ use re_byte_size::{MemUsageTree, MemUsageTreeCapture}; use re_chunk_store::ChunkStoreEvent; use re_entity_db::EntityDb; use re_log_types::StoreId; -use re_mutex::Mutex; +use re_mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use crate::Cache; +use crate::{Cache, CacheEntryAccess}; /// A wrapper around a cache that allows for shared access with its own lock. /// @@ -16,7 +16,7 @@ use crate::Cache; /// instead of a single lock for all caches. struct SharedCache { name: &'static str, - cache: Mutex>, + cache: RwLock>, } impl SharedCache { @@ -24,12 +24,16 @@ impl SharedCache { let cache = Box::::default(); Self { name: cache.name(), - cache: Mutex::new(cache), + cache: RwLock::new(cache), } } - fn lock(&self) -> re_mutex::MutexGuard<'_, Box> { - self.cache.lock() + fn read(&self) -> RwLockReadGuard<'_, Box> { + self.cache.read() + } + + fn write(&self) -> RwLockWriteGuard<'_, Box> { + self.cache.write() } } @@ -40,9 +44,9 @@ pub struct Memoizers { /// Master map from cache type to the cache itself. /// - /// The master mutex is only held briefly to look up or insert a cache. - /// Each cache has its own mutex for actual access. - caches: Mutex>>, + /// The master lock is only held briefly to look up or insert a cache. + /// Each cache has its own lock for actual access. + caches: RwLock>>, /// How much memory we used after the last call to [`Self::purge_memory`]. memory_use_after_last_purge: u64, @@ -52,7 +56,7 @@ impl Memoizers { /// Creates a new instance of [`Memoizers`] associated with a specific store. pub fn new(store_id: StoreId) -> Self { Self { - caches: Mutex::new(HashMap::default()), + caches: RwLock::new(HashMap::default()), store_id, memory_use_after_last_purge: 0, } @@ -68,9 +72,9 @@ impl Memoizers { re_tracing::profile_function!(); #[expect(clippy::iter_over_hash_type)] // order doesn't matter here - for cache in self.caches.lock().values() { + for cache in self.caches.read().values() { re_tracing::profile_scope!(cache.name); - cache.lock().begin_frame(); + cache.write().begin_frame(); } } @@ -92,9 +96,9 @@ impl Memoizers { let mut cache_vram: Vec<_> = self .caches - .lock() + .read() .values() - .map(|cache| (cache.name, cache.lock().vram_usage())) + .map(|cache| (cache.name, cache.read().vram_usage())) .collect(); cache_vram.sort_by_key(|(cache_name, _)| *cache_name); @@ -111,9 +115,9 @@ impl Memoizers { re_tracing::profile_function!(); #[expect(clippy::iter_over_hash_type)] // order doesn't matter here - for cache in self.caches.lock().values() { + for cache in self.caches.read().values() { re_tracing::profile_scope!(cache.name); - cache.lock().purge_memory(); + cache.write().purge_memory(); } self.memory_use_after_last_purge = self.capture_mem_usage_tree().size_bytes(); @@ -134,9 +138,31 @@ impl Memoizers { } #[expect(clippy::iter_over_hash_type)] // order doesn't matter here - for cache in self.caches.lock().values() { + for cache in self.caches.read().values() { re_tracing::profile_scope!(cache.name); - cache.lock().on_store_events(&relevant_events, entity_db); + cache.write().on_store_events(&relevant_events, entity_db); + } + } + + /// Gets or creates a shared cache for the given type. + fn shared_cache_entry(&self) -> Arc { + let caches = { + re_tracing::profile_wait!("master-cache-read-lock"); + self.caches.upgradable_read() + }; + + let type_id = TypeId::of::(); + if let Some(cache) = caches.get(&type_id) { + cache.clone() + } else { + let mut caches = { + re_tracing::profile_wait!("master-cache-upgrade-lock"); + caches.upgrade() + }; + caches + .entry(type_id) + .or_insert_with(|| Arc::new(SharedCache::new::())) + .clone() } } @@ -144,20 +170,12 @@ impl Memoizers { /// /// Adds the cache lazily if it wasn't already there. pub fn entry(&self, f: impl FnOnce(&mut C) -> R) -> R { - let shared_cache = { - re_tracing::profile_wait!("master-cache-lock"); - // Only hold master lock briefly to get or create the cache entry - let mut guard = self.caches.lock(); - guard - .entry(TypeId::of::()) - .or_insert_with(|| Arc::new(SharedCache::new::())) - .clone() - }; + let shared_cache = self.shared_cache_entry::(); - // Now lock only this specific cache + // Now lock only this specific cache. let mut cache_guard = { - re_tracing::profile_wait!("cache-lock", shared_cache.name); - shared_cache.lock() + re_tracing::profile_wait!("cache-write-lock", shared_cache.name); + shared_cache.write() }; let cache = cache_guard.as_mut(); f((cache as &mut dyn std::any::Any) @@ -166,6 +184,63 @@ impl Memoizers { "Downcast failed, this indicates a bug in how `Memoizers` adds new cache types.", )) } + + /// Accesses an existing cache for reading. + /// + /// Returns `None` if the cache has not been created yet. + pub fn read(&self, f: impl FnOnce(&C) -> R) -> Option { + let shared_cache = { + re_tracing::profile_wait!("master-cache-read-lock"); + let guard = self.caches.read(); + guard.get(&TypeId::of::()).cloned() + }?; + + let cache_guard = { + re_tracing::profile_wait!("cache-read-lock", shared_cache.name); + shared_cache.read() + }; + let cache = cache_guard.as_ref(); + Some(f((cache as &dyn std::any::Any).downcast_ref::().expect( + "Downcast failed, this indicates a bug in how `Memoizers` adds new cache types.", + ))) + } + + /// Tries to read an existing memoization cache entry, then computes it through mutable access on miss. + /// + /// Use this if you're working with init-only cache entries, expect your cache entry to be usually present + /// and want to avoid the overhead of a write lock. + /// Note that this _adds_ overhead for the miss path compared to `memoizer`, so don't use this if you expect many misses! + /// (UI code typically doesn't need to care about this optimization, since it's usually single-threaded already.) + pub fn read_or_compute + Default, Key, Value>( + &self, + key: &Key, + ) -> Value { + let cache_entry = self.shared_cache_entry::(); + + let cache = { + re_tracing::profile_wait!("cache-read-lock"); + cache_entry.cache.upgradable_read() + }; + + let cache_accessor = (cache.as_ref() as &dyn std::any::Any) + .downcast_ref::() + .expect( + "Downcast failed, this indicates a bug in how `Memoizers` adds new cache types.", + ); + + if let Some(value) = cache_accessor.read(key) { + value + } else { + let mut cache = { + re_tracing::profile_wait!("cache-upgrade-lock"); + cache.upgrade() + }; + let cache_accessor = (cache.as_mut() as &mut dyn std::any::Any).downcast_mut::().expect( + "Downcast failed, this indicates a bug in how `Memoizers` adds new cache types.", + ); + cache_accessor.compute(key) + } + } } impl MemUsageTreeCapture for Memoizers { @@ -176,9 +251,9 @@ impl MemUsageTreeCapture for Memoizers { let mut cache_trees: Vec<_> = self .caches - .lock() + .read() .values() - .map(|cache| (cache.name, cache.lock().capture_mem_usage_tree())) + .map(|cache| (cache.name, cache.read().capture_mem_usage_tree())) .collect(); cache_trees.sort_by_key(|(cache_name, _)| *cache_name); diff --git a/crates/viewer/re_viewer_context/src/cache/mod.rs b/crates/viewer/re_viewer_context/src/cache/mod.rs index 3159a8dfbad5..aade03491f29 100644 --- a/crates/viewer/re_viewer_context/src/cache/mod.rs +++ b/crates/viewer/re_viewer_context/src/cache/mod.rs @@ -3,8 +3,11 @@ //! Caches are registered lazily upon first use, see [`Memoizers::entry`]. //! The concrete caches exposed here are always available for all viewer crates. +mod app_caches; mod cache_trait; +mod encoded_depth_image_stats_cache; mod image_decode_cache; +mod image_histogram_cache; mod image_stats_cache; mod memoizers; mod store_cache; @@ -13,20 +16,23 @@ mod transform_database_store; mod video_asset_cache; mod video_stream_cache; -pub use cache_trait::Cache; +pub use app_caches::AppCaches; +pub use cache_trait::{Cache, CacheEntryAccess}; pub use memoizers::Memoizers; pub use store_cache::StoreCache; // TODO(andreas): Do we _really_ have to have all these caches in `re_viewer_context`? // Caches are fully dynamic and registration based, so they can be added at runtime by any crate. // The reason this happens it that various viewer crates wants to access these, mostly for ui purposes. // Ideally, they would only depend on the ones needed. +pub use encoded_depth_image_stats_cache::EncodedDepthImageStatsCache; pub use image_decode_cache::ImageDecodeCache; +pub use image_histogram_cache::{ImageHistogramCache, Rgb8Histogram}; pub use image_stats_cache::ImageStatsCache; -pub use tensor_stats_cache::TensorStatsCache; +pub use tensor_stats_cache::{TensorStatsAccessor, TensorStatsCache}; pub use transform_database_store::TransformDatabaseStoreCache; pub use video_asset_cache::VideoAssetCache; pub use video_stream_cache::{ - SharablePlayableVideoStream, VideoStreamCache, VideoStreamProcessingError, + SharablePlayableVideoStream, VideoStoreSource, VideoStreamCache, VideoStreamProcessingError, }; // ---- diff --git a/crates/viewer/re_viewer_context/src/cache/store_cache.rs b/crates/viewer/re_viewer_context/src/cache/store_cache.rs index 17f72f4b1b91..e2da3b95d8dc 100644 --- a/crates/viewer/re_viewer_context/src/cache/store_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/store_cache.rs @@ -6,7 +6,7 @@ use re_log_types::StoreId; use crate::view::visualizer_entity_subscriber::VisualizerEntitySubscriber; use crate::{ - Cache, IndicatedEntities, Memoizers, PerVisualizerType, ViewClassRegistry, + Cache, CacheEntryAccess, IndicatedEntities, Memoizers, PerVisualizerType, ViewClassRegistry, ViewSystemIdentifier, VisualizableEntities, }; @@ -125,6 +125,26 @@ impl StoreCache { self.memoizers.entry::(f) } + /// Accesses an existing memoization cache for reading. + /// + /// Returns `None` if the cache has not been created yet. + pub fn memoizer_read(&self, f: impl FnOnce(&C) -> R) -> Option { + self.memoizers.read::(f) + } + + /// Tries to read an existing memoization cache entry, then computes it through mutable access on miss. + /// + /// Use this if you're working with init-only cache entries, expect your cache entry to be usually present + /// and want to avoid the overhead of a write lock. + /// Note that this _adds_ overhead for the miss path compared to `memoizer`, so don't use this if you expect many misses! + /// (UI code typically doesn't need to care about this optimization, since it's usually single-threaded already.) + pub fn memoizer_read_or_compute(&self, key: &Key) -> Value + where + C: CacheEntryAccess + Default, + { + self.memoizers.read_or_compute::(key) + } + /// For each visualizer, return the set of entities that may be visualizable with it. pub fn visualizable_entities_for_visualizer_systems( &self, diff --git a/crates/viewer/re_viewer_context/src/cache/tensor_stats_cache.rs b/crates/viewer/re_viewer_context/src/cache/tensor_stats_cache.rs index 161deecd496d..1880d3bf3c19 100644 --- a/crates/viewer/re_viewer_context/src/cache/tensor_stats_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/tensor_stats_cache.rs @@ -7,7 +7,7 @@ use re_log_types::hash::Hash64; use re_sdk_types::archetypes::Tensor; use re_sdk_types::datatypes::TensorData; -use crate::{Cache, TensorStats}; +use crate::{Cache, CacheEntryAccess, TensorStats}; /// Caches tensor stats. /// @@ -15,10 +15,17 @@ use crate::{Cache, TensorStats}; #[derive(Default)] pub struct TensorStatsCache(HashMap); -impl TensorStatsCache { +pub struct TensorStatsAccessor<'a> { /// The `RowId` of the `TensorData` may be used as a cache key. /// NOTE: `TensorData` is never batched (they are mono-components), /// so we don't need the instance id here. + pub tensor_cache_key: Hash64, + + /// The tensor data over which we're computing stats. This is needed for the cache miss case. + pub tensor: &'a TensorData, +} + +impl TensorStatsCache { pub fn entry(&mut self, tensor_cache_key: Hash64, tensor: &TensorData) -> TensorStats { *self .0 @@ -27,6 +34,16 @@ impl TensorStatsCache { } } +impl<'a> CacheEntryAccess, TensorStats> for TensorStatsCache { + fn read(&self, key: &TensorStatsAccessor<'a>) -> Option { + self.0.get(&key.tensor_cache_key).copied() + } + + fn compute(&mut self, key: &TensorStatsAccessor<'a>) -> TensorStats { + self.entry(key.tensor_cache_key, key.tensor) + } +} + impl Cache for TensorStatsCache { fn name(&self) -> &'static str { "TensorStatsCache" diff --git a/crates/viewer/re_viewer_context/src/cache/transform_database_store.rs b/crates/viewer/re_viewer_context/src/cache/transform_database_store.rs index f5474e410e72..01b08e4bb43b 100644 --- a/crates/viewer/re_viewer_context/src/cache/transform_database_store.rs +++ b/crates/viewer/re_viewer_context/src/cache/transform_database_store.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use ahash::HashSet; use parking_lot::{ArcRwLockReadGuard, RawRwLock}; -use re_byte_size::SizeBytes; +use re_byte_size::SizeBytes as _; use re_chunk::{LatestAtQuery, TimelineName}; -use re_chunk_store::ChunkStoreEvent; +use re_chunk_store::{ChunkStoreEvent, MissingChunkReporter}; use re_entity_db::EntityDb; use re_tf::{ CachedTransformsForTimeline, FrameIdRegistry, TransformForest, TransformResolutionCache, @@ -15,7 +15,8 @@ use super::Cache; /// Stores a [`TransformResolutionCache`] for each recording. /// /// Ensures that the cache stays up to date. -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] +#[size_bytes(profile)] pub struct TransformDatabaseStoreCache { transform_cache: Option, @@ -40,6 +41,16 @@ impl TransformDatabaseStoreCache { transform_cache.frame_id_registry() } + /// Returns the registry of all known frames if it has already been initialized, or `None` if it hasn't. + #[inline] + pub fn cached_frame_id_registry( + &self, + ) -> Option> { + self.transform_cache + .as_ref() + .map(TransformResolutionCache::frame_id_registry) + } + /// Accesses the transform component tracking data for a given timeline. #[inline] pub fn transforms_for_timeline( @@ -60,6 +71,40 @@ impl TransformDatabaseStoreCache { transform_cache.transforms_for_timeline(timeline) } + /// Returns a snapshot of the transform cache for a single latest-at time. + /// + /// The snapshot contains registered frames matching the frame filter plus latest direct + /// transform edges between them. + pub fn latest_at_transform_cache_snapshot( + &mut self, + entity_db: &EntityDb, + missing_chunk_reporter: &MissingChunkReporter, + query: &LatestAtQuery, + filter: re_tf::transform_cache_snapshot::SnapshotFilter, + ) -> re_tf::transform_cache_snapshot::Snapshot { + let transform_cache = self + .transform_cache + .get_or_insert_with(|| TransformResolutionCache::new(entity_db)); + + if let Some(timeline) = query.timeline() { + // Remember that this timeline was used this frame. + self.used_timelines.insert(timeline); + + transform_cache + .ensure_timeline_is_initialized(entity_db.storage_engine().store(), timeline); + } + + let frame_id_registry = transform_cache.frame_id_registry(); + let transforms = transform_cache.transforms_for_timeline(query.timeline()); + transforms.latest_at_transform_cache_snapshot( + &frame_id_registry, + entity_db, + missing_chunk_reporter, + query, + filter, + ) + } + pub fn update_transform_forest( &mut self, entity_db: &EntityDb, @@ -85,22 +130,6 @@ impl TransformDatabaseStoreCache { } } -impl SizeBytes for TransformDatabaseStoreCache { - fn heap_size_bytes(&self) -> u64 { - re_tracing::profile_function!(); - - let Self { - transform_cache, - transform_forest, - used_timelines, - } = self; - - transform_cache.heap_size_bytes() - + transform_forest.heap_size_bytes() - + used_timelines.heap_size_bytes() - } -} - impl Cache for TransformDatabaseStoreCache { fn name(&self) -> &'static str { "TransformDatabaseStoreCache" diff --git a/crates/viewer/re_viewer_context/src/cache/video_asset_cache.rs b/crates/viewer/re_viewer_context/src/cache/video_asset_cache.rs index 56fe16d7bab0..50b5ffee9eab 100644 --- a/crates/viewer/re_viewer_context/src/cache/video_asset_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/video_asset_cache.rs @@ -19,7 +19,10 @@ use crate::image_info::StoredBlobCacheKey; // ---------------------------------------------------------------------------- +#[derive(re_byte_size::SizeBytes)] struct Entry { + // `AtomicBool` doesn't impl `SizeBytes`; it's POD (no heap). + #[size_bytes(ignore)] used_this_frame: AtomicBool, /// Keeps failed loads around, so we can don't try again and again. @@ -29,17 +32,6 @@ struct Entry { debug_name: String, } -impl re_byte_size::SizeBytes for Entry { - fn heap_size_bytes(&self) -> u64 { - let Self { - used_this_frame: _, - video, - debug_name, - } = self; - debug_name.heap_size_bytes() + video.heap_size_bytes() - } -} - /// Caches videos assets and their players based on media type & row id. #[derive(Default)] pub struct VideoAssetCache(HashMap>); @@ -83,12 +75,11 @@ impl VideoAssetCache { .or_default() .entry(inner_key) .or_insert_with(|| { + let _ = blob_row_id; // used to be the source id, now unused let video = re_video::VideoDataDescription::load_from_bytes( video_buffer, &media_type, &debug_name, - // For video assets we use the row-id as the source identifier. - blob_row_id.as_tuid(), ) .map(|data| Video::load(debug_name.clone(), data, decode_settings)); Entry { diff --git a/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs b/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs index 5b856508b3ff..69d7669f7bfe 100644 --- a/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/video_stream_cache.rs @@ -8,37 +8,100 @@ use arrow::datatypes::DataType; use egui::NumExt as _; use parking_lot::RwLock; use re_byte_size::SizeBytes as _; -use re_chunk::{ChunkId, EntityPath, Span, Timeline, TimelineName}; -use re_chunk_store::{ChunkDirectLineageReport, ChunkStoreDiff, ChunkStoreEvent}; +use re_chunk::{ChunkId, EntityPath, Span, TimelineName}; +use re_chunk_store::{ + ChunkDirectLineageReport, ChunkStoreDiff, ChunkStoreEvent, ChunkTrackingMode, +}; use re_entity_db::EntityDb; use re_log::{debug_assert, debug_panic}; use re_log_types::{EntityPathHash, TimeType}; use re_sdk_types::archetypes::VideoStream; use re_sdk_types::components; -use re_video::{DecodeSettings, SampleMetadataState, StableIndexDeque}; +use re_video::player::GetVideoSource; +use re_video::{DecodeSettings, SampleMetadataState, StableIndexDeque, VideoSource}; use crate::Cache; #[cfg(test)] mod test_player; +pub struct VideoStoreSource<'a> { + pub store: &'a re_chunk_store::ChunkStore, + pub sample_component: re_chunk::ComponentIdentifier, + + /// Should the used chunks be indicated as more potentially being downloaded? + pub indicate: bool, +} + +impl GetVideoSource for VideoStoreSource<'_> { + fn get_video_chunk(&self, source: VideoSource) -> &[u8] { + let lookup = |id: re_log_types::external::re_tuid::Tuid, + sub_id: Option| + -> Option<&[u8]> { + let chunk = if self.indicate { + self.store + .use_chunk_or_report_missing(&ChunkId::from_tuid(id)) + } else { + self.store + .use_transient_chunk_or_report_missing(&ChunkId::from_tuid(id)) + }?; + let sub_id = sub_id?; + let (offsets, buffer) = re_arrow_util::blob_arrays_offsets_and_buffer( + chunk.raw_component_array(self.sample_component)?, + )?; + let row_idx = chunk.row_index_of(re_sdk_types::RowId::from_tuid(sub_id))?; + let start = offsets[row_idx] as usize; + let end = offsets[row_idx + 1] as usize; + Some(&buffer.as_slice()[start..end]) + }; + + match source { + VideoSource::Id { id, sub_id } => lookup(id, sub_id).unwrap_or(&[]), + VideoSource::Span(_) => &[], + } + } + + fn require_video_source(&self, source: VideoSource) { + match source { + VideoSource::Id { id, sub_id: _ } => { + if self.indicate { + self.store + .use_chunk_or_report_missing(&ChunkId::from_tuid(id)); + } else { + self.store + .use_transient_chunk_or_report_missing(&ChunkId::from_tuid(id)); + } + } + VideoSource::Span(_) => {} + } + } + + fn indicate_video_source(&self, source: VideoSource) { + match source { + VideoSource::Id { id, sub_id: _ } => { + if self.indicate { + self.store.use_chunk_or_indicate(&ChunkId::from_tuid(id)); + } else { + self.store + .use_transient_chunk_or_report_missing(&ChunkId::from_tuid(id)); + } + } + VideoSource::Span(_) => {} + } + } +} + /// Video stream from the store, ready for playback. /// /// This is compromised of: /// * raw video stream data (pointers into all live rerun-chunks holding video frame data) /// * metadata with that we know about the stream (where are I-frames etc.) /// * active players for this stream and their state +#[derive(re_byte_size::SizeBytes)] pub struct PlayableVideoStream { pub video_renderer: re_renderer::video::Video, } -impl re_byte_size::SizeBytes for PlayableVideoStream { - fn heap_size_bytes(&self) -> u64 { - let Self { video_renderer } = self; - video_renderer.heap_size_bytes() - } -} - impl PlayableVideoStream { pub fn video_descr(&self) -> &re_video::VideoDataDescription { self.video_renderer.data_descr() @@ -48,46 +111,22 @@ impl PlayableVideoStream { /// Entry in the video stream cache. /// /// Keeps track of usage so we know when to remove from the cache. +#[derive(re_byte_size::SizeBytes)] struct VideoStreamCacheEntry { used_this_frame: AtomicBool, video_stream: Arc>, known_chunk_ranges: BTreeMap, } -impl re_byte_size::SizeBytes for VideoStreamCacheEntry { - fn heap_size_bytes(&self) -> u64 { - let Self { - used_this_frame: _, - video_stream, - known_chunk_ranges, - } = self; - - video_stream.read().heap_size_bytes() + known_chunk_ranges.heap_size_bytes() - } -} - /// Identifies a video stream. -#[derive(Clone, Copy, Hash, Eq, PartialEq)] +#[derive(Clone, Copy, Hash, Eq, PartialEq, re_byte_size::SizeBytes)] struct VideoStreamKey { entity_path: EntityPathHash, timeline: TimelineName, sample_component: re_chunk::ComponentIdentifier, } -impl re_byte_size::SizeBytes for VideoStreamKey { - fn heap_size_bytes(&self) -> u64 { - let Self { - entity_path, - timeline, - sample_component, - } = self; - entity_path.heap_size_bytes() - + timeline.heap_size_bytes() - + sample_component.heap_size_bytes() - } -} - /// Caches metadata and active players for video streams. /// /// It also keeps track of any additions and removals of video chunks. @@ -141,45 +180,46 @@ impl VideoStreamCache { entity_path: &EntityPath, timeline: TimelineName, decode_settings: DecodeSettings, + report_mode: ChunkTrackingMode, ) -> Result { let sample_component = VideoStream::descriptor_sample().component; let codec_component = VideoStream::descriptor_codec().component; + let query_result = store.storage_engine().cache().latest_at( + report_mode, + // Get the last logged codec. Should be unchanging so if correctly + // logged it doesn't matter which one we get. + &re_chunk::LatestAtQuery::new(timeline, re_chunk::TimeInt::MAX), + entity_path, + [codec_component], + ); + + let codec_chunk = query_result.get_required(codec_component).map_err(|_err| { + if store + .storage_engine() + .store() + .entity_has_component_on_timeline(Some(&timeline), entity_path, codec_component) + { + VideoStreamProcessingError::UnloadedCodec + } else { + VideoStreamProcessingError::MissingCodec + } + })?; + + // Translate codec by looking at the last codec. + // TODO(andreas): Should validate whether all codecs ever logged are the same, but it's a bit tedious. + let last_codec = codec_chunk + .component_mono::(codec_component) + .ok_or(VideoStreamProcessingError::MissingCodec)? + .map_err(|err| VideoStreamProcessingError::FailedReadingCodec(Box::new(err)))?; + self.entry( store, entity_path, timeline, decode_settings, sample_component, - &|| { - let query_result = store.storage_engine().cache().latest_at( - // Get the last logged codec. Should be unchanging so if correctly - // logged it doesn't matter which one we get. - &re_chunk::LatestAtQuery::new(timeline, re_chunk::TimeInt::MAX), - entity_path, - [codec_component], - ); - - let codec_chunk = query_result.get_required(codec_component).map_err(|_err| { - if store - .storage_engine() - .store() - .entity_has_component_on_timeline(&timeline, entity_path, codec_component) - { - VideoStreamProcessingError::UnloadedCodec - } else { - VideoStreamProcessingError::MissingCodec - } - })?; - - // Translate codec by looking at the last codec. - // TODO(andreas): Should validate whether all codecs ever logged are the same, but it's a bit tedious. - let last_codec = codec_chunk - .component_mono::(codec_component) - .ok_or(VideoStreamProcessingError::MissingCodec)? - .map_err(|err| VideoStreamProcessingError::FailedReadingCodec(Box::new(err)))?; - Ok(last_codec.into()) - }, + last_codec.into(), ) } @@ -196,7 +236,7 @@ impl VideoStreamCache { timeline: TimelineName, decode_settings: DecodeSettings, sample_component: re_chunk::ComponentIdentifier, - get_codec: &dyn Fn() -> Result, + new_codec: re_video::VideoCodec, ) -> Result { re_tracing::profile_function!(); @@ -207,16 +247,27 @@ impl VideoStreamCache { }; let entry = match self.entries.entry(key) { - std::collections::hash_map::Entry::Occupied(occupied_entry) => { + std::collections::hash_map::Entry::Occupied(occupied_entry) + if occupied_entry + .get() + .video_stream + .read_arc() + .video_descr() + .codec + == new_codec => + { occupied_entry.into_mut() } - std::collections::hash_map::Entry::Vacant(vacant_entry) => { + entry => { + // Reloading an existing entry on a codec change keeps the same key. + let is_new_entry = matches!(entry, std::collections::hash_map::Entry::Vacant(_)); + let (video_descr, known_chunk_ranges) = load_video_data_from_chunks( store, entity_path, timeline, sample_component, - get_codec, + new_codec, )?; let video = re_renderer::video::Video::load( @@ -225,18 +276,22 @@ impl VideoStreamCache { decode_settings, ); - self.keys_per_entity - .entry(key.entity_path) - .or_default() - .push(key); - - vacant_entry.insert(VideoStreamCacheEntry { - used_this_frame: AtomicBool::new(true), - video_stream: Arc::new(RwLock::new(PlayableVideoStream { - video_renderer: video, - })), - known_chunk_ranges, - }) + if is_new_entry { + self.keys_per_entity + .entry(key.entity_path) + .or_default() + .push(key); + } + + entry + .insert_entry(VideoStreamCacheEntry { + used_this_frame: AtomicBool::new(true), + video_stream: Arc::new(RwLock::new(PlayableVideoStream { + video_renderer: video, + })), + known_chunk_ranges, + }) + .into_mut() } }; @@ -251,7 +306,7 @@ impl VideoStreamCache { &mut self, entity_db: &EntityDb, event: &ChunkStoreEvent, - timeline: &Timeline, + timeline_name: TimelineName, key: &VideoStreamKey, ) { let Some(entry) = self.entries.get_mut(key) else { @@ -264,8 +319,6 @@ impl VideoStreamCache { let video_data = video_renderer.data_descr_mut(); video_data.delivery_method = re_video::VideoDeliveryMethod::new_stream(); - let timeline_name = *timeline.name(); - let encoding_details_before = video_data.encoding_details.clone(); let mut insertion_info: Option = None; @@ -356,7 +409,7 @@ impl VideoStreamCache { let known_ranges = &mut entry.known_chunk_ranges; handle_deletion( entity_db, - timeline, + timeline_name, video_data, &del.chunk, known_ranges, @@ -449,7 +502,7 @@ impl VideoStreamCache { /// sample list we reset this video cache entry. fn handle_deletion( entity_db: &EntityDb, - timeline: &Timeline, + timeline: TimelineName, video_data: &mut re_video::VideoDataDescription, deleted_chunk: &re_chunk::Chunk, known_ranges: &mut BTreeMap, @@ -469,7 +522,7 @@ fn handle_deletion( .manifest() .and_then(|manifest| manifest.temporal_map().get(deleted_chunk.entity_path())) .and_then(|per_timeline| { - let per_component = per_timeline.get(timeline)?; + let (_, per_component) = per_timeline.iter().find(|(t, _)| *t.name() == timeline)?; per_component.get(&sample_component) }) .unwrap_or(&tmp); @@ -493,7 +546,7 @@ fn handle_deletion( video_data.samples.min_index(), ), |(mut count, mut other_min, mut other_max), (idx, sample)| { - if sample.source_id() == deleted_chunk.id().as_tuid() { + if sample.source_primary_id() == Some(deleted_chunk.id().as_tuid()) { count += 1; } else { other_min = other_min.min(idx); @@ -570,7 +623,7 @@ fn handle_deletion( break 'outer; }; - if sample.source_id() != deleted_chunk.id().as_tuid() { + if sample.source_primary_id() != Some(deleted_chunk.id().as_tuid()) { continue; } @@ -650,7 +703,7 @@ fn handle_compacted_chunk_addition( video_data .samples .iter_index_range_clamped_mut(&range.idx_range()) - .filter(|(_, s)| s.source_id() == chunk.id().as_tuid()) + .filter(|(_, s)| s.source_primary_id() == Some(chunk.id().as_tuid())) .find(|(_, s)| s.is_unloaded()) .map(|(idx, _)| idx) }) @@ -684,11 +737,11 @@ fn handle_compacted_chunk_addition( for (idx, sample) in video_data .samples .iter_index_range_clamped_mut(&range.idx_range()) - .filter(|(_, s)| s.source_id() == reused_chunk.id().as_tuid()) + .filter(|(_, s)| s.source_primary_id() == Some(reused_chunk.id().as_tuid())) { update_min_max(idx); - *sample.source_id_mut() = compacted_chunk.id().as_tuid(); + sample.set_source_primary_id(compacted_chunk.id().as_tuid()); } } @@ -702,7 +755,7 @@ fn handle_compacted_chunk_addition( for mut sample in chunk_samples.samples { // Use the compacted chunk's source_id so that `read_samples_from_known_chunk` // can find these samples when filtering by the compacted chunk's id. - *sample.source_id_mut() = compacted_chunk.id().as_tuid(); + sample.set_source_primary_id(compacted_chunk.id().as_tuid()); let idx = video_data.samples.next_index(); @@ -779,11 +832,10 @@ fn handle_split_chunk_addition( let mut samples = video_data .samples .iter_index_range_clamped_mut(&old_known_range.idx_range()) - .filter(|(_, s)| s.source_id() == original_chunk.id().as_tuid()); + .filter(|(_, s)| s.source_primary_id() == Some(original_chunk.id().as_tuid())); flatten_chunk_samples( - std::iter::once(split_chunk) - .chain(siblings.iter().map(|c| &**c)) + std::iter::chain(std::iter::once(split_chunk), siblings.iter().map(|c| &**c)) .filter_map(|chunk| ChunkSamples::from_physical(chunk, timeline, sample_component)) .collect(), known_chunk_ranges, @@ -837,7 +889,7 @@ fn load_video_data_from_chunks( entity_path: &EntityPath, timeline: TimelineName, sample_component: re_chunk::ComponentIdentifier, - get_codec: &dyn Fn() -> Result, + codec: re_video::VideoCodec, ) -> Result< ( re_video::VideoDataDescription, @@ -847,8 +899,6 @@ fn load_video_data_from_chunks( > { re_tracing::profile_function!(); - let codec = get_codec()?; - // Query for all video chunks on the **entire** timeline. // Tempting to bypass the query cache for this, but we don't expect to get new video chunks every frame // even for a running stream, so let's stick with the cache! @@ -858,6 +908,8 @@ fn load_video_data_from_chunks( let entire_timeline_query = re_chunk::RangeQuery::new(timeline, re_log_types::AbsoluteTimeRange::EVERYTHING); let query_results = store.storage_engine().cache().range( + // Ignore, since the video player will handle requesting sample chunks. + ChunkTrackingMode::Ignore, &entire_timeline_query, entity_path, [sample_component], @@ -944,7 +996,7 @@ fn timescale_for_timeline( /// This is the inclusive range all samples of the chunk is in. But there /// may also be samples from other chunks in this range. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, re_byte_size::SizeBytes)] struct ChunkSampleRange { first_sample: re_video::SampleIndex, @@ -1002,12 +1054,6 @@ impl ChunkSampleRange { } } -impl re_byte_size::SizeBytes for ChunkSampleRange { - fn heap_size_bytes(&self) -> u64 { - 0 - } -} - /// Reads video samples from a chunk that already has allocated samples /// in the video description. /// @@ -1052,20 +1098,30 @@ fn read_samples_from_known_chunk( let mut samples_iter = samples .iter_index_range_clamped_mut(&load_range.idx_range()) - .filter(|(_, c)| c.source_id() == chunk.id().as_tuid()); - - for (component_offset, (time, _row_id)) in chunk - .iter_component_offsets(sample_component) - .zip(chunk.iter_component_indices(timeline, sample_component)) - .filter(|(component_offset, _)| component_offset.len > 0) - // Iterate over the relevant range. - .skip( - known_range - .sample_count - .saturating_sub(load_range.sample_count), + .filter(|(_, c)| c.source_primary_id() == Some(chunk.id().as_tuid())); + + let rows = std::iter::zip( + chunk.iter_component_offsets(sample_component), + chunk.iter_component_indices(timeline, sample_component), + ) + .filter(|(component_offset, _)| component_offset.len > 0); + + // A static chunk holds a single current value, which is the row with the highest `RowId`. + // So we keep only that row and drop the rest. + let rows = if chunk.is_static() { + itertools::Either::Left(rows.max_by_key(|(_, (_, row_id))| *row_id).into_iter()) + } else { + itertools::Either::Right( + rows.skip( + known_range + .sample_count + .saturating_sub(load_range.sample_count), + ) + .take(load_range.sample_count), ) - .take(load_range.sample_count) - { + }; + + for (component_offset, (time, row_id)) in rows { if component_offset.len != 1 { re_log::warn_once!( "Expected only a single VideoSample per row (it is a mono-component)" @@ -1081,16 +1137,14 @@ fn read_samples_from_known_chunk( // Do **not** use the `component_offset.start` for determining the sample index // as it is only for the offset in the underlying arrow arrays which means that // it may in theory step arbitrarily through the data. - let byte_span = Span { + // We only use `buffer_span` to peek at the sample bytes for keyframe + // detection; the player resolves the bytes from `(chunk_id, row_id)` at + // decode time, so we don't need to store any byte offset here. + let buffer_span = Span { start: offsets[component_offset.start] as usize, len: lengths[component_offset.start], }; - let sample_bytes = &values[byte_span.range()]; - - let Some(byte_span) = byte_span.try_cast::() else { - re_log::warn_once!("Video byte range does not fit in u32: {byte_span:?}"); - continue; - }; + let sample_bytes = &values[buffer_span.range()]; // Note that the conversion of this time value is already handled by `VideoDataDescription::timescale`: // For sequence time we use a scale of 1, for nanoseconds time we use a scale of 1_000_000_000. @@ -1113,8 +1167,7 @@ fn read_samples_from_known_chunk( // Filled out later for everything but the last frame. duration: None, - source_id: chunk.id().as_tuid(), - byte_span, + source: re_video::VideoSource::id(chunk.id().as_tuid(), row_id.as_tuid()), }); } @@ -1122,7 +1175,7 @@ fn read_samples_from_known_chunk( let _samples_iter = samples_iter; re_log::debug_assert_eq!( _samples_iter - .map(|(idx, s)| (idx, s.source_id())) + .map(|(idx, s)| (idx, s.source_primary_id())) .collect::>() .as_slice(), &[], @@ -1302,6 +1355,9 @@ fn read_samples_from_new_chunk( return Err(VideoStreamProcessingError::OutOfOrderSamples); } } + None if chunk.is_static() => { + // Static chunks have no timeline, so there's no insertion ordering to validate. + } None => { // This chunk doesn't have any data on this timeline. return Ok(()); @@ -1317,39 +1373,45 @@ fn read_samples_from_new_chunk( let sample_base_idx = samples.next_index(); let chunk_id = chunk.id(); + + let rows = std::iter::zip( + chunk.iter_component_offsets(sample_component), + chunk.iter_component_indices(timeline, sample_component), + ) + .filter(|(component_offset, _)| { + if component_offset.len != 1 { + re_log::warn_once!("Expected only a single sample per row (it is a mono-component)"); + return false; + } + + component_offset.len > 0 + }); + + // A static chunk holds a single current value, which is the row with the highest `RowId`. + // So we keep only that row and drop the rest. + let rows = if chunk.is_static() { + itertools::Either::Left(rows.max_by_key(|(_, (_, row_id))| *row_id).into_iter()) + } else { + itertools::Either::Right(rows) + }; + // Extract sample metadata. samples.extend( - chunk - .iter_component_offsets(sample_component) - .zip(chunk.iter_component_indices(timeline, sample_component)) - .enumerate() - .filter_map(move |(idx, (component_offset, (time, _row_id)))| { - if component_offset.len == 0 { - // Ignore empty samples. - return None; - } - if component_offset.len != 1 { - re_log::warn_once!( - "Expected only a single VideoSample per row (it is a mono-component)" - ); - return None; - } - + rows.enumerate() + .map(move |(idx, (component_offset, (time, row_id)))| { // Do **not** use the `component_offset.start` for determining the sample index // as it is only for the offset in the underlying arrow arrays which means that // it may in theory step arbitrarily through the data. let sample_idx = sample_base_idx + idx; - let byte_span = Span { + // Peek at the sample bytes for keyframe detection. The player + // resolves the bytes from `(chunk_id, row_id)` at decode time, + // so we don't need to store any byte offset here. + let buffer_span = Span { start: offsets[component_offset.start] as usize, len: lengths[component_offset.start], }; - let sample_bytes = &values[byte_span.range()]; - - let Some(byte_span) = byte_span.try_cast::() else { - re_log::warn_once!("Video byte range does not fit in u32: {byte_span:?}"); - return None; - }; + let sample_bytes = &values[buffer_span.range()]; // Note that the conversion of this time value is already handled by `VideoDataDescription::timescale`: // For sequence time we use a scale of 1, for nanoseconds time we use a scale of 1_000_000_000. @@ -1369,7 +1431,7 @@ fn read_samples_from_new_chunk( keyframe_indices.push(sample_idx); } - Some(SampleMetadataState::Present(re_video::SampleMetadata { + SampleMetadataState::Present(re_video::SampleMetadata { is_sync, // TODO(#10090): No b-frames for now. Therefore sample_idx == frame_nr. @@ -1380,9 +1442,8 @@ fn read_samples_from_new_chunk( // Filled out later for everything but the last frame. duration: None, - source_id: chunk_id.as_tuid(), - byte_span, - })) + source: re_video::VideoSource::id(chunk_id.as_tuid(), row_id.as_tuid()), + }) }), ); @@ -1464,13 +1525,14 @@ impl Cache for VideoStreamCache { continue; } - let Some(col) = delta_chunk.timelines().get(&key.timeline) else { + // Static chunks carry no timeline column but belong to every timeline, so only + // skip a temporal chunk that doesn't touch this stream's timeline. + if !delta_chunk.is_static() && !delta_chunk.timelines().contains_key(&key.timeline) + { continue; - }; - - let timeline = col.timeline(); + } - self.handle_store_event(entity_db, event, timeline, &key); + self.handle_store_event(entity_db, event, key.timeline, &key); } } } @@ -1500,24 +1562,44 @@ impl ChunkSamples { timeline: TimelineName, sample_component: re_chunk::ComponentIdentifier, ) -> Option { - let mut samples: Vec<_> = chunk - .iter_component_timepoints(sample_component) - .filter_map(|t| t.get(&timeline)) - .map(|time| SampleMetadataState::Unloaded { - source_id: chunk.id().as_tuid(), - min_dts: re_video::Time::new(time.get()), + if chunk.is_static() { + Some(Self { + samples: std::iter::once(SampleMetadataState::Unloaded { + source_id: chunk.id().as_tuid(), + min_dts: re_video::Time::ZERO, + }) + .collect(), }) - .collect(); + } else { + let mut samples: Vec<_> = chunk + .iter_component_timepoints(sample_component) + .filter_map(|t| t.get(&timeline)) + .map(|time| SampleMetadataState::Unloaded { + source_id: chunk.id().as_tuid(), + min_dts: re_video::Time::new(time.get()), + }) + .collect(); - if samples.is_empty() { - return None; - } + if samples.is_empty() { + return None; + } - samples.sort_by_key(|s| s.decode_timestamp()); + samples.sort_by_key(|s| s.decode_timestamp()); - Some(Self { - samples: VecDeque::from(samples), - }) + Some(Self { + samples: VecDeque::from(samples), + }) + } + } + + fn from_static_root(id: ChunkId) -> Self { + Self { + samples: vec![SampleMetadataState::Unloaded { + source_id: id.as_tuid(), + min_dts: re_video::Time::ZERO, + }] + .into(), + } } /// Create new chunk samples from a [`re_log_encoding::RrdManifestTemporalMapEntry`]. @@ -1526,7 +1608,7 @@ impl ChunkSamples { // TODO(isse): Since samples could potentially be anywhere. We could // get into a situation where a chunk has a sample that should be in // a certain gop, but doesn't get distributed there by this method. - fn from_root( + fn from_temporal_root( id: ChunkId, entry: &re_log_encoding::RrdManifestTemporalMapEntry, ) -> Option { @@ -1602,11 +1684,9 @@ impl ChunkSampleIterators { "We make sure to never keep empty queues around here" ); - if !f(next.samples.front()?.decode_timestamp()) { - return None; - } - - let sample = next.samples.pop_front()?; + let sample = next + .samples + .pop_front_if(|sample| f(sample.decode_timestamp()))?; // Don't keep empty queues around. if next.samples.is_empty() { @@ -1623,12 +1703,13 @@ impl ChunkSampleIterators { mut handle_sample: impl FnMut(SampleMetadataState) -> usize, ) { while let Some(sample) = self.next_if(&predicate) { - let id = sample.source_id(); + let chunk_id = sample.source_primary_id(); // This is loaded, but will be populated later. let idx = handle_sample(sample); + let Some(chunk_id) = chunk_id else { continue }; known_chunk_ranges - .entry(ChunkId::from_tuid(id)) + .entry(ChunkId::from_tuid(chunk_id)) .and_modify(|range| { range.add_sample(idx); }) @@ -1681,14 +1762,32 @@ fn load_known_chunk_ranges( ) { re_tracing::profile_function!(); - let chunks_from_manifest = if let Some(manifest) = store.rrd_manifest_index().manifest() - && let Some(entity_timelines) = manifest.temporal_map().get(entity_path) - && let Some((_, components)) = entity_timelines.iter().find(|(t, _)| *t.name() == timeline) - && let Some(chunks) = components.get(&sample_component) + let dummy_chunks = BTreeMap::new(); + let (static_chunk_from_manifest, temporal_chunks_from_manifest) = if let Some(manifest) = + store.rrd_manifest_index().manifest() { - chunks + let static_chunk = if let Some(entity_components) = manifest.static_map().get(entity_path) + && let Some(c) = entity_components.get(&sample_component) + { + Some(*c) + } else { + None + }; + + let temporal_chunks = if let Some(entity_timelines) = + manifest.temporal_map().get(entity_path) + && let Some((_, components)) = + entity_timelines.iter().find(|(t, _)| *t.name() == timeline) + && let Some(chunks) = components.get(&sample_component) + { + chunks + } else { + &dummy_chunks + }; + + (static_chunk, temporal_chunks) } else { - &BTreeMap::new() + (None, &dummy_chunks) }; let storage_engine = store.storage_engine(); @@ -1709,28 +1808,31 @@ fn load_known_chunk_ranges( } // Sorted iterator over all chunks we're going to keep track of ranges for. - let chunk_timepoints: Vec = chunks_from_manifest - .iter() - .filter_map(|(id, entry)| { - let loaded = loaded_chunks_counts.get(id).copied().unwrap_or(0); - let remaining = entry.num_rows.saturating_sub(loaded); - if remaining == 0 { - return None; - } - ChunkSamples::from_root( - *id, - &re_log_encoding::RrdManifestTemporalMapEntry { - num_rows: remaining, - ..*entry - }, - ) - }) - .chain( - loaded_chunks - .iter() - .filter_map(|c| ChunkSamples::from_physical(c, timeline, sample_component)), - ) - .collect(); + let chunk_timepoints: Vec = itertools::chain!( + static_chunk_from_manifest + .filter(|id| loaded_chunks_counts.get(id).copied().unwrap_or(0) == 0) + .map(ChunkSamples::from_static_root), + temporal_chunks_from_manifest + .iter() + .filter_map(|(id, entry)| { + let loaded = loaded_chunks_counts.get(id).copied().unwrap_or(0); + let remaining = entry.num_rows.saturating_sub(loaded); + if remaining == 0 { + return None; + } + ChunkSamples::from_temporal_root( + *id, + &re_log_encoding::RrdManifestTemporalMapEntry { + num_rows: remaining, + ..*entry + }, + ) + }), + loaded_chunks + .iter() + .filter_map(|c| ChunkSamples::from_physical(c, timeline, sample_component)), + ) + .collect(); flatten_chunk_samples(chunk_timepoints, known_chunk_ranges, |sample| { let idx = data_descr.samples.next_index(); @@ -1784,7 +1886,7 @@ fn find_affected_sample_range( .position(|idx| { video_descr.samples.get(*idx).is_some_and(|s| { // Don't trust timepoints of the conflicting chunk. - s.source_id() == conflicting_chunk.id().as_tuid() + s.source_primary_id() == Some(conflicting_chunk.id().as_tuid()) || s.decode_timestamp() > affected_range_min }) }) @@ -1807,7 +1909,7 @@ fn find_affected_sample_range( // Find the index range we have to re-order. for (idx, sample) in video_descr.samples.iter_index_range_clamped(&range) { // Skip the conflicting samples if there are any. - if sample.source_id() == conflicting_chunk.id().as_tuid() { + if sample.source_primary_id() == Some(conflicting_chunk.id().as_tuid()) { if start_sample.is_none() { start_sample = Some(idx); } @@ -1894,7 +1996,10 @@ fn handle_out_of_order_chunk( .samples .iter_index_range_clamped(&(*sample_range.start()..sample_range.end() + 1)) { - let id = ChunkId::from_tuid(sample.source_id()); + let Some(primary_id) = sample.source_primary_id() else { + continue; + }; + let id = ChunkId::from_tuid(primary_id); // Skip samples from the conflicting chunk. They'll be added again // via `conflicting_chunk_samples` which has the latest data. @@ -1927,9 +2032,11 @@ fn handle_out_of_order_chunk( let mut new_samples = Vec::new(); flatten_chunk_samples( - std::iter::once(conflicting_chunk_samples) - .chain(chunk_samples.into_values()) - .collect(), + std::iter::chain( + std::iter::once(conflicting_chunk_samples), + chunk_samples.into_values(), + ) + .collect(), known_ranges, |sample| { let idx = *sample_range.start() + new_samples.len(); @@ -2226,6 +2333,7 @@ mod tests { &"vid".into(), *timeline.name(), DecodeSettings::default(), + ChunkTrackingMode::Report, ) .unwrap(); let video_stream = video_stream_lock.read(); @@ -2261,6 +2369,7 @@ mod tests { &"vid".into(), *timeline.name(), DecodeSettings::default(), + ChunkTrackingMode::Report, ) .unwrap(); let video_stream = video_stream_lock.read(); @@ -2304,6 +2413,7 @@ mod tests { &"vid".into(), *timeline.name(), DecodeSettings::default(), + ChunkTrackingMode::Report, ) .unwrap(); validate_stream_from_test_data(&video_stream.read(), 1); @@ -2328,6 +2438,7 @@ mod tests { &"vid".into(), *timeline.name(), DecodeSettings::default(), + ChunkTrackingMode::Report, ) .unwrap(); validate_stream_from_test_data(&video_stream.read(), t as usize + 1); @@ -2364,6 +2475,7 @@ mod tests { &"vid".into(), *timeline.name(), DecodeSettings::default(), + ChunkTrackingMode::Report, ) .unwrap(); @@ -2391,6 +2503,7 @@ mod tests { &"vid".into(), *timeline.name(), DecodeSettings::default(), + ChunkTrackingMode::Report, ) .unwrap(); let video_stream = video_stream_lock.read(); @@ -2405,4 +2518,121 @@ mod tests { ); assert_eq!(data_descr.keyframe_indices.first(), Some(&10)); } + + /// A single statically-logged encoded image should produce a valid one-sample + /// video description. + #[test] + fn video_stream_cache_from_single_static_encoded_image() { + let jpeg_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../tests/assets/image/grinda.jpg" + ); + let jpeg_data = std::fs::read(jpeg_path).unwrap(); + + let mut cache = VideoStreamCache::default(); + let mut store = re_entity_db::EntityDb::new(StoreId::random( + re_log_types::StoreKind::Recording, + "test_app", + )); + let timeline = Timeline::new_sequence("frame"); + + // Statically logged image => static chunk (empty timepoint). + let chunk = ChunkBuilder::new(ChunkId::new(), "img".into()) + .with_archetype_auto_row( + TimePoint::default(), + &re_sdk_types::archetypes::EncodedImage::new(jpeg_data) + .with_media_type("image/jpeg"), + ) + .build() + .unwrap(); + assert!(chunk.is_static()); + store.add_chunk(&Arc::new(chunk)).unwrap(); + + let video_stream_lock = cache + .entry( + &store, + &"img".into(), + *timeline.name(), + DecodeSettings::default(), + re_sdk_types::archetypes::EncodedImage::descriptor_blob().component, + re_video::VideoCodec::ImageSequence(Some("image/jpeg".to_owned())), + ) + .unwrap(); + let video_stream = video_stream_lock.read(); + + let data_descr = video_stream.video_renderer.data_descr(); + data_descr.sanity_check().unwrap(); + + assert_eq!( + data_descr.codec, + re_video::VideoCodec::ImageSequence(Some("image/jpeg".to_owned())) + ); + + // Exactly one sample, and it's a keyframe (images are always keyframes). + assert_eq!(data_descr.samples.num_elements(), 1); + assert_eq!( + data_descr.keyframe_indices, + vec![data_descr.samples.min_index()] + ); + + let encoding_details = data_descr.encoding_details.clone().unwrap(); + assert_eq!(encoding_details.codec_string, "image/jpeg"); + assert_eq!(encoding_details.coded_dimensions, [640, 480]); + } + + /// A static chunk with several rows holds a single current value: the row with the highest + /// `RowId`. The emitted sample should point at that row, not at an earlier shadowed one. + #[test] + fn video_stream_cache_static_picks_highest_row_id() { + let jpeg_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../tests/assets/image/grinda.jpg" + ); + let jpeg_data = std::fs::read(jpeg_path).unwrap(); + let image = + re_sdk_types::archetypes::EncodedImage::new(jpeg_data).with_media_type("image/jpeg"); + + let mut cache = VideoStreamCache::default(); + let mut store = re_entity_db::EntityDb::new(StoreId::random( + re_log_types::StoreKind::Recording, + "test_app", + )); + let timeline = Timeline::new_sequence("frame"); + + // Two statically logged rows. The second one has the higher `RowId` and shadows the first. + let low_row_id = RowId::new(); + let high_row_id = RowId::new(); + assert!(high_row_id > low_row_id); + + let chunk = ChunkBuilder::new(ChunkId::new(), "img".into()) + .with_archetype(low_row_id, TimePoint::default(), &image) + .with_archetype(high_row_id, TimePoint::default(), &image) + .build() + .unwrap(); + assert!(chunk.is_static()); + store.add_chunk(&Arc::new(chunk)).unwrap(); + + let video_stream_lock = cache + .entry( + &store, + &"img".into(), + *timeline.name(), + DecodeSettings::default(), + re_sdk_types::archetypes::EncodedImage::descriptor_blob().component, + re_video::VideoCodec::ImageSequence(Some("image/jpeg".to_owned())), + ) + .unwrap(); + let video_stream = video_stream_lock.read(); + + let data_descr = video_stream.video_renderer.data_descr(); + data_descr.sanity_check().unwrap(); + + // Exactly one sample, sourced from the highest-`RowId` row. + assert_eq!(data_descr.samples.num_elements(), 1); + let sample = data_descr.samples.iter().next().unwrap().sample().unwrap(); + let re_video::VideoSource::Id { sub_id, .. } = sample.source else { + panic!("expected an id-based video source"); + }; + assert_eq!(sub_id, Some(high_row_id.as_tuid())); + } } diff --git a/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player.rs b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player.rs deleted file mode 100644 index 3c3a103eb383..000000000000 --- a/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player.rs +++ /dev/null @@ -1,1504 +0,0 @@ -use std::{iter::once, ops::Range, sync::Arc}; - -use crossbeam::channel::{Receiver, Sender}; -use re_chunk::{Chunk, RowId, TimeInt, Timeline}; -use re_entity_db::EntityDb; -use re_log_types::{AbsoluteTimeRange, StoreId, external::re_tuid::Tuid}; -use re_sdk_types::{archetypes::VideoStream, components::VideoCodec}; -use re_video::player::{VideoPlayer, VideoPlayerError, VideoSampleDecoder}; -use re_video::{ - AV1_TEST_INTER_FRAME, AV1_TEST_KEYFRAME, AsyncDecoder, SampleIndex, SampleMetadataState, Time, - VideoDataDescription, -}; - -use crate::{ - Cache as _, SharablePlayableVideoStream, VideoStreamCache, VideoStreamProcessingError, -}; - -struct TestDecoder { - sender: re_video::Sender>, - sample_tx: Sender, - min_num_samples_to_enqueue_ahead: usize, -} - -impl AsyncDecoder for TestDecoder { - fn submit_chunk(&mut self, chunk: re_video::Chunk) -> re_video::DecodeResult<()> { - re_quota_channel::send_crossbeam(&self.sample_tx, chunk.sample_idx).unwrap(); - - self.sender - .send(Ok(re_video::Frame { - content: re_video::FrameContent { - data: Vec::new(), - width: 0, - height: 0, - format: re_video::PixelFormat::Rgb8Unorm, - }, - info: re_video::FrameInfo { - is_sync: Some(chunk.is_sync), - sample_idx: Some(chunk.sample_idx), - frame_nr: Some(chunk.frame_nr), - presentation_timestamp: chunk.presentation_timestamp, - duration: chunk.duration, - latest_decode_timestamp: Some(chunk.decode_timestamp), - }, - })) - .unwrap(); - - Ok(()) - } - - fn reset( - &mut self, - _video_descr: &re_video::VideoDataDescription, - ) -> re_video::DecodeResult<()> { - Ok(()) - } - - fn min_num_samples_to_enqueue_ahead(&self) -> usize { - self.min_num_samples_to_enqueue_ahead - } -} - -struct TestVideoPlayer { - video: VideoPlayer<()>, - sample_rx: Receiver, - video_descr: VideoDataDescription, - video_descr_source: Option VideoDataDescription>>, - time: f64, -} - -impl TestVideoPlayer { - fn from_descr(video_descr: VideoDataDescription) -> Self { - #![expect(clippy::disallowed_methods)] // it's a test - let (sample_tx, sample_rx) = crossbeam::channel::unbounded(); - let video = VideoPlayer::new_with_decoder( - VideoSampleDecoder::new("test_decoder".to_owned(), |sender| { - Ok(Box::new(TestDecoder { - sample_tx, - min_num_samples_to_enqueue_ahead: 2, - sender, - })) - }) - .unwrap(), - ); - - Self { - video, - sample_rx, - video_descr, - video_descr_source: None, - time: 0.0, - } - } - - fn from_stream(stream: SharablePlayableVideoStream) -> Self { - let video_descr_source = - Box::new(move || stream.read_arc().video_renderer.data_descr().clone()); - let mut this = Self::from_descr(video_descr_source()); - - this.video_descr_source = Some(video_descr_source); - - this - } - - fn play(&mut self, range: Range, time_step: f64) -> Result<(), VideoPlayerError> { - self.play_with_buffer(range, time_step, &|_| &[]) - } - - fn play_with_buffer<'a>( - &mut self, - range: Range, - time_step: f64, - get_buffer: &dyn Fn(Tuid) -> &'a [u8], - ) -> Result<(), VideoPlayerError> { - if let Some(source) = &self.video_descr_source { - self.video_descr = source(); - } - self.time = range.start; - for i in 0..((range.end - self.time) / time_step).next_down().floor() as i32 { - let time = self.time + i as f64 * time_step; - self.frame_at(time, get_buffer)?; - } - - self.time = range.end; - - Ok(()) - } - - fn frame_at<'a>( - &mut self, - time: f64, - get_buffer: &dyn Fn(Tuid) -> &'a [u8], - ) -> Result<(), VideoPlayerError> { - self.video.frame_at( - Time::from_secs(time, re_video::Timescale::NANOSECOND), - &self.video_descr, - &mut |(), _| Ok(()), - get_buffer, - )?; - - Ok(()) - } - - #[track_caller] - fn expect_decoded_samples(&self, samples: impl IntoIterator) { - let received = self.sample_rx.try_iter().collect::>(); - let expected = samples.into_iter().collect::>(); - - if let Some((e, r)) = expected.iter().zip(received.iter()).find(|(a, b)| a != b) { - panic!( - " Expected: {expected:?}\n Received: {received:?}\nFirst Issue: expected {e}, got {r}" - ); - } - } - - fn set_sample(&mut self, idx: SampleIndex, mut sample: SampleMetadataState) { - if let Some(sample) = sample.sample_mut() { - sample.frame_nr = idx as u32; - sample.decode_timestamp = re_video::Time::from_secs( - sample - .decode_timestamp - .into_secs(re_video::Timescale::NANOSECOND) - - 0.1, - re_video::Timescale::NANOSECOND, - ); - } - - match ( - self.video_descr.samples[idx] - .sample() - .is_some_and(|s| s.is_sync), - sample.sample().is_some_and(|s| s.is_sync), - ) { - (false, false) | (true, true) => {} - - (true, false) => { - let keyframe_idx = self - .video_descr - .keyframe_indices - .partition_point(|i| *i < idx); - - self.video_descr.keyframe_indices.remove(keyframe_idx); - } - - (false, true) => { - let keyframe_idx = self - .video_descr - .keyframe_indices - .partition_point(|i| *i < idx); - - self.video_descr.keyframe_indices.insert(keyframe_idx, idx); - } - } - self.video_descr.samples[idx] = sample; - - super::update_sample_durations(idx..idx + 1, &mut self.video_descr.samples).unwrap(); - } -} - -fn unloaded(time: f64) -> SampleMetadataState { - let time = Time::from_secs(time, re_video::Timescale::NANOSECOND); - SampleMetadataState::Unloaded { - source_id: Tuid::new(), - min_dts: time, - } -} - -/// An inter frame -fn frame(time: f64) -> SampleMetadataState { - let time = Time::from_secs(time, re_video::Timescale::NANOSECOND); - SampleMetadataState::Present(re_video::SampleMetadata { - is_sync: false, - decode_timestamp: time, - presentation_timestamp: time, - - // Assigned later. - frame_nr: 0, - duration: None, - - // Not relevant for these tests. - source_id: Tuid::new(), - byte_span: re_video::Span { start: 0, len: 0 }, - }) -} - -fn keyframe(time: f64) -> SampleMetadataState { - let time = Time::from_secs(time, re_video::Timescale::NANOSECOND); - SampleMetadataState::Present(re_video::SampleMetadata { - is_sync: true, - decode_timestamp: time, - presentation_timestamp: time, - - // Assigned later. - frame_nr: 0, - duration: None, - - // Not relevant for these tests. - source_id: Tuid::new(), - byte_span: re_video::Span { start: 0, len: 0 }, - }) -} - -fn create_video( - samples: impl IntoIterator, -) -> Result { - let mut samples: re_video::StableIndexDeque = - samples.into_iter().collect(); - - let mut keyframe_indices = Vec::new(); - for (idx, sample) in samples.iter_indexed_mut() { - if let Some(sample) = sample.sample_mut() { - sample.frame_nr = idx as u32; - sample.decode_timestamp = re_video::Time::from_secs( - sample - .decode_timestamp - .into_secs(re_video::Timescale::NANOSECOND) - - 0.1, - re_video::Timescale::NANOSECOND, - ); - if sample.is_sync { - keyframe_indices.push(idx); - } - } - } - - super::update_sample_durations(0..samples.next_index(), &mut samples)?; - - let video_descr = VideoDataDescription { - delivery_method: re_video::VideoDeliveryMethod::Stream { - last_time_updated_samples: std::time::Instant::now(), - }, - keyframe_indices, - samples_statistics: re_video::SamplesStatistics::new(&samples), - samples, - - // Unused for these tests. - codec: re_video::VideoCodec::H265, - encoding_details: None, - mp4_tracks: Default::default(), - timescale: None, - }; - - Ok(TestVideoPlayer::from_descr(video_descr)) -} - -fn test_simple_video(mut video: TestVideoPlayer, count: usize, dt: f64, max_time: f64) { - re_log::setup_logging(); - - video.play(0.0..max_time, dt).unwrap(); - - video.expect_decoded_samples(0..count); - - // try again at 0.5x speed. - - video.play(0.0..max_time, dt * 0.5).unwrap(); - - video.expect_decoded_samples(0..count); - - // and at 2x speed. - - video.play(0.0..max_time, dt * 2.0).unwrap(); - - video.expect_decoded_samples(0..count); -} - -#[test] -fn player_all_keyframes() { - let count = 10; - let dt = 0.1; - let max_time = count as f64 * dt; - let video = create_video((0..count).map(|t| keyframe(t as f64 * dt))).unwrap(); - - test_simple_video(video, count, dt, max_time); -} - -#[test] -fn player_one_keyframe() { - let count = 10; - let dt = 0.1; - let max_time = count as f64 * dt; - let video = - create_video(once(keyframe(0.0)).chain((1..count).map(|t| frame(t as f64 * dt)))).unwrap(); - - test_simple_video(video, count, dt, max_time); -} - -#[test] -fn player_keyframes_then_frames() { - let count = 50usize; - let keyframe_range_size = 10; - let dt = 0.1; - let max_time = count as f64 * dt; - let video = create_video((0..count).map(|t| { - let time = t as f64 * dt; - if t.is_multiple_of(keyframe_range_size) { - keyframe(time) - } else { - frame(time) - } - })) - .unwrap(); - - test_simple_video(video, count, dt, max_time); -} - -#[test] -fn player_irregular() { - let samples = [ - keyframe(0.0), - keyframe(0.1), - frame(0.11), - frame(0.12), - frame(0.125), - frame(0.13), - keyframe(1.0), - frame(2.0), - frame(50.0), - keyframe(1000.0), - keyframe(2000.0), - frame(2001.0), - frame(2201.0), - frame(2221.0), - ]; - let count = samples.len(); - let video = create_video(samples).unwrap(); - - test_simple_video(video, count, 0.1, 2500.0); -} - -#[test] -fn player_unsorted() { - let samples = [keyframe(0.0), keyframe(1.0), keyframe(2.0), keyframe(1.0)]; - let Err(err) = create_video(samples) else { - panic!("Video creation shouldn't succeed for unordered samples"); - }; - - assert!( - matches!(err, VideoStreamProcessingError::OutOfOrderSamples), - "Expected {} got {err}", - VideoStreamProcessingError::OutOfOrderSamples - ); -} - -#[track_caller] -fn assert_loading(err: Result<(), VideoPlayerError>) { - let err = err.unwrap_err(); - assert!( - matches!(err, VideoPlayerError::UnloadedSampleData(_)), - "Expected 'VideoPlayerError::UnloadedSampleData(_)' got '{err}'", - ); -} - -#[test] -fn player_with_unloaded() { - let mut video = create_video([ - keyframe(0.), - frame(1.), - frame(2.), - frame(3.), - unloaded(4.), - unloaded(5.), - unloaded(6.), - unloaded(7.), - keyframe(8.), - frame(9.), - frame(10.), - frame(11.), - keyframe(12.), - frame(13.), - frame(14.), - frame(15.), - unloaded(16.), - unloaded(17.), - unloaded(18.), - unloaded(19.), - keyframe(20.), - frame(21.), - frame(22.), - frame(23.), - ]) - .unwrap(); - - video.play(0.0..3.0, 1.0).unwrap(); - video.expect_decoded_samples(0..3); - - assert_loading(video.play(4.0..8.0, 1.0)); - video.expect_decoded_samples(None); - - video.play(8.0..15.0, 1.0).unwrap(); - video.expect_decoded_samples(8..15); - - video.play(20.0..24.0, 1.0).unwrap(); - video.expect_decoded_samples(20..24); - - // Play & load progressively - video.play(0.0..3.0, 1.0).unwrap(); - - video.set_sample(4, keyframe(4.)); - video.set_sample(5, frame(5.)); - video.set_sample(6, frame(6.)); - video.set_sample(7, frame(7.)); - - video.play(4.0..15.0, 1.0).unwrap(); - - video.set_sample(16, keyframe(16.)); - video.set_sample(17, frame(17.)); - - video.play(16.0..17.0, 1.0).unwrap(); - - video.set_sample(18, frame(18.)); - video.set_sample(19, frame(19.)); - - video.play(18.0..24.0, 1.0).unwrap(); - - video.expect_decoded_samples(0..24); -} - -#[test] -fn player_fetching_unloaded() { - let samples = [ - unloaded(0.), - unloaded(1.), - frame(2.), - unloaded(3.), - keyframe(4.), - unloaded(5.), - frame(6.), - keyframe(7.), - frame(8.), - frame(9.), - frame(10.), - unloaded(11.), - frame(12.), - frame(13.), - frame(14.), - ]; - - let mut video = create_video(samples.clone()).unwrap(); - - let fetched = parking_lot::RwLock::new(Vec::new()); - assert_loading(video.play_with_buffer(2.0..4.0, 1.0, &|source| { - fetched.write().push(source); - - &[] - })); - assert_eq!( - fetched.read().as_slice(), - &[samples[2].source_id(), samples[1].source_id()] - ); - - video.expect_decoded_samples(None); - - fetched.write().clear(); - assert_loading(video.play_with_buffer(4.0..6.0, 1.0, &|source| { - fetched.write().push(source); - - &[] - })); - assert_eq!( - fetched.read().as_slice(), - &[ - // First keyframe at 4.0 from `request_keyframe_before` - samples[4].source_id(), - // Then again keyframe at 4.0 when enqueueing it - samples[4].source_id(), - // Then unloaded when pre-loading - samples[5].source_id() - ] - ); - - video.expect_decoded_samples(std::iter::once(4)); - - fetched.write().clear(); - assert_loading(video.play_with_buffer(10.0..12.0, 1.0, &|source| { - fetched.write().push(source); - - &[] - })); - assert_eq!( - fetched.read().as_slice(), - &[ - // in `request_keyframe_before` (reversed) - samples[10].source_id(), - samples[9].source_id(), - samples[8].source_id(), - samples[7].source_id(), - // in `enqueue_sample_range` - samples[7].source_id(), - samples[8].source_id(), - samples[9].source_id(), - samples[10].source_id(), - // Then unloaded when pre-loading - samples[11].source_id(), - ] - ); - - video.expect_decoded_samples(7..11); - - fetched.write().clear(); - assert_loading(video.play_with_buffer(12.0..14.0, 1.0, &|source| { - let i = samples - .iter() - .position(|c| c.source_id() == source) - .unwrap(); - eprintln!( - "\n#{i}\n{}", - std::backtrace::Backtrace::capture() - .to_string() - .lines() - .filter(|l| l.contains("player")) - .collect::>() - .join("\n") - ); - fetched.write().push(source); - - &[] - })); - assert_eq!( - fetched.read().as_slice(), - // Both in `request_keyframe_before` (reversed). - &[samples[12].source_id(), samples[11].source_id()] - ); - - video.expect_decoded_samples(None); -} - -impl TestVideoPlayer { - fn play_store( - &mut self, - range: Range, - time_step: f64, - store: &re_entity_db::EntityDb, - ) -> Result<(), VideoPlayerError> { - let storage_engine = store.storage_engine(); - self.play_with_buffer(range, time_step, &|tuid| { - let buffer = storage_engine - .store() - .physical_chunk(&re_chunk::ChunkId::from_tuid(tuid)) - .and_then(|chunk| { - let raw = chunk.raw_component_array( - re_sdk_types::archetypes::VideoStream::descriptor_sample().component, - )?; - - let (_offsets, buffer) = re_arrow_util::blob_arrays_offsets_and_buffer(raw)?; - - Some(buffer.as_slice()) - }); - - buffer.unwrap_or(&[]) - }) - } -} - -const STREAM_ENTITY: &str = "/stream"; -const TIMELINE_NAME: &str = "video"; - -#[track_caller] -fn unload_chunks(store: &EntityDb, cache: &mut super::VideoStreamCache, keep_range: Range) { - let loaded_chunks_before = store.storage_engine().store().num_physical_chunks(); - let store_events = store.gc(&re_chunk_store::GarbageCollectionOptions { - target: re_chunk_store::GarbageCollectionTarget::Everything, - time_budget: std::time::Duration::from_secs(u64::MAX), - protect_latest: 0, - protected_chunks: Default::default(), - protected_time_ranges: std::iter::once(( - re_chunk::TimelineName::new(TIMELINE_NAME), - AbsoluteTimeRange::new( - TimeInt::from_secs(keep_range.start), - TimeInt::from_secs(keep_range.end.next_down()), - ), - )) - .collect(), - furthest_from: None, - perform_deep_deletions: false, - }); - - let loaded_chunks_after = store.storage_engine().store().num_physical_chunks(); - - assert!( - loaded_chunks_before > loaded_chunks_after, - "Expected some chunks to be gc'd" - ); - - cache.on_store_events(&store_events.iter().collect::>(), store); -} - -fn load_chunks(store: &mut EntityDb, cache: &mut super::VideoStreamCache, chunks: &[Arc]) { - let mut store_events = Vec::::new(); - - for chunk in chunks { - store_events.extend(store.add_chunk(chunk).unwrap()); - } - - cache.on_store_events(&store_events.iter().collect::>(), store); -} - -fn codec_chunk() -> Chunk { - let mut builder = Chunk::builder(STREAM_ENTITY); - - builder = builder.with_archetype( - RowId::new(), - [( - Timeline::new_duration(TIMELINE_NAME), - TimeInt::from_secs(0.0), - )], - &VideoStream::new(VideoCodec::AV1), - ); - - builder.build().unwrap() -} - -fn video_chunk(start_time: f64, dt: f64, gop_count: u64, samples_per_gop: u64) -> Chunk { - let timeline = Timeline::new_duration(TIMELINE_NAME); - let mut builder = Chunk::builder(STREAM_ENTITY); - - for i in 0..gop_count { - let gop_start_time = start_time + (i * samples_per_gop) as f64 * dt; - builder = builder.with_archetype( - RowId::new(), - [(timeline, TimeInt::from_secs(gop_start_time))], - &VideoStream::update_fields().with_sample(AV1_TEST_KEYFRAME), - ); - - for i in 1..samples_per_gop { - let time = gop_start_time + i as f64 * dt; - builder = builder.with_archetype( - RowId::new(), - [(timeline, TimeInt::from_secs(time))], - &VideoStream::update_fields().with_sample(AV1_TEST_INTER_FRAME), - ); - } - } - - builder.build().unwrap() -} - -fn playable_stream(cache: &mut VideoStreamCache, store: &EntityDb) -> SharablePlayableVideoStream { - cache - .video_entry( - store, - &re_chunk::EntityPath::from(STREAM_ENTITY), - TIMELINE_NAME.into(), - re_video::DecodeSettings { - hw_acceleration: Default::default(), - ffmpeg_path: Some(std::path::PathBuf::from("/not/used")), - }, - ) - .unwrap() -} - -fn load_into_rrd_manifest(store: &mut EntityDb, chunks: &[Arc]) { - let manifest = re_log_encoding::RrdManifest::build_in_memory_from_chunks( - store.store_id().clone(), - chunks.iter().map(|c| &**c), - ) - .unwrap(); - - store.add_rrd_manifest_message(manifest); -} - -#[test] -fn cache_with_manifest() { - let mut cache = VideoStreamCache::default(); - - let mut store = EntityDb::new(StoreId::recording("test", "test")); - - let chunks: Vec<_> = (0..10) - .map(|i| video_chunk(i as f64, 0.25, 1, 4)) - .chain(once(codec_chunk())) - .map(Arc::new) - .collect(); - - load_into_rrd_manifest(&mut store, &chunks); - - // load codec chunk - load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); - - let video_stream = playable_stream(&mut cache, &store); - - // Load some chunks. - load_chunks(&mut store, &mut cache, &chunks[4..5]); - - let mut player = TestVideoPlayer::from_stream(video_stream); - - assert_loading(player.play_store(6.0..10.0, 0.25, &store)); - player.expect_decoded_samples(None); - - player.play_store(4.0..4.75, 0.25, &store).unwrap(); - - player.expect_decoded_samples(16..19); - - load_chunks(&mut store, &mut cache, &chunks[0..2]); - - player.play_store(0.0..1.75, 0.25, &store).unwrap(); - - load_chunks(&mut store, &mut cache, &chunks[2..4]); - - player.play_store(1.75..4.75, 0.25, &store).unwrap(); - - player.expect_decoded_samples(0..19); - - unload_chunks(&store, &mut cache, 4.0..5.0); - - load_chunks(&mut store, &mut cache, &chunks[4..7]); - - player.play_store(4.75..6.75, 0.25, &store).unwrap(); - - player.expect_decoded_samples(20..27); - - // Load the ones we unloaded again - load_chunks(&mut store, &mut cache, &chunks[0..4]); - - player.play_store(0.0..6.75, 0.25, &store).unwrap(); - - player.expect_decoded_samples(0..27); -} - -#[test] -fn cache_with_streaming() { - let mut cache = VideoStreamCache::default(); - - let mut store = EntityDb::with_store_config( - StoreId::recording("test", "test"), - true, - re_chunk_store::ChunkStoreConfig { - enable_changelog: true, - chunk_max_bytes: u64::MAX, - chunk_max_rows: 12, - chunk_max_rows_if_unsorted: 12, - }, - ); - - let chunk_count = 100; - - let dt = 0.25; - let chunks: Vec<_> = (0..chunk_count) - .map(|i| video_chunk(i as f64, dt, 1, 4)) - .chain(once(codec_chunk())) - .map(Arc::new) - .collect(); - - // load codec chunk - load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); - - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - // Load all sample chunks. - load_chunks(&mut store, &mut cache, &chunks[0..chunk_count]); - - player.play_store(0.0..25.0, dt, &store).unwrap(); - - player.expect_decoded_samples(0..chunk_count); - - unload_chunks(&store, &mut cache, 15.0..25.0); - - // Try dropping chunks at the start. - player.play_store(15.0..25.0, dt, &store).unwrap(); - - player.expect_decoded_samples(60..chunk_count); -} - -#[test] -fn cache_with_manifest_and_streaming() { - let mut cache = VideoStreamCache::default(); - - let mut store = EntityDb::new(StoreId::recording("test", "test")); - - let chunks: Vec<_> = once(codec_chunk()) - .chain((0..6).map(|i| video_chunk(i as f64 + 1.0, 0.25, 1, 4))) - .map(Arc::new) - .collect(); - - // Load first 5 chunks into the manifest. - load_into_rrd_manifest(&mut store, &chunks[..5]); - - // load codec chunk - load_chunks(&mut store, &mut cache, &chunks[..1]); - - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - // Load some chunks. - load_chunks(&mut store, &mut cache, &chunks[3..5]); - - assert_loading(player.play_store(1.0..3.0, 0.25, &store)); - player.expect_decoded_samples(None); - - player.play_store(3.0..5.0, 0.25, &store).unwrap(); - player.expect_decoded_samples(8..16); - - load_chunks(&mut store, &mut cache, &chunks[5..6]); - player.play_store(5.0..6.0, 0.25, &store).unwrap(); - player.expect_decoded_samples(16..20); - - load_chunks(&mut store, &mut cache, &chunks[6..7]); - player.play_store(6.0..7.0, 0.25, &store).unwrap(); - player.expect_decoded_samples(20..24); - - player.play_store(3.0..7.0, 0.25, &store).unwrap(); - player.expect_decoded_samples(8..24); - - load_chunks(&mut store, &mut cache, &chunks[1..3]); - player.play_store(1.0..7.0, 0.25, &store).unwrap(); - player.expect_decoded_samples(0..24); - - unload_chunks(&store, &mut cache, 4.0..6.0); - // Check that all remaining samples are still playable. - player.play_store(4.0..6.0, 0.25, &store).unwrap(); - player.expect_decoded_samples(12..20); -} - -#[track_caller] -fn assert_splits_happened(store: &EntityDb) { - let engine = store.storage_engine(); - let store = engine.store(); - - assert!( - store - .iter_physical_chunks() - .any(|c| { store.descends_from_a_split(&c.id()) }), - "This test is testing how the video cache handles splits, but no split happened" - ); -} - -#[test] -fn cache_with_streaming_splits() { - let mut cache = VideoStreamCache::default(); - - let mut store = EntityDb::with_store_config( - StoreId::recording("test", "test"), - true, - re_chunk_store::ChunkStoreConfig { - enable_changelog: true, - chunk_max_bytes: u64::MAX, - chunk_max_rows: 100, - chunk_max_rows_if_unsorted: 100, - }, - ); - - let chunk_count = 4; - let gops_per_chunk = 10; - let samples_per_gop = 200; - - let dt = 0.1; - - let samples_per_chunk = gops_per_chunk * samples_per_gop; - let sample_count = chunk_count * samples_per_chunk; - let time_per_chunk = samples_per_chunk as f64 * dt; - - let chunks: Vec<_> = (0..chunk_count) - .map(|i| { - video_chunk( - i as f64 * time_per_chunk, - dt, - gops_per_chunk, - samples_per_gop, - ) - }) - .chain(once(codec_chunk())) - .map(Arc::new) - .collect(); - - // load codec chunk - load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); - - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - // Load all sample chunks. - load_chunks(&mut store, &mut cache, &chunks[0..4]); - - player - .play_store(0.0..sample_count as f64 * dt, dt, &store) - .unwrap(); - - player.expect_decoded_samples(0..sample_count as SampleIndex); - - assert_splits_happened(&store); -} - -#[test] -fn cache_with_manifest_splits() { - let mut cache = VideoStreamCache::default(); - - let mut store = EntityDb::with_store_config( - StoreId::recording("test", "test"), - true, - re_chunk_store::ChunkStoreConfig { - enable_changelog: true, - chunk_max_bytes: u64::MAX, - chunk_max_rows: 100, - chunk_max_rows_if_unsorted: 100, - }, - ); - - let chunk_count = 4; - let gops_per_chunk = 10; - let samples_per_gop = 200; - - let dt = 0.1; - let samples_per_chunk = gops_per_chunk * samples_per_gop; - let time_per_chunk = samples_per_chunk as f64 * dt; - - let chunks: Vec<_> = (0..chunk_count) - .map(|i| { - video_chunk( - time_per_chunk * i as f64, - dt, - gops_per_chunk, - samples_per_gop, - ) - }) - .chain(once(codec_chunk())) - .map(Arc::new) - .collect(); - - load_into_rrd_manifest(&mut store, &chunks); - - // load codec chunk - load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); - - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - load_chunks(&mut store, &mut cache, &chunks[1..2]); - - player - .play_store(time_per_chunk..time_per_chunk * 2.0 - dt, dt, &store) - .unwrap(); - - let samples_per_chunk = samples_per_chunk as usize; - player.expect_decoded_samples(samples_per_chunk..samples_per_chunk * 2 - 1); - - load_chunks(&mut store, &mut cache, &chunks[2..3]); - player - .play_store(time_per_chunk * 2.0..time_per_chunk * 3.0 - dt, dt, &store) - .unwrap(); - - player.expect_decoded_samples(samples_per_chunk * 2..samples_per_chunk * 3 - 1); - - let min_loaded = 1.7; - let max_loaded = 2.3; - - unload_chunks( - &store, - &mut cache, - time_per_chunk * min_loaded..time_per_chunk * max_loaded, - ); - - // Assert that the beginning/end splits have been gc'd - assert_loading(player.play_store(time_per_chunk..time_per_chunk * 1.5, dt, &store)); - player.expect_decoded_samples(None); - - let play_store = player.play_store(time_per_chunk * 2.5..time_per_chunk * 3.0 - dt, dt, &store); - player.expect_decoded_samples(None); - assert_loading(play_store); - - player - .play_store( - time_per_chunk * min_loaded..time_per_chunk * max_loaded - dt, - dt, - &store, - ) - .unwrap(); - - let end = (samples_per_chunk as f64 * max_loaded) as usize; - player.expect_decoded_samples((samples_per_chunk as f64 * min_loaded).ceil() as usize..end); - - load_chunks(&mut store, &mut cache, &chunks[0..2]); - player - .play_store(0.0..time_per_chunk * max_loaded - dt, dt, &store) - .unwrap(); - - player.expect_decoded_samples(0..end); - - assert_splits_happened(&store); -} - -#[test] -fn cache_with_unordered_chunks() { - let mut cache = VideoStreamCache::default(); - - let mut store = EntityDb::new(StoreId::recording("test", "test")); - - let chunk_count = 100; - - let gop_count = 1; - let samples_per_gop = 4; - - let dt = 0.25; - let chunks: Vec<_> = (0..chunk_count) - .map(|i| { - let timeline = Timeline::new_duration(TIMELINE_NAME); - let mut builder = Chunk::builder(STREAM_ENTITY); - let mut row_ids: Vec<_> = (0..gop_count * samples_per_gop) - .map(|_| RowId::new()) - .collect(); - - use rand::SeedableRng as _; - use rand::seq::SliceRandom as _; - let mut rng = rand::rngs::StdRng::seed_from_u64(i as u64); - - // Shuffle row ids to make the chunk (very likely) unsorted on the timeline. - row_ids.shuffle(&mut rng); - - let start_time = i as f64; - for i in 0..gop_count { - let gop_start_time = start_time + (i * samples_per_gop) as f64 * dt; - - builder = builder.with_archetype( - row_ids.pop().unwrap(), - [(timeline, TimeInt::from_secs(gop_start_time))], - &VideoStream::update_fields().with_sample(AV1_TEST_KEYFRAME), - ); - - for i in 1..samples_per_gop { - let time = gop_start_time + i as f64 * dt; - builder = builder.with_archetype( - row_ids.pop().unwrap(), - [(timeline, TimeInt::from_secs(time))], - &VideoStream::update_fields().with_sample(AV1_TEST_INTER_FRAME), - ); - } - } - - let mut chunk = builder.build().unwrap(); - - chunk.sort_if_unsorted(); - - chunk - }) - .chain(once(codec_chunk())) - .map(Arc::new) - .collect(); - - assert!( - chunks.iter().any(|chunk| { - chunk - .timelines() - .get(&re_chunk::TimelineName::new(TIMELINE_NAME)) - .is_some_and(|t| !t.is_sorted()) - }), - "We are testing unsorted chunks, at least one should end up unsorted" - ); - - // load codec chunk - load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); - - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - // Load all sample chunks. - load_chunks(&mut store, &mut cache, &chunks[0..chunk_count]); - - player.play_store(0.0..25.0, dt, &store).unwrap(); - - player.expect_decoded_samples(0..chunk_count); -} - -/// Test that chunks arriving out of temporal order are handled correctly -/// via delta re-merge (the `handle_out_of_order_chunk` path). -/// -/// Loads chunks in non-chronological order so that a later-arriving chunk -/// has timestamps that fall before existing samples, triggering the -/// out-of-order detection and re-merge. -#[test] -fn cache_with_out_of_order_chunk_arrival() { - let mut cache = VideoStreamCache::default(); - - let mut store = EntityDb::new(StoreId::recording("test", "test")); - - let dt = 0.25; - let samples_per_gop = 4; - - // 10 chunks, each 1 GOP of 4 samples. - let chunk_count = 10usize; - let chunks: Vec<_> = (0..chunk_count) - .map(|i| video_chunk(i as f64, dt, 1, samples_per_gop)) - .chain(once(codec_chunk())) - .map(Arc::new) - .collect(); - - // Load codec chunk and create the cache entry. - load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - // Load chunks 0, 1, 2 in order. - load_chunks(&mut store, &mut cache, &chunks[0..3]); - - player.play_store(0.0..3.0, dt, &store).unwrap(); - player.expect_decoded_samples(0..12); - - // Skip chunk 3 and load chunk 4 first, still in order relative to - // what was already loaded. - load_chunks(&mut store, &mut cache, &chunks[4..5]); - - player.play_store(4.0..5.0, dt, &store).unwrap(); - player.expect_decoded_samples(12..16); - - // Now load chunk 3 which has times [3.0, 3.25, 3.5, 3.75] -- this - // falls between the already-loaded chunks 2 and 4, triggering the - // out-of-order / delta re-merge path. - load_chunks(&mut store, &mut cache, &chunks[3..4]); - - // The cache entry should still exist (delta re-merge, not removal). - assert!( - cache - .entries - .contains_key(&crate::cache::video_stream_cache::VideoStreamKey { - entity_path: re_chunk::EntityPath::from(STREAM_ENTITY).hash(), - timeline: re_chunk::TimelineName::new(TIMELINE_NAME), - sample_component: VideoStream::descriptor_sample().component, - }), - "Cache entry should survive delta re-merge" - ); - - // All 20 samples (chunks 0-4) should be playable. - player.play_store(0.0..5.0, dt, &store).unwrap(); - player.expect_decoded_samples(0..20); - - // Load chunks 7, 8, 9 (skipping 5, 6). - load_chunks(&mut store, &mut cache, &chunks[7..10]); - - player.play_store(7.0..10.0, dt, &store).unwrap(); - player.expect_decoded_samples(20..32); - - // Now load the skipped chunks 5 and 6 out of order. - load_chunks(&mut store, &mut cache, &chunks[5..7]); - - // Everything from 0 through 10 should work. - player.play_store(0.0..10.0, dt, &store).unwrap(); - player.expect_decoded_samples(0..40); -} - -/// Test for out-of-order chunk arrival that should not trigger a cache reset, -/// followed by compaction. -/// -/// This tests for the scenario where a `ChunkSampleRange` has -/// less samples than the amount of samples it spans. -#[test] -fn cache_out_of_order_arrival_with_compaction() { - let mut cache = VideoStreamCache::default(); - - let mut store = EntityDb::with_store_config( - StoreId::recording("test", "test"), - true, - re_chunk_store::ChunkStoreConfig { - enable_changelog: true, - chunk_max_bytes: u64::MAX, - chunk_max_rows: 4, - chunk_max_rows_if_unsorted: 4, - }, - ); - - let codec_chunk = Arc::new(codec_chunk()); - - // Create chunk0 with 4 rows so it won't compact. - let chunk0 = Arc::new(video_chunk(0.0, 2.0, 1, 4)); // times: 0.0, 2.0, 4.0, 6.0 - - // Create chunk1 and chunk2 with less than 4 rows combined so they compact. - let chunk1 = Arc::new(video_chunk(5.0, 2.0, 1, 2)); // times: 5.0, 7.0 - let chunk2 = Arc::new(video_chunk(8.0, 0.0, 1, 1)); // time: 8.0 - - let codec_chunk_id = codec_chunk.id(); - let chunk0_id = chunk0.id(); - let chunk1_id = chunk1.id(); - let chunk2_id = chunk2.id(); - - let replace_id = |s: &str| -> String { - s.replace( - &codec_chunk_id.to_string(), - &format!("chunk_codec {}", codec_chunk_id.short_string()), - ) - .replace( - &chunk0_id.to_string(), - &format!("chunk0 {}", chunk0_id.short_string()), - ) - .replace( - &chunk1_id.to_string(), - &format!("chunk1 {}", chunk1_id.short_string()), - ) - .replace( - &chunk2_id.to_string(), - &format!("chunk2 {}", chunk2_id.short_string()), - ) - }; - - // Load codec chunk and chunk0. - load_chunks(&mut store, &mut cache, &[codec_chunk, chunk0]); - - let video_stream_before = playable_stream(&mut cache, &store); - - let mut player = TestVideoPlayer::from_stream(video_stream_before); - - player.play_store(0.0..8.0, 1.0, &store).unwrap(); - player.expect_decoded_samples(0..4); - - // This triggers out-of-order handling because time 5 < time 6. - // With delta re-merge, the cache entry is NOT cleared. - load_chunks(&mut store, &mut cache, &[chunk1]); - - assert!( - store - .storage_engine() - .store() - .iter_physical_chunks() - .zip([Some(codec_chunk_id), Some(chunk0_id), Some(chunk1_id), None]) - .all(|(c, expected_id)| { - let eq = Some(c.id()) == expected_id; - - if !eq { - eprintln!( - "Expected {}, got {} with lineage:\n{}", - expected_id - .map(|c| c.short_string()) - .unwrap_or_else(|| "nothing".to_owned()), - c.id().short_string(), - replace_id(&store.storage_engine().store().format_lineage(&c.id())), - ); - } - - eq - }), - "No compaction should've occurred yet" - ); - - // The cache entry should still exist (delta re-merge instead of removal). - assert!( - cache - .entries - .contains_key(&crate::cache::video_stream_cache::VideoStreamKey { - entity_path: re_chunk::EntityPath::from(STREAM_ENTITY).hash(), - timeline: re_chunk::TimelineName::new(TIMELINE_NAME), - sample_component: VideoStream::descriptor_sample().component, - }), - "The video stream cache entry should still exist after delta re-merge" - ); - - // Use the same video stream -- it was re-merged in place. - let video_stream_after = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream_after); - - player.play_store(0.0..8.0, 1.0, &store).unwrap(); - player.expect_decoded_samples(0..6); - - // This should compact with chunk1. - load_chunks(&mut store, &mut cache, &[chunk2]); - - assert!( - store - .storage_engine() - .store() - .iter_physical_chunks() - .any(|c| { - if let Some(re_chunk_store::ChunkDirectLineage::CompactedFrom(chunks)) = - store.storage_engine().store().direct_lineage(&c.id()) - { - *chunks == [chunk1_id, chunk2_id].into_iter().collect() - } else { - false - } - }), - "chunk 1 & 2, should've been compacted.\nchunks:\n{}", - replace_id( - &store - .storage_engine() - .store() - .iter_physical_chunks() - .map(|c| store.storage_engine().store().format_lineage(&c.id())) - .collect::>() - .join("\n\n") - ), - ); - - player.play_store(0.0..9.0, 1.0, &store).unwrap(); - - player.expect_decoded_samples(0..7); -} - -/// Test that `ChunkSamples::from_root` placement of samples at the start -/// is handled as expected resulting in an incomplete gop. But then -/// recovered via delta re-merge. -/// -/// `from_root` places all unloaded samples at the start of the chunk's time -/// range. When a manifest chunk spans multiple GOPs and another chunk's -/// samples fall between those GOPs, loading the multi-GOP chunk causes an -/// out-of-order situation that triggers the re-merge path. -#[test] -fn cache_with_manifest_load_resulting_in_incomplete_gop() { - let mut cache = VideoStreamCache::default(); - let mut store = EntityDb::new(StoreId::recording("test", "test")); - - let dt = 0.25; - - // Chunk A: 2 GOPs of 4 samples each, times [0, 1.75]. - // In the manifest, from_root places all 8 samples at time 0. - let chunk_a = video_chunk(0.0, dt, 2, 4); - - // Chunk B: 1 GOP of 3 samples, times [0.875, 1.375]. - // Falls between A's two GOPs (GOP 0: 0-0.75, GOP 1: 1.0-1.75). - // B's min time (0.875) is within A's range so after loading A, - // some of A's actual samples (1.0-1.75) end up after B's - // causing an out-of-order situation. - let chunk_b = video_chunk(0.875, dt, 1, 3); - - let chunks: Vec<_> = [chunk_a, chunk_b, codec_chunk()] - .into_iter() - .map(Arc::new) - .collect(); - - load_into_rrd_manifest(&mut store, &chunks); - - // Load codec. - load_chunks(&mut store, &mut cache, &chunks[2..3]); - - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - // Load chunk B first. - load_chunks(&mut store, &mut cache, &chunks[1..2]); - - player.play_store(0.875..1.375, dt, &store).unwrap(); - player.expect_decoded_samples(8..11); - - // Playing in A's unloaded range should fail with loading. - assert_loading(player.play_store(0.0..0.75, dt, &store)); - player.expect_decoded_samples(None); - - // Load chunk A. from_root placed A's 8 samples at time 0 (indices 0-7). - // A's actual second GOP (times 1.0-1.75) falls after B's samples - // (times 0.875-1.375), triggering out-of-order detection and re-merge. - load_chunks(&mut store, &mut cache, &chunks[0..1]); - - // The cache entry should survive the delta re-merge. - assert!( - cache - .entries - .contains_key(&crate::cache::video_stream_cache::VideoStreamKey { - entity_path: re_chunk::EntityPath::from(STREAM_ENTITY).hash(), - timeline: re_chunk::TimelineName::new(TIMELINE_NAME), - sample_component: VideoStream::descriptor_sample().component, - }), - "Cache entry should survive delta re-merge" - ); - - // After re-merge, all 11 samples should be in the correct time order - // and playable. - player.play_store(0.0..2.0, dt, &store).unwrap(); - player.expect_decoded_samples(0..11); -} - -/// When a conflicting chunk (chunk 1) is loaded last, its manifest-placed -/// keyframe may sit right before the affected range. The reorder logic must -/// walk back past that keyframe to include earlier samples (from chunk 0) -/// whose real timestamps interleave with the conflicting chunk. -/// -/// Without skipping the conflicting chunk's keyframe, chunk 0's sample at -/// time 3 would be outside the reorder range, leaving the final sequence -/// as [1, 3, 2, 4, 5, 6] instead of the correct [1, 2, 3, 4, 5, 6]. -/// -/// Timeline picture (each chunk is one GOP): -/// chunk 0: 1 . 3 . . -/// chunk 1: . 2 . 4 . -/// chunk 2: . . . . 5 6 -/// -/// Load order: 0, 2, 1 -#[test] -fn cache_with_manifest_skips_conflicting_chunk_keyframe() { - let mut cache = VideoStreamCache::default(); - let mut store = EntityDb::new(StoreId::recording("test", "test")); - - // Chunk 0: 1 GOP of 2 samples, times [1, 3]. - let chunk_0 = video_chunk(1.0, 2.0, 1, 2); - - // Chunk 1: 1 GOP of 2 samples, times [2, 4]. - // Interleaves with chunk 0, causing out-of-order when loaded last. - let chunk_1 = video_chunk(2.0, 2.0, 1, 2); - - // Chunk 2: 1 GOP of 2 samples, times [5, 6]. - let chunk_2 = video_chunk(5.0, 1.0, 1, 2); - - let chunks: Vec<_> = [chunk_0, chunk_1, chunk_2, codec_chunk()] - .into_iter() - .map(Arc::new) - .collect(); - - load_into_rrd_manifest(&mut store, &chunks); - - // Load codec. - load_chunks(&mut store, &mut cache, &chunks[3..4]); - - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - // Load chunk 0, then chunk 2. - load_chunks(&mut store, &mut cache, &chunks[0..1]); - load_chunks(&mut store, &mut cache, &chunks[2..3]); - - player.play_store(5.0..7.0, 1.0, &store).unwrap(); - player.expect_decoded_samples(4..6); - - // Loading chunk 1 triggers out-of-order: chunk 0's sample at time 3 - // (index 1) is followed by chunk 1's sample at time 2 (index 2). - // - // In find_affected_sample_range, chunk 1's keyframe (at index 2, time 2) - // is found right before chunk 2's keyframe (at index 4, time 5). - // Since it belongs to the conflicting chunk, the while loop walks back - // past it to chunk 0's keyframe at index 0, ensuring chunk 0's sample - // at time 3 is included in the reorder range. - load_chunks(&mut store, &mut cache, &chunks[1..2]); - - // The cache entry should survive the delta re-merge. - assert!( - cache - .entries - .contains_key(&crate::cache::video_stream_cache::VideoStreamKey { - entity_path: re_chunk::EntityPath::from(STREAM_ENTITY).hash(), - timeline: re_chunk::TimelineName::new(TIMELINE_NAME), - sample_component: VideoStream::descriptor_sample().component, - }), - "Cache entry should survive delta re-merge" - ); - - // After re-merge, all 6 samples should be in the correct time order. - player.play_store(1.0..7.0, 1.0, &store).unwrap(); - player.expect_decoded_samples(0..6); -} - -/// A chunk loaded second has timestamps that interleave with an already-loaded chunk, -/// triggering a sample reorder. After GC removes the interleaving chunk, the remaining -/// chunk's samples must all be decodable from its own keyframe. -#[test] -fn cache_with_gc_after_interleaved_arrival() { - let mut cache = VideoStreamCache::default(); - let mut store = EntityDb::new(StoreId::recording("test", "test")); - - let codec = Arc::new(codec_chunk()); - let chunk_y = Arc::new(video_chunk(1.0, 1.0, 1, 4)); - // Starts before Y but loaded second, triggering out-of-order handling. - let chunk_x = Arc::new(video_chunk(0.5, 1.0, 2, 1)); - - load_chunks(&mut store, &mut cache, std::slice::from_ref(&codec)); - load_chunks(&mut store, &mut cache, std::slice::from_ref(&chunk_y)); - - // Create a live entry before chunk_x arrives, so handle_deletion runs on it later. - let _ = playable_stream(&mut cache, &store); - - // Loading X after Y triggers handle_out_of_order_chunk, interleaving the deques. - load_chunks(&mut store, &mut cache, std::slice::from_ref(&chunk_x)); - - // Evict X and the codec while keeping Y. - unload_chunks(&store, &mut cache, 2.0..5.0); - - // Reload the codec. - load_chunks(&mut store, &mut cache, std::slice::from_ref(&codec)); - - // The entry was either correctly rebuilt (correct path) or corrupted (buggy path). - let video_stream = playable_stream(&mut cache, &store); - let mut player = TestVideoPlayer::from_stream(video_stream); - - player.play_store(2.0..5.0, 0.25, &store).unwrap(); - player.expect_decoded_samples(0..4); -} diff --git a/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/encoded_depth_image.rs b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/encoded_depth_image.rs new file mode 100644 index 000000000000..4649b1a4f6b7 --- /dev/null +++ b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/encoded_depth_image.rs @@ -0,0 +1,273 @@ +use std::sync::Arc; + +use re_chunk::{Chunk, RowId, TimeInt, Timeline}; +use re_chunk_store::ChunkTrackingMode; +use re_entity_db::EntityDb; +use re_log_types::StoreId; +use re_sdk_types::archetypes::EncodedDepthImage; + +use crate::{SharablePlayableVideoStream, VideoStreamCache}; + +use super::{STREAM_ENTITY, TIMELINE_NAME, TestVideoPlayer, load_chunks, unload_chunks}; + +fn test_depth_png_blob() -> Vec { + let mut buf = Vec::new(); + { + let encoder = image::codecs::png::PngEncoder::new(&mut buf); + image::ImageEncoder::write_image(encoder, &[0u8; 2], 1, 1, image::ColorType::L16.into()) + .unwrap(); + } + buf +} + +fn codec_chunk() -> Chunk { + Chunk::builder(STREAM_ENTITY) + .with_archetype( + RowId::new(), + [( + Timeline::new_duration(TIMELINE_NAME), + TimeInt::from_secs(0.0), + )], + &EncodedDepthImage::update_fields().with_media_type("image/png"), + ) + .build() + .unwrap() +} + +fn depth_image_chunk(start_time: f64, dt: f64, count: u64) -> Chunk { + let timeline = Timeline::new_duration(TIMELINE_NAME); + let blob = test_depth_png_blob(); + let mut builder = Chunk::builder(STREAM_ENTITY); + + for i in 0..count { + let time = start_time + i as f64 * dt; + builder = builder.with_archetype( + RowId::new(), + [(timeline, TimeInt::from_secs(time))], + &EncodedDepthImage::update_fields().with_blob(blob.clone()), + ); + } + + builder.build().unwrap() +} + +fn playable_stream(cache: &mut VideoStreamCache, store: &EntityDb) -> SharablePlayableVideoStream { + let blob_component = EncodedDepthImage::descriptor_blob().component; + let media_type_component = EncodedDepthImage::descriptor_media_type().component; + let query_result = store.storage_engine().cache().latest_at( + ChunkTrackingMode::Report, + &re_chunk::LatestAtQuery::new(TIMELINE_NAME.into(), re_chunk::TimeInt::MAX), + &re_chunk::EntityPath::from(STREAM_ENTITY), + [media_type_component], + ); + let media_type = query_result + .get_required(media_type_component) + .ok() + .and_then(|chunk| { + chunk + .component_mono::(media_type_component)? + .ok() + .map(|mt| mt.to_string()) + }); + + cache + .entry( + store, + &re_chunk::EntityPath::from(STREAM_ENTITY), + TIMELINE_NAME.into(), + re_video::DecodeSettings { + hw_acceleration: Default::default(), + ffmpeg_path: Some(std::path::PathBuf::from("/not/used")), + }, + blob_component, + re_video::VideoCodec::ImageSequence(media_type), + ) + .unwrap() +} + +#[test] +fn basic_playback() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(codec_chunk()); + let frames = Arc::new(depth_image_chunk(1.0, 1.0, 4)); + + load_chunks(&mut store, &mut cache, &[codec]); + load_chunks(&mut store, &mut cache, &[frames]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + player + .play_store_with_component( + 1.0..5.0, + 1.0, + &store, + EncodedDepthImage::descriptor_blob().component, + ) + .unwrap(); + player.expect_decoded_samples(0..4); +} + +#[test] +fn multi_chunk_with_gc() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(codec_chunk()); + let chunk_a = Arc::new(depth_image_chunk(1.0, 1.0, 4)); + let chunk_b = Arc::new(depth_image_chunk(5.0, 1.0, 4)); + + load_chunks(&mut store, &mut cache, &[codec.clone(), chunk_a, chunk_b]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + player + .play_store_with_component( + 1.0..9.0, + 1.0, + &store, + EncodedDepthImage::descriptor_blob().component, + ) + .unwrap(); + player.expect_decoded_samples(0..8); + + // GC chunk_a, keep chunk_b. + unload_chunks(&store, &mut cache, 5.0..9.0); + + load_chunks(&mut store, &mut cache, &[codec]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Only chunk_b's 4 frames remain, keeping their original indices. + player + .play_store_with_component( + 5.0..9.0, + 1.0, + &store, + EncodedDepthImage::descriptor_blob().component, + ) + .unwrap(); + player.expect_decoded_samples(4..8); +} + +/// A 2x2 L16 PNG logged as `EncodedDepthImage` should be recognized with +/// the correct dimensions and bit depth. +#[test] +fn png_decoding() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let width = 2u32; + let height = 2u32; + let depth_values: [u16; 4] = [0, 1, 2, 3]; + + let mut encoded_png = Vec::new(); + { + let encoder = image::codecs::png::PngEncoder::new(&mut encoded_png); + image::ImageEncoder::write_image( + encoder, + bytemuck::cast_slice(&depth_values), + width, + height, + image::ColorType::L16.into(), + ) + .unwrap(); + } + + let codec = Chunk::builder(STREAM_ENTITY) + .with_archetype( + RowId::new(), + [( + Timeline::new_duration(TIMELINE_NAME), + TimeInt::from_secs(0.0), + )], + &EncodedDepthImage::new(encoded_png.clone()).with_media_type("image/png"), + ) + .build() + .unwrap(); + + load_chunks(&mut store, &mut cache, &[Arc::new(codec)]); + + let video_stream = playable_stream(&mut cache, &store); + let descr = video_stream.read_arc().video_descr().clone(); + + assert_eq!( + descr.samples.next_index(), + 1, + "should have exactly 1 sample" + ); + + let encoding_details = descr + .encoding_details + .as_ref() + .expect("should have encoding details"); + assert_eq!( + encoding_details.coded_dimensions, + [width as u16, height as u16] + ); + assert_eq!(encoding_details.bit_depth, Some(16)); +} + +/// An `EncodedDepthImage` without an explicit media type should still be +/// loadable when the format can be guessed from the blob data. +#[test] +fn guesses_png_media_type() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let depth_values: [u16; 4] = [0, 1, 2, 3]; + + let mut encoded_png = Vec::new(); + { + let encoder = image::codecs::png::PngEncoder::new(&mut encoded_png); + image::ImageEncoder::write_image( + encoder, + bytemuck::cast_slice(&depth_values), + 2, + 2, + image::ColorType::L16.into(), + ) + .unwrap(); + } + + // No media type set. + let codec = Chunk::builder(STREAM_ENTITY) + .with_archetype( + RowId::new(), + [( + Timeline::new_duration(TIMELINE_NAME), + TimeInt::from_secs(0.0), + )], + &EncodedDepthImage::new(encoded_png), + ) + .build() + .unwrap(); + + load_chunks(&mut store, &mut cache, &[Arc::new(codec)]); + + let blob_component = EncodedDepthImage::descriptor_blob().component; + let result = cache.entry( + &store, + &re_chunk::EntityPath::from(STREAM_ENTITY), + TIMELINE_NAME.into(), + re_video::DecodeSettings { + hw_acceleration: Default::default(), + ffmpeg_path: Some(std::path::PathBuf::from("/not/used")), + }, + blob_component, + re_video::VideoCodec::ImageSequence(None), + ); + + assert!( + result.is_ok(), + "should succeed even without explicit media type" + ); + + let video_stream = result.unwrap(); + let descr = video_stream.read_arc().video_descr().clone(); + assert_eq!(descr.samples.next_index(), 1); +} diff --git a/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/encoded_image.rs b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/encoded_image.rs new file mode 100644 index 000000000000..b42eaccd52da --- /dev/null +++ b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/encoded_image.rs @@ -0,0 +1,371 @@ +use std::sync::Arc; + +use re_chunk::{Chunk, RowId, TimeInt, TimePoint, Timeline}; +use re_chunk_store::ChunkTrackingMode; +use re_entity_db::EntityDb; +use re_log_types::StoreId; +use re_sdk_types::archetypes::EncodedImage; + +use crate::cache::cache_trait::Cache as _; +use crate::{SharablePlayableVideoStream, VideoStreamCache}; + +use super::{ + STREAM_ENTITY, TIMELINE_NAME, TestVideoPlayer, assert_loading, load_chunks, + load_into_rrd_manifest, unload_chunks, +}; + +fn test_png_blob() -> Vec { + let mut buf = Vec::new(); + { + let encoder = image::codecs::png::PngEncoder::new(&mut buf); + image::ImageEncoder::write_image(encoder, &[0u8; 4], 1, 1, image::ColorType::Rgba8.into()) + .unwrap(); + } + buf +} + +fn codec_chunk() -> Chunk { + Chunk::builder(STREAM_ENTITY) + .with_archetype( + RowId::new(), + [( + Timeline::new_duration(TIMELINE_NAME), + TimeInt::from_secs(0.0), + )], + &EncodedImage::update_fields().with_media_type("image/png"), + ) + .build() + .unwrap() +} + +fn image_chunk(start_time: f64, dt: f64, count: u64) -> Chunk { + let timeline = Timeline::new_duration(TIMELINE_NAME); + let blob = test_png_blob(); + let mut builder = Chunk::builder(STREAM_ENTITY); + + for i in 0..count { + let time = start_time + i as f64 * dt; + builder = builder.with_archetype( + RowId::new(), + [(timeline, TimeInt::from_secs(time))], + &EncodedImage::update_fields().with_blob(blob.clone()), + ); + } + + builder.build().unwrap() +} + +fn playable_stream(cache: &mut VideoStreamCache, store: &EntityDb) -> SharablePlayableVideoStream { + let blob_component = EncodedImage::descriptor_blob().component; + let media_type_component = EncodedImage::descriptor_media_type().component; + let query_result = store.storage_engine().cache().latest_at( + ChunkTrackingMode::Report, + &re_chunk::LatestAtQuery::new(TIMELINE_NAME.into(), re_chunk::TimeInt::MAX), + &re_chunk::EntityPath::from(STREAM_ENTITY), + [media_type_component], + ); + let media_type = query_result + .get_required(media_type_component) + .ok() + .and_then(|chunk| { + chunk + .component_mono::(media_type_component)? + .ok() + .map(|mt| mt.to_string()) + }); + cache + .entry( + store, + &re_chunk::EntityPath::from(STREAM_ENTITY), + TIMELINE_NAME.into(), + re_video::DecodeSettings { + hw_acceleration: Default::default(), + ffmpeg_path: Some(std::path::PathBuf::from("/not/used")), + }, + blob_component, + re_video::VideoCodec::ImageSequence(media_type), + ) + .unwrap() +} + +#[test] +fn basic_playback() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(codec_chunk()); + let frames = Arc::new(image_chunk(1.0, 1.0, 4)); + + load_chunks(&mut store, &mut cache, &[codec]); + load_chunks(&mut store, &mut cache, &[frames]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + player + .play_store_with_component( + 1.0..5.0, + 1.0, + &store, + EncodedImage::descriptor_blob().component, + ) + .unwrap(); + player.expect_decoded_samples(0..4); +} + +#[test] +fn multi_chunk_with_gc() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(codec_chunk()); + let chunk_a = Arc::new(image_chunk(1.0, 1.0, 4)); + let chunk_b = Arc::new(image_chunk(5.0, 1.0, 4)); + + load_chunks(&mut store, &mut cache, &[codec.clone(), chunk_a, chunk_b]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + player + .play_store_with_component( + 1.0..9.0, + 1.0, + &store, + EncodedImage::descriptor_blob().component, + ) + .unwrap(); + player.expect_decoded_samples(0..8); + + // GC chunk_a, keep chunk_b (times 5..9). + unload_chunks(&store, &mut cache, 5.0..9.0); + + // Reload codec since GC may have removed it. + load_chunks(&mut store, &mut cache, &[codec]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Only chunk_b's 4 frames remain, keeping their original indices. + player + .play_store_with_component( + 5.0..9.0, + 1.0, + &store, + EncodedImage::descriptor_blob().component, + ) + .unwrap(); + player.expect_decoded_samples(4..8); +} + +/// A statically logged encoded image carries no timeline, so it isn't in the manifest's +/// `temporal_map`. +fn static_codec_chunk() -> Chunk { + Chunk::builder(STREAM_ENTITY) + .with_archetype( + RowId::new(), + TimePoint::default(), + &EncodedImage::update_fields().with_media_type("image/png"), + ) + .build() + .unwrap() +} + +fn static_image_chunk() -> Chunk { + Chunk::builder(STREAM_ENTITY) + .with_archetype( + RowId::new(), + TimePoint::default(), + &EncodedImage::update_fields().with_blob(test_png_blob()), + ) + .build() + .unwrap() +} + +/// A static sample chunk described only by the manifest is still pre-allocated, and decodes +/// once the chunk is materialized. +#[test] +fn static_image_from_manifest() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(static_codec_chunk()); + let image = Arc::new(static_image_chunk()); + + // The manifest describes both static chunks, but only the media type is materialized. + load_into_rrd_manifest(&mut store, &[codec.clone(), image.clone()]); + load_chunks(&mut store, &mut cache, &[codec]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream.clone()); + + // The static sample comes from the manifest's `static_map`, not its `temporal_map`. Its + // data isn't loaded yet, so playback reports loading. + assert_eq!( + video_stream + .read() + .video_renderer + .data_descr() + .samples + .num_elements(), + 1 + ); + assert_loading(player.play_store_with_component( + 0.0..2.0, + 1.0, + &store, + EncodedImage::descriptor_blob().component, + )); + player.expect_decoded_samples(None); + + // Materializing the static chunk fills the pre-allocated slot without adding another sample. + load_chunks(&mut store, &mut cache, &[image]); + assert_eq!( + video_stream + .read() + .video_renderer + .data_descr() + .samples + .num_elements(), + 1 + ); + player + .play_store_with_component( + 0.0..2.0, + 1.0, + &store, + EncodedImage::descriptor_blob().component, + ) + .unwrap(); + player.expect_decoded_samples(0..1); +} + +/// When a static sample chunk is both described by the manifest and materialized, the two +/// describe the same sample rather than being counted separately. +#[test] +fn static_image_not_double_counted() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(static_codec_chunk()); + let image = Arc::new(static_image_chunk()); + + load_into_rrd_manifest(&mut store, &[codec.clone(), image.clone()]); + load_chunks(&mut store, &mut cache, &[codec, image]); + + let video_stream = playable_stream(&mut cache, &store); + let guard = video_stream.read(); + let descr = guard.video_renderer.data_descr(); + + assert_eq!(descr.samples.num_elements(), 1); + assert!(descr.samples[descr.samples.min_index()].sample().is_some()); +} + +/// Logging a newer static image to the same entity replaces the previous one, leaving the +/// stream backed by the newest chunk. +#[test] +fn static_image_overwrite() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(static_codec_chunk()); + let first = Arc::new(static_image_chunk()); + load_chunks(&mut store, &mut cache, &[codec, first]); + + let video_stream = playable_stream(&mut cache, &store); + assert_eq!( + video_stream + .read() + .video_renderer + .data_descr() + .samples + .num_elements(), + 1 + ); + + let second = Arc::new(static_image_chunk()); + load_chunks(&mut store, &mut cache, std::slice::from_ref(&second)); + + let guard = video_stream.read(); + let descr = guard.video_renderer.data_descr(); + assert_eq!(descr.samples.num_elements(), 1); + assert_eq!( + descr.samples[descr.samples.min_index()].source_primary_id(), + Some(second.id().as_tuid()) + ); +} + +/// Static data isn't garbage collected, so a materialized static image stays loaded across a +/// collection that evicts everything else. +#[test] +fn static_image_survives_gc() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(static_codec_chunk()); + let image = Arc::new(static_image_chunk()); + load_into_rrd_manifest(&mut store, &[codec.clone(), image.clone()]); + load_chunks(&mut store, &mut cache, &[codec, image]); + + let video_stream = playable_stream(&mut cache, &store); + { + let guard = video_stream.read(); + let descr = guard.video_renderer.data_descr(); + assert_eq!(descr.samples.num_elements(), 1); + assert!(descr.samples[descr.samples.min_index()].sample().is_some()); + } + + let events = store.gc(&re_chunk_store::GarbageCollectionOptions { + target: re_chunk_store::GarbageCollectionTarget::Everything, + time_budget: std::time::Duration::from_secs(u64::MAX), + protect_latest: 0, + protected_chunks: Default::default(), + protected_time_ranges: Default::default(), + furthest_from: None, + perform_deep_deletions: false, + }); + cache.on_store_events(&events.iter().collect::>(), &store); + + // The static chunk is untouched by the collection, so its sample stays loaded. + let guard = video_stream.read(); + let descr = guard.video_renderer.data_descr(); + assert_eq!(descr.samples.num_elements(), 1); + assert!(descr.samples[descr.samples.min_index()].sample().is_some()); +} + +/// Overwriting a manifest-backed static image with a newer one leaves the stream backed by +/// the newest chunk. +#[test] +fn static_image_overwrite_with_manifest() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(static_codec_chunk()); + let first = Arc::new(static_image_chunk()); + + // The first image is described by the manifest and materialized. + load_into_rrd_manifest(&mut store, &[codec.clone(), first.clone()]); + load_chunks(&mut store, &mut cache, &[codec, first]); + + let video_stream = playable_stream(&mut cache, &store); + assert_eq!( + video_stream + .read() + .video_renderer + .data_descr() + .samples + .num_elements(), + 1 + ); + + let second = Arc::new(static_image_chunk()); + load_chunks(&mut store, &mut cache, std::slice::from_ref(&second)); + + let guard = video_stream.read(); + let descr = guard.video_renderer.data_descr(); + assert_eq!(descr.samples.num_elements(), 1); + assert_eq!( + descr.samples[descr.samples.min_index()].source_primary_id(), + Some(second.id().as_tuid()) + ); +} diff --git a/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/mod.rs b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/mod.rs new file mode 100644 index 000000000000..78e97dff97e8 --- /dev/null +++ b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/mod.rs @@ -0,0 +1,833 @@ +mod encoded_depth_image; +mod encoded_image; +mod video_stream; + +use std::{iter::once, ops::Range, sync::Arc}; + +use crossbeam::channel::{Receiver, Sender}; +use re_chunk::{Chunk, RowId, TimeInt, Timeline}; +use re_entity_db::EntityDb; +use re_log_types::{AbsoluteTimeRange, external::re_tuid::Tuid}; +use re_sdk_types::archetypes::VideoStream; +use re_sdk_types::components::VideoCodec; +use re_video::player::{GetVideoSource, VideoPlayer, VideoPlayerError, VideoSampleDecoder}; +use re_video::{ + AV1_TEST_INTER_FRAME, AV1_TEST_KEYFRAME, AsyncDecoder, SampleIndex, SampleMetadataState, Time, + VideoDataDescription, +}; + +use crate::{ + Cache as _, SharablePlayableVideoStream, VideoStreamCache, VideoStreamProcessingError, +}; + +struct TestDecoder { + sender: re_video::Sender>, + sample_tx: Sender, + min_num_samples_to_enqueue_ahead: usize, +} + +impl AsyncDecoder for TestDecoder { + fn submit_chunk(&mut self, chunk: re_video::Chunk) -> re_video::DecodeResult<()> { + re_quota_channel::send_crossbeam(&self.sample_tx, chunk.sample_idx).unwrap(); + + self.sender + .send(Ok(re_video::Frame { + content: re_video::FrameContent { + data: Vec::new(), + width: 0, + height: 0, + format: re_video::PixelFormat::Rgb8Unorm, + }, + info: re_video::FrameInfo { + is_sync: Some(chunk.is_sync), + sample_idx: Some(chunk.sample_idx), + frame_nr: Some(chunk.frame_nr), + presentation_timestamp: chunk.presentation_timestamp, + duration: chunk.duration, + latest_decode_timestamp: Some(chunk.decode_timestamp), + }, + })) + .unwrap(); + + Ok(()) + } + + fn reset( + &mut self, + _video_descr: &re_video::VideoDataDescription, + ) -> re_video::DecodeResult<()> { + Ok(()) + } + + fn min_num_samples_to_enqueue_ahead(&self) -> usize { + self.min_num_samples_to_enqueue_ahead + } +} + +/// A [`GetVideoSource`] for tests that hands back a one-byte placeholder for +/// every sample and forwards each accessed source to a hook. +/// +/// The fake decoder never inspects the bytes, but `SampleMetadata::get` returns +/// `None` for an empty buffer, so the placeholder has to be at least one byte. +/// The hook lets a test observe which sources the player touches. +struct TestVideoSource { + on_source: F, +} + +impl TestVideoSource { + fn new(on_source: F) -> Self { + Self { on_source } + } +} + +impl GetVideoSource for TestVideoSource { + fn get_video_chunk(&self, source: re_video::VideoSource) -> &[u8] { + (self.on_source)(source); + &[0] + } + + fn require_video_source(&self, source: re_video::VideoSource) { + (self.on_source)(source); + } + + fn indicate_video_source(&self, source: re_video::VideoSource) { + (self.on_source)(source); + } +} + +pub(super) struct TestVideoPlayer { + video: VideoPlayer<()>, + sample_rx: Receiver, + video_descr: VideoDataDescription, + video_descr_source: Option VideoDataDescription>>, + time: f64, +} + +impl TestVideoPlayer { + fn from_descr(video_descr: VideoDataDescription) -> Self { + #![expect(clippy::disallowed_methods)] // it's a test + let (sample_tx, sample_rx) = crossbeam::channel::unbounded(); + let video = VideoPlayer::new_with_decoder( + VideoSampleDecoder::new("test_decoder".to_owned(), |sender| { + Ok(Box::new(TestDecoder { + sample_tx, + min_num_samples_to_enqueue_ahead: 2, + sender, + })) + }) + .unwrap(), + ); + + Self { + video, + sample_rx, + video_descr, + video_descr_source: None, + time: 0.0, + } + } + + fn from_stream(stream: SharablePlayableVideoStream) -> Self { + let video_descr_source = + Box::new(move || stream.read_arc().video_renderer.data_descr().clone()); + let mut this = Self::from_descr(video_descr_source()); + + this.video_descr_source = Some(video_descr_source); + + this + } + + fn play(&mut self, range: Range, time_step: f64) -> Result<(), VideoPlayerError> { + self.play_with_buffer( + range, + time_step, + &TestVideoSource::new(|_: re_video::VideoSource| {}), + ) + } + + fn play_with_buffer( + &mut self, + range: Range, + time_step: f64, + video_source: &dyn GetVideoSource, + ) -> Result<(), VideoPlayerError> { + if let Some(source) = &self.video_descr_source { + self.video_descr = source(); + } + self.time = range.start; + for i in 0..((range.end - self.time) / time_step).next_down().floor() as i32 { + let time = self.time + i as f64 * time_step; + self.frame_at(time, video_source)?; + } + + self.time = range.end; + + Ok(()) + } + + fn frame_at( + &mut self, + time: f64, + video_source: &dyn GetVideoSource, + ) -> Result<(), VideoPlayerError> { + self.video.frame_at( + Time::from_secs(time, re_video::Timescale::NANOSECOND), + &self.video_descr, + &mut |(), _| Ok(()), + video_source, + )?; + + Ok(()) + } + + #[track_caller] + fn expect_decoded_samples(&self, samples: impl IntoIterator) { + let received = self.sample_rx.try_iter().collect::>(); + let expected = samples.into_iter().collect::>(); + + if let Some((e, r)) = std::iter::zip(&expected, &received).find(|(a, b)| a != b) { + panic!( + " Expected: {expected:?}\n Received: {received:?}\nFirst Issue: expected {e}, got {r}" + ); + } + } + + fn set_sample(&mut self, idx: SampleIndex, mut sample: SampleMetadataState) { + if let Some(sample) = sample.sample_mut() { + sample.frame_nr = idx as u32; + sample.decode_timestamp = re_video::Time::from_secs( + sample + .decode_timestamp + .into_secs(re_video::Timescale::NANOSECOND) + - 0.1, + re_video::Timescale::NANOSECOND, + ); + } + + match ( + self.video_descr.samples[idx] + .sample() + .is_some_and(|s| s.is_sync), + sample.sample().is_some_and(|s| s.is_sync), + ) { + (false, false) | (true, true) => {} + + (true, false) => { + let keyframe_idx = self + .video_descr + .keyframe_indices + .partition_point(|i| *i < idx); + + self.video_descr.keyframe_indices.remove(keyframe_idx); + } + + (false, true) => { + let keyframe_idx = self + .video_descr + .keyframe_indices + .partition_point(|i| *i < idx); + + self.video_descr.keyframe_indices.insert(keyframe_idx, idx); + } + } + self.video_descr.samples[idx] = sample; + + super::update_sample_durations(idx..idx + 1, &mut self.video_descr.samples).unwrap(); + } +} + +fn unloaded(time: f64) -> SampleMetadataState { + let time = Time::from_secs(time, re_video::Timescale::NANOSECOND); + SampleMetadataState::Unloaded { + source_id: Tuid::new(), + min_dts: time, + } +} + +/// An inter frame +fn frame(time: f64) -> SampleMetadataState { + let time = Time::from_secs(time, re_video::Timescale::NANOSECOND); + SampleMetadataState::Present(re_video::SampleMetadata { + is_sync: false, + decode_timestamp: time, + presentation_timestamp: time, + + // Assigned later. + frame_nr: 0, + duration: None, + + // Not relevant for these tests. + source: re_video::VideoSource::id(Tuid::new(), Tuid::new()), + }) +} + +fn keyframe(time: f64) -> SampleMetadataState { + let time = Time::from_secs(time, re_video::Timescale::NANOSECOND); + SampleMetadataState::Present(re_video::SampleMetadata { + is_sync: true, + decode_timestamp: time, + presentation_timestamp: time, + + // Assigned later. + frame_nr: 0, + duration: None, + + // Not relevant for these tests. + source: re_video::VideoSource::id(Tuid::new(), Tuid::new()), + }) +} + +fn create_video( + samples: impl IntoIterator, +) -> Result { + let mut samples: re_video::StableIndexDeque = + samples.into_iter().collect(); + + let mut keyframe_indices = Vec::new(); + for (idx, sample) in samples.iter_indexed_mut() { + if let Some(sample) = sample.sample_mut() { + sample.frame_nr = idx as u32; + sample.decode_timestamp = re_video::Time::from_secs( + sample + .decode_timestamp + .into_secs(re_video::Timescale::NANOSECOND) + - 0.1, + re_video::Timescale::NANOSECOND, + ); + if sample.is_sync { + keyframe_indices.push(idx); + } + } + } + + super::update_sample_durations(0..samples.next_index(), &mut samples)?; + + let video_descr = VideoDataDescription { + delivery_method: re_video::VideoDeliveryMethod::Stream { + last_time_updated_samples: std::time::Instant::now(), + }, + keyframe_indices, + samples_statistics: re_video::SamplesStatistics::new(&samples), + samples, + + // Unused for these tests. + codec: re_video::VideoCodec::H265, + encoding_details: None, + mp4_tracks: Default::default(), + timescale: None, + }; + + Ok(TestVideoPlayer::from_descr(video_descr)) +} + +fn test_simple_video(mut video: TestVideoPlayer, count: usize, dt: f64, max_time: f64) { + re_log::setup_logging(); + + video.play(0.0..max_time, dt).unwrap(); + + video.expect_decoded_samples(0..count); + + // try again at 0.5x speed. + + video.play(0.0..max_time, dt * 0.5).unwrap(); + + video.expect_decoded_samples(0..count); + + // and at 2x speed. + + video.play(0.0..max_time, dt * 2.0).unwrap(); + + video.expect_decoded_samples(0..count); +} + +#[test] +fn player_all_keyframes() { + let count = 10; + let dt = 0.1; + let max_time = count as f64 * dt; + let video = create_video((0..count).map(|t| keyframe(t as f64 * dt))).unwrap(); + + test_simple_video(video, count, dt, max_time); +} + +#[test] +fn player_one_keyframe() { + let count = 10; + let dt = 0.1; + let max_time = count as f64 * dt; + let video = create_video(std::iter::chain( + once(keyframe(0.0)), + (1..count).map(|t| frame(t as f64 * dt)), + )) + .unwrap(); + + test_simple_video(video, count, dt, max_time); +} + +#[test] +fn player_keyframes_then_frames() { + let count = 50usize; + let keyframe_range_size = 10; + let dt = 0.1; + let max_time = count as f64 * dt; + let video = create_video((0..count).map(|t| { + let time = t as f64 * dt; + if t.is_multiple_of(keyframe_range_size) { + keyframe(time) + } else { + frame(time) + } + })) + .unwrap(); + + test_simple_video(video, count, dt, max_time); +} + +#[test] +fn player_irregular() { + let samples = [ + keyframe(0.0), + keyframe(0.1), + frame(0.11), + frame(0.12), + frame(0.125), + frame(0.13), + keyframe(1.0), + frame(2.0), + frame(50.0), + keyframe(1000.0), + keyframe(2000.0), + frame(2001.0), + frame(2201.0), + frame(2221.0), + ]; + let count = samples.len(); + let video = create_video(samples).unwrap(); + + test_simple_video(video, count, 0.1, 2500.0); +} + +#[test] +fn player_unsorted() { + let samples = [keyframe(0.0), keyframe(1.0), keyframe(2.0), keyframe(1.0)]; + let Err(err) = create_video(samples) else { + panic!("Video creation shouldn't succeed for unordered samples"); + }; + + assert!( + matches!(err, VideoStreamProcessingError::OutOfOrderSamples), + "Expected {} got {err}", + VideoStreamProcessingError::OutOfOrderSamples + ); +} + +/// Walking back from the first sample of a stream whose front has been popped +/// should return `None` rather than panic. +#[test] +fn previous_presented_sample_after_front_eviction() { + let mut samples: re_video::StableIndexDeque = + (0..10).map(|t| keyframe(t as f64 * 0.1)).collect(); + + for sample in samples.iter_mut() { + if let Some(sample) = sample.sample_mut() { + sample.decode_timestamp = sample.presentation_timestamp; + } + } + + let last_dropped = samples.min_index() + 2; + samples.remove_all_with_index_smaller_equal(last_dropped); + + let keyframe_indices: Vec<_> = samples + .iter_indexed() + .filter_map(|(idx, s)| s.sample().is_some_and(|s| s.is_sync).then_some(idx)) + .collect(); + + let video_descr = VideoDataDescription { + delivery_method: re_video::VideoDeliveryMethod::Stream { + last_time_updated_samples: std::time::Instant::now(), + }, + keyframe_indices, + samples_statistics: re_video::SamplesStatistics::new(&samples), + samples, + + codec: re_video::VideoCodec::H265, + encoding_details: None, + mp4_tracks: Default::default(), + timescale: None, + }; + + let first_sample = video_descr + .samples + .get(video_descr.samples.min_index()) + .and_then(|s| s.sample()) + .expect("first surviving sample should be present") + .clone(); + + assert!( + video_descr + .previous_presented_sample(&first_sample) + .is_none() + ); +} + +#[track_caller] +pub(super) fn assert_loading(err: Result<(), VideoPlayerError>) { + let err = err.unwrap_err(); + assert!( + matches!(err, VideoPlayerError::UnloadedSampleData(_)), + "Expected 'VideoPlayerError::UnloadedSampleData(_)' got '{err}'", + ); +} + +#[test] +fn player_with_unloaded() { + let mut video = create_video([ + keyframe(0.), + frame(1.), + frame(2.), + frame(3.), + unloaded(4.), + unloaded(5.), + unloaded(6.), + unloaded(7.), + keyframe(8.), + frame(9.), + frame(10.), + frame(11.), + keyframe(12.), + frame(13.), + frame(14.), + frame(15.), + unloaded(16.), + unloaded(17.), + unloaded(18.), + unloaded(19.), + keyframe(20.), + frame(21.), + frame(22.), + frame(23.), + ]) + .unwrap(); + + video.play(0.0..3.0, 1.0).unwrap(); + video.expect_decoded_samples(0..3); + + assert_loading(video.play(4.0..8.0, 1.0)); + video.expect_decoded_samples(None); + + video.play(8.0..15.0, 1.0).unwrap(); + video.expect_decoded_samples(8..15); + + video.play(20.0..24.0, 1.0).unwrap(); + video.expect_decoded_samples(20..24); + + // Play & load progressively + video.play(0.0..3.0, 1.0).unwrap(); + + video.set_sample(4, keyframe(4.)); + video.set_sample(5, frame(5.)); + video.set_sample(6, frame(6.)); + video.set_sample(7, frame(7.)); + + video.play(4.0..15.0, 1.0).unwrap(); + + video.set_sample(16, keyframe(16.)); + video.set_sample(17, frame(17.)); + + video.play(16.0..17.0, 1.0).unwrap(); + + video.set_sample(18, frame(18.)); + video.set_sample(19, frame(19.)); + + video.play(18.0..24.0, 1.0).unwrap(); + + video.expect_decoded_samples(0..24); +} + +#[test] +fn player_fetching_unloaded() { + let samples = [ + unloaded(0.), + unloaded(1.), + frame(2.), + unloaded(3.), + keyframe(4.), + unloaded(5.), + frame(6.), + keyframe(7.), + frame(8.), + frame(9.), + frame(10.), + unloaded(11.), + frame(12.), + frame(13.), + frame(14.), + ]; + + let mut video = create_video(samples.clone()).unwrap(); + + let fetched = parking_lot::RwLock::new(Vec::new()); + assert_loading(video.play_with_buffer( + 2.0..4.0, + 1.0, + &TestVideoSource::new(|source: re_video::VideoSource| { + fetched.write().push(source.primary_id()); + }), + )); + assert_eq!( + fetched.read().as_slice(), + &[ + samples[2].source_primary_id(), + samples[1].source_primary_id() + ] + ); + + video.expect_decoded_samples(None); + + fetched.write().clear(); + assert_loading(video.play_with_buffer( + 4.0..6.0, + 1.0, + &TestVideoSource::new(|source: re_video::VideoSource| { + fetched.write().push(source.primary_id()); + }), + )); + assert_eq!( + fetched.read().as_slice(), + &[ + // First keyframe at 4.0 from `request_keyframe_before` + samples[4].source_primary_id(), + // Then again keyframe at 4.0 when enqueueing it + samples[4].source_primary_id(), + // Then unloaded when pre-loading + samples[5].source_primary_id() + ] + ); + + video.expect_decoded_samples(std::iter::once(4)); + + fetched.write().clear(); + assert_loading(video.play_with_buffer( + 10.0..12.0, + 1.0, + &TestVideoSource::new(|source: re_video::VideoSource| { + fetched.write().push(source.primary_id()); + }), + )); + assert_eq!( + fetched.read().as_slice(), + &[ + // in `request_keyframe_before` (reversed) + samples[10].source_primary_id(), + samples[9].source_primary_id(), + samples[8].source_primary_id(), + samples[7].source_primary_id(), + // in `enqueue_sample_range` + samples[7].source_primary_id(), + samples[8].source_primary_id(), + samples[9].source_primary_id(), + samples[10].source_primary_id(), + // Then unloaded when pre-loading + samples[11].source_primary_id(), + ] + ); + + video.expect_decoded_samples(7..11); + + fetched.write().clear(); + assert_loading(video.play_with_buffer( + 12.0..14.0, + 1.0, + &TestVideoSource::new(|source: re_video::VideoSource| { + let primary_id = source.primary_id(); + let i = samples + .iter() + .position(|c| c.source_primary_id() == primary_id) + .unwrap(); + eprintln!( + "\n#{i}\n{}", + std::backtrace::Backtrace::capture() + .to_string() + .lines() + .filter(|l| l.contains("player")) + .collect::>() + .join("\n") + ); + fetched.write().push(primary_id); + }), + )); + assert_eq!( + fetched.read().as_slice(), + // Both in `request_keyframe_before` (reversed). + &[ + samples[12].source_primary_id(), + samples[11].source_primary_id() + ] + ); + + video.expect_decoded_samples(None); +} + +impl TestVideoPlayer { + pub(super) fn play_store( + &mut self, + range: Range, + time_step: f64, + store: &re_entity_db::EntityDb, + ) -> Result<(), VideoPlayerError> { + self.play_store_with_component( + range, + time_step, + store, + re_sdk_types::archetypes::VideoStream::descriptor_sample().component, + ) + } + + pub(super) fn play_store_with_component( + &mut self, + range: Range, + time_step: f64, + store: &re_entity_db::EntityDb, + sample_component: re_sdk_types::ComponentIdentifier, + ) -> Result<(), VideoPlayerError> { + let engine = store.storage_engine(); + let video_source = super::VideoStoreSource { + store: engine.store(), + sample_component, + indicate: true, + }; + self.play_with_buffer(range, time_step, &video_source) + } +} + +pub(super) const STREAM_ENTITY: &str = "/stream"; +pub(super) const TIMELINE_NAME: &str = "video"; + +#[track_caller] +pub(super) fn unload_chunks( + store: &EntityDb, + cache: &mut super::VideoStreamCache, + keep_range: Range, +) { + let loaded_chunks_before = store.storage_engine().store().num_physical_chunks(); + let store_events = store.gc(&re_chunk_store::GarbageCollectionOptions { + target: re_chunk_store::GarbageCollectionTarget::Everything, + time_budget: std::time::Duration::from_secs(u64::MAX), + protect_latest: 0, + protected_chunks: Default::default(), + protected_time_ranges: std::iter::once(( + re_chunk::TimelineName::from(TIMELINE_NAME), + AbsoluteTimeRange::new( + TimeInt::from_secs(keep_range.start), + TimeInt::from_secs(keep_range.end.next_down()), + ), + )) + .collect(), + furthest_from: None, + perform_deep_deletions: false, + }); + + let loaded_chunks_after = store.storage_engine().store().num_physical_chunks(); + + assert!( + loaded_chunks_before > loaded_chunks_after, + "Expected some chunks to be gc'd" + ); + + cache.on_store_events(&store_events.iter().collect::>(), store); +} + +pub(super) fn load_chunks( + store: &mut EntityDb, + cache: &mut super::VideoStreamCache, + chunks: &[Arc], +) { + let mut store_events = Vec::::new(); + + for chunk in chunks { + store_events.extend(store.add_chunk(chunk).unwrap()); + } + + cache.on_store_events(&store_events.iter().collect::>(), store); +} + +pub(super) fn codec_chunk() -> Chunk { + let mut builder = Chunk::builder(STREAM_ENTITY); + + builder = builder.with_archetype( + RowId::new(), + [( + Timeline::new_duration(TIMELINE_NAME), + TimeInt::from_secs(0.0), + )], + &VideoStream::new(VideoCodec::AV1), + ); + + builder.build().unwrap() +} + +pub(super) fn video_chunk(start_time: f64, dt: f64, gop_count: u64, samples_per_gop: u64) -> Chunk { + let timeline = Timeline::new_duration(TIMELINE_NAME); + let mut builder = Chunk::builder(STREAM_ENTITY); + + for i in 0..gop_count { + let gop_start_time = start_time + (i * samples_per_gop) as f64 * dt; + builder = builder.with_archetype( + RowId::new(), + [(timeline, TimeInt::from_secs(gop_start_time))], + &VideoStream::update_fields().with_sample(AV1_TEST_KEYFRAME), + ); + + for i in 1..samples_per_gop { + let time = gop_start_time + i as f64 * dt; + builder = builder.with_archetype( + RowId::new(), + [(timeline, TimeInt::from_secs(time))], + &VideoStream::update_fields().with_sample(AV1_TEST_INTER_FRAME), + ); + } + } + + builder.build().unwrap() +} + +pub(super) fn playable_stream( + cache: &mut VideoStreamCache, + store: &EntityDb, +) -> SharablePlayableVideoStream { + cache + .video_entry( + store, + &re_chunk::EntityPath::from(STREAM_ENTITY), + TIMELINE_NAME.into(), + re_video::DecodeSettings { + hw_acceleration: Default::default(), + ffmpeg_path: Some(std::path::PathBuf::from("/not/used")), + }, + re_chunk_store::ChunkTrackingMode::Report, + ) + .unwrap() +} + +pub(super) fn load_into_rrd_manifest(store: &mut EntityDb, chunks: &[Arc]) { + let manifest = re_log_encoding::RrdManifest::build_in_memory_from_chunks( + store.store_id().clone(), + chunks.iter().map(|c| &**c), + ) + .unwrap(); + + store.add_rrd_manifest_message(manifest); +} + +#[track_caller] +pub(super) fn assert_splits_happened(store: &EntityDb) { + let engine = store.storage_engine(); + let store = engine.store(); + + assert!( + store + .iter_physical_chunks() + .any(|c| { store.descends_from_a_split(&c.id()) }), + "This test is testing how the video cache handles splits, but no split happened" + ); +} diff --git a/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/video_stream.rs b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/video_stream.rs new file mode 100644 index 000000000000..5e2113ee6209 --- /dev/null +++ b/crates/viewer/re_viewer_context/src/cache/video_stream_cache/test_player/video_stream.rs @@ -0,0 +1,779 @@ +use std::{iter::once, sync::Arc}; + +use re_chunk::Chunk; +use re_entity_db::EntityDb; +use re_log_types::StoreId; +use re_sdk_types::archetypes::VideoStream; + +use crate::VideoStreamCache; + +use super::{ + STREAM_ENTITY, TIMELINE_NAME, TestVideoPlayer, assert_loading, assert_splits_happened, + codec_chunk, load_chunks, load_into_rrd_manifest, playable_stream, unload_chunks, video_chunk, +}; + +#[test] +fn cache_with_manifest() { + let mut cache = VideoStreamCache::default(); + + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let chunks: Vec<_> = std::iter::chain( + (0..10).map(|i| video_chunk(i as f64, 0.25, 1, 4)), + once(codec_chunk()), + ) + .map(Arc::new) + .collect(); + + load_into_rrd_manifest(&mut store, &chunks); + + // load codec chunk + load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); + + let video_stream = playable_stream(&mut cache, &store); + + // Load some chunks. + load_chunks(&mut store, &mut cache, &chunks[4..5]); + + let mut player = TestVideoPlayer::from_stream(video_stream); + + assert_loading(player.play_store(6.0..10.0, 0.25, &store)); + player.expect_decoded_samples(None); + + player.play_store(4.0..4.75, 0.25, &store).unwrap(); + + player.expect_decoded_samples(16..19); + + load_chunks(&mut store, &mut cache, &chunks[0..2]); + + player.play_store(0.0..1.75, 0.25, &store).unwrap(); + + load_chunks(&mut store, &mut cache, &chunks[2..4]); + + player.play_store(1.75..4.75, 0.25, &store).unwrap(); + + player.expect_decoded_samples(0..19); + + unload_chunks(&store, &mut cache, 4.0..5.0); + + load_chunks(&mut store, &mut cache, &chunks[4..7]); + + player.play_store(4.75..6.75, 0.25, &store).unwrap(); + + player.expect_decoded_samples(20..27); + + // Load the ones we unloaded again + load_chunks(&mut store, &mut cache, &chunks[0..4]); + + player.play_store(0.0..6.75, 0.25, &store).unwrap(); + + player.expect_decoded_samples(0..27); +} + +#[test] +fn cache_with_streaming() { + let mut cache = VideoStreamCache::default(); + + let mut store = EntityDb::with_store_config( + StoreId::recording("test", "test"), + true, + re_chunk_store::ChunkStoreConfig { + enable_changelog: true, + chunk_max_bytes: u64::MAX, + chunk_max_rows: 12, + chunk_max_rows_if_unsorted: 12, + }, + ); + + let chunk_count = 100; + + let dt = 0.25; + let chunks: Vec<_> = std::iter::chain( + (0..chunk_count).map(|i| video_chunk(i as f64, dt, 1, 4)), + once(codec_chunk()), + ) + .map(Arc::new) + .collect(); + + // load codec chunk + load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Load all sample chunks. + load_chunks(&mut store, &mut cache, &chunks[0..chunk_count]); + + player.play_store(0.0..25.0, dt, &store).unwrap(); + + player.expect_decoded_samples(0..chunk_count); + + unload_chunks(&store, &mut cache, 15.0..25.0); + + // Try dropping chunks at the start. + player.play_store(15.0..25.0, dt, &store).unwrap(); + + player.expect_decoded_samples(60..chunk_count); +} + +#[test] +fn cache_with_manifest_and_streaming() { + let mut cache = VideoStreamCache::default(); + + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let chunks: Vec<_> = std::iter::chain( + once(codec_chunk()), + (0..6).map(|i| video_chunk(i as f64 + 1.0, 0.25, 1, 4)), + ) + .map(Arc::new) + .collect(); + + // Load first 5 chunks into the manifest. + load_into_rrd_manifest(&mut store, &chunks[..5]); + + // load codec chunk + load_chunks(&mut store, &mut cache, &chunks[..1]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Load some chunks. + load_chunks(&mut store, &mut cache, &chunks[3..5]); + + assert_loading(player.play_store(1.0..3.0, 0.25, &store)); + player.expect_decoded_samples(None); + + player.play_store(3.0..5.0, 0.25, &store).unwrap(); + player.expect_decoded_samples(8..16); + + load_chunks(&mut store, &mut cache, &chunks[5..6]); + player.play_store(5.0..6.0, 0.25, &store).unwrap(); + player.expect_decoded_samples(16..20); + + load_chunks(&mut store, &mut cache, &chunks[6..7]); + player.play_store(6.0..7.0, 0.25, &store).unwrap(); + player.expect_decoded_samples(20..24); + + player.play_store(3.0..7.0, 0.25, &store).unwrap(); + player.expect_decoded_samples(8..24); + + load_chunks(&mut store, &mut cache, &chunks[1..3]); + player.play_store(1.0..7.0, 0.25, &store).unwrap(); + player.expect_decoded_samples(0..24); + + unload_chunks(&store, &mut cache, 4.0..6.0); + // Check that all remaining samples are still playable. + player.play_store(4.0..6.0, 0.25, &store).unwrap(); + player.expect_decoded_samples(12..20); +} + +#[test] +fn cache_with_streaming_splits() { + let mut cache = VideoStreamCache::default(); + + let mut store = EntityDb::with_store_config( + StoreId::recording("test", "test"), + true, + re_chunk_store::ChunkStoreConfig { + enable_changelog: true, + chunk_max_bytes: u64::MAX, + chunk_max_rows: 100, + chunk_max_rows_if_unsorted: 100, + }, + ); + + let chunk_count = 4; + let gops_per_chunk = 10; + let samples_per_gop = 200; + + let dt = 0.1; + + let samples_per_chunk = gops_per_chunk * samples_per_gop; + let sample_count = chunk_count * samples_per_chunk; + let time_per_chunk = samples_per_chunk as f64 * dt; + + let chunks: Vec<_> = std::iter::chain( + (0..chunk_count).map(|i| { + video_chunk( + i as f64 * time_per_chunk, + dt, + gops_per_chunk, + samples_per_gop, + ) + }), + once(codec_chunk()), + ) + .map(Arc::new) + .collect(); + + // load codec chunk + load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Load all sample chunks. + load_chunks(&mut store, &mut cache, &chunks[0..4]); + + player + .play_store(0.0..sample_count as f64 * dt, dt, &store) + .unwrap(); + + player.expect_decoded_samples(0..sample_count as re_video::SampleIndex); + + assert_splits_happened(&store); +} + +#[test] +fn cache_with_manifest_splits() { + let mut cache = VideoStreamCache::default(); + + let mut store = EntityDb::with_store_config( + StoreId::recording("test", "test"), + true, + re_chunk_store::ChunkStoreConfig { + enable_changelog: true, + chunk_max_bytes: u64::MAX, + chunk_max_rows: 100, + chunk_max_rows_if_unsorted: 100, + }, + ); + + let chunk_count = 4; + let gops_per_chunk = 10; + let samples_per_gop = 200; + + let dt = 0.1; + let samples_per_chunk = gops_per_chunk * samples_per_gop; + let time_per_chunk = samples_per_chunk as f64 * dt; + + let chunks: Vec<_> = std::iter::chain( + (0..chunk_count).map(|i| { + video_chunk( + time_per_chunk * i as f64, + dt, + gops_per_chunk, + samples_per_gop, + ) + }), + once(codec_chunk()), + ) + .map(Arc::new) + .collect(); + + load_into_rrd_manifest(&mut store, &chunks); + + // load codec chunk + load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + load_chunks(&mut store, &mut cache, &chunks[1..2]); + + player + .play_store(time_per_chunk..time_per_chunk * 2.0 - dt, dt, &store) + .unwrap(); + + let samples_per_chunk = samples_per_chunk as usize; + player.expect_decoded_samples(samples_per_chunk..samples_per_chunk * 2 - 1); + + load_chunks(&mut store, &mut cache, &chunks[2..3]); + player + .play_store(time_per_chunk * 2.0..time_per_chunk * 3.0 - dt, dt, &store) + .unwrap(); + + player.expect_decoded_samples(samples_per_chunk * 2..samples_per_chunk * 3 - 1); + + let min_loaded = 1.7; + let max_loaded = 2.3; + + unload_chunks( + &store, + &mut cache, + time_per_chunk * min_loaded..time_per_chunk * max_loaded, + ); + + // Assert that the beginning/end splits have been gc'd + assert_loading(player.play_store(time_per_chunk..time_per_chunk * 1.5, dt, &store)); + player.expect_decoded_samples(None); + + let play_store = player.play_store(time_per_chunk * 2.5..time_per_chunk * 3.0 - dt, dt, &store); + player.expect_decoded_samples(None); + assert_loading(play_store); + + player + .play_store( + time_per_chunk * min_loaded..time_per_chunk * max_loaded - dt, + dt, + &store, + ) + .unwrap(); + + let end = (samples_per_chunk as f64 * max_loaded) as usize; + player.expect_decoded_samples((samples_per_chunk as f64 * min_loaded).ceil() as usize..end); + + load_chunks(&mut store, &mut cache, &chunks[0..2]); + player + .play_store(0.0..time_per_chunk * max_loaded - dt, dt, &store) + .unwrap(); + + player.expect_decoded_samples(0..end); + + assert_splits_happened(&store); +} + +#[test] +fn cache_with_unordered_chunks() { + use re_chunk::{RowId, TimeInt, Timeline}; + use re_video::AV1_TEST_INTER_FRAME; + use re_video::AV1_TEST_KEYFRAME; + + let mut cache = VideoStreamCache::default(); + + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let chunk_count = 100; + + let gop_count = 1; + let samples_per_gop = 4; + + let dt = 0.25; + let chunks: Vec<_> = std::iter::chain( + (0..chunk_count).map(|i| { + let timeline = Timeline::new_duration(TIMELINE_NAME); + let mut builder = Chunk::builder(STREAM_ENTITY); + let mut row_ids: Vec<_> = (0..gop_count * samples_per_gop) + .map(|_| RowId::new()) + .collect(); + + use rand::SeedableRng as _; + use rand::seq::SliceRandom as _; + let mut rng = rand::rngs::StdRng::seed_from_u64(i as u64); + + // Shuffle row ids to make the chunk (very likely) unsorted on the timeline. + row_ids.shuffle(&mut rng); + + let start_time = i as f64; + for i in 0..gop_count { + let gop_start_time = start_time + (i * samples_per_gop) as f64 * dt; + + builder = builder.with_archetype( + row_ids.pop().unwrap(), + [(timeline, TimeInt::from_secs(gop_start_time))], + &VideoStream::update_fields().with_sample(AV1_TEST_KEYFRAME), + ); + + for i in 1..samples_per_gop { + let time = gop_start_time + i as f64 * dt; + builder = builder.with_archetype( + row_ids.pop().unwrap(), + [(timeline, TimeInt::from_secs(time))], + &VideoStream::update_fields().with_sample(AV1_TEST_INTER_FRAME), + ); + } + } + + let mut chunk = builder.build().unwrap(); + + chunk.sort_by_row_ids_if_needed(); + + chunk + }), + once(codec_chunk()), + ) + .map(Arc::new) + .collect(); + + assert!( + chunks.iter().any(|chunk| { + chunk + .timelines() + .get(&re_chunk::TimelineName::from(TIMELINE_NAME)) + .is_some_and(|t| !t.is_sorted()) + }), + "We are testing unsorted chunks, at least one should end up unsorted" + ); + + // load codec chunk + load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Load all sample chunks. + load_chunks(&mut store, &mut cache, &chunks[0..chunk_count]); + + player.play_store(0.0..25.0, dt, &store).unwrap(); + + player.expect_decoded_samples(0..chunk_count); +} + +/// Loads chunks in non-chronological order so that a later-arriving chunk +/// has timestamps that fall before existing samples, triggering +/// out-of-order detection and re-merge. +#[test] +fn cache_with_out_of_order_chunk_arrival() { + let mut cache = VideoStreamCache::default(); + + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let dt = 0.25; + let samples_per_gop = 4; + + // 10 chunks, each 1 GOP of 4 samples. + let chunk_count = 10usize; + let chunks: Vec<_> = std::iter::chain( + (0..chunk_count).map(|i| video_chunk(i as f64, dt, 1, samples_per_gop)), + once(codec_chunk()), + ) + .map(Arc::new) + .collect(); + + // Load codec chunk and create the cache entry. + load_chunks(&mut store, &mut cache, &chunks[chunks.len() - 1..]); + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Load chunks 0, 1, 2 in order. + load_chunks(&mut store, &mut cache, &chunks[0..3]); + + player.play_store(0.0..3.0, dt, &store).unwrap(); + player.expect_decoded_samples(0..12); + + // Skip chunk 3 and load chunk 4 first, still in order relative to + // what was already loaded. + load_chunks(&mut store, &mut cache, &chunks[4..5]); + + player.play_store(4.0..5.0, dt, &store).unwrap(); + player.expect_decoded_samples(12..16); + + // Now load chunk 3 which has times [3.0, 3.25, 3.5, 3.75] -- this + // falls between the already-loaded chunks 2 and 4, triggering the + // out-of-order / delta re-merge path. + load_chunks(&mut store, &mut cache, &chunks[3..4]); + + // The cache entry should still exist (delta re-merge, not removal). + assert!( + cache + .entries + .contains_key(&crate::cache::video_stream_cache::VideoStreamKey { + entity_path: re_chunk::EntityPath::from(STREAM_ENTITY).hash(), + timeline: re_chunk::TimelineName::from(TIMELINE_NAME), + sample_component: VideoStream::descriptor_sample().component, + }), + "Cache entry should survive delta re-merge" + ); + + // All 20 samples (chunks 0-4) should be playable. + player.play_store(0.0..5.0, dt, &store).unwrap(); + player.expect_decoded_samples(0..20); + + // Load chunks 7, 8, 9 (skipping 5, 6). + load_chunks(&mut store, &mut cache, &chunks[7..10]); + + player.play_store(7.0..10.0, dt, &store).unwrap(); + player.expect_decoded_samples(20..32); + + // Now load the skipped chunks 5 and 6 out of order. + load_chunks(&mut store, &mut cache, &chunks[5..7]); + + // Everything from 0 through 10 should work. + player.play_store(0.0..10.0, dt, &store).unwrap(); + player.expect_decoded_samples(0..40); +} + +/// Out-of-order chunk arrival followed by compaction, where a +/// `ChunkSampleRange` has less samples than the amount of samples it spans. +#[test] +fn cache_out_of_order_arrival_with_compaction() { + let mut cache = VideoStreamCache::default(); + + let mut store = EntityDb::with_store_config( + StoreId::recording("test", "test"), + true, + re_chunk_store::ChunkStoreConfig { + enable_changelog: true, + chunk_max_bytes: u64::MAX, + chunk_max_rows: 4, + chunk_max_rows_if_unsorted: 4, + }, + ); + + let codec_chunk = Arc::new(codec_chunk()); + + // Create chunk0 with 4 rows so it won't compact. + let chunk0 = Arc::new(video_chunk(0.0, 2.0, 1, 4)); // times: 0.0, 2.0, 4.0, 6.0 + + // Create chunk1 and chunk2 with less than 4 rows combined so they compact. + let chunk1 = Arc::new(video_chunk(5.0, 2.0, 1, 2)); // times: 5.0, 7.0 + let chunk2 = Arc::new(video_chunk(8.0, 0.0, 1, 1)); // time: 8.0 + + let codec_chunk_id = codec_chunk.id(); + let chunk0_id = chunk0.id(); + let chunk1_id = chunk1.id(); + let chunk2_id = chunk2.id(); + + let replace_id = |s: &str| -> String { + s.replace( + &codec_chunk_id.to_string(), + &format!("chunk_codec {}", codec_chunk_id.short_string()), + ) + .replace( + &chunk0_id.to_string(), + &format!("chunk0 {}", chunk0_id.short_string()), + ) + .replace( + &chunk1_id.to_string(), + &format!("chunk1 {}", chunk1_id.short_string()), + ) + .replace( + &chunk2_id.to_string(), + &format!("chunk2 {}", chunk2_id.short_string()), + ) + }; + + // Load codec chunk and chunk0. + load_chunks(&mut store, &mut cache, &[codec_chunk, chunk0]); + + let video_stream_before = playable_stream(&mut cache, &store); + + let mut player = TestVideoPlayer::from_stream(video_stream_before); + + player.play_store(0.0..8.0, 1.0, &store).unwrap(); + player.expect_decoded_samples(0..4); + + // This triggers out-of-order handling because time 5 < time 6. + // With delta re-merge, the cache entry is NOT cleared. + load_chunks(&mut store, &mut cache, &[chunk1]); + + assert!( + std::iter::zip( + store.storage_engine().store().iter_physical_chunks(), + [Some(codec_chunk_id), Some(chunk0_id), Some(chunk1_id), None], + ) + .all(|(c, expected_id)| { + let eq = Some(c.id()) == expected_id; + + if !eq { + eprintln!( + "Expected {}, got {} with lineage:\n{}", + expected_id + .map(|c| c.short_string()) + .unwrap_or_else(|| "nothing".to_owned()), + c.id().short_string(), + replace_id(&store.storage_engine().store().format_lineage(&c.id())), + ); + } + + eq + }), + "No compaction should've occurred yet" + ); + + // The cache entry should still exist (delta re-merge instead of removal). + assert!( + cache + .entries + .contains_key(&crate::cache::video_stream_cache::VideoStreamKey { + entity_path: re_chunk::EntityPath::from(STREAM_ENTITY).hash(), + timeline: re_chunk::TimelineName::from(TIMELINE_NAME), + sample_component: VideoStream::descriptor_sample().component, + }), + "The video stream cache entry should still exist after delta re-merge" + ); + + // Use the same video stream -- it was re-merged in place. + let video_stream_after = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream_after); + + player.play_store(0.0..8.0, 1.0, &store).unwrap(); + player.expect_decoded_samples(0..6); + + // This should compact with chunk1. + load_chunks(&mut store, &mut cache, &[chunk2]); + + assert!( + store + .storage_engine() + .store() + .iter_physical_chunks() + .any(|c| { + if let Some(re_chunk_store::ChunkDirectLineage::CompactedFrom(chunks)) = + store.storage_engine().store().direct_lineage(&c.id()) + { + *chunks == [chunk1_id, chunk2_id].into_iter().collect() + } else { + false + } + }), + "chunk 1 & 2, should've been compacted.\nchunks:\n{}", + replace_id( + &store + .storage_engine() + .store() + .iter_physical_chunks() + .map(|c| store.storage_engine().store().format_lineage(&c.id())) + .collect::>() + .join("\n\n") + ), + ); + + player.play_store(0.0..9.0, 1.0, &store).unwrap(); + + player.expect_decoded_samples(0..7); +} + +/// When manifest-placed samples are loaded, `from_root` places all unloaded +/// samples at the start of the chunk's time range. A multi-GOP chunk whose +/// second GOP falls after another chunk's samples causes out-of-order +/// detection and re-merge. +#[test] +fn cache_with_manifest_load_resulting_in_incomplete_gop() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let dt = 0.25; + + // Chunk A: 2 GOPs of 4 samples each, times [0, 1.75]. + let chunk_a = video_chunk(0.0, dt, 2, 4); + + // Chunk B: 1 GOP of 3 samples, times [0.875, 1.375]. + let chunk_b = video_chunk(0.875, dt, 1, 3); + + let chunks: Vec<_> = [chunk_a, chunk_b, codec_chunk()] + .into_iter() + .map(Arc::new) + .collect(); + + load_into_rrd_manifest(&mut store, &chunks); + + // Load codec. + load_chunks(&mut store, &mut cache, &chunks[2..3]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Load chunk B first. + load_chunks(&mut store, &mut cache, &chunks[1..2]); + + player.play_store(0.875..1.375, dt, &store).unwrap(); + player.expect_decoded_samples(8..11); + + // Playing in A's unloaded range should fail with loading. + assert_loading(player.play_store(0.0..0.75, dt, &store)); + player.expect_decoded_samples(None); + + // Load chunk A. from_root placed A's 8 samples at time 0 (indices 0-7). + load_chunks(&mut store, &mut cache, &chunks[0..1]); + + // The cache entry should survive the delta re-merge. + assert!( + cache + .entries + .contains_key(&crate::cache::video_stream_cache::VideoStreamKey { + entity_path: re_chunk::EntityPath::from(STREAM_ENTITY).hash(), + timeline: re_chunk::TimelineName::from(TIMELINE_NAME), + sample_component: VideoStream::descriptor_sample().component, + }), + "Cache entry should survive delta re-merge" + ); + + // After re-merge, all 11 samples should be in the correct time order. + player.play_store(0.0..2.0, dt, &store).unwrap(); + player.expect_decoded_samples(0..11); +} + +/// When a conflicting chunk is loaded last, its manifest-placed keyframe +/// may sit right before the affected range. The reorder logic must walk +/// back past that keyframe to include earlier samples whose real +/// timestamps interleave with the conflicting chunk. +#[test] +fn cache_with_manifest_skips_conflicting_chunk_keyframe() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + // Chunk 0: 1 GOP of 2 samples, times [1, 3]. + let chunk_0 = video_chunk(1.0, 2.0, 1, 2); + + // Chunk 1: 1 GOP of 2 samples, times [2, 4]. + let chunk_1 = video_chunk(2.0, 2.0, 1, 2); + + // Chunk 2: 1 GOP of 2 samples, times [5, 6]. + let chunk_2 = video_chunk(5.0, 1.0, 1, 2); + + let chunks: Vec<_> = [chunk_0, chunk_1, chunk_2, codec_chunk()] + .into_iter() + .map(Arc::new) + .collect(); + + load_into_rrd_manifest(&mut store, &chunks); + + // Load codec. + load_chunks(&mut store, &mut cache, &chunks[3..4]); + + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + // Load chunk 0, then chunk 2. + load_chunks(&mut store, &mut cache, &chunks[0..1]); + load_chunks(&mut store, &mut cache, &chunks[2..3]); + + player.play_store(5.0..7.0, 1.0, &store).unwrap(); + player.expect_decoded_samples(4..6); + + // Loading chunk 1 triggers out-of-order. + load_chunks(&mut store, &mut cache, &chunks[1..2]); + + // The cache entry should survive the delta re-merge. + assert!( + cache + .entries + .contains_key(&crate::cache::video_stream_cache::VideoStreamKey { + entity_path: re_chunk::EntityPath::from(STREAM_ENTITY).hash(), + timeline: re_chunk::TimelineName::from(TIMELINE_NAME), + sample_component: VideoStream::descriptor_sample().component, + }), + "Cache entry should survive delta re-merge" + ); + + // After re-merge, all 6 samples should be in the correct time order. + player.play_store(1.0..7.0, 1.0, &store).unwrap(); + player.expect_decoded_samples(0..6); +} + +/// Interleaved chunk arrival followed by GC of the interleaving chunk. +/// The remaining chunk's samples must still be decodable from its own keyframe. +#[test] +fn cache_with_gc_after_interleaved_arrival() { + let mut cache = VideoStreamCache::default(); + let mut store = EntityDb::new(StoreId::recording("test", "test")); + + let codec = Arc::new(codec_chunk()); + let chunk_y = Arc::new(video_chunk(1.0, 1.0, 1, 4)); + // Starts before Y but loaded second, triggering out-of-order handling. + let chunk_x = Arc::new(video_chunk(0.5, 1.0, 2, 1)); + + load_chunks(&mut store, &mut cache, std::slice::from_ref(&codec)); + load_chunks(&mut store, &mut cache, std::slice::from_ref(&chunk_y)); + + // Create a live entry before chunk_x arrives, so handle_deletion runs on it later. + let _ = playable_stream(&mut cache, &store); + + // Loading X after Y triggers handle_out_of_order_chunk, interleaving the deques. + load_chunks(&mut store, &mut cache, std::slice::from_ref(&chunk_x)); + + // Evict X and the codec while keeping Y. + unload_chunks(&store, &mut cache, 2.0..5.0); + + // Reload the codec. + load_chunks(&mut store, &mut cache, std::slice::from_ref(&codec)); + + // The entry was either correctly rebuilt or corrupted. + let video_stream = playable_stream(&mut cache, &store); + let mut player = TestVideoPlayer::from_stream(video_stream); + + player.play_store(2.0..5.0, 0.25, &store).unwrap(); + player.expect_decoded_samples(0..4); +} diff --git a/crates/viewer/re_viewer_context/src/command_sender.rs b/crates/viewer/re_viewer_context/src/command_sender.rs index cf9097823528..9f3270e8a9f5 100644 --- a/crates/viewer/re_viewer_context/src/command_sender.rs +++ b/crates/viewer/re_viewer_context/src/command_sender.rs @@ -6,7 +6,7 @@ use re_chunk_store::external::re_chunk::Chunk; use re_data_source::LogDataSource; use re_log_channel::LogReceiver; use re_log_types::StoreId; -use re_ui::{UICommand, UICommandSender}; +use re_ui::{RecordingCommand, RecordingCommandSender, UICommand, UICommandSender}; use crate::time_control::TimeControlCommand; use crate::{AuthContext, RecordingOrTable, Route, ScreenshotTarget, ViewId}; @@ -34,12 +34,29 @@ pub enum SystemCommand { /// Add a new server to the redap browser. AddRedapServer(re_uri::Origin), + /// Refresh the whole catalog (all datasets & tables) of an already-known redap server. + RefreshRedapServer(re_uri::Origin), + + /// Refresh the contents of a single entry (dataset or table) on a redap server. + RefreshRedapEntry { + origin: re_uri::Origin, + entry_id: re_log_types::EntryId, + }, + /// Remove a server from the redap browser and clean up associated blueprints. RemoveRedapServer(re_uri::Origin), /// Open a modal to edit this redap server. EditRedapServerModal(EditRedapServerModalCommand), + /// A command acting on a specific redap server, + /// e.g. from the command palette or a server context menu. + RedapServer(re_ui::RedapServerCommand), + + /// A command acting on a specific redap entry (dataset or table), + /// e.g. from the command palette or a keyboard shortcut. + Table(re_ui::TableCommand), + /// Activates the setting route. OpenSettings, @@ -146,9 +163,14 @@ pub enum SystemCommand { /// Show a notification to the user ShowNotification(re_ui::notifications::Notification), - /// Start polling a texture we're reading back from the gpu, and then prompt - /// the user to save a png of the texture. - ReadbackAndSaveTexture(re_renderer::texture_readback::TextureReadbackId), + /// Start polling a texture we're reading back from the gpu. + /// Then depending on the given action either: + /// - Prompt the user to save a png of the texture. + /// - Copy the png to the clipboard. + ReadbackAndSaveTexture { + texture: re_renderer::texture_readback::TextureReadbackId, + action: DownloadAction, + }, /// Add a task, run on a background thread, that saves something to disk. #[cfg(not(target_arch = "wasm32"))] @@ -163,7 +185,7 @@ pub enum SystemCommand { email: String, }, - /// Logout from rerun cloud + /// Logout from Rerun Hub Logout, /// Save a screenshot to a file. @@ -174,6 +196,9 @@ pub enum SystemCommand { /// Optional view id to screenshot a specific view. /// If None, screenshots the entire viewer. view_id: Option, + + /// Whether to show a user-facing notification (info toast) when the screenshot is done. + notify: bool, }, } @@ -187,6 +212,12 @@ impl SystemCommand { } } +/// What to do with a download. +pub enum DownloadAction { + CopyToClipboard, + Save, +} + /// What triggered this item to be selected? /// /// See [`crate::ViewerContext::handle_select_focus_sync`] why this is useful. @@ -252,12 +283,14 @@ pub type StaticLocation = &'static Location<'static>; pub struct CommandSender { system_sender: crossbeam::channel::Sender<(StaticLocation, SystemCommand)>, ui_sender: crossbeam::channel::Sender, + recording_sender: crossbeam::channel::Sender, } /// Receiver for the [`CommandSender`] pub struct CommandReceiver { system_receiver: crossbeam::channel::Receiver<(StaticLocation, SystemCommand)>, ui_receiver: crossbeam::channel::Receiver, + recording_receiver: crossbeam::channel::Receiver, } impl CommandReceiver { @@ -276,6 +309,13 @@ impl CommandReceiver { // is if the sender has been dropped. self.ui_receiver.try_recv().ok() } + + /// Receive a [`RecordingCommand`] to be executed if any is queued. + pub fn recv_recording(&self) -> Option { + // The only way this can fail (other than being empty) + // is if the sender has been dropped. + self.recording_receiver.try_recv().ok() + } } /// Creates a new command channel. @@ -285,14 +325,17 @@ pub fn command_channel() -> (CommandSender, CommandReceiver) { #![cfg_attr(not(target_arch = "wasm32"), expect(clippy::disallowed_methods))] let (system_sender, system_receiver) = crossbeam::channel::unbounded(); let (ui_sender, ui_receiver) = crossbeam::channel::unbounded(); + let (recording_sender, recording_receiver) = crossbeam::channel::unbounded(); ( CommandSender { system_sender, ui_sender, + recording_sender, }, CommandReceiver { system_receiver, ui_receiver, + recording_receiver, }, ) } @@ -316,6 +359,26 @@ impl UICommandSender for CommandSender { } } +impl RecordingCommandSender for CommandSender { + /// Send a command to be executed. + fn send_recording_command(&self, command: RecordingCommand) { + // The only way this can fail is if the receiver has been dropped. + re_quota_channel::send_crossbeam(&self.recording_sender, command).ok(); + } +} + +impl re_ui::RedapServerCommandSender for CommandSender { + fn send_redap_server_command(&self, command: re_ui::RedapServerCommand) { + self.send_system(SystemCommand::RedapServer(command)); + } +} + +impl re_ui::TableCommandSender for CommandSender { + fn send_table_command(&self, command: re_ui::TableCommand) { + self.send_system(SystemCommand::Table(command)); + } +} + /// Command to open the edit redap server modal. /// /// This exists as a separate struct to make it convenient to funnel it through the redap browser diff --git a/crates/viewer/re_viewer_context/src/component_fallbacks.rs b/crates/viewer/re_viewer_context/src/component_fallbacks.rs index dca3011c6618..dc2f1bdb4161 100644 --- a/crates/viewer/re_viewer_context/src/component_fallbacks.rs +++ b/crates/viewer/re_viewer_context/src/component_fallbacks.rs @@ -31,7 +31,7 @@ pub fn typed_fallback_for( .ok() .and_then(|v| v.into_iter().next()) else { - panic!("Invalid fallback provider for `{component}`, failed deserializing result.",); + panic!("Invalid fallback provider for `{component}`, failed deserializing result."); }; v diff --git a/crates/viewer/re_viewer_context/src/component_ui_registry.rs b/crates/viewer/re_viewer_context/src/component_ui_registry.rs index a56d48b4f23e..34d7a8731de8 100644 --- a/crates/viewer/re_viewer_context/src/component_ui_registry.rs +++ b/crates/viewer/re_viewer_context/src/component_ui_registry.rs @@ -8,7 +8,7 @@ use re_log_types::{Instance, StoreId}; use re_sdk_types::{ComponentDescriptor, ComponentType}; use re_ui::{UiExt as _, UiLayout}; -use crate::{MaybeMutRef, QueryContext, StoreViewContext}; +use crate::{AppContext, MaybeMutRef, QueryContext, StoreViewContext}; /// Describes where an edit should be written to if any pub struct EditTarget { @@ -50,7 +50,7 @@ impl ComponentUiTypes { type LegacyDisplayComponentUiCallback = Box< dyn Fn( - &StoreViewContext<'_>, + &AppContext<'_>, &mut egui::Ui, UiLayout, &EntityPath, @@ -69,7 +69,7 @@ pub enum EditOrView { View, } -re_string_interner::declare_new_type!( +re_string_interner::declare_new_type_nonempty!( /// The name of a UI variant (see [`ComponentUiIdentifier::Variant`]). pub struct VariantName; ); @@ -103,7 +103,7 @@ impl From for ComponentUiIdentifier { /// If no edit was made, should return `None`. pub type UntypedComponentEditOrViewCallback = Box< dyn Fn( - &StoreViewContext<'_>, + &AppContext<'_>, &mut egui::Ui, &ComponentDescriptor, Option, @@ -197,11 +197,7 @@ impl ComponentUiRegistry { /// * Make sure that changes are propagated via [`egui::Response::mark_changed`] if necessary. pub fn add_singleline_edit_or_view( &mut self, - callback: impl Fn( - &StoreViewContext<'_>, - &mut egui::Ui, - &mut MaybeMutRef<'_, C>, - ) -> egui::Response + callback: impl Fn(&AppContext<'_>, &mut egui::Ui, &mut MaybeMutRef<'_, C>) -> egui::Response + Send + Sync + 'static, @@ -216,7 +212,7 @@ impl ComponentUiRegistry { &mut self, component_identifier: ComponentIdentifier, callback: impl Fn( - &StoreViewContext<'_>, + &AppContext<'_>, &mut egui::Ui, &ComponentDescriptor, &mut MaybeMutRef<'_, C>, @@ -285,11 +281,7 @@ impl ComponentUiRegistry { /// * Make sure that changes are propagated via [`egui::Response::mark_changed`] if necessary. pub fn add_multiline_edit_or_view( &mut self, - callback: impl Fn( - &StoreViewContext<'_>, - &mut egui::Ui, - &mut MaybeMutRef<'_, C>, - ) -> egui::Response + callback: impl Fn(&AppContext<'_>, &mut egui::Ui, &mut MaybeMutRef<'_, C>) -> egui::Response + Send + Sync + 'static, @@ -317,7 +309,7 @@ impl ComponentUiRegistry { pub fn add_singleline_array_edit_or_view( &mut self, callback: impl Fn( - &StoreViewContext<'_>, + &AppContext<'_>, &mut egui::Ui, &mut MaybeMutRef<'_, Vec>, ) -> egui::Response @@ -346,7 +338,7 @@ impl ComponentUiRegistry { pub fn add_multiline_array_edit_or_view( &mut self, callback: impl Fn( - &StoreViewContext<'_>, + &AppContext<'_>, &mut egui::Ui, &mut MaybeMutRef<'_, Vec>, ) -> egui::Response @@ -361,11 +353,7 @@ impl ComponentUiRegistry { fn add_editor_ui( &mut self, multiline: bool, - callback: impl Fn( - &StoreViewContext<'_>, - &mut egui::Ui, - &mut MaybeMutRef<'_, C>, - ) -> egui::Response + callback: impl Fn(&AppContext<'_>, &mut egui::Ui, &mut MaybeMutRef<'_, C>) -> egui::Response + Send + Sync + 'static, @@ -407,7 +395,7 @@ impl ComponentUiRegistry { &mut self, multiline: bool, callback: impl Fn( - &StoreViewContext<'_>, + &AppContext<'_>, &mut egui::Ui, &mut MaybeMutRef<'_, Vec>, ) -> egui::Response @@ -463,11 +451,14 @@ impl ComponentUiRegistry { } /// Registers singleline UI to view Arrow data using a specific [`VariantName`]. + /// + /// `variant_name` must be a valid [`VariantName`] (i.e. non-empty); passing an empty + /// string literal/const will panic. pub fn add_variant_ui( &mut self, variant_name: impl Into, callback: impl Fn( - &StoreViewContext<'_>, + &AppContext<'_>, &mut egui::Ui, ComponentIdentifier, Option, @@ -495,12 +486,7 @@ impl ComponentUiRegistry { "UI for variant {variant_name} failed to display the provided data {err}" ); - fallback_ui( - ui, - UiLayout::List, - ctx.app_options().timestamp_format, - value, - ); + fallback_ui(ui, UiLayout::List, ctx.app_options.timestamp_format, value); } None @@ -557,7 +543,6 @@ impl ComponentUiRegistry { /// /// Has a fallback to show an info text if the instance is not specific, /// but in these cases `LatestAtComponentResults::data_ui` should be used instead! - #[expect(clippy::too_many_arguments)] pub fn component_ui( &self, ctx: &StoreViewContext<'_>, @@ -670,10 +655,9 @@ impl ComponentUiRegistry { } /// Show a UI for a single raw component. - #[expect(clippy::too_many_arguments)] pub fn component_ui_raw( &self, - ctx: &StoreViewContext<'_>, + ctx: &AppContext<'_>, ui: &mut egui::Ui, ui_layout: UiLayout, entity_path: &EntityPath, @@ -727,16 +711,15 @@ impl ComponentUiRegistry { fallback_ui( ui, ui_layout, - ctx.app_options().timestamp_format, + ctx.app_options.timestamp_format, component_raw, ); } /// Show a UI corresponding to the provided variant name. - #[expect(clippy::too_many_arguments)] pub fn variant_ui_raw( &self, - ctx: &StoreViewContext<'_>, + ctx: &AppContext<'_>, ui: &mut egui::Ui, ui_layout: UiLayout, variant_name: VariantName, @@ -781,7 +764,7 @@ impl ComponentUiRegistry { fallback_ui( ui, ui_layout, - ctx.app_options().timestamp_format, + ctx.app_options.timestamp_format, component_raw, ); } @@ -791,7 +774,6 @@ impl ComponentUiRegistry { /// Changes will be written to the blueprint store at the given override path. /// Any change is expected to be effective next frame and passed in via the `component_query_result` parameter. /// (Otherwise, this method is agnostic to where the component data is stored.) - #[expect(clippy::too_many_arguments)] pub fn multiline_edit_ui( &self, query_ctx: &QueryContext<'_>, @@ -820,7 +802,6 @@ impl ComponentUiRegistry { /// Changes will be written to the blueprint store at the given override path. /// Any change is expected to be effective next frame and passed in via the `component_query_result` parameter. /// (Otherwise, this method is agnostic to where the component data is stored.) - #[expect(clippy::too_many_arguments)] pub fn singleline_edit_ui( &self, query_ctx: &QueryContext<'_>, @@ -844,7 +825,6 @@ impl ComponentUiRegistry { ); } - #[expect(clippy::too_many_arguments)] fn edit_ui( &self, query_ctx: &QueryContext<'_>, @@ -884,7 +864,6 @@ impl ComponentUiRegistry { } /// For blueprint editing - #[expect(clippy::too_many_arguments)] pub fn edit_ui_raw( &self, query_ctx: &QueryContext<'_>, diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index 9c500fb4e328..6d976d35854f 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -39,7 +39,7 @@ use std::fmt::{Display, Formatter}; use itertools::Itertools as _; use re_entity_db::InstancePath; -use re_log_types::EntityPath; +use re_log_types::{ComponentPath, EntityPath}; use re_ui::UiExt as _; use crate::{Contents, DataResultInteractionAddress, Item, ItemCollection}; @@ -52,6 +52,9 @@ pub enum DragAndDropPayload { /// The dragged content is made of entities. Entities { entities: Vec }, + /// The dragged content is made of components. + Components { component_paths: Vec }, + /// The dragged content is made of a collection of [`Item`]s we do know how to handle. Invalid, } @@ -62,6 +65,8 @@ impl DragAndDropPayload { Self::Contents { contents } } else if let Some(entities) = try_item_collection_to_entities(selected_items) { Self::Entities { entities } + } else if let Some(component_paths) = try_item_collection_to_components(selected_items) { + Self::Components { component_paths } } else { Self::Invalid } @@ -87,6 +92,16 @@ fn try_item_collection_to_entities(items: &ItemCollection) -> Option Option> { + items + .iter() + .map(|(item, _)| match item { + Item::ComponentPath(component_path) => Some(component_path.clone()), + _ => None, + }) + .collect() +} + impl std::fmt::Display for DragAndDropPayload { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut item_counter = ItemCounter::default(); @@ -104,6 +119,12 @@ impl std::fmt::Display for DragAndDropPayload { } } + Self::Components { component_paths } => { + for component_path in component_paths { + item_counter.add(&Item::ComponentPath(component_path.clone())); + } + } + // this is not used in the UI Self::Invalid => {} } @@ -129,8 +150,10 @@ pub enum DragAndDropFeedback { /// The payload type is correct, but it's content cannot be accepted by the current drop location. /// - /// For example, a view might reject an entity because it already contains it. - Reject, + /// For example, a view might reject an entity because it already contains it. The optional + /// string is a short, user-facing reason; when present, it is shown next to the cursor with a + /// warning icon. When `None`, the pill is rendered as normal (just a "no-drop" cursor). + Reject(Option<&'static str>), } /// Helper to handle drag-and-drop operations. @@ -185,12 +208,10 @@ impl DragAndDropManager { if let Some(payload) = egui::DragAndDrop::payload::(ctx) && let Some(pointer_pos) = ctx.pointer_interact_pos() { - let icon = match payload.as_ref() { - DragAndDropPayload::Contents { .. } => &re_ui::icons::DND_MOVE, - DragAndDropPayload::Entities { .. } => &re_ui::icons::DND_ADD_TO_EXISTING, - // don't draw anything for invalid selection - DragAndDropPayload::Invalid => return, - }; + // Don't draw anything for invalid selection. + if matches!(payload.as_ref(), DragAndDropPayload::Invalid) { + return; + } let layer_id = egui::LayerId::new( egui::Order::Tooltip, @@ -215,12 +236,27 @@ impl DragAndDropManager { ctx.set_cursor_icon(egui::CursorIcon::Grabbing); ui.set_opacity(0.5); } - DragAndDropFeedback::Reject => { + DragAndDropFeedback::Reject(_) => { ctx.set_cursor_icon(egui::CursorIcon::NoDrop); ui.set_opacity(0.5); } } + // On reject, show the reason + let payload_text = payload.to_string(); + let (icon, text) = if let DragAndDropFeedback::Reject(Some(reason)) = feedback { + (&re_ui::icons::WARNING, reason) + } else { + let icon = match payload.as_ref() { + DragAndDropPayload::Contents { .. } => &re_ui::icons::DND_MOVE, + DragAndDropPayload::Entities { .. } | DragAndDropPayload::Components { .. } => { + &re_ui::icons::DND_ADD_TO_EXISTING + } + DragAndDropPayload::Invalid => return, + }; + (icon, payload_text.as_str()) + }; + let payload_is_currently_droppable = feedback == DragAndDropFeedback::Accept; let response = drag_pill_frame(ui.tokens(), payload_is_currently_droppable) .show(&mut ui, |ui| { @@ -230,7 +266,7 @@ impl DragAndDropManager { ui.spacing_mut().item_spacing.x = 2.0; ui.small_icon(icon, Some(text_color)); - ui.label(egui::RichText::new(payload.to_string()).color(text_color)); + ui.label(egui::RichText::new(text).color(text_color)); }); }) .response; diff --git a/crates/viewer/re_viewer_context/src/gpu_bridge/colormap.rs b/crates/viewer/re_viewer_context/src/gpu_bridge/colormap.rs index 4bd34a18010f..2272d034c8dd 100644 --- a/crates/viewer/re_viewer_context/src/gpu_bridge/colormap.rs +++ b/crates/viewer/re_viewer_context/src/gpu_bridge/colormap.rs @@ -62,6 +62,7 @@ fn colormap_preview_ui( rect, colormapped_texture, egui::TextureOptions::LINEAR, + re_renderer::ViewBuilderId::new(response.id.value()), debug_name.into(), )?; @@ -98,7 +99,7 @@ fn colormap_variant_ui( } fn colormap_category_ui( - ctx: &crate::StoreViewContext<'_>, + ctx: &crate::AppContext<'_>, ui: &mut egui::Ui, category: ColormapCategory, selected: &mut re_sdk_types::components::Colormap, @@ -124,7 +125,7 @@ fn colormap_category_ui( .iter() .filter(|&&colormap| colormap.category() == category) { - response |= colormap_variant_ui(ctx.render_ctx(), ui, option, selected); + response |= colormap_variant_ui(ctx.render_ctx, ui, option, selected); } response @@ -132,7 +133,7 @@ fn colormap_category_ui( /// Show the colormap editor/viewer with the given selection of colormap categories. pub fn colormap_edit_or_view_ui_with_selection( - ctx: &crate::StoreViewContext<'_>, + ctx: &crate::AppContext<'_>, ui: &mut egui::Ui, map: &mut MaybeMutRef<'_, re_sdk_types::components::Colormap>, selection: ColormapSelection, @@ -167,7 +168,7 @@ pub fn colormap_edit_or_view_ui_with_selection( } else { let map: re_sdk_types::components::Colormap = **map; let colormap_response = { - let result = colormap_preview_ui(ctx.render_ctx(), ui, map); + let result = colormap_preview_ui(ctx.render_ctx, ui, map); if let Err(err) = &result { re_log::error_once!("Failed to paint colormap preview: {err}"); } @@ -185,7 +186,7 @@ pub fn colormap_edit_or_view_ui_with_selection( /// Show the colormap editor/viewer with the standard set of colormap categories. pub fn colormap_edit_or_view_ui( - ctx: &crate::StoreViewContext<'_>, + ctx: &crate::AppContext<'_>, ui: &mut egui::Ui, map: &mut MaybeMutRef<'_, re_sdk_types::components::Colormap>, ) -> egui::Response { diff --git a/crates/viewer/re_viewer_context/src/gpu_bridge/mod.rs b/crates/viewer/re_viewer_context/src/gpu_bridge/mod.rs index e9b7124f9917..a9e1f3e8f0cc 100644 --- a/crates/viewer/re_viewer_context/src/gpu_bridge/mod.rs +++ b/crates/viewer/re_viewer_context/src/gpu_bridge/mod.rs @@ -107,6 +107,7 @@ pub fn render_image( image_rect_on_screen: egui::Rect, colormapped_texture: ColormappedTexture, texture_options: egui::TextureOptions, + view_id: re_renderer::ViewBuilderId, debug_name: re_renderer::Label, ) -> anyhow::Result<()> { re_tracing::profile_function!(); @@ -173,7 +174,7 @@ pub fn render_image( ..Default::default() }; - let mut view_builder = ViewBuilder::new(render_ctx, target_config)?; + let mut view_builder = ViewBuilder::new(render_ctx, target_config, view_id)?; view_builder.queue_draw( render_ctx, diff --git a/crates/viewer/re_viewer_context/src/image_info.rs b/crates/viewer/re_viewer_context/src/image_info.rs index 4d30abda9281..5e69a063cefa 100644 --- a/crates/viewer/re_viewer_context/src/image_info.rs +++ b/crates/viewer/re_viewer_context/src/image_info.rs @@ -55,61 +55,23 @@ pub fn resolution_of_image_at( let video = ctx .store_context .memoizer(|c: &mut crate::VideoStreamCache| { - c.entry( - entity_db, - entity_path, - *ctx.time_ctrl.timeline_name(), - ctx.app_options().video_decoder_settings(), - video_stream_sample_component, - &|| { - entity_db - .latest_at_component::( - entity_path, - query, - archetypes::EncodedImage::descriptor_media_type().component, - ) - .map(|(_, c)| re_video::VideoCodec::from(c)) - .ok_or(crate::VideoStreamProcessingError::MissingCodec) - }, - ) - }); + let codec = entity_db + .latest_at_component::( + entity_path, + query, + archetypes::EncodedImage::descriptor_media_type().component, + ) + .map(|(_, c)| re_video::VideoCodec::from(c)) + .ok_or(crate::VideoStreamProcessingError::MissingCodec); + let codec = codec?; - if let Ok(video) = video - && let Some(encoding_details) = &video.read_arc().video_descr().encoding_details - { - return Some(components::Resolution::from( - encoding_details.coded_dimensions.map(|e| e as f32), - )); - } - } - - // Check for an encoded image. - let encoded_image_blob_component = archetypes::EncodedImage::descriptor_blob().component; - if let Some(((_time, _), _)) = entity_db.latest_at_component::( - entity_path, - query, - encoded_image_blob_component, - ) { - let video = ctx - .store_context - .memoizer(|c: &mut crate::VideoStreamCache| { c.entry( entity_db, entity_path, *ctx.time_ctrl.timeline_name(), ctx.app_options().video_decoder_settings(), - encoded_image_blob_component, - &|| { - let media_type = entity_db - .latest_at_component::( - entity_path, - query, - archetypes::EncodedImage::descriptor_media_type().component, - ) - .map(|(_, c)| c.to_string()); - - Ok(re_video::VideoCodec::ImageSequence(media_type)) - }, + video_stream_sample_component, + codec, ) }); @@ -122,35 +84,60 @@ pub fn resolution_of_image_at( } } - // Check for an encoded depth image. - if let Some(((_time, row_id), blob)) = entity_db - .latest_at_component::( - entity_path, - query, - archetypes::EncodedDepthImage::descriptor_blob().component, - ) - { - let media_type = entity_db - .latest_at_component::( + // Check for an encoded image & encoded depth image. + let encoded_image_resolution = |image_blob_component, media_type_component| { + if let Some(((_time, _), _)) = entity_db + .latest_at_component::( entity_path, query, - archetypes::EncodedDepthImage::descriptor_media_type().component, + image_blob_component, ) - .map(|(_, c)| c); + { + let video = ctx + .store_context + .memoizer(|c: &mut crate::VideoStreamCache| { + let media_type = entity_db + .latest_at_component::( + entity_path, + query, + media_type_component, + ) + .map(|(_, c)| c.to_string()); + + c.entry( + entity_db, + entity_path, + *ctx.time_ctrl.timeline_name(), + ctx.app_options().video_decoder_settings(), + image_blob_component, + re_video::VideoCodec::ImageSequence(media_type), + ) + }); + + if let Ok(video) = video + && let Some(encoding_details) = &video.read_arc().video_descr().encoding_details + { + return Some(components::Resolution::from( + encoding_details.coded_dimensions.map(|e| e as f32), + )); + } + } - let depth_image = ctx - .store_context - .memoizer(|c: &mut crate::ImageDecodeCache| { - c.entry_encoded_depth( - row_id, - archetypes::EncodedDepthImage::descriptor_blob().component, - &blob, - media_type.as_ref(), - ) - }); + None + }; - if let Ok(depth_image) = depth_image { - return Some(depth_image.width_height_f32().into()); + for (image_blob_component, media_type_component) in [ + ( + archetypes::EncodedImage::descriptor_blob().component, + archetypes::EncodedImage::descriptor_media_type().component, + ), + ( + archetypes::EncodedDepthImage::descriptor_blob().component, + archetypes::EncodedDepthImage::descriptor_media_type().component, + ), + ] { + if let Some(res) = encoded_image_resolution(image_blob_component, media_type_component) { + return Some(res); } } @@ -185,19 +172,9 @@ impl ColormapWithRange { } /// Hash used for identifying blobs stored in a store. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, re_byte_size::SizeBytes)] pub struct StoredBlobCacheKey(pub Hash64); -impl re_byte_size::SizeBytes for StoredBlobCacheKey { - fn heap_size_bytes(&self) -> u64 { - 0 - } - - fn is_pod() -> bool { - true - } -} - impl StoredBlobCacheKey { pub const ZERO: Self = Self(Hash64::ZERO); diff --git a/crates/viewer/re_viewer_context/src/item.rs b/crates/viewer/re_viewer_context/src/item.rs index 3e7cb4126156..cb00af6d3e9b 100644 --- a/crates/viewer/re_viewer_context/src/item.rs +++ b/crates/viewer/re_viewer_context/src/item.rs @@ -139,6 +139,26 @@ impl Item { } } + /// Does validating this item require a viewport blueprint? + /// + /// `View`, `Container`, and `DataResult` only exist within a viewport blueprint, so without one + /// they can neither be validated nor make sense as a selection. All other items (recordings, + /// redap entries/servers, tables, …) are independent of the blueprint. + pub fn requires_blueprint(&self) -> bool { + match self { + Self::View(_) | Self::Container(_) | Self::DataResult(_) => true, + + Self::AppId(_) + | Self::DataSource(_) + | Self::StoreId(_) + | Self::TableId(_) + | Self::InstancePath(_) + | Self::ComponentPath(_) + | Self::RedapEntry { .. } + | Self::RedapServer(_) => false, + } + } + pub fn entity_path(&self) -> Option<&EntityPath> { match self { Self::AppId(_) @@ -386,7 +406,7 @@ pub fn resolve_mono_instance_path( // NOTE: While we normally frown upon direct queries to the datastore, `all_components` is fine. let Some(components) = engine .store() - .all_components_on_timeline(&query.timeline(), &instance.entity_path) + .all_components_on_timeline(query.timeline().as_ref(), &instance.entity_path) else { // No components at all, return unindexed entity. return re_entity_db::InstancePath::entity_all(instance.entity_path.clone()); @@ -396,7 +416,12 @@ pub fn resolve_mono_instance_path( for component in components { if let Some(array) = engine .cache() - .latest_at(query, &instance.entity_path, [component]) + .latest_at( + re_chunk_store::ChunkTrackingMode::ReportTransient, + query, + &instance.entity_path, + [component], + ) .component_batch_raw(component) && array.len() > 1 { diff --git a/crates/viewer/re_viewer_context/src/item_collection.rs b/crates/viewer/re_viewer_context/src/item_collection.rs index e9e1815ba196..9bafad50418c 100644 --- a/crates/viewer/re_viewer_context/src/item_collection.rs +++ b/crates/viewer/re_viewer_context/src/item_collection.rs @@ -6,16 +6,18 @@ use re_chunk::EntityPath; use re_entity_db::EntityDb; use re_log_types::StoreKind; use re_sdk_types::external::glam; +use re_tf::TransformFrameIdHash; use crate::{DataResultInteractionAddress, Item, ViewId, resolve_mono_instance_path_item}; /// Context information that a view might attach to an item from [`ItemCollection`] and useful /// for how a selection might be displayed and interacted with. -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, PartialEq)] pub enum ItemContext { /// Hovering/Selecting in a 2D space. TwoD { - space_2d: EntityPath, + /// The target frame of the 2D (sub)space in which the pointer is hovering. + space_2d_target_frame: TransformFrameIdHash, /// Where in this 2D space (+ depth)? pos: glam::Vec3, @@ -23,8 +25,8 @@ pub enum ItemContext { /// Hovering/Selecting in a 3D space. ThreeD { - /// The 3D space with the camera(s) - space_3d: EntityPath, + /// The target frame of the 3D space in which the pointer is hovering. + space_3d_target_frame: TransformFrameIdHash, /// The point in 3D space that is hovered, if any. pos: Option, @@ -34,7 +36,7 @@ pub enum ItemContext { tracked_entity: Option, /// Corresponding 2D spaces and pixel coordinates (with Z=depth) - point_in_space_cameras: Vec<(EntityPath, Option)>, + point_in_2d_spaces: Vec<(TransformFrameIdHash, Option)>, }, /// Hovering/selecting in one of the streams trees. @@ -227,12 +229,10 @@ impl ItemCollection { Item::TableId(_) => None, // TODO(grtlr): Make `TableId`s copyable too Item::DataSource(source) => match source { - LogSource::File { path, .. } => { + LogSource::File { path } => { Some((ClipboardTextDesc::FilePath, path.to_string_lossy().into())) } - LogSource::HttpStream { url, follow: _ } => { - Some((ClipboardTextDesc::Url, url.clone())) - } + LogSource::HttpStream { url } => Some((ClipboardTextDesc::Url, url.clone())), LogSource::RrdWebEvent => None, LogSource::JsChannel { .. } => None, LogSource::Sdk => None, diff --git a/crates/viewer/re_viewer_context/src/lib.rs b/crates/viewer/re_viewer_context/src/lib.rs index 0d13a68947fc..f77e5ef73f09 100644 --- a/crates/viewer/re_viewer_context/src/lib.rs +++ b/crates/viewer/re_viewer_context/src/lib.rs @@ -32,6 +32,7 @@ mod heuristics; mod image_info; mod item; mod item_collection; +mod link_button; mod maybe_mut_ref; pub mod open_url; mod query_context; @@ -72,13 +73,14 @@ pub use self::blueprint_id::{ BlueprintId, BlueprintIdRegistry, ContainerId, GLOBAL_VIEW_ID, ViewId, }; pub use self::cache::{ - Cache, ImageDecodeCache, ImageStatsCache, Memoizers, SharablePlayableVideoStream, StoreCache, - TensorStatsCache, TransformDatabaseStoreCache, VideoAssetCache, VideoStreamCache, - VideoStreamProcessingError, + AppCaches, Cache, CacheEntryAccess, EncodedDepthImageStatsCache, ImageDecodeCache, + ImageHistogramCache, ImageStatsCache, Memoizers, Rgb8Histogram, SharablePlayableVideoStream, + StoreCache, TensorStatsAccessor, TensorStatsCache, TransformDatabaseStoreCache, + VideoAssetCache, VideoStoreSource, VideoStreamCache, VideoStreamProcessingError, }; pub use self::collapsed_id::{CollapseItem, CollapseScope, CollapsedId}; pub use self::command_sender::{ - CommandReceiver, CommandSender, EditRedapServerModalCommand, SystemCommand, + CommandReceiver, CommandSender, DownloadAction, EditRedapServerModalCommand, SystemCommand, SystemCommandSender, command_channel, }; pub use self::component_fallbacks::{ @@ -100,6 +102,9 @@ pub use self::item::{ resolve_mono_instance_path_item, }; pub use self::item_collection::{ItemCollection, ItemContext}; +pub use self::link_button::{ + LinkKind, ResolvedEntry, UrlNameLookup, make_url_decorator, segment_button_atoms, url_atoms, +}; pub use self::maybe_mut_ref::MaybeMutRef; pub use self::query_context::{ DataQueryResult, DataResultHandle, DataResultNode, DataResultTree, QueryContext, @@ -118,7 +123,8 @@ pub use self::tables::{TableStore, TableStores}; pub use self::tensor::{ImageStats, TensorStats}; pub use self::time_control::{ MoveDirection, MoveSpeed, TIME_PANEL_PATH, TimeControl, TimeControlCommand, - TimeControlResponse, TimeControlUpdateParams, TimeView, time_panel_blueprint_entity_path, + TimeControlResponse, TimeControlUpdateParams, TimeRangeHighlight, TimeRangeHighlightKind, + TimeView, time_panel_blueprint_entity_path, }; pub use self::typed_entity_collections::{ BufferAndFormatMatch, DatatypeMatch, IndicatedEntities, PerVisualizerInstruction, @@ -132,7 +138,7 @@ pub use self::utils::{ }; pub use self::view::{ BufferAndFormatConstraint, DataResult, IdentifiedViewSystem, OptionalViewEntityHighlight, - PerSystemEntities, RecommendedMappings, RecommendedView, RecommendedVisualizers, + PerSystemEntities, PreviewState, RecommendedMappings, RecommendedView, RecommendedVisualizers, SingleRequiredComponentConstraint, SystemExecutionOutput, ViewClass, ViewClassExt, ViewClassLayoutPriority, ViewClassPlaceholder, ViewClassRegistry, ViewClassRegistryError, ViewContext, ViewContextCollection, ViewContextSystem, ViewContextSystemOncePerFrameResult, @@ -150,10 +156,14 @@ pub use self::visitor_flow_control::VisitorControlFlow; // Historical reasons pub mod external { #[cfg(not(target_arch = "wasm32"))] pub use tokio; - pub use {nohash_hasher, re_chunk_store, re_entity_db, re_log_types, re_query, re_ui}; + pub use { + nohash_hasher, re_chunk_store, re_entity_db, re_log_types, re_query, re_string_interner, + re_tf, re_ui, + }; } // Re-export +pub use re_byte_size::SizeBytes; pub use re_chunk_store::MissingChunkReporter; // --------------------------------------------------------------------------- @@ -164,6 +174,16 @@ pub enum NeedsRepaint { No, } +impl NeedsRepaint { + pub fn or(self, other: Self) -> Self { + if self == Self::Yes || other == Self::Yes { + Self::Yes + } else { + Self::No + } + } +} + // --- /// Determines the icon to use for a given container kind. @@ -199,6 +219,9 @@ pub struct ScreenshotInfo { /// Where to put the screenshot. pub target: ScreenshotTarget, + + /// Whether to show a user-facing notification (info toast) when the screenshot is done. + pub notify: bool, } /// Where to put the screenshot. diff --git a/crates/viewer/re_viewer_context/src/link_button.rs b/crates/viewer/re_viewer_context/src/link_button.rs new file mode 100644 index 000000000000..b3155ba920cd --- /dev/null +++ b/crates/viewer/re_viewer_context/src/link_button.rs @@ -0,0 +1,209 @@ +//! Turn built-in viewer URLs into [`re_ui::LinkButton`]s. + +use std::str::FromStr as _; +use std::sync::Arc; + +use ahash::HashMap; + +use egui::{AtomExt as _, Theme}; + +use re_log_types::{EntryId, EntryName}; +use re_ui::{Icon, LinkButton, icons}; + +use crate::open_url::{INTRA_RECORDING_URL_SCHEME, ViewerOpenUrl}; + +/// The icon to use +#[derive(Clone, Copy)] +pub enum LinkKind { + Recording, + Dataset, + Table, + Folder, + Proxy, +} + +impl LinkKind { + /// The themed link icon for this kind. These are full-color (fixed blue arrow), so they come in + /// a light and dark variant rather than being tinted to the text color. + fn icon(self, theme: Theme) -> Icon { + match (self, theme) { + (Self::Recording, Theme::Light) => icons::LINK_RECORDING_LIGHT, + (Self::Recording, Theme::Dark) => icons::LINK_RECORDING_DARK, + (Self::Dataset, Theme::Light) => icons::LINK_DATASET_LIGHT, + (Self::Dataset, Theme::Dark) => icons::LINK_DATASET_DARK, + (Self::Table, Theme::Light) => icons::LINK_TABLE_LIGHT, + (Self::Table, Theme::Dark) => icons::LINK_TABLE_DARK, + (Self::Folder, Theme::Light) => icons::LINK_FOLDER_LIGHT, + (Self::Folder, Theme::Dark) => icons::LINK_FOLDER_DARK, + (Self::Proxy, Theme::Light) => icons::LINK_PROXY_LIGHT, + (Self::Proxy, Theme::Dark) => icons::LINK_PROXY_DARK, + } + } +} + +/// Tint for the monochrome single-variant icons. +fn icon_tint(theme: Theme) -> egui::Color32 { + match theme { + Theme::Light => egui::Color32::BLACK, + Theme::Dark => egui::Color32::WHITE, + } +} + +/// Resolved display info for a redap entry (dataset/table), used to label a link button. +#[derive(Clone)] +pub struct ResolvedEntry { + pub name: EntryName, + pub kind: LinkKind, +} + +/// Maps a redap entry reference to its resolved name + icon. +/// +/// Built once per frame by the app and captured by the installed URL decorator. +pub type UrlNameLookup = HashMap<(re_uri::Origin, EntryId), ResolvedEntry>; + +/// Build the global URL decorator closure for this frame's `lookup` snapshot. +/// +/// Install it with [`re_ui::UrlDecorator::set`]. +pub fn make_url_decorator( + lookup: Arc, + theme: Theme, +) -> impl Fn(&str) -> Option + Send + Sync + 'static { + move |url| url_atoms(url, &lookup, theme) +} + +/// Parse a known viewer URL and build its decorated button. +/// +/// Dataset/entry ids are resolved to catalog names via `lookup`, falling back to a short-id +/// placeholder on a miss. +pub fn url_atoms(url: &str, lookup: &UrlNameLookup, theme: Theme) -> Option { + let button = match ViewerOpenUrl::from_str(url).ok()? { + ViewerOpenUrl::RedapDatasetSegment(uri) => { + let (_, dataset_label) = resolve(lookup, &uri.origin, EntryId::from(uri.dataset_id)); + let atoms = dataset_segment_button(&dataset_label, uri.segment_id.as_str(), theme); + Some(LinkButton::new(url, atoms)) + } + + ViewerOpenUrl::RedapEntry(uri) => { + let (kind, label) = resolve(lookup, &uri.origin, uri.entry_id); + Some(LinkButton::new(url, (kind.icon(theme), label))) + } + + ViewerOpenUrl::RedapCatalog(uri) => { + let label = uri.origin.host.to_string(); + Some(LinkButton::new(url, (LinkKind::Proxy.icon(theme), label))) + } + + ViewerOpenUrl::RedapProxy(uri) => { + let label = uri.origin.host.to_string(); + Some(LinkButton::new(url, (LinkKind::Proxy.icon(theme), label))) + } + + ViewerOpenUrl::RedapFolder(uri) => { + let label = folder_leaf(&uri.path); + Some(LinkButton::new(url, (LinkKind::Folder.icon(theme), label))) + } + + ViewerOpenUrl::IntraRecordingSelection(_) => { + let image = icons::ENTITY.as_image().tint(icon_tint(theme)); + let label = url + .strip_prefix(INTRA_RECORDING_URL_SCHEME) + .unwrap_or(url) + .to_owned(); + Some(LinkButton::new(url, (image, label))) + } + + ViewerOpenUrl::HttpUrl(http_url) => { + // Show just the file name: + let name = http_url + .path_segments() + .and_then(|mut segments| segments.rfind(|segment| !segment.is_empty())) + .map(str::to_owned) + .unwrap_or_else(|| http_url.host_str().unwrap_or(http_url.as_str()).to_owned()); + Some(LinkButton::new( + url, + (LinkKind::Recording.icon(theme), name), + )) + } + + #[cfg(not(target_arch = "wasm32"))] + ViewerOpenUrl::FilePath(path) => { + // Show just the file name: + let name = path + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.to_string_lossy().into_owned()); + Some(LinkButton::new( + url, + (LinkKind::Recording.icon(theme), name), + )) + } + + ViewerOpenUrl::WebViewerUrl { url_parameters, .. } => { + // A web-viewer share link wrapping one or more content URLs: show the inner one's button, + // but keep opening the outer share link on click. + let inner = url_atoms( + &url_parameters.first().sharable_url(None).ok()?, + lookup, + theme, + )?; + Some(LinkButton::new(url, inner.into_atoms())) + } + + // No meaningful button content. + ViewerOpenUrl::WebEventListener + | ViewerOpenUrl::Settings + | ViewerOpenUrl::ChunkStoreBrowser { .. } => None, + }; + + // The icons have a blue arrow so we may not tint them: + button.map(|b| b.tint_icons(false)) +} + +/// Button atoms for a lone segment, used where the dataset is already implied by context (e.g. a +/// segment link shown within its own dataset's table, where repeating the dataset name is redundant). +pub fn segment_button_atoms(segment_id: &str, theme: Theme) -> egui::Atoms<'static> { + egui::Atoms::new((LinkKind::Recording.icon(theme), segment_id.to_owned())) +} + +fn dataset_segment_button(dataset: &str, segment: &str, theme: Theme) -> egui::Atoms<'static> { + let tint = icon_tint(theme); + egui::Atoms::new(( + icons::DATASET.as_image().tint(tint), + dataset.to_owned(), + icons::BREADCRUMBS_SEPARATOR.as_image().tint(tint), + LinkKind::Recording.icon(theme).as_image(), + segment.to_owned().atom_shrink(true), + )) +} + +/// Resolve an entry to its `(link kind, label)`, or a dataset-kind + short-id placeholder on a miss. +fn resolve( + lookup: &UrlNameLookup, + origin: &re_uri::Origin, + entry_id: EntryId, +) -> (LinkKind, String) { + if let Some(resolved) = lookup.get(&(origin.clone(), entry_id)) { + (resolved.kind, resolved.name.to_string()) + } else { + (LinkKind::Dataset, short_id(&entry_id.to_string())) + } +} + +/// First few characters of a hex id — enough to recognize, while the full URL is shown on hover. +fn short_id(id: &str) -> String { + const N: usize = 8; + if id.len() > N { + format!("{}…", &id[..N]) + } else { + id.to_owned() + } +} + +/// The leaf of a dotted dataset-hierarchy folder path (e.g. `perception.detection` → `detection`). +fn folder_leaf(path: &str) -> String { + path.rsplit('.') + .next() + .filter(|leaf| !leaf.is_empty()) + .unwrap_or(path) + .to_owned() +} diff --git a/crates/viewer/re_viewer_context/src/open_url.rs b/crates/viewer/re_viewer_context/src/open_url.rs index 60146c6b23ab..dcdf47397bc5 100644 --- a/crates/viewer/re_viewer_context/src/open_url.rs +++ b/crates/viewer/re_viewer_context/src/open_url.rs @@ -250,11 +250,6 @@ pub fn base_url(url: &Url) -> Url { #[derive(Debug, Clone, Copy)] pub struct OpenUrlOptions { - /// Follow live HTTP or file paths. - // - // TODO(emilk): consider making this part of `ViewerOpenUrl::RrdHttpUrl/FilePath` instead - pub follow: bool, - pub recording_open_behavior: RecordingOpenBehavior, /// Shows the loading screen. @@ -264,7 +259,6 @@ pub struct OpenUrlOptions { impl Default for OpenUrlOptions { fn default() -> Self { Self { - follow: false, recording_open_behavior: RecordingOpenBehavior::Open, show_loader: false, } @@ -352,10 +346,7 @@ impl ViewerOpenUrl { "Can't share links to recordings streamed from stdin." )), - LogSource::RedapGrpcStream { - uri, - open_behavior: _, - } => Ok(Self::RedapDatasetSegment(uri.clone())), + LogSource::RedapGrpcStream { uri, .. } => Ok(Self::RedapDatasetSegment(uri.clone())), LogSource::MessageProxy(proxy_uri) => Ok(Self::RedapProxy(proxy_uri.clone())), } @@ -547,16 +538,13 @@ impl ViewerOpenUrl { Self::HttpUrl(url) => Some(LogSource::HttpStream { url: url.to_string(), - follow: false, }), #[cfg(not(target_arch = "wasm32"))] - Self::FilePath(path) => Some(LogSource::File { - path: path.clone(), - follow: false, - }), + Self::FilePath(path) => Some(LogSource::File { path: path.clone() }), Self::RedapDatasetSegment(uri) => Some(LogSource::RedapGrpcStream { uri: uri.clone(), open_behavior: RecordingOpenBehavior::Background, + table_blueprint: None, }), Self::RedapProxy(uri) => Some(LogSource::MessageProxy(uri.clone())), Self::WebEventListener => Some(LogSource::RrdWebEvent), @@ -603,7 +591,6 @@ impl ViewerOpenUrl { Self::HttpUrl(url) => { command_sender.send_system(SystemCommand::LoadDataSource(LogDataSource::HttpUrl { url, - follow: options.follow, })); } #[cfg(not(target_arch = "wasm32"))] @@ -612,7 +599,6 @@ impl ViewerOpenUrl { LogDataSource::FilePath { file_source: re_log_types::FileSource::Uri, path, - follow: options.follow, }, )); } @@ -635,18 +621,24 @@ impl ViewerOpenUrl { } Self::RedapCatalog(uri) => { command_sender.send_system(SystemCommand::AddRedapServer(uri.origin.clone())); + command_sender.send_system(SystemCommand::RefreshRedapServer(uri.origin.clone())); let item = Item::RedapServer(uri.origin); command_sender.send_system(SystemCommand::set_selection(item.clone())); command_sender.send_system(SystemCommand::SetFocus(item.into())); } Self::RedapEntry(uri) => { command_sender.send_system(SystemCommand::AddRedapServer(uri.origin.clone())); + command_sender.send_system(SystemCommand::RefreshRedapEntry { + origin: uri.origin.clone(), + entry_id: uri.entry_id, + }); let item = Item::from(uri); command_sender.send_system(SystemCommand::set_selection(item.clone())); command_sender.send_system(SystemCommand::SetFocus(item.into())); } Self::RedapFolder(uri) => { command_sender.send_system(SystemCommand::AddRedapServer(uri.origin.clone())); + command_sender.send_system(SystemCommand::RefreshRedapServer(uri.origin.clone())); let item = Item::RedapEntry { origin: uri.origin, kind: crate::RedapEntryKind::Folder(uri.path), @@ -1050,6 +1042,9 @@ mod tests { "", " ", "aaaaaaaaaaa", + // The filesystem root exists, but should not be treated as an openable path + // (a leading `/` is how the user searches for an entity path in the command palette): + "/", ]; for url in invalid_urls { @@ -1110,7 +1105,7 @@ mod tests { ViewerOpenUrl::from_route( &store_hub, &Route::Settings { - previous: Box::new(dummy_mode.clone()) + return_route: Box::new(dummy_mode.clone()) } ) .unwrap(), @@ -1123,7 +1118,7 @@ mod tests { &Route::ChunkStoreBrowser { store_id: Some(StoreId::empty_recording()), selected_chunk: None, - previous: Box::new(dummy_mode), + return_route: Box::new(dummy_mode), } ) .unwrap(), @@ -1153,7 +1148,6 @@ mod tests { &mut store_hub, Some(LogSource::File { path: std::path::PathBuf::from("/path/to/test.rrd"), - follow: false, }), ); assert_eq!( @@ -1167,7 +1161,6 @@ mod tests { &mut store_hub, Some(LogSource::HttpStream { url: "https://example.com/recording.rrd".to_owned(), - follow: false, }), ); assert_eq!( @@ -1218,6 +1211,7 @@ mod tests { Some(LogSource::RedapGrpcStream { uri: uri.parse().unwrap(), open_behavior: RecordingOpenBehavior::Background, + table_blueprint: None, }), ); @@ -1241,7 +1235,7 @@ mod tests { component: None, }), when: Some(( - re_chunk::TimelineName::new("test"), + re_chunk::TimelineName::from("test"), re_log_types::TimeCell { typ: re_log_types::TimeType::DurationNs, value: re_log_types::NonMinI64::ONE, @@ -1404,9 +1398,7 @@ mod tests { ) .sharable_url(base_url_param) .unwrap(), - format!( - "https://foo.com/test?url=rerun%3A%2F%2F127.0.0.1%3A1234%2Fdataset%2F1830B33B45B963E7774455beb91701ae%3Fsegment_id%3Dpid" - ) + "https://foo.com/test?url=rerun%3A%2F%2F127.0.0.1%3A1234%2Fdataset%2F1830B33B45B963E7774455beb91701ae%3Fsegment_id%3Dpid".to_owned() ); assert_eq!( @@ -1479,7 +1471,7 @@ mod tests { ViewerOpenUrl::RedapDatasetSegment(DatasetSegmentUri { origin: "rerun+http://localhost:51234".parse().unwrap(), dataset_id: "187A3200CAE4DD795748a7ad187e21a3".parse().unwrap(), - segment_id: "6977dcfd524a45b3b786c9a5a0bde4e1".parse().unwrap(), + segment_id: "6977dcfd524a45b3b786c9a5a0bde4e1".into(), fragment: Default::default(), }), ), @@ -1488,7 +1480,7 @@ mod tests { ViewerOpenUrl::RedapDatasetSegment(DatasetSegmentUri { origin: "rerun+http://localhost:51234".parse().unwrap(), dataset_id: "187A3200CAE4DD795748a7ad187e21a3".parse().unwrap(), - segment_id: "6977dcfd524a45b3b786c9a5a0bde4e1".parse().unwrap(), + segment_id: "6977dcfd524a45b3b786c9a5a0bde4e1".into(), fragment: re_uri::Fragment { time_selection: Some("stable_time@+1.096s..+2.097s".parse().unwrap()), ..Default::default() @@ -1500,7 +1492,7 @@ mod tests { ViewerOpenUrl::RedapDatasetSegment(DatasetSegmentUri { origin: "rerun+http://localhost:51234".parse().unwrap(), dataset_id: "187A3200CAE4DD795748a7ad187e21a3".parse().unwrap(), - segment_id: "6977dcfd524a45b3b786c9a5a0bde4e1".parse().unwrap(), + segment_id: "6977dcfd524a45b3b786c9a5a0bde4e1".into(), fragment: re_uri::Fragment { when: Some(( "stable_time".into(), diff --git a/crates/viewer/re_viewer_context/src/route.rs b/crates/viewer/re_viewer_context/src/route.rs index d4b2041abad9..a78526ce5f0d 100644 --- a/crates/viewer/re_viewer_context/src/route.rs +++ b/crates/viewer/re_viewer_context/src/route.rs @@ -4,13 +4,12 @@ use re_log_types::{ApplicationId, StoreId, TableId}; use crate::{Item, RedapEntryKind, open_url::EXAMPLES_ORIGIN}; /// What are we currently showing in the viewer? -// TODO(RR-3033): This needs to be further cleaned up #[derive(Clone, PartialEq, Eq)] pub enum Route { /// The settings dialog for application-wide configuration. Settings { /// What to return to when exiting this mode. - previous: Box, + return_route: Box, }, // TODO(isse): It would be nice to only switch to newly loaded items if we @@ -19,9 +18,10 @@ pub enum Route { Loading(Box), /// Regular view of the local recordings, including the current recording's viewport. + /// + /// This includes recordings we're streaming from a Redap server. LocalRecording { recording_id: StoreId, - // TODO(RR-3033): add blueprint id }, LocalTable(TableId), @@ -46,7 +46,7 @@ pub enum Route { selected_chunk: Option, /// What to return to when exiting this mode. - previous: Box, + return_route: Box, }, } @@ -86,7 +86,7 @@ impl Route { match self { Self::LocalRecording { recording_id } => Some(recording_id), Self::ChunkStoreBrowser { store_id, .. } => store_id.as_ref(), - Self::Settings { previous } => previous.recording_id(), + Self::Settings { return_route } => return_route.recording_id(), Self::Loading { .. } | Self::LocalTable { .. } | Self::RedapEntry { .. } @@ -94,14 +94,15 @@ impl Route { } } - // TODO(RR-3033): remove this app-id centric world + // TODO(andreas): remove this app-id centric world. + // We use this mostly for blueprint association which is very brittle and not very well defined right now. See also RR-3033. pub fn app_id(&self) -> Option<&ApplicationId> { match self { Self::LocalRecording { recording_id } => Some(recording_id.application_id()), Self::ChunkStoreBrowser { store_id, .. } => { store_id.as_ref().map(StoreId::application_id) } - Self::Settings { previous } => previous.app_id(), + Self::Settings { return_route } => return_route.app_id(), Self::RedapServer(server) => { if server == &*EXAMPLES_ORIGIN { Some(crate::StoreHub::welcome_screen_app_id()) @@ -197,12 +198,12 @@ impl Route { Some(uri.origin().clone()) } - Self::Settings { previous } + Self::Settings { return_route } | Self::ChunkStoreBrowser { store_id: None, - previous, + return_route, .. - } => previous.redap_origin(store_hub), + } => return_route.redap_origin(store_hub), Self::Loading(log_source) => { let uri = log_source.redap_uri()?; diff --git a/crates/viewer/re_viewer_context/src/selection_state.rs b/crates/viewer/re_viewer_context/src/selection_state.rs index 586450aca81e..7b7d7f31c5da 100644 --- a/crates/viewer/re_viewer_context/src/selection_state.rs +++ b/crates/viewer/re_viewer_context/src/selection_state.rs @@ -81,20 +81,28 @@ pub enum SelectionChange<'a> { impl ApplicationSelectionState { /// Called at the start of each frame. + /// + /// `resolve_item` decides the fate of each currently selected item: returning `None` drops it, + /// while returning `Some(item)` keeps it — possibly replacing it with a different item (e.g. + /// downgrading a no-longer-valid data result to a plain entity selection). pub fn on_frame_start( &mut self, - item_retain_condition: impl Fn(&Item) -> bool, + resolve_item: impl Fn(&Item) -> Option, fallback_selection: Option, ) -> SelectionChange<'_> { // Use a different name so we don't get a collision in puffin. re_tracing::profile_scope!("SelectionState::on_frame_start"); - let start_len = self.selection.len(); - // Purge selection of invalid items. - self.selection.retain(|item, _| item_retain_condition(item)); + // Purge or repair invalid items. + let resolved = ItemCollection::from_items_and_context( + self.selection + .iter() + .filter_map(|(item, ctx)| resolve_item(item).map(|item| (item, ctx.clone()))), + ); - if start_len != self.selection.len() { + if resolved != self.selection { self.selection_changed = Some(SelectionSource::Other); + self.selection = resolved; } // Set to fallback if empty. diff --git a/crates/viewer/re_viewer_context/src/store_hub.rs b/crates/viewer/re_viewer_context/src/store_hub.rs index 9d2f298db61d..19b75aa931f7 100644 --- a/crates/viewer/re_viewer_context/src/store_hub.rs +++ b/crates/viewer/re_viewer_context/src/store_hub.rs @@ -19,17 +19,19 @@ use re_sdk_types::components::Timestamp; use crate::{ ActiveStoreContext, BlueprintUndoState, RecordingOrTable, Route, StorageContext, StoreCache, - TableStore, TableStores, ViewClassRegistry, + TableStore, TableStores, TimeControl, ViewClassRegistry, }; // --- /// Per-frame usage tracking for an [`EntityDb`]. /// -/// Tracks two states giving context to how it's used: +/// Tracks a few states giving context to how it's used: /// - `was_preview`: If the entity db was used the render a preview last frame. /// - `opened`: If the entity db was explicitly opened by the user and should be /// shown in the recording list. This is not tracked per frame. +/// - `blueprint_pending`: If the recording was streamed without its server blueprint +/// (as previews are), and we still owe a blueprint fetch should it be opened for real. pub struct EntityDbUsages { /// Whether this store was rendered as a preview cell in the previous frame. prev_preview: bool, @@ -42,6 +44,13 @@ pub struct EntityDbUsages { /// Unlike the frame-based preview flag, this persists across frames /// and is not reset by [`Self::update`]. pub opened: bool, + + /// True if this recording was streamed without its server blueprint, and we + /// haven't fetched that blueprint since. + /// + /// Previews skip the blueprint download, so a recording that was only ever a + /// preview carries this debt until it is opened for real. + pub blueprint_pending: bool, } impl Clone for EntityDbUsages { @@ -50,6 +59,7 @@ impl Clone for EntityDbUsages { prev_preview: self.prev_preview, new_preview: std::sync::atomic::AtomicBool::new(false), opened: self.opened, + blueprint_pending: self.blueprint_pending, } } } @@ -60,6 +70,7 @@ impl EntityDbUsages { prev_preview: false, new_preview: std::sync::atomic::AtomicBool::new(false), opened: false, + blueprint_pending: false, } } @@ -111,6 +122,9 @@ pub struct StoreHub { default_blueprint_by_app_id: HashMap, active_blueprint_by_app_id: HashMap, + /// Blueprints associated with tables rather than [`ApplicationId`] + table_blueprints: HashMap, + data_source_order: DataSourceOrder, store_bundle: StoreBundle, table_stores: HashMap, @@ -173,10 +187,11 @@ pub struct BlueprintPersistence { pub deleter: Option>, } -/// Convenient information used for `MemoryPanel`. +/// Convenient information used for `DevPanel`. /// /// This is per [`StoreId`], which could be either a recording or a blueprint. pub struct StoreStats { + pub store_source: Option, pub store_config: ChunkStoreConfig, pub store_stats: ChunkStoreStats, @@ -187,7 +202,7 @@ pub struct StoreStats { pub cache_vram_usage: MemUsageTree, } -/// Convenient information used for `MemoryPanel` +/// Convenient information used for `DevPanel` #[derive(Default)] pub struct StoreHubStats { pub store_stats: BTreeMap, @@ -264,6 +279,7 @@ impl StoreHub { store_usages: Default::default(), table_stores: TableStores::default(), + table_blueprints: Default::default(), } } @@ -310,6 +326,21 @@ impl StoreHub { self.store_usages.get(store_id).is_some_and(|u| u.opened) } + /// Set or clear the [`EntityDbUsages::blueprint_pending`] flag for a store. + pub fn set_blueprint_pending(&mut self, store_id: &StoreId, pending: bool) { + self.store_usages + .entry(store_id.clone()) + .or_insert_with(EntityDbUsages::new) + .blueprint_pending = pending; + } + + /// Whether this recording was streamed without its server blueprint and still owes a fetch. + pub fn is_blueprint_pending(&self, store_id: &StoreId) -> bool { + self.store_usages + .get(store_id) + .is_some_and(|u| u.blueprint_pending) + } + // --------------------- // Accessors @@ -332,10 +363,15 @@ impl StoreHub { /// /// When returned, all of the references to blueprints and recordings will /// have a matching [`ApplicationId`]. - pub fn read_context( - &mut self, + /// + /// The caller must provide the `active_time_ctrl` for the route's recording (if any). + /// It's only used to populate [`ActiveStoreContext::time_ctrl`] when a context is returned; + /// for routes without a recording it's ignored, so passing a default is fine there. + pub fn read_context<'a>( + &'a mut self, route: &Route, - ) -> (StorageContext<'_>, Option>) { + active_time_ctrl: &'a TimeControl, + ) -> (StorageContext<'a>, Option>) { // Used as stand-ins within the `Some` branch when only parts of a // context are available (e.g. we have a blueprint but no recording). static EMPTY_RECORDING: LazyLock = @@ -353,6 +389,14 @@ impl StoreHub { break 'ctx None; }; + // The welcome/example screen has an app-id and a blueprint, but no + // real recording. It must never surface as an active store context, + // or downstream UI (menus, panels, …) will treat it as if a + // recording is active. + if app_id == Self::welcome_screen_app_id() { + break 'ctx None; + } + self.ensure_active_blueprint_for_app(app_id); let should_enable_heuristics = self.should_enable_heuristics_by_app_id.remove(app_id); @@ -387,6 +431,7 @@ impl StoreHub { default_blueprint, recording: recording.unwrap_or(&EMPTY_RECORDING), caches, + time_ctrl: active_time_ctrl, should_enable_heuristics, }) }; @@ -427,25 +472,23 @@ impl StoreHub { /// Called once a frame to make sure the data source order is correct. pub fn update_data_source_order(&mut self, loading_sources: &[Arc]) { - let keep: HashSet<&LogSource> = loading_sources - .iter() - .map(|source| &**source) - .chain( - self.store_bundle - .recordings() - .filter_map(|db| db.data_source.as_ref()), - ) - .collect(); + let keep: HashSet<&LogSource> = std::iter::chain( + loading_sources.iter().map(|source| &**source), + self.store_bundle + .recordings() + .filter_map(|db| db.data_source.as_ref()), + ) + .collect(); self.data_source_order .ordering .retain(|source, _| keep.contains(source)); - for source in self - .store_bundle - .recordings() - .filter_map(|db| db.data_source.as_ref()) - .chain(loading_sources.iter().map(|s| &**s)) - { + for source in std::iter::chain( + self.store_bundle + .recordings() + .filter_map(|db| db.data_source.as_ref()), + loading_sources.iter().map(|s| &**s), + ) { self.data_source_order.add(source); } } @@ -493,6 +536,38 @@ impl StoreHub { self.table_stores.insert(id, store) } + /// Register a fully-loaded blueprint store as the blueprint for a table. + pub fn associate_table_blueprint( + &mut self, + table_id: TableId, + store_id: &StoreId, + ) -> anyhow::Result<()> { + let store = self + .store_bundle + .get(store_id) + .with_context(|| format!("missing table blueprint store: {store_id:?}"))?; + + anyhow::ensure!( + store.store_kind() == StoreKind::Blueprint, + "table blueprint store must be a blueprint store, got {:?}", + store.store_kind() + ); + + if let Some(old_store_id) = self.table_blueprints.insert(table_id, store_id.clone()) + && &old_store_id != store_id + { + self.remove_store(&old_store_id); + } + + Ok(()) + } + + /// Look up the decoded blueprint [`EntityDb`] for a table, if one was stored. + pub fn table_blueprint(&self, table_id: &TableId) -> Option<&EntityDb> { + let store_id = self.table_blueprints.get(table_id)?; + self.store_bundle.get(store_id) + } + fn remove_store(&mut self, store_id: &StoreId) { _ = self.store_caches.remove(store_id); let removed_store = self.store_bundle.remove(store_id); @@ -546,6 +621,9 @@ impl StoreHub { } RecordingOrTable::Table { table_id } => { self.table_stores.remove(table_id); + if let Some(blueprint_store_id) = self.table_blueprints.remove(table_id) { + self.store_bundle.remove(&blueprint_store_id); + } } } } @@ -571,8 +649,7 @@ impl StoreHub { /// /// Ignores any blueprint stores. /// - /// If the data source is a grpc uri, it will ignore any fragments. - /// If the data source is a http url, it will ignore the follow flag. + /// If the data source is a grpc or HTTP URI, it will ignore any fragments. pub fn find_recording_store_by_source( &self, data_source: &re_log_channel::LogSource, @@ -602,6 +679,7 @@ impl StoreHub { .retain(|store_id, _| store_ids_retained.contains(store_id)); self.table_stores.clear(); + self.table_blueprints.clear(); } // --------------------- @@ -680,7 +758,6 @@ impl StoreHub { /// Ensure caches and blueprints are set up for the given recording. /// /// Call this when a recording becomes active (e.g. via [`Route::LocalRecording`]). - // TODO(RR-3033): get rid of this? pub fn load_blueprint_and_caches( &mut self, recording_id: &StoreId, @@ -738,8 +815,27 @@ impl StoreHub { blueprint_id.application_id(), blueprint_id ); + let app_id = blueprint_id.application_id().clone(); self.default_blueprint_by_app_id - .insert(blueprint_id.application_id().clone(), blueprint_id.clone()); + .insert(app_id.clone(), blueprint_id.clone()); + + // If the active blueprint for this app was auto-created as empty (i.e. no + // chunks have ever been written to it), the user hasn't touched it yet, + // so it's safe to replace it with a clone of the newly-registered default. + // This handles the race where the user opens a recording (which creates + // an empty active blueprint via `ensure_active_blueprint_for_app`) before + // the dataset's default blueprint has finished streaming. + if let Some(active_id) = self.active_blueprint_by_app_id.get(&app_id) + && let Some(active_blueprint) = self.store_bundle.get(active_id) + && active_blueprint.latest_row_id().is_none() + { + let blueprint_id = blueprint_id.clone(); + if let Err(err) = self.set_cloned_blueprint_active_for_app(&blueprint_id) { + re_log::warn!( + "Failed to promote new default blueprint to active for '{app_id}': {err}" + ); + } + } Ok(()) } @@ -814,6 +910,8 @@ impl StoreHub { /// Make blueprint active for a given [`ApplicationId`] /// /// We never activate a blueprint directly. Instead, we clone it and activate the clone. + /// Any previously-active blueprint for this app is dropped from the store bundle + /// so it doesn't linger as an orphan. //TODO(jleibs): In the future this can probably be handled with snapshots instead. pub fn set_cloned_blueprint_active_for_app( &mut self, @@ -840,10 +938,21 @@ impl StoreHub { let new_blueprint = blueprint.clone_with_new_id(new_id.clone())?; + let previous_active_id = self.active_blueprint_by_app_id.get(&app_id).cloned(); + self.store_bundle.insert(new_blueprint); self.active_blueprint_by_app_id.insert(app_id, new_id); + // Drop the previous active store now that nothing references it. Skip if it + // happens to be the source we just cloned from (i.e. caller passed the + // already-active id), since `remove_store` would otherwise discard it. + if let Some(prev_id) = previous_active_id + && &prev_id != blueprint_id + { + self.remove_store(&prev_id); + } + Ok(()) } @@ -990,7 +1099,7 @@ impl StoreHub { let target = GarbageCollectionTarget::DropAtLeastFraction(fraction_to_purge as _); - for store_id in preview_recordings.iter().chain(&active_recordings) { + for store_id in std::iter::chain(&preview_recordings, &active_recordings) { let time_cursor = time_cursor_for(store_id); num_bytes_freed += self.gc_store(target, store_id, time_cursor); } @@ -1061,6 +1170,17 @@ impl StoreHub { store_size_before.saturating_sub(store_size_after) } + /// Find a recording whose redap URI matches the given `uri`, ignoring fragments. + pub fn find_recording_by_uri(&self, uri: &re_uri::DatasetSegmentUri) -> Option<&EntityDb> { + self.store_bundle.recordings().find(|db| { + db.redap_uri().is_some_and(|redap_uri| { + redap_uri.origin == uri.origin + && redap_uri.dataset_id == uri.dataset_id + && redap_uri.segment_id == uri.segment_id + }) + }) + } + /// Remove any recordings with a network source pointing at this `uri`. pub fn remove_recording_by_uri(&mut self, uri: &str) { self.retain_recordings(|db| { @@ -1086,11 +1206,10 @@ impl StoreHub { pub fn gc_blueprints(&mut self, undo_state: &HashMap) { re_tracing::profile_function!(); - for blueprint_id in self - .active_blueprint_by_app_id - .values() - .chain(self.default_blueprint_by_app_id.values()) - { + for blueprint_id in std::iter::chain( + self.active_blueprint_by_app_id.values(), + self.default_blueprint_by_app_id.values(), + ) { if let Some(blueprint) = self.store_bundle.get_mut(blueprint_id) { if self.blueprint_last_gc.get(blueprint_id) == Some(&blueprint.generation()) { continue; // no change since last gc @@ -1274,6 +1393,7 @@ impl StoreHub { active_blueprint_by_app_id: _, store_bundle, table_stores, + table_blueprints: _, data_source_order: _, should_enable_heuristics_by_app_id: _, @@ -1292,9 +1412,11 @@ impl StoreHub { .get(store_id) .map(|cache| cache.vram_usage()) .unwrap_or_default(); + store_stats.insert( store_id.clone(), StoreStats { + store_source: store.data_source.clone(), store_config: engine.store().config().clone(), store_stats: engine.store().stats(), query_cache_stats: engine.cache().stats(), @@ -1331,6 +1453,7 @@ impl MemUsageTreeCapture for StoreHub { persistence: _, default_blueprint_by_app_id: _, active_blueprint_by_app_id: _, + table_blueprints: _, data_source_order: _, should_enable_heuristics_by_app_id: _, @@ -1381,3 +1504,113 @@ impl MemUsageTreeCapture for StoreHub { node.into_tree() } } + +#[cfg(test)] +mod tests { + use super::*; + + use re_chunk::Chunk; + use re_log_types::TimePoint; + use re_sdk_types::archetypes::Points2D; + + fn dummy_chunk() -> Arc { + Arc::new( + Chunk::builder("foo") + .with_archetype( + re_chunk::RowId::new(), + TimePoint::STATIC, + &Points2D::new([(0.0_f32, 0.0_f32)]), + ) + .build() + .expect("chunk should build"), + ) + } + + /// When the active blueprint for an app is auto-created empty and then a + /// default blueprint is registered, the active should be replaced by a clone + /// of the default. (Regression for the OSS server / `segment_table` flow, + /// rerun#12773.) + #[test] + fn registering_default_replaces_empty_active_blueprint() { + let mut hub = StoreHub::test_hub(); + let app_id = ApplicationId::from("test_app"); + + // Simulate the race: the user opens the route before the dataset + // blueprint has landed, so an empty active blueprint is created first. + hub.ensure_active_blueprint_for_app(&app_id); + let original_active_id = hub + .active_blueprint_id_for_app(&app_id) + .expect("active blueprint should exist") + .clone(); + assert!( + hub.store_bundle + .get(&original_active_id) + .unwrap() + .latest_row_id() + .is_none(), + "active blueprint should be empty" + ); + + // Now the dataset's default blueprint finishes streaming and registers. + let default_id = StoreId::random(StoreKind::Blueprint, app_id.clone()); + hub.store_bundle.blueprint_entry(&default_id); + hub.add_chunk_for_tests(&default_id, &dummy_chunk()) + .unwrap(); + hub.set_default_blueprint_for_app(&default_id).unwrap(); + + let new_active_id = hub + .active_blueprint_id_for_app(&app_id) + .expect("active blueprint should still exist") + .clone(); + assert_ne!( + new_active_id, original_active_id, + "empty active blueprint should have been replaced" + ); + assert_eq!( + hub.store_bundle.get(&new_active_id).unwrap().cloned_from(), + Some(&default_id), + "new active should be a clone of the newly-registered default" + ); + assert!( + hub.store_bundle.get(&original_active_id).is_none(), + "previous empty active blueprint should be dropped from the bundle" + ); + } + + /// If the active blueprint has any user-written content, registering a new + /// default must not clobber it. + #[test] + fn registering_default_preserves_modified_active_blueprint() { + let mut hub = StoreHub::test_hub(); + let app_id = ApplicationId::from("test_app"); + + hub.ensure_active_blueprint_for_app(&app_id); + let active_id = hub + .active_blueprint_id_for_app(&app_id) + .expect("active blueprint should exist") + .clone(); + + // User has touched the active blueprint. + hub.add_chunk_for_tests(&active_id, &dummy_chunk()).unwrap(); + assert!( + hub.store_bundle + .get(&active_id) + .unwrap() + .latest_row_id() + .is_some() + ); + + // A default blueprint arrives. + let default_id = StoreId::random(StoreKind::Blueprint, app_id.clone()); + hub.store_bundle.blueprint_entry(&default_id); + hub.add_chunk_for_tests(&default_id, &dummy_chunk()) + .unwrap(); + hub.set_default_blueprint_for_app(&default_id).unwrap(); + + assert_eq!( + hub.active_blueprint_id_for_app(&app_id), + Some(&active_id), + "modified active blueprint should NOT be replaced" + ); + } +} diff --git a/crates/viewer/re_viewer_context/src/store_view_context.rs b/crates/viewer/re_viewer_context/src/store_view_context.rs index dabe790943c5..cb89496c64d1 100644 --- a/crates/viewer/re_viewer_context/src/store_view_context.rs +++ b/crates/viewer/re_viewer_context/src/store_view_context.rs @@ -1,7 +1,7 @@ use re_chunk::{Timeline, TimelineName}; use re_entity_db::EntityDb; -use crate::{AppContext, AppOptions, Cache, StoreCache, TimeControl}; +use crate::{AppContext, AppOptions, Cache, CacheEntryAccess, StoreCache, TimeControl}; /// Context for viewing a specific store, /// (either a recording, or a blueprint). @@ -31,6 +31,18 @@ impl<'a> std::ops::Deref for StoreViewContext<'a> { } impl<'a> StoreViewContext<'a> { + /// Builds a [`StoreViewContext`] for the active recording of an [`AppContext`], if any. + /// + /// Returns `None` when no recording is active (e.g. catalog browsing, welcome screen). + pub fn for_active_recording(app_ctx: &'a AppContext<'a>) -> Option { + app_ctx.active_store_context.map(|store_ctx| Self { + app_ctx, + db: store_ctx.recording, + time_ctrl: store_ctx.time_ctrl, + caches: store_ctx.caches, + }) + } + /// Move time cursor #[must_use] pub fn with_time_ctrl(&self, time_ctrl: &'a TimeControl) -> Self { @@ -47,7 +59,7 @@ impl<'a> StoreViewContext<'a> { /// The currently selected timeline for this store. pub fn timeline_name(&self) -> TimelineName { - self.query().timeline() + *self.time_ctrl.timeline_name() } /// The currently selected timeline for this store. @@ -79,4 +91,26 @@ impl<'a> StoreViewContext<'a> { pub fn memoizer(&self, f: impl FnOnce(&mut C) -> R) -> R { self.caches.memoizer(f) } + + /// Accesses an existing memoization cache for reading. + /// + /// Shorthand for `self.caches.memoizer_read(f)`. + pub fn memoizer_read(&self, f: impl FnOnce(&C) -> R) -> Option { + self.caches.memoizer_read(f) + } + + /// Tries to read an existing memoization cache entry, then computes it through mutable access on miss. + /// + /// Use this if you're working with init-only cache entries, expect your cache entry to be usually present + /// and want to avoid the overhead of a write lock. + /// Note that this _adds_ overhead for the miss path compared to `memoizer`, so don't use this if you expect many misses! + /// (UI code typically doesn't need to care about this optimization, since it's usually single-threaded already.) + /// + /// Shorthand for `self.caches.memoizer_read_or_compute(key)`. + pub fn memoizer_read_or_compute(&self, key: &Key) -> Value + where + C: CacheEntryAccess + Default, + { + self.caches.memoizer_read_or_compute::(key) + } } diff --git a/crates/viewer/re_viewer_context/src/tensor/image_stats.rs b/crates/viewer/re_viewer_context/src/tensor/image_stats.rs index 1a880f38ef1f..704714d23be4 100644 --- a/crates/viewer/re_viewer_context/src/tensor/image_stats.rs +++ b/crates/viewer/re_viewer_context/src/tensor/image_stats.rs @@ -4,7 +4,7 @@ use re_sdk_types::datatypes::ChannelDatatype; use crate::ImageInfo; /// Stats about an image. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, re_byte_size::SizeBytes)] pub struct ImageStats { /// The range of values, ignoring `NaN`s. /// @@ -18,16 +18,6 @@ pub struct ImageStats { pub finite_range: (f64, f64), } -impl re_byte_size::SizeBytes for ImageStats { - fn heap_size_bytes(&self) -> u64 { - 0 - } - - fn is_pod() -> bool { - true - } -} - impl ImageStats { pub fn from_image(image: &ImageInfo) -> Self { re_tracing::profile_function!(); diff --git a/crates/viewer/re_viewer_context/src/tensor/tensor_stats.rs b/crates/viewer/re_viewer_context/src/tensor/tensor_stats.rs index 9eb88a7e2e93..a0e74f4258bb 100644 --- a/crates/viewer/re_viewer_context/src/tensor/tensor_stats.rs +++ b/crates/viewer/re_viewer_context/src/tensor/tensor_stats.rs @@ -3,7 +3,7 @@ use ndarray::ArrayViewD; use re_sdk_types::tensor_data::TensorDataType; /// Stats about a tensor or image. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, re_byte_size::SizeBytes)] pub struct TensorStats { /// The range of values, ignoring `NaN`s. /// @@ -17,16 +17,6 @@ pub struct TensorStats { pub finite_range: (f64, f64), } -impl re_byte_size::SizeBytes for TensorStats { - fn heap_size_bytes(&self) -> u64 { - 0 - } - - fn is_pod() -> bool { - true - } -} - impl TensorStats { pub fn from_tensor(tensor: &re_sdk_types::datatypes::TensorData) -> Self { re_tracing::profile_function!(); @@ -175,13 +165,7 @@ impl TensorStats { }; // If we didn't find a finite range, set it to None. - finite_range.and_then(|r| { - if r.0.is_finite() && r.1.is_finite() { - Some(r) - } else { - None - } - }) + finite_range.filter(|&r| r.0.is_finite() && r.1.is_finite()) } .unwrap_or_else(|| (tensor.dtype().min_value(), tensor.dtype().max_value())); diff --git a/crates/viewer/re_viewer_context/src/time_control.rs b/crates/viewer/re_viewer_context/src/time_control.rs deleted file mode 100644 index dad0ba849692..000000000000 --- a/crates/viewer/re_viewer_context/src/time_control.rs +++ /dev/null @@ -1,1516 +0,0 @@ -use std::collections::BTreeMap; - -use re_chunk::{EntityPath, TimelineName}; -use re_entity_db::EntityDb; -use re_log_types::{ - AbsoluteTimeRange, AbsoluteTimeRangeF, Duration, TimeCell, TimeInt, TimeReal, TimeType, - Timeline, -}; -use re_sdk_types::blueprint::archetypes::TimePanelBlueprint; -use re_sdk_types::blueprint::components::{LoopMode, PlayState}; - -use crate::NeedsRepaint; -use crate::blueprint_helpers::BlueprintContext; - -pub const TIME_PANEL_PATH: &str = "time_panel"; - -pub fn time_panel_blueprint_entity_path() -> EntityPath { - TIME_PANEL_PATH.into() -} - -/// Helper trait to write time panel related blueprint components. -trait TimeBlueprintExt { - fn set_timeline(&self, timeline: TimelineName); - - fn timeline(&self) -> Option; - - /// Replaces the current timeline with the automatic one. - fn clear_timeline(&self); - - fn set_playback_speed(&self, playback_speed: f64); - fn playback_speed(&self) -> Option; - - fn set_fps(&self, fps: f64); - fn fps(&self) -> Option; - - fn set_play_state(&self, play_state: PlayState); - fn play_state(&self) -> Option; - - fn set_loop_mode(&self, loop_mode: LoopMode); - fn loop_mode(&self) -> Option; - - fn set_time_selection(&self, time_range: AbsoluteTimeRange); - fn time_selection(&self) -> Option; - fn clear_time_selection(&self); -} - -impl TimeBlueprintExt for T { - fn set_timeline(&self, timeline: TimelineName) { - self.save_blueprint_component( - time_panel_blueprint_entity_path(), - &TimePanelBlueprint::descriptor_timeline(), - &re_sdk_types::blueprint::components::TimelineName::from(timeline.as_str()), - ); - } - - fn timeline(&self) -> Option { - let (_, timeline) = self - .current_blueprint() - .latest_at_component_quiet::( - &time_panel_blueprint_entity_path(), - self.blueprint_query(), - TimePanelBlueprint::descriptor_timeline().component, - )?; - - Some(TimelineName::new(timeline.as_str())) - } - - fn clear_timeline(&self) { - self.clear_blueprint_component( - time_panel_blueprint_entity_path(), - TimePanelBlueprint::descriptor_timeline(), - ); - } - - fn set_playback_speed(&self, playback_speed: f64) { - self.save_blueprint_component( - time_panel_blueprint_entity_path(), - &TimePanelBlueprint::descriptor_playback_speed(), - &re_sdk_types::blueprint::components::PlaybackSpeed(playback_speed.into()), - ); - } - - fn playback_speed(&self) -> Option { - let (_, playback_speed) = self - .current_blueprint() - .latest_at_component_quiet::( - &time_panel_blueprint_entity_path(), - self.blueprint_query(), - TimePanelBlueprint::descriptor_playback_speed().component, - )?; - - Some(**playback_speed) - } - - fn set_fps(&self, fps: f64) { - self.save_blueprint_component( - time_panel_blueprint_entity_path(), - &TimePanelBlueprint::descriptor_fps(), - &re_sdk_types::blueprint::components::Fps(fps.into()), - ); - } - - fn fps(&self) -> Option { - let (_, fps) = self - .current_blueprint() - .latest_at_component_quiet::( - &time_panel_blueprint_entity_path(), - self.blueprint_query(), - TimePanelBlueprint::descriptor_fps().component, - )?; - - Some(**fps) - } - - fn set_play_state(&self, play_state: PlayState) { - self.save_static_blueprint_component( - time_panel_blueprint_entity_path(), - &TimePanelBlueprint::descriptor_play_state(), - &play_state, - ); - } - - fn play_state(&self) -> Option { - let (_, play_state) = self - .current_blueprint() - .latest_at_component_quiet::( - &time_panel_blueprint_entity_path(), - self.blueprint_query(), - TimePanelBlueprint::descriptor_play_state().component, - )?; - - Some(play_state) - } - - fn set_loop_mode(&self, loop_mode: LoopMode) { - self.save_blueprint_component( - time_panel_blueprint_entity_path(), - &TimePanelBlueprint::descriptor_loop_mode(), - &loop_mode, - ); - } - - fn loop_mode(&self) -> Option { - let (_, loop_mode) = self - .current_blueprint() - .latest_at_component_quiet::( - &time_panel_blueprint_entity_path(), - self.blueprint_query(), - TimePanelBlueprint::descriptor_loop_mode().component, - )?; - - Some(loop_mode) - } - - fn set_time_selection(&self, time_range: AbsoluteTimeRange) { - self.save_blueprint_component( - time_panel_blueprint_entity_path(), - &TimePanelBlueprint::descriptor_time_selection(), - &re_sdk_types::blueprint::components::AbsoluteTimeRange( - re_sdk_types::datatypes::AbsoluteTimeRange { - min: time_range.min.as_i64().into(), - max: time_range.max.as_i64().into(), - }, - ), - ); - } - - fn time_selection(&self) -> Option { - let (_, time_range) = self - .current_blueprint() - .latest_at_component_quiet::( - &time_panel_blueprint_entity_path(), - self.blueprint_query(), - TimePanelBlueprint::descriptor_time_selection().component, - )?; - - Some(AbsoluteTimeRange::new(time_range.min, time_range.max)) - } - - fn clear_time_selection(&self) { - self.clear_blueprint_component( - time_panel_blueprint_entity_path(), - TimePanelBlueprint::descriptor_time_selection(), - ); - } -} - -/// The time range we are currently zoomed in on. -#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct TimeView { - /// Where start of the range. - pub min: TimeReal, - - /// How much time the full view covers. - /// - /// The unit is either nanoseconds or sequence numbers. - /// - /// If there is gaps in the data, the actual amount of viewed time might be less. - pub time_spanned: f64, -} - -impl From for TimeView { - fn from(value: AbsoluteTimeRange) -> Self { - Self { - min: value.min().into(), - time_spanned: value.abs_length() as f64, - } - } -} - -/// Direction for time movement commands. -#[derive(Debug, Clone, Copy)] -pub enum MoveDirection { - Back, - Forward, -} - -/// Speed for time movement commands. -#[derive(Debug, Clone, Copy)] -pub enum MoveSpeed { - Normal, - Fast, -} - -/// A command used to mutate `TimeControl`. -/// -/// Can be sent using [`crate::SystemCommand::TimeControlCommands`]. -#[derive(Debug)] -pub enum TimeControlCommand { - HighlightRange(AbsoluteTimeRange), - ClearHighlightedRange, - - /// Reset the active timeline to instead be automatically assigned. - ResetActiveTimeline, - SetActiveTimeline(TimelineName), - - /// Set the current looping state. - SetLoopMode(LoopMode), - SetPlayState(PlayState), - Pause, - TogglePlayPause, - StepTimeBack, - StepTimeForward, - Move { - direction: MoveDirection, - speed: MoveSpeed, - }, - MoveBeginning, - MoveEnd, - - /// Restart the time cursor to the start. - /// - /// Stops any ongoing following. - Restart, - - /// Set playback speed. - SetSpeed(f32), - - /// Set playback fps. - SetFps(f32), - - /// Set the current time selection without enabling looping. - SetTimeSelection(AbsoluteTimeRange), - - /// Remove the current time selection. - /// - /// If the current loop mode is selection, turns off looping. - RemoveTimeSelection, - - /// Sets the current time cursor. - SetTime(TimeReal), - - /// Set the range of time we are currently zoomed in on. - SetTimeView(TimeView), - - /// Reset the range of time we are currently zoomed in on. - /// - /// The view will instead fall back to the default which is - /// showing all received data. - ResetTimeView, -} - -/// State per timeline. -#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)] -struct TimeState { - /// The current time (play marker). - time: TimeReal, - - /// The last time this timeline was paused at. - /// - /// Used for the web url. - #[serde(skip)] - last_paused_time: Option, - - /// Frames per second, when playing sequences (they are often video recordings). - fps: f32, - - /// Selected time range, if any. - #[serde(default)] - time_selection: Option, - - /// The time range we are currently zoomed in on. - /// - /// `None` means "everything", and is the default value. - /// In this case, the view will expand while new data is added. - /// Only when the user actually zooms or pans will this be set. - #[serde(default)] - view: Option, -} - -impl TimeState { - fn new(time: impl Into) -> Self { - Self { - time: time.into(), - last_paused_time: None, - fps: 30.0, // TODO(emilk): estimate based on data - time_selection: Default::default(), - view: None, - } - } -} - -/// Which timeline is currently active in the time panel. -/// -/// The active timeline can be in one of three states: -/// - Automatically chosen based on heuristics (e.g. the timeline with most data), -/// - Explicitly selected by the user, -/// - Or "pending": requested by name (via blueprint or user action) but not yet -/// present in the entity database. A pending timeline is promoted to `UserEdited` -/// once data containing that timeline arrives (see [`TimeControl::select_valid_timeline`]). -// TODO(andreas): This should be a blueprint property and follow the usual rules of how we determine fallbacks. -#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq, Debug)] -enum ActiveTimeline { - /// Automatically selected based on heuristics. Re-evaluated every frame. - Auto(Timeline), - - /// Explicitly selected by the user or resolved from blueprint. - UserEdited(Timeline), - - /// A timeline was requested by name but hasn't been seen in the data yet. - /// - /// This happens when the blueprint or a [`TimeControlCommand::SetActiveTimeline`] references - /// a timeline that doesn't exist in the current [`EntityDb`]. We store only the name - /// and wait for matching data to arrive, at which point this becomes `UserEdited`. - Pending(TimelineName), -} - -impl ActiveTimeline { - /// The name of the active timeline, regardless of its state. - pub fn name(&self) -> &TimelineName { - match self { - Self::Auto(timeline) | Self::UserEdited(timeline) => timeline.name(), - Self::Pending(timeline_name) => timeline_name, - } - } - - /// The full [`Timeline`], if available. - /// - /// Returns `None` for [`Self::Pending`] since the timeline hasn't been - /// resolved against the entity database yet. - pub fn timeline(&self) -> Option<&Timeline> { - match self { - Self::Auto(timeline) | Self::UserEdited(timeline) => Some(timeline), - Self::Pending(_) => None, - } - } -} - -/// Controls the global view and progress of the time. -/// -/// Modifications to this can be done via sending [`TimeControlCommand`]s -/// which are handled at the end of frames. -/// -/// The commands write both to this struct and to blueprints when -/// applicable. -#[derive(Clone, PartialEq)] -pub struct TimeControl { - /// Name of the timeline (e.g. `log_time`). - timeline: ActiveTimeline, - - states: BTreeMap, - - /// If true, we are either in [`PlayState::Playing`] or [`PlayState::Following`]. - playing: bool, - - /// If true, we are in "follow" mode (see [`PlayState::Following`]). - /// Ignored when [`Self::playing`] is `false`. - following: bool, - - /// If `true`, and we're [`Self::playing`], then we only advance - /// the time cursor once we have all the data needed loaded locally. - /// - /// This is a larger-than-ram feature: only once we have loaded - /// the necessary data from redap do we start playback. - wait_for_data: bool, - - speed: f32, - - loop_mode: LoopMode, - - /// Range with special highlight. - /// - /// This is used during UI interactions. E.g. to show visual history range that's highlighted. - pub highlighted_range: Option, -} - -impl Default for TimeControl { - fn default() -> Self { - Self { - timeline: ActiveTimeline::Auto(Timeline::pick_best_timeline([], |_| 0)), - states: Default::default(), - playing: true, - following: true, - wait_for_data: true, - speed: 1.0, - loop_mode: LoopMode::Off, - highlighted_range: None, - } - } -} - -/// Parameters for [`TimeControl::update`]. -pub struct TimeControlUpdateParams { - /// The time step in seconds. - pub stable_dt: f32, - - /// Is more data expected to arrive (e.g. still connected to a data source)? - /// - /// Set to true e.g. when viewing live data, - /// or we're still downloading a recording. - pub more_data_is_streaming_in: bool, - - /// True if we're waiting for chunks to be downloaded, - /// and they are expected to come (eventually). - pub is_buffering: bool, - - /// Should we diff state changes to trigger callbacks? - pub should_diff_state: bool, -} - -#[must_use] -pub struct TimeControlResponse { - pub needs_repaint: NeedsRepaint, - - /// Set if play state changed. - /// - /// * `Some(true)` if playing changed to `true` - /// * `Some(false)` if playing changed to `false` - /// * `None` if playing did not change - pub playing_change: Option, - - /// Set if timeline changed. - /// - /// Contains the timeline name and the current time. - pub timeline_change: Option<(Timeline, TimeReal)>, - - /// Set if the time changed. - pub time_change: Option, -} - -impl TimeControlResponse { - fn no_repaint() -> Self { - Self::new(NeedsRepaint::No) - } - - fn new(needs_repaint: NeedsRepaint) -> Self { - Self { - needs_repaint, - playing_change: None, - timeline_change: None, - time_change: None, - } - } -} - -impl TimeControl { - pub fn from_blueprint(blueprint_ctx: &impl BlueprintContext) -> Self { - let mut this = Self::default(); - - this.update_from_blueprint(blueprint_ctx, None); - - this - } - - /// Read from the time panel blueprint and update the state from that. - /// - /// If `entity_db` is some this will also make sure we are on a valid timeline. - pub fn update_from_blueprint( - &mut self, - blueprint_ctx: &impl BlueprintContext, - entity_db: Option<&EntityDb>, - ) { - if let Some(timeline) = blueprint_ctx.timeline() { - if matches!(self.timeline, ActiveTimeline::Auto(_)) - || timeline.as_str() != self.timeline_name().as_str() - { - self.timeline = ActiveTimeline::Pending(timeline); - } - } else if let Some(timeline) = self.timeline() { - self.timeline = ActiveTimeline::Auto(*timeline); - } - - let old_timeline = *self.timeline_name(); - // Make sure we are on a valid timeline. - if let Some(entity_db) = entity_db { - self.select_valid_timeline(entity_db); - } - - if let Some(new_play_state) = blueprint_ctx.play_state() - && new_play_state != self.play_state() - { - self.set_play_state(entity_db, new_play_state, Some(blueprint_ctx)); - } - - if let Some(new_loop_mode) = blueprint_ctx.loop_mode() { - self.loop_mode = new_loop_mode; - - if self.loop_mode != LoopMode::Off { - if self.play_state() == PlayState::Following { - self.set_play_state(entity_db, PlayState::Playing, Some(blueprint_ctx)); - } - - // It makes no sense with looping and follow. - self.following = false; - } - } - - if let Some(playback_speed) = blueprint_ctx.playback_speed() { - self.speed = playback_speed as f32; - } - - let play_state = self.play_state(); - - // Update the last paused time if we are paused. - let timeline = *self.timeline_name(); - if let Some(state) = self.states.get_mut(&timeline) { - if let Some(fps) = blueprint_ctx.fps() { - state.fps = fps as f32; - } - - let bp_loop_section = blueprint_ctx.time_selection(); - // If we've switched timeline, use the new timeline's cached time selection. - if old_timeline == timeline { - state.time_selection = bp_loop_section.map(|r| r.into()); - } else { - match state.time_selection { - Some(selection) => blueprint_ctx.set_time_selection(selection.to_int()), - None => { - blueprint_ctx.clear_time_selection(); - } - } - } - - match play_state { - PlayState::Paused => { - state.last_paused_time = Some(state.time); - } - PlayState::Playing | PlayState::Following => {} - } - } - } - - /// Sets the current time. - /// - /// This will NOT update the blueprint! - pub fn set_time_ad_hoc(&mut self, time: TimeReal) { - self.set_time_cursor_ad_hoc(*self.timeline_name(), time); - } - - /// Sets the current time. - /// - /// This will NOT update the blueprint! - pub fn set_time_cursor_ad_hoc(&mut self, timeline: TimelineName, time: TimeReal) { - self.states - .entry(timeline) - .or_insert_with(|| TimeState::new(time)) - .time = time; - } - - /// Create [`TimeControlCommand`]s to move the time forward (if playing), and perhaps pause if - /// we've reached the end. - pub fn update( - &mut self, - entity_db: &EntityDb, - params: &TimeControlUpdateParams, - blueprint_ctx: Option<&impl BlueprintContext>, - ) -> TimeControlResponse { - let TimeControlUpdateParams { - stable_dt, - more_data_is_streaming_in, - is_buffering, - should_diff_state, - } = *params; - - let (old_playing, old_timeline, old_state) = ( - self.playing, - self.timeline().copied(), - self.states.get(self.timeline_name()).copied(), - ); - - if let Some(blueprint_ctx) = blueprint_ctx { - self.update_from_blueprint(blueprint_ctx, Some(entity_db)); - } else { - self.select_valid_timeline(entity_db); - } - - let Some(full_range) = entity_db.time_range_for(self.timeline_name()) else { - return TimeControlResponse::no_repaint(); // we have no data on this timeline yet, so bail - }; - - let needs_repaint = match self.play_state() { - PlayState::Paused => { - // It's possible that the playback is paused because e.g. it reached its end, but - // then the user decides to switch timelines. - // When they do so, it might be the case that they switch to a timeline they've - // never interacted with before, in which case we don't even have a time state yet. - let state = self.states.entry(*self.timeline_name()).or_insert_with(|| { - TimeState::new(if self.following { - full_range.max() - } else { - full_range.min() - }) - }); - - state.last_paused_time = Some(state.time); - self.wait_for_data = true; // in case we hit play again! - NeedsRepaint::No - } - - PlayState::Playing => { - let state = self - .states - .entry(*self.timeline_name()) - .or_insert_with(|| TimeState::new(full_range.min())); - - if self.wait_for_data && is_buffering { - // Do not move time cursor until we are done buffering - NeedsRepaint::No - } else { - self.wait_for_data = false; // Don't auto-pause once we are actually playing - - let dt = stable_dt.min(0.1) * self.speed; - - if self.loop_mode == LoopMode::Off && full_range.max() <= state.time { - // We've reached the end of the data - self.set_time_ad_hoc(full_range.max().into()); - - if more_data_is_streaming_in { - // then let's wait for it without pausing! - } else { - self.pause(blueprint_ctx); - } - NeedsRepaint::No - } else { - let mut new_time = state.time; - - let loop_range = match self.loop_mode { - LoopMode::Off => None, - LoopMode::Selection => state.time_selection, - LoopMode::All => Some(full_range.into()), - }; - - match self.timeline.timeline().map(|t| t.typ()) { - Some(TimeType::Sequence) => { - new_time += TimeReal::from(state.fps * dt); - } - Some(TimeType::DurationNs | TimeType::TimestampNs) => { - new_time += TimeReal::from(Duration::from_secs(dt)); - } - None => {} - } - - if let Some(loop_range) = loop_range - && loop_range.max < new_time - { - new_time = loop_range.min; // loop! - } - - self.set_time_ad_hoc(new_time); - - NeedsRepaint::Yes - } - } - } - PlayState::Following => { - // Set the time to the max: - self.set_time_ad_hoc(full_range.max().into()); - - NeedsRepaint::No // no need for request_repaint - we already repaint when new data arrives - } - }; - - self.apply_state_diff_if_needed( - TimeControlResponse::new(needs_repaint), - should_diff_state, - entity_db, - old_timeline, - old_playing, - old_state, - ) - } - - /// Apply state diff to response if needed. - #[expect(clippy::fn_params_excessive_bools)] // TODO(emilk): remove bool parameters - fn apply_state_diff_if_needed( - &mut self, - response: TimeControlResponse, - should_diff_state: bool, - entity_db: &EntityDb, - old_timeline: Option, - old_playing: bool, - old_state: Option, - ) -> TimeControlResponse { - let mut response = response; - - if should_diff_state && entity_db.time_range_for(self.timeline_name()).is_some() { - self.diff_with(&mut response, old_timeline, old_playing, old_state); - } - - response - } - - /// Handle updating last frame state and trigger callbacks on changes. - fn diff_with( - &mut self, - response: &mut TimeControlResponse, - old_timeline: Option, - old_playing: bool, - old_state: Option, - ) { - if old_playing != self.playing { - response.playing_change = Some(self.playing); - } - - if old_timeline != self.timeline().copied() { - let time = self - .time_for_timeline(*self.timeline_name()) - .unwrap_or(TimeReal::MIN); - - response.timeline_change = self.timeline().map(|t| (*t, time)); - } - - if let Some(state) = self.states.get_mut(self.timeline.name()) { - // TODO(jan): throttle? - if old_state.is_none_or(|old_state| old_state.time != state.time) { - response.time_change = Some(state.time); - } - } - } - - pub fn play_state(&self) -> PlayState { - if self.playing { - if self.following { - PlayState::Following - } else { - PlayState::Playing - } - } else { - PlayState::Paused - } - } - - pub fn loop_mode(&self) -> LoopMode { - if self.play_state() == PlayState::Following { - LoopMode::Off - } else { - self.loop_mode - } - } - - pub fn handle_time_commands( - &mut self, - blueprint_ctx: Option<&impl BlueprintContext>, - entity_db: &EntityDb, - commands: &[TimeControlCommand], - ) -> TimeControlResponse { - let mut response = TimeControlResponse { - needs_repaint: NeedsRepaint::No, - playing_change: None, - timeline_change: None, - time_change: None, - }; - - let (old_playing, old_timeline, old_state) = ( - self.playing, - self.timeline().copied(), - self.states.get(self.timeline_name()).copied(), - ); - - for command in commands { - let needs_repaint = self.handle_time_command(blueprint_ctx, entity_db, command); - - if needs_repaint == NeedsRepaint::Yes { - response.needs_repaint = NeedsRepaint::Yes; - } - } - - self.diff_with(&mut response, old_timeline, old_playing, old_state); - - response - } - - /// Applies a time command with respect to the current timeline. - /// - /// If `blueprint_ctx` is some, this also writes to that blueprint - /// for applicable commands. - /// - /// Returns if the command should cause a repaint. - fn handle_time_command( - &mut self, - blueprint_ctx: Option<&impl BlueprintContext>, - entity_db: &EntityDb, - command: &TimeControlCommand, - ) -> NeedsRepaint { - match command { - // TODO(isse): Changing the highlighted range should technically cause a repaint. But this causes issues - // because right now the selection panel wants to clear the range if it's some each frame, and maybe set - // it again at later point. - // - // This is (right now) always caused by hovering on something, so the mouse movement will cause repaints - // in all current cases. - // - // A better fix for this would be to collect all time commands before handling them, and for highlight - // ranges only keep the last one. And requesting a repaint here again. - TimeControlCommand::HighlightRange(range) => { - self.highlighted_range = Some(*range); - NeedsRepaint::No - } - TimeControlCommand::ClearHighlightedRange => { - self.highlighted_range = None; - NeedsRepaint::No - } - TimeControlCommand::ResetActiveTimeline => { - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.clear_timeline(); - } - if let Some(timeline) = self - .timeline() - .copied() - .or_else(|| entity_db.timelines().into_values().next()) - { - self.timeline = ActiveTimeline::Auto(timeline); - } - self.select_valid_timeline(entity_db); - - NeedsRepaint::Yes - } - TimeControlCommand::SetActiveTimeline(timeline_name) => { - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.set_timeline(*timeline_name); - } - - if let Some(timeline) = entity_db.timelines().get(timeline_name) { - self.timeline = ActiveTimeline::UserEdited(*timeline); - } else { - self.timeline = ActiveTimeline::Pending(*timeline_name); - } - - if let Some(state) = self.states.get(timeline_name) { - // Use the new timeline's cached time selection. - if let Some(blueprint_ctx) = blueprint_ctx { - match state.time_selection { - Some(selection) => blueprint_ctx.set_time_selection(selection.to_int()), - None => blueprint_ctx.clear_time_selection(), - } - } - } else if let Some(full_range) = entity_db.time_range_for(timeline_name) { - self.states - .insert(*timeline_name, TimeState::new(full_range.min)); - } - - NeedsRepaint::Yes - } - TimeControlCommand::SetLoopMode(loop_mode) => { - if self.loop_mode == *loop_mode { - NeedsRepaint::No - } else { - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.set_loop_mode(*loop_mode); - } - self.loop_mode = *loop_mode; - if self.loop_mode != LoopMode::Off { - if self.play_state() == PlayState::Following { - self.set_play_state(Some(entity_db), PlayState::Playing, blueprint_ctx); - } - - // It makes no sense with looping and follow. - self.following = false; - } - - NeedsRepaint::Yes - } - } - TimeControlCommand::SetPlayState(play_state) => { - if self.play_state() == *play_state { - NeedsRepaint::No - } else { - self.set_play_state(Some(entity_db), *play_state, blueprint_ctx); - - if self.following { - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.set_loop_mode(LoopMode::Off); - } - self.loop_mode = LoopMode::Off; - } - - NeedsRepaint::Yes - } - } - TimeControlCommand::Pause => { - if self.playing { - self.pause(blueprint_ctx); - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - - TimeControlCommand::TogglePlayPause => { - self.toggle_play_pause(entity_db, blueprint_ctx); - - NeedsRepaint::Yes - } - TimeControlCommand::StepTimeBack => { - self.step_time_back(entity_db, blueprint_ctx); - - NeedsRepaint::Yes - } - TimeControlCommand::StepTimeForward => { - self.step_time_fwd(entity_db, blueprint_ctx); - - NeedsRepaint::Yes - } - TimeControlCommand::Move { direction, speed } => { - self.move_time(entity_db, blueprint_ctx, *direction, *speed); - NeedsRepaint::Yes - } - TimeControlCommand::MoveBeginning => { - if let Some(full_range) = entity_db.time_range_for(self.timeline_name()) { - self.states - .entry(*self.timeline_name()) - .or_insert_with(|| TimeState::new(full_range.min)) - .time = full_range.min.into(); - - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - TimeControlCommand::MoveEnd => { - if let Some(full_range) = entity_db.time_range_for(self.timeline_name()) { - self.states - .entry(*self.timeline_name()) - .or_insert_with(|| TimeState::new(full_range.max)) - .time = full_range.max.into(); - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - TimeControlCommand::Restart => { - if let Some(full_range) = entity_db.time_range_for(self.timeline_name()) { - self.following = false; - - if let Some(state) = self.states.get_mut(self.timeline.name()) { - state.time = full_range.min.into(); - } - - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - TimeControlCommand::SetSpeed(speed) => { - if *speed == self.speed { - NeedsRepaint::No - } else { - self.speed = *speed; - - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.set_playback_speed(*speed as f64); - } - - NeedsRepaint::Yes - } - } - TimeControlCommand::SetFps(fps) => { - if let Some(state) = self.states.get_mut(self.timeline.name()) - && state.fps != *fps - { - state.fps = *fps; - - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.set_fps(*fps as f64); - } - - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - TimeControlCommand::SetTimeSelection(time_range) => { - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.set_time_selection(*time_range); - } - - let state = self - .states - .entry(*self.timeline_name()) - .or_insert_with(|| TimeState::new(time_range.min)); - - let repaint = state.time_selection.map(|r| r.to_int()) != Some(*time_range); - - state.time_selection = Some((*time_range).into()); - - if repaint { - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - TimeControlCommand::RemoveTimeSelection => { - if let Some(state) = self.states.get_mut(self.timeline.name()) { - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.clear_time_selection(); - } - state.time_selection = None; - if self.loop_mode == LoopMode::Selection { - self.loop_mode = LoopMode::Off; - - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.set_loop_mode(self.loop_mode); - } - } - - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - TimeControlCommand::SetTime(time) => { - let time_int = time.floor(); - let repaint = self.time_int() != Some(time_int); - let state = self - .states - .entry(*self.timeline_name()) - .or_insert_with(|| TimeState::new(*time)); - state.time = *time; - - self.exit_follow_mode(entity_db, blueprint_ctx); - self.wait_for_data = true; - - if repaint { - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - TimeControlCommand::SetTimeView(time_view) => { - if let Some(state) = self.states.get_mut(self.timeline.name()) { - state.view = Some(*time_view); - - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - TimeControlCommand::ResetTimeView => { - if let Some(state) = self.states.get_mut(self.timeline.name()) { - state.view = None; - - NeedsRepaint::Yes - } else { - NeedsRepaint::No - } - } - } - } - - /// Updates the current play-state. - /// - /// If `blueprint_ctx` is specified this writes to the related - /// blueprint. - pub fn set_play_state( - &mut self, - entity_db: Option<&EntityDb>, - play_state: PlayState, - blueprint_ctx: Option<&impl BlueprintContext>, - ) { - if let Some(blueprint_ctx) = blueprint_ctx - && Some(play_state) != blueprint_ctx.play_state() - { - blueprint_ctx.set_play_state(play_state); - } - - match play_state { - PlayState::Paused => { - self.playing = false; - } - PlayState::Playing => { - self.playing = true; - self.following = false; - self.wait_for_data = true; - - // Start from beginning if we are at the end: - if let Some(entity_db) = entity_db - && let Some(range) = entity_db.time_range_for(self.timeline_name()) - { - if let Some(state) = self.states.get_mut(self.timeline.name()) { - if range.max <= state.time { - state.time = range.min.into(); - } - } else { - self.states - .insert(*self.timeline_name(), TimeState::new(range.min)); - } - } - } - PlayState::Following => { - self.playing = true; - self.following = true; - - if let Some(entity_db) = entity_db - && let Some(range) = entity_db.time_range_for(self.timeline_name()) - { - // Set the time to the max: - self.states - .entry(*self.timeline_name()) - .or_insert_with(|| TimeState::new(range.max)) - .time = range.max.into(); - } - } - } - } - - fn step_time_back( - &mut self, - entity_db: &EntityDb, - blueprint_ctx: Option<&impl BlueprintContext>, - ) { - re_tracing::profile_function!(); - self.pause(blueprint_ctx); - self.step_time_back_no_pause(entity_db); - } - - fn step_time_fwd( - &mut self, - entity_db: &EntityDb, - blueprint_ctx: Option<&impl BlueprintContext>, - ) { - re_tracing::profile_function!(); - self.pause(blueprint_ctx); - self.step_time_fwd_no_pause(entity_db); - } - - fn step_time_back_no_pause(&mut self, entity_db: &EntityDb) { - if let Some(time) = self.time() { - let timeline = self.timeline_name(); - let prev = entity_db.prev_time_on_timeline(timeline, time.ceil()); - - let new_time = if let Some(loop_range) = self.active_loop_selection() { - if let Some(prev) = prev - && TimeReal::from(prev) >= loop_range.min - { - prev.into() - } else { - // Wrap to end of loop - if let Some(prev_from_end) = - entity_db.prev_time_on_timeline(timeline, loop_range.max.ceil()) - { - prev_from_end.into() - } else { - loop_range.max - } - } - } else if let Some(prev) = prev { - prev.into() - } else { - // Wrap to the end - if let Some(range) = entity_db.time_range_for(timeline) { - range.max.into() - } else { - return; - } - }; - - if let Some(state) = self.states.get_mut(self.timeline.name()) { - state.time = new_time; - } - } - } - - fn step_time_fwd_no_pause(&mut self, entity_db: &EntityDb) { - if let Some(time) = self.time() { - let timeline = self.timeline_name(); - let next = entity_db.next_time_on_timeline(timeline, time.floor()); - - let new_time = if let Some(loop_range) = self.active_loop_selection() { - if let Some(next) = next - && TimeReal::from(next) <= loop_range.max - { - next.into() - } else { - // Wrap to start of loop - if let Some(next_from_start) = - entity_db.next_time_on_timeline(timeline, loop_range.min.floor()) - { - next_from_start.into() - } else { - loop_range.min - } - } - } else if let Some(next) = next { - next.into() - } else { - // Wrap to the start - if let Some(range) = entity_db.time_range_for(timeline) { - range.min.into() - } else { - return; - } - }; - - if let Some(state) = self.states.get_mut(self.timeline.name()) { - state.time = new_time; - } - } - } - - /// Move time by arrow keys. Preserves play/pause state, but exits follow mode. - fn move_time( - &mut self, - entity_db: &EntityDb, - blueprint_ctx: Option<&impl BlueprintContext>, - direction: MoveDirection, - speed: MoveSpeed, - ) { - self.exit_follow_mode(entity_db, blueprint_ctx); - - match self.time_type() { - Some(TimeType::Sequence) => { - let steps = match speed { - MoveSpeed::Normal => 1, - MoveSpeed::Fast => 10, - }; - for _ in 0..steps { - match direction { - MoveDirection::Back => { - self.step_time_back_no_pause(entity_db); - } - MoveDirection::Forward => { - self.step_time_fwd_no_pause(entity_db); - } - } - } - } - Some(TimeType::DurationNs | TimeType::TimestampNs) => { - let seconds = match (direction, speed) { - (MoveDirection::Back, MoveSpeed::Normal) => -0.1, - (MoveDirection::Forward, MoveSpeed::Normal) => 0.1, - (MoveDirection::Back, MoveSpeed::Fast) => -1.0, - (MoveDirection::Forward, MoveSpeed::Fast) => 1.0, - }; - self.move_by_seconds_temporal(entity_db, seconds); - } - None => {} - } - } - - fn move_by_seconds_temporal(&mut self, entity_db: &EntityDb, seconds: f64) { - if let Some(time) = self.time() { - let mut new_time = time + TimeReal::from_secs(seconds); - - let range = self.time_selection().or_else(|| { - entity_db - .time_range_for(self.timeline_name()) - .map(|r| r.into()) - }); - if let Some(range) = range { - if time == range.min && new_time < range.min { - // jump right to the end - new_time = range.max; - } else if new_time < range.min { - // we are right at the end, wrap to the start - new_time = range.min; - } else if time == range.max && new_time > range.max { - // jump right to the start - new_time = range.min; - } else if new_time > range.max { - // we are right at the start, wrap to the end - new_time = range.max; - } - } - - if let Some(state) = self.states.get_mut(self.timeline.name()) { - state.time = new_time; - } - } - } - - /// If following, switch to playing. Otherwise keep the current play state. - fn exit_follow_mode( - &mut self, - entity_db: &EntityDb, - blueprint_ctx: Option<&impl BlueprintContext>, - ) { - if self.following { - self.set_play_state(Some(entity_db), PlayState::Playing, blueprint_ctx); - } - } - - fn pause(&mut self, blueprint_ctx: Option<&impl BlueprintContext>) { - self.playing = false; - if let Some(blueprint_ctx) = blueprint_ctx { - blueprint_ctx.set_play_state(PlayState::Paused); - } - if let Some(state) = self.states.get_mut(self.timeline.name()) { - state.last_paused_time = Some(state.time); - } - } - - fn toggle_play_pause( - &mut self, - entity_db: &EntityDb, - blueprint_ctx: Option<&impl BlueprintContext>, - ) { - if self.playing { - self.pause(blueprint_ctx); - } else { - // Start from beginning if we are at the end: - if let Some(range) = entity_db.time_range_for(self.timeline_name()) - && let Some(state) = self.states.get_mut(self.timeline.name()) - && range.max <= state.time - { - state.time = range.min.into(); - self.playing = true; - self.following = false; - return; - } - - self.set_play_state(Some(entity_db), PlayState::Playing, blueprint_ctx); - } - } - - /// Get the current [`re_entity_db::PrefetchTimeCursor`]. - /// - /// If the whole recording is looped the loop range is - /// `TimeInt::MIN..=TimeInt::MAX`. - pub fn time_cursor(&self) -> Option { - let typ = self.time_type()?; - let speed_if_unpaused = match typ { - TimeType::DurationNs | TimeType::TimestampNs => { - TimeInt::from_secs(1.0).as_f64() * self.speed as f64 - } - TimeType::Sequence => self.fps()? as f64 * self.speed as f64, - }; - - let loop_range = if self.loop_mode == LoopMode::All { - Some(AbsoluteTimeRange::new(TimeInt::MIN, TimeInt::MAX)) - } else { - self.active_loop_selection().map(|r| r.to_int()) - }; - - Some(re_entity_db::PrefetchTimeCursor { - time_cursor: re_log_types::TimelinePoint { - name: *self.timeline_name(), - typ, - time: self.time_int()?, - }, - speed_if_unpaused, - loop_range, - }) - } - - /// playback speed - pub fn speed(&self) -> f32 { - self.speed - } - - /// playback fps - pub fn fps(&self) -> Option { - self.states.get(self.timeline_name()).map(|state| state.fps) - } - - /// Make sure the selected timeline is a valid one - fn select_valid_timeline(&mut self, entity_db: &EntityDb) { - let timelines = entity_db.timelines(); - - let reset_timeline = match &self.timeline { - // If the timeline is auto refresh it every frame. - ActiveTimeline::Auto(_) => true, - // If it's user edited, refresh it if it's invalid. - ActiveTimeline::UserEdited(selected) => !timelines.contains_key(selected.name()), - // If it's pending never automatically refresh it. - ActiveTimeline::Pending(timeline_name) => { - // If the pending timeline is valid, it shouldn't be pending anymore. - if let Some(timeline) = timelines.get(timeline_name) { - self.timeline = ActiveTimeline::UserEdited(*timeline); - } - - false - } - }; - - if reset_timeline || matches!(self.timeline, ActiveTimeline::Auto(_)) { - self.timeline = - ActiveTimeline::Auto(Timeline::pick_best_timeline(timelines.values(), |t| { - entity_db.num_temporal_rows_on_timeline(t.name()) - })); - } - } - - /// The currently selected timeline - #[inline] - pub fn timeline(&self) -> Option<&Timeline> { - self.timeline.timeline() - } - - pub fn timeline_name(&self) -> &TimelineName { - self.timeline.name() - } - - /// The time type of the currently selected timeline - pub fn time_type(&self) -> Option { - self.timeline().map(|t| t.typ()) - } - - /// The current time. - pub fn time(&self) -> Option { - self.states - .get(self.timeline_name()) - .map(|state| state.time) - } - - pub fn last_paused_time(&self) -> Option { - if matches!(self.play_state(), PlayState::Paused) { - self.time() - } else { - self.states - .get(self.timeline_name()) - .and_then(|state| state.last_paused_time) - } - } - - /// The current time & timeline. - pub fn time_cell(&self) -> Option { - let t = self.time()?; - Some(TimeCell::new(self.time_type()?, t.floor().as_i64())) - } - - /// The current time. - pub fn time_int(&self) -> Option { - self.time().map(|t| t.floor()) - } - - /// The current time. - pub fn time_i64(&self) -> Option { - self.time().map(|t| t.floor().as_i64()) - } - - /// Query for latest value at the currently selected time on the currently selected timeline. - pub fn current_query(&self) -> re_chunk_store::LatestAtQuery { - re_chunk_store::LatestAtQuery::new( - *self.timeline_name(), - self.time().map_or(TimeInt::MAX, |t| t.floor()), - ) - } - - /// The current loop range, if selection looping is turned on. - pub fn active_loop_selection(&self) -> Option { - if self.loop_mode == LoopMode::Selection { - self.states.get(self.timeline_name())?.time_selection - } else { - None - } - } - - /// The selected slice of time that is called the "loop selection". - /// - /// This can still return `Some` even if looping is currently off. - pub fn time_selection(&self) -> Option { - self.states.get(self.timeline_name())?.time_selection - } - - /// Is the current time in the selection range (if any), or at the current time mark? - pub fn is_time_selected(&self, timeline: &TimelineName, needle: TimeInt) -> bool { - if timeline != self.timeline_name() { - return false; - } - - if let Some(state) = self.states.get(self.timeline_name()) { - state.time.floor() == needle - } else { - false - } - } - - /// Is the active timeline pending resolution? - /// - /// When `true`, the requested timeline name hasn't been found in the data yet, - /// so [`Self::timeline()`] returns `None` and time-dependent queries may not work. - pub fn is_pending(&self) -> bool { - matches!(self.timeline, ActiveTimeline::Pending(_)) - } - - pub fn time_for_timeline(&self, timeline: TimelineName) -> Option { - self.states.get(&timeline).map(|state| state.time) - } - - /// The range of time we are currently zoomed in on. - pub fn time_view(&self) -> Option { - self.states - .get(self.timeline_name()) - .and_then(|state| state.view) - } -} diff --git a/crates/viewer/re_viewer_context/src/time_control/blueprint_ext.rs b/crates/viewer/re_viewer_context/src/time_control/blueprint_ext.rs new file mode 100644 index 000000000000..b3181ec7c51a --- /dev/null +++ b/crates/viewer/re_viewer_context/src/time_control/blueprint_ext.rs @@ -0,0 +1,180 @@ +use re_chunk::{EntityPath, TimelineName}; +use re_log::ResultExt as _; +use re_log_types::AbsoluteTimeRange; +use re_sdk_types::blueprint::archetypes::TimePanelBlueprint; +use re_sdk_types::blueprint::components::{LoopMode, PlayState}; + +use crate::blueprint_helpers::BlueprintContext; + +pub const TIME_PANEL_PATH: &str = "time_panel"; + +pub fn time_panel_blueprint_entity_path() -> EntityPath { + TIME_PANEL_PATH.into() +} + +/// Helper trait to write time panel related blueprint components. +pub(super) trait TimeBlueprintExt { + fn set_timeline(&self, timeline: TimelineName); + + fn timeline(&self) -> Option; + + /// Replaces the current timeline with the automatic one. + fn clear_timeline(&self); + + fn set_playback_speed(&self, playback_speed: f64); + fn playback_speed(&self) -> Option; + + fn set_fps(&self, fps: f64); + fn fps(&self) -> Option; + + fn set_play_state(&self, play_state: PlayState); + fn play_state(&self) -> Option; + + fn set_loop_mode(&self, loop_mode: LoopMode); + fn loop_mode(&self) -> Option; + + fn set_time_selection(&self, time_range: AbsoluteTimeRange); + fn time_selection(&self) -> Option; + fn clear_time_selection(&self); +} + +impl TimeBlueprintExt for T { + fn set_timeline(&self, timeline: TimelineName) { + self.save_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_timeline(), + &re_sdk_types::blueprint::components::TimelineName::from(timeline.as_str()), + ); + } + + fn timeline(&self) -> Option { + let (_, timeline) = self + .current_blueprint() + .latest_at_component_quiet::( + &time_panel_blueprint_entity_path(), + self.blueprint_query(), + TimePanelBlueprint::descriptor_timeline().component, + )?; + + TimelineName::try_new(timeline.as_str()).ok_or_log_error_once() + } + + fn clear_timeline(&self) { + self.clear_blueprint_component( + time_panel_blueprint_entity_path(), + TimePanelBlueprint::descriptor_timeline(), + ); + } + + fn set_playback_speed(&self, playback_speed: f64) { + self.save_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_playback_speed(), + &re_sdk_types::blueprint::components::PlaybackSpeed(playback_speed.into()), + ); + } + + fn playback_speed(&self) -> Option { + let (_, playback_speed) = self + .current_blueprint() + .latest_at_component_quiet::( + &time_panel_blueprint_entity_path(), + self.blueprint_query(), + TimePanelBlueprint::descriptor_playback_speed().component, + )?; + + Some(**playback_speed) + } + + fn set_fps(&self, fps: f64) { + self.save_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_fps(), + &re_sdk_types::blueprint::components::Fps(fps.into()), + ); + } + + fn fps(&self) -> Option { + let (_, fps) = self + .current_blueprint() + .latest_at_component_quiet::( + &time_panel_blueprint_entity_path(), + self.blueprint_query(), + TimePanelBlueprint::descriptor_fps().component, + )?; + + Some(**fps) + } + + fn set_play_state(&self, play_state: PlayState) { + self.save_static_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_play_state(), + &play_state, + ); + } + + fn play_state(&self) -> Option { + let (_, play_state) = self + .current_blueprint() + .latest_at_component_quiet::( + &time_panel_blueprint_entity_path(), + self.blueprint_query(), + TimePanelBlueprint::descriptor_play_state().component, + )?; + + Some(play_state) + } + + fn set_loop_mode(&self, loop_mode: LoopMode) { + self.save_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_loop_mode(), + &loop_mode, + ); + } + + fn loop_mode(&self) -> Option { + let (_, loop_mode) = self + .current_blueprint() + .latest_at_component_quiet::( + &time_panel_blueprint_entity_path(), + self.blueprint_query(), + TimePanelBlueprint::descriptor_loop_mode().component, + )?; + + Some(loop_mode) + } + + fn set_time_selection(&self, time_range: AbsoluteTimeRange) { + self.save_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_time_selection(), + &re_sdk_types::blueprint::components::AbsoluteTimeRange( + re_sdk_types::datatypes::AbsoluteTimeRange { + min: time_range.min.as_i64().into(), + max: time_range.max.as_i64().into(), + }, + ), + ); + } + + fn time_selection(&self) -> Option { + let (_, time_range) = self + .current_blueprint() + .latest_at_component_quiet::( + &time_panel_blueprint_entity_path(), + self.blueprint_query(), + TimePanelBlueprint::descriptor_time_selection().component, + )?; + + Some(AbsoluteTimeRange::new(time_range.min, time_range.max)) + } + + fn clear_time_selection(&self) { + self.clear_blueprint_component( + time_panel_blueprint_entity_path(), + TimePanelBlueprint::descriptor_time_selection(), + ); + } +} diff --git a/crates/viewer/re_viewer_context/src/time_control/command.rs b/crates/viewer/re_viewer_context/src/time_control/command.rs new file mode 100644 index 000000000000..7913c6ebec4b --- /dev/null +++ b/crates/viewer/re_viewer_context/src/time_control/command.rs @@ -0,0 +1,708 @@ +use re_chunk::TimelineName; +use re_entity_db::EntityDb; +use re_log_types::{AbsoluteTimeRange, AbsoluteTimeRangeF, TimeReal, TimeType}; +use re_sdk_types::blueprint::components::{LoopMode, PlayState}; + +use crate::NeedsRepaint; +use crate::blueprint_helpers::BlueprintContext; + +use super::blueprint_ext::TimeBlueprintExt as _; +use super::{TimeControl, TimeControlResponse, TimeRangeHighlight, TimeState, TimeView}; + +/// Direction for time movement commands. +#[derive(Debug, Clone, Copy)] +pub enum MoveDirection { + Back, + Forward, +} + +/// Speed for time movement commands. +#[derive(Debug, Clone, Copy)] +pub enum MoveSpeed { + Normal, + Fast, +} + +/// A command used to mutate `TimeControl`. +/// +/// Can be sent using [`crate::SystemCommand::TimeControlCommands`]. +#[derive(Debug)] +pub enum TimeControlCommand { + /// Highlight a time range for this frame. + /// + /// Stored in `TimeControl::highlighted_range_next_frame` by the command + /// handler; becomes readable via [`TimeControl::highlighted_range`] on the + /// next `TimeControl::update`. Last-write-wins within a frame. Producers + /// must re-publish each frame they want the highlight to remain visible + /// (typically from a hover handler). + HighlightRange(TimeRangeHighlight), + + /// Reset the active timeline to instead be automatically assigned. + ResetActiveTimeline, + SetActiveTimeline(TimelineName), + + /// Set the current looping state. + SetLoopMode(LoopMode), + SetPlayState(PlayState), + Pause, + TogglePlayPause, + StepTimeBack, + StepTimeForward, + Move { + direction: MoveDirection, + speed: MoveSpeed, + }, + MoveBeginning, + MoveEnd, + + /// Restart the time cursor to the start. + /// + /// Stops any ongoing following. + Restart, + + /// Set playback speed. + SetSpeed(f32), + + /// Set playback fps. + SetFps(f32), + + /// Set the current time selection exactly as given, without enabling looping. + /// + /// The range is honored as-is — no intersection with currently-loaded data. + /// Use for blueprint/URL/programmatic selections that must apply independent + /// of chunk arrival order. + SetTimeSelection(AbsoluteTimeRange), + + /// Like [`Self::SetTimeSelection`], but intersected with the currently-loaded + /// data range; an empty intersection turns the selection off. + /// + /// Use for interactive UI scrub/drag where clamping to visible data is the + /// expected behavior. + SetTimeSelectionClamped(AbsoluteTimeRange), + + /// Remove the current time selection. + /// + /// If the current loop mode is selection, turns off looping. + RemoveTimeSelection, + + /// Set the current time cursor exactly as given, without any clamping. + /// + /// Use for blueprint/URL/JS/undo-driven seeks that must apply independent + /// of chunk arrival order. + SetTime(TimeReal), + + /// Like [`Self::SetTime`], but clamped to the currently-loaded data range. + /// + /// Use for interactive UI scrub/drag against the visible timeline. + SetTimeClamped(TimeReal), + + /// Set the range of time we are currently zoomed in on. + SetTimeView(TimeView), + + /// Reset the range of time we are currently zoomed in on. + /// + /// The view will instead fall back to the default which is + /// showing all received data. + ResetTimeView, + + /// Causes the time control to buffer next frame, used for the video visualizer. + Buffer, +} + +impl TimeControlCommand { + /// Convert the temporal parts of a URL fragment into time-control commands. + /// + /// A temporal `when=…` anchor represents a fixed time cursor, so applying it + /// must always pause playback before seeking. + pub fn from_url_fragment(fragment: &re_uri::Fragment) -> Vec { + let mut time_commands = Vec::new(); + + if let Some(time_selection) = &fragment.time_selection { + time_commands.push(Self::SetActiveTimeline(*time_selection.timeline.name())); + time_commands.push(Self::SetTimeSelection(time_selection.range)); + time_commands.push(Self::SetLoopMode(LoopMode::Selection)); + } + + if let Some((timeline, timecell)) = &fragment.when { + time_commands.push(Self::SetActiveTimeline(*timeline)); + time_commands.push(Self::SetPlayState(PlayState::Paused)); + time_commands.push(Self::SetTime(timecell.value.into())); + } + + time_commands + } +} + +impl TimeControl { + pub fn handle_time_commands( + &mut self, + blueprint_ctx: Option<&impl BlueprintContext>, + db: &EntityDb, + commands: &[TimeControlCommand], + ) -> TimeControlResponse { + let mut response = TimeControlResponse { + needs_repaint: NeedsRepaint::No, + playing_change: None, + timeline_change: None, + time_change: None, + }; + + let (old_playing, old_timeline, old_state) = ( + self.playing, + self.timeline().copied(), + self.states.get(self.timeline_name()).copied(), + ); + + for command in commands { + let needs_repaint = self.handle_time_command(blueprint_ctx, db, command); + + if needs_repaint == NeedsRepaint::Yes { + response.needs_repaint = NeedsRepaint::Yes; + } + } + + self.diff_with(&mut response, old_timeline, old_playing, old_state); + + response + } + + /// Applies a time command with respect to the current timeline. + /// + /// If `blueprint_ctx` is some, this also writes to that blueprint + /// for applicable commands. + /// + /// Returns if the command should cause a repaint. + fn handle_time_command( + &mut self, + blueprint_ctx: Option<&impl BlueprintContext>, + db: &EntityDb, + command: &TimeControlCommand, + ) -> NeedsRepaint { + match command { + TimeControlCommand::HighlightRange(highlight) => { + self.highlighted_range_next_frame = Some(highlight.clone()); + if self.highlighted_range_next_frame != self.highlighted_range { + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::ResetActiveTimeline => { + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.clear_timeline(); + } + if let Some(timeline) = self + .timeline() + .copied() + .or_else(|| db.timelines().into_values().next()) + { + self.timeline = super::ActiveTimeline::Auto(timeline); + } + self.select_valid_timeline(db); + + self.just_interacted = true; + + NeedsRepaint::Yes + } + TimeControlCommand::SetActiveTimeline(timeline_name) => { + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_timeline(*timeline_name); + } + + if let Some(timeline) = db.timelines().get(timeline_name).copied() { + self.timeline = super::ActiveTimeline::UserEdited(timeline); + } else { + self.timeline = super::ActiveTimeline::Pending(*timeline_name); + } + + if let Some(state) = self.states.get(timeline_name) { + // Use the new timeline's cached time selection. + if let Some(blueprint_ctx) = blueprint_ctx { + match state.time_selection { + Some(selection) => blueprint_ctx.set_time_selection(selection.to_int()), + None => blueprint_ctx.clear_time_selection(), + } + } + } else if let Some(full_range) = db.time_range_for(timeline_name) { + // Hazard: inserts a fresh `TimeState` with `time = range.min`. + // Any caller that wants to seed a non-default cursor for this + // timeline (e.g. blueprint cursor restore) must run *after* + // `SetActiveTimeline`, or this branch will overwrite it. + self.states + .insert(*timeline_name, TimeState::new(full_range.min)); + } + + self.just_interacted = true; + + NeedsRepaint::Yes + } + TimeControlCommand::SetLoopMode(loop_mode) => { + if self.loop_mode == *loop_mode { + NeedsRepaint::No + } else { + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_loop_mode(*loop_mode); + } + self.loop_mode = *loop_mode; + if self.loop_mode != LoopMode::Off { + if self.play_state() == PlayState::Following { + self.set_play_state(Some(db), PlayState::Playing, blueprint_ctx); + } + + // It makes no sense with looping and follow. + self.following = false; + } + + NeedsRepaint::Yes + } + } + TimeControlCommand::SetPlayState(play_state) => { + if self.play_state() == *play_state { + NeedsRepaint::No + } else { + self.set_play_state(Some(db), *play_state, blueprint_ctx); + + if self.following { + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_loop_mode(LoopMode::Off); + } + self.loop_mode = LoopMode::Off; + } + + NeedsRepaint::Yes + } + } + TimeControlCommand::Pause => { + if self.playing { + self.pause(blueprint_ctx); + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + + TimeControlCommand::TogglePlayPause => { + self.toggle_play_pause(db, blueprint_ctx); + + NeedsRepaint::Yes + } + TimeControlCommand::StepTimeBack => { + self.step_time_back(db, blueprint_ctx); + + NeedsRepaint::Yes + } + TimeControlCommand::StepTimeForward => { + self.step_time_fwd(db, blueprint_ctx); + + NeedsRepaint::Yes + } + TimeControlCommand::Move { direction, speed } => { + self.move_time(db, blueprint_ctx, *direction, *speed); + NeedsRepaint::Yes + } + TimeControlCommand::MoveBeginning => { + if let Some(full_range) = db.time_range_for(self.timeline_name()) { + self.states + .entry(*self.timeline_name()) + .or_insert_with(|| TimeState::new(full_range.min)) + .time = full_range.min.into(); + + self.just_interacted = true; + + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::MoveEnd => { + if let Some(full_range) = db.time_range_for(self.timeline_name()) { + self.states + .entry(*self.timeline_name()) + .or_insert_with(|| TimeState::new(full_range.max)) + .time = full_range.max.into(); + + self.just_interacted = true; + + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::Restart => { + if let Some(full_range) = db.time_range_for(self.timeline_name()) { + self.following = false; + + if let Some(state) = self.states.get_mut(self.timeline.name()) { + state.time = full_range.min.into(); + } + + self.just_interacted = true; + + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::SetSpeed(speed) => { + if *speed == self.speed { + NeedsRepaint::No + } else { + self.speed = *speed; + + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_playback_speed(*speed as f64); + } + + NeedsRepaint::Yes + } + } + TimeControlCommand::SetFps(fps) => { + if let Some(state) = self.states.get_mut(self.timeline.name()) + && state.fps != *fps + { + state.fps = *fps; + + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_fps(*fps as f64); + } + + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::SetTimeSelection(time_range) => { + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_time_selection(*time_range); + } + + let state = self + .states + .entry(*self.timeline_name()) + .or_insert_with(|| TimeState::new(time_range.min)); + + let repaint = state.time_selection.map(|r| r.to_int()) != Some(*time_range); + + state.time_selection = Some((*time_range).into()); + + if repaint { + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::SetTimeSelectionClamped(time_range) => { + let timeline_range = db + .time_range_for(self.timeline_name()) + .unwrap_or(AbsoluteTimeRange::EVERYTHING); + + let Some(time_range) = timeline_range.intersection(*time_range) else { + return self.handle_time_command( + blueprint_ctx, + db, + &TimeControlCommand::RemoveTimeSelection, + ); + }; + + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_time_selection(time_range); + } + + let state = self + .states + .entry(*self.timeline_name()) + .or_insert_with(|| TimeState::new(time_range.min)); + + let repaint = state.time_selection.map(|r| r.to_int()) != Some(time_range); + + state.time_selection = Some((time_range).into()); + + if repaint { + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::RemoveTimeSelection => { + if let Some(state) = self.states.get_mut(self.timeline.name()) { + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.clear_time_selection(); + } + state.time_selection = None; + if self.loop_mode == LoopMode::Selection { + self.loop_mode = LoopMode::Off; + + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_loop_mode(self.loop_mode); + } + } + + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::SetTimeClamped(time) => { + let timeline_range = db + .time_range_for(self.timeline_name()) + .unwrap_or(AbsoluteTimeRange::EVERYTHING); + + // If the floating point time is inside the range, use that. + let timeline_rangef = AbsoluteTimeRangeF::from(timeline_range); + let clamped_time = if timeline_rangef.contains(*time) { + *time + } else { + (*time).clamp(timeline_rangef.min, timeline_rangef.max) + }; + + let time_int = clamped_time.floor(); + + let repaint = self.time_int() != Some(time_int); + + let state = self + .states + .entry(*self.timeline_name()) + .or_insert_with(|| TimeState::new(clamped_time)); + state.time = clamped_time; + + self.exit_follow_mode(db, blueprint_ctx); + self.just_interacted = true; + + if repaint { + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::SetTime(time) => { + let time_int = time.floor(); + + let repaint = self.time_int() != Some(time_int); + + // Exit follow mode first — `set_play_state(Playing)` may reset + // `state.time` to `range.min` when the cursor sits past the + // current data range, which is exactly the case we're trying + // to preserve. Set the time *after* that runs. + self.exit_follow_mode(db, blueprint_ctx); + + let state = self + .states + .entry(*self.timeline_name()) + .or_insert_with(|| TimeState::new(*time)); + state.time = *time; + + self.just_interacted = true; + + if repaint { + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::SetTimeView(time_view) => { + if let Some(state) = self.states.get_mut(self.timeline.name()) { + state.view = Some(*time_view); + + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::ResetTimeView => { + if let Some(state) = self.states.get_mut(self.timeline.name()) { + state.view = None; + + NeedsRepaint::Yes + } else { + NeedsRepaint::No + } + } + TimeControlCommand::Buffer => { + self.buffer_next_frame = true; + + NeedsRepaint::No + } + } + } + + fn step_time_back(&mut self, db: &EntityDb, blueprint_ctx: Option<&impl BlueprintContext>) { + re_tracing::profile_function!(); + self.pause(blueprint_ctx); + self.step_time_back_no_pause(db); + } + + fn step_time_fwd(&mut self, db: &EntityDb, blueprint_ctx: Option<&impl BlueprintContext>) { + re_tracing::profile_function!(); + self.pause(blueprint_ctx); + self.step_time_fwd_no_pause(db); + } + + fn step_time_back_no_pause(&mut self, db: &EntityDb) { + if let Some(time) = self.time() { + let timeline = self.timeline_name(); + let prev = db.prev_time_on_timeline(timeline, time.ceil()); + + let new_time = if let Some(loop_range) = self.active_loop_selection() { + if let Some(prev) = prev + && TimeReal::from(prev) >= loop_range.min + { + prev.into() + } else { + // Wrap to end of loop + if let Some(prev_from_end) = + db.prev_time_on_timeline(timeline, loop_range.max.ceil()) + { + prev_from_end.into() + } else { + loop_range.max + } + } + } else if let Some(prev) = prev { + prev.into() + } else { + // Wrap to the end + if let Some(range) = db.time_range_for(timeline) { + range.max.into() + } else { + return; + } + }; + + if let Some(state) = self.states.get_mut(self.timeline.name()) { + state.time = new_time; + } + } + } + + fn step_time_fwd_no_pause(&mut self, db: &EntityDb) { + if let Some(time) = self.time() { + let timeline = self.timeline_name(); + let next = db.next_time_on_timeline(timeline, time.floor()); + + let new_time = if let Some(loop_range) = self.active_loop_selection() { + if let Some(next) = next + && TimeReal::from(next) <= loop_range.max + { + next.into() + } else { + // Wrap to start of loop + if let Some(next_from_start) = + db.next_time_on_timeline(timeline, loop_range.min.floor()) + { + next_from_start.into() + } else { + loop_range.min + } + } + } else if let Some(next) = next { + next.into() + } else { + // Wrap to the start + if let Some(range) = db.time_range_for(timeline) { + range.min.into() + } else { + return; + } + }; + + if let Some(state) = self.states.get_mut(self.timeline.name()) { + state.time = new_time; + } + } + } + + /// Move time by arrow keys. Preserves play/pause state, but exits follow mode. + fn move_time( + &mut self, + db: &EntityDb, + blueprint_ctx: Option<&impl BlueprintContext>, + direction: MoveDirection, + speed: MoveSpeed, + ) { + self.exit_follow_mode(db, blueprint_ctx); + + match self.time_type() { + Some(TimeType::Sequence) => { + let steps = match speed { + MoveSpeed::Normal => 1, + MoveSpeed::Fast => 10, + }; + for _ in 0..steps { + match direction { + MoveDirection::Back => { + self.step_time_back_no_pause(db); + } + MoveDirection::Forward => { + self.step_time_fwd_no_pause(db); + } + } + } + } + Some(TimeType::DurationNs | TimeType::TimestampNs) => { + let seconds = match (direction, speed) { + (MoveDirection::Back, MoveSpeed::Normal) => -0.1, + (MoveDirection::Forward, MoveSpeed::Normal) => 0.1, + (MoveDirection::Back, MoveSpeed::Fast) => -1.0, + (MoveDirection::Forward, MoveSpeed::Fast) => 1.0, + }; + self.move_by_seconds_temporal(db, seconds); + } + None => {} + } + } + + fn move_by_seconds_temporal(&mut self, db: &EntityDb, seconds: f64) { + if let Some(time) = self.time() { + let mut new_time = time + TimeReal::from_secs(seconds); + + let range = self + .time_selection() + .or_else(|| db.time_range_for(self.timeline_name()).map(|r| r.into())); + if let Some(range) = range { + if time == range.min && new_time < range.min { + // jump right to the end + new_time = range.max; + } else if new_time < range.min { + // we are right at the end, wrap to the start + new_time = range.min; + } else if time == range.max && new_time > range.max { + // jump right to the start + new_time = range.min; + } else if new_time > range.max { + // we are right at the start, wrap to the end + new_time = range.max; + } + } + + if let Some(state) = self.states.get_mut(self.timeline.name()) { + state.time = new_time; + } + } + } + + /// If following, switch to playing. Otherwise keep the current play state. + fn exit_follow_mode(&mut self, db: &EntityDb, blueprint_ctx: Option<&impl BlueprintContext>) { + if self.following { + self.set_play_state(Some(db), PlayState::Playing, blueprint_ctx); + } + } + + fn toggle_play_pause(&mut self, db: &EntityDb, blueprint_ctx: Option<&impl BlueprintContext>) { + if self.playing { + self.pause(blueprint_ctx); + } else { + // Start from beginning if we are at the end: + if let Some(range) = db.time_range_for(self.timeline_name()) + && let Some(state) = self.states.get_mut(self.timeline.name()) + && range.max <= state.time + { + state.time = range.min.into(); + self.playing = true; + self.following = false; + return; + } + + self.set_play_state(Some(db), PlayState::Playing, blueprint_ctx); + } + } +} diff --git a/crates/viewer/re_viewer_context/src/time_control/mod.rs b/crates/viewer/re_viewer_context/src/time_control/mod.rs new file mode 100644 index 000000000000..b2f109769c1b --- /dev/null +++ b/crates/viewer/re_viewer_context/src/time_control/mod.rs @@ -0,0 +1,880 @@ +mod blueprint_ext; +mod command; + +use std::collections::BTreeMap; + +use re_chunk::TimelineName; +use re_entity_db::EntityDb; +use re_log_types::{ + AbsoluteTimeRange, AbsoluteTimeRangeF, Duration, TimeCell, TimeInt, TimeReal, TimeType, + Timeline, +}; +use re_sdk_types::blueprint::components::{LoopMode, PlayState}; + +use crate::NeedsRepaint; +use crate::blueprint_helpers::BlueprintContext; + +use blueprint_ext::TimeBlueprintExt as _; + +pub use blueprint_ext::{TIME_PANEL_PATH, time_panel_blueprint_entity_path}; +pub use command::{MoveDirection, MoveSpeed, TimeControlCommand}; + +/// What sort of thing a [`TimeRangeHighlight`] represents. +/// +/// Lets each consumer (time panel, time series view, state timeline view, …) decide +/// independently whether to draw a given highlight. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TimeRangeHighlightKind { + /// User is hovering a time range configuration UI — e.g. a visible-time-range + /// editor, a dataframe filter, the time-axis view range. + TimeRangeConfiguration, + + /// User is hovering a state phase in the state timeline view. + StateTimeline, +} + +/// A single time range highlighted this frame. +/// +/// Producers publish via [`TimeControlCommand::HighlightRange`] from a hover handler +/// each frame the highlight should remain visible. Consumers read via +/// [`TimeControl::highlighted_range`] and filter on `timeline` / `kind`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TimeRangeHighlight { + pub range: AbsoluteTimeRange, + pub timeline: TimelineName, + pub kind: TimeRangeHighlightKind, + + /// Preferred fill color (including alpha) chosen by the producer. + pub color: Option, +} + +impl re_byte_size::SizeBytes for TimeRangeHighlight { + fn heap_size_bytes(&self) -> u64 { + 0 + } +} + +/// The time range we are currently zoomed in on. +#[derive( + Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, re_byte_size::SizeBytes, +)] +pub struct TimeView { + /// Where start of the range. + pub min: TimeReal, + + /// How much time the full view covers. + /// + /// The unit is either nanoseconds or sequence numbers. + /// + /// If there is gaps in the data, the actual amount of viewed time might be less. + pub time_spanned: f64, +} + +impl From for TimeView { + fn from(value: AbsoluteTimeRange) -> Self { + Self { + min: value.min().into(), + time_spanned: value.abs_length() as f64, + } + } +} + +/// State per timeline. +#[derive( + Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, re_byte_size::SizeBytes, +)] +struct TimeState { + /// The current time (play marker). + time: TimeReal, + + /// The last time this timeline was paused at. + /// + /// Used for the web url. + #[serde(skip)] + last_paused_time: Option, + + /// Frames per second, when playing sequences (they are often video recordings). + fps: f32, + + /// Selected time range, if any. + #[serde(default)] + time_selection: Option, + + /// The time range we are currently zoomed in on. + /// + /// `None` means "everything", and is the default value. + /// In this case, the view will expand while new data is added. + /// Only when the user actually zooms or pans will this be set. + #[serde(default)] + view: Option, +} + +impl TimeState { + fn new(time: impl Into) -> Self { + Self { + time: time.into(), + last_paused_time: None, + fps: 30.0, // TODO(emilk): estimate based on data + time_selection: Default::default(), + view: None, + } + } +} + +/// Which timeline is currently active in the time panel. +/// +/// The active timeline can be in one of three states: +/// - Automatically chosen based on heuristics (e.g. the timeline with most data), +/// - Explicitly selected by the user, +/// - Or "pending": requested by name (via blueprint or user action) but not yet +/// present in the entity database. A pending timeline is promoted to `UserEdited` +/// once data containing that timeline arrives (see [`TimeControl::select_valid_timeline`]). +// TODO(andreas): This should be a blueprint property and follow the usual rules of how we determine fallbacks. +#[derive( + serde::Deserialize, serde::Serialize, Clone, PartialEq, Debug, re_byte_size::SizeBytes, +)] +enum ActiveTimeline { + /// Automatically selected based on heuristics. Re-evaluated every frame. + Auto(Timeline), + + /// Explicitly selected by the user or resolved from blueprint. + UserEdited(Timeline), + + /// A timeline was requested by name but hasn't been seen in the data yet. + /// + /// This happens when the blueprint or a [`TimeControlCommand::SetActiveTimeline`] references + /// a timeline that doesn't exist in the current [`re_entity_db::EntityDb`]. We store only the name + /// and wait for matching data to arrive, at which point this becomes `UserEdited`. + Pending(TimelineName), +} + +impl ActiveTimeline { + /// The name of the active timeline, regardless of its state. + pub fn name(&self) -> &TimelineName { + match self { + Self::Auto(timeline) | Self::UserEdited(timeline) => timeline.name(), + Self::Pending(timeline_name) => timeline_name, + } + } + + /// The full [`Timeline`], if available. + /// + /// Returns `None` for [`Self::Pending`] since the timeline hasn't been + /// resolved against the entity database yet. + pub fn timeline(&self) -> Option<&Timeline> { + match self { + Self::Auto(timeline) | Self::UserEdited(timeline) => Some(timeline), + Self::Pending(_) => None, + } + } +} + +/// Controls the global view and progress of the time. +/// +/// Modifications to this can be done via sending [`TimeControlCommand`]s +/// which are handled at the end of frames. +/// +/// The commands write both to this struct and to blueprints when +/// applicable. +#[derive(Clone, PartialEq, re_byte_size::SizeBytes)] +pub struct TimeControl { + /// Name of the timeline (e.g. `log_time`). + timeline: ActiveTimeline, + + states: BTreeMap, + + /// If true, we are either in [`PlayState::Playing`] or [`PlayState::Following`]. + playing: bool, + + /// If true, we are in "follow" mode (see [`PlayState::Following`]). + /// Ignored when [`Self::playing`] is `false`. + following: bool, + + speed: f32, + + loop_mode: LoopMode, + + /// Highlight published last frame; this is what consumers read each frame. + /// + /// This is used during UI interactions. E.g. to show visual history range that's highlighted. + highlighted_range: Option, + + /// Highlight published this frame, set by the command handler. + /// + /// Becomes `highlighted_range` on the next [`TimeControl::update`]. + highlighted_range_next_frame: Option, + + /// If the user has interacted since the last `update`, if so don't update time this frame. + just_interacted: bool, + + /// Should the time control buffer next update? + buffer_next_frame: bool, + + /// The value of `buffer_next_frame` last update. + was_buffering: bool, +} + +impl Default for TimeControl { + fn default() -> Self { + Self { + timeline: ActiveTimeline::Auto(Timeline::pick_best_timeline([], |_| 0)), + states: Default::default(), + playing: true, + following: true, + speed: 1.0, + loop_mode: LoopMode::Off, + highlighted_range: None, + highlighted_range_next_frame: None, + + just_interacted: false, + buffer_next_frame: false, + was_buffering: false, + } + } +} + +/// Parameters for [`TimeControl::update`]. +pub struct TimeControlUpdateParams { + /// The time step in seconds. + pub stable_dt: f32, + + /// Is more data expected to arrive (e.g. still connected to a data source)? + /// + /// Set to true e.g. when viewing live data, + /// or we're still downloading a recording. + pub more_data_is_streaming_in: bool, + + /// True if we're waiting for chunks to be downloaded, + /// and they are expected to come (eventually). + pub is_buffering: bool, + + /// Should we diff state changes to trigger callbacks? + pub should_diff_state: bool, +} + +#[must_use] +pub struct TimeControlResponse { + pub needs_repaint: NeedsRepaint, + + /// Set if play state changed. + /// + /// * `Some(true)` if playing changed to `true` + /// * `Some(false)` if playing changed to `false` + /// * `None` if playing did not change + pub playing_change: Option, + + /// Set if timeline changed. + /// + /// Contains the timeline name and the current time. + pub timeline_change: Option<(Timeline, TimeReal)>, + + /// Set if the time changed. + pub time_change: Option, +} + +impl TimeControlResponse { + fn no_repaint() -> Self { + Self::new(NeedsRepaint::No) + } + + fn new(needs_repaint: NeedsRepaint) -> Self { + Self { + needs_repaint, + playing_change: None, + timeline_change: None, + time_change: None, + } + } +} + +impl TimeControl { + /// Create a time control that plays in a loop, not backed by any blueprint. + /// + /// This will also always wait for data while buffering. + pub fn preview_time_control() -> Self { + Self { + playing: true, + following: false, + loop_mode: LoopMode::All, + ..Self::default() + } + } + + /// Was this time control marked as buffering by [`TimeControlCommand::Buffer`]? + pub fn was_marked_as_buffering(&self) -> bool { + self.was_buffering + } + + pub fn from_blueprint(blueprint_ctx: &impl BlueprintContext) -> Self { + let mut this = Self::default(); + + this.update_from_blueprint(blueprint_ctx, None); + + this + } + + /// Like [`Self::from_blueprint`], but applies `fallback_play_state` when the + /// blueprint does not specify one. + /// + /// Use this for the initial construction of a [`TimeControl`] for a recording, + /// where the caller has computed a data-source-derived default that should + /// only kick in if the user's blueprint hasn't already pinned the value. + pub fn from_blueprint_with_fallback_play_state( + blueprint_ctx: &impl BlueprintContext, + db: Option<&EntityDb>, + fallback_play_state: PlayState, + ) -> Self { + let mut this = Self::default(); + this.update_from_blueprint(blueprint_ctx, db); + if blueprint_ctx.play_state().is_none() { + this.set_play_state(db, fallback_play_state, Some(blueprint_ctx)); + } + this + } + + /// Read from the time panel blueprint and update the state from that. + /// + /// If `db` is some this will also make sure we are on a valid timeline. + pub fn update_from_blueprint( + &mut self, + blueprint_ctx: &impl BlueprintContext, + db: Option<&EntityDb>, + ) { + if let Some(timeline) = blueprint_ctx.timeline() { + if matches!(self.timeline, ActiveTimeline::Auto(_)) + || timeline.as_str() != self.timeline_name().as_str() + { + self.timeline = ActiveTimeline::Pending(timeline); + } + } else if let Some(timeline) = self.timeline() { + self.timeline = ActiveTimeline::Auto(*timeline); + } + + let old_timeline = *self.timeline_name(); + // Make sure we are on a valid timeline. + if let Some(db) = db { + self.select_valid_timeline(db); + } + + if let Some(new_play_state) = blueprint_ctx.play_state() + && new_play_state != self.play_state() + { + self.set_play_state(db, new_play_state, Some(blueprint_ctx)); + } + + if let Some(new_loop_mode) = blueprint_ctx.loop_mode() { + self.loop_mode = new_loop_mode; + + if self.loop_mode != LoopMode::Off { + if self.play_state() == PlayState::Following { + self.set_play_state(db, PlayState::Playing, Some(blueprint_ctx)); + } + + // It makes no sense with looping and follow. + self.following = false; + } + } + + if let Some(playback_speed) = blueprint_ctx.playback_speed() { + self.speed = playback_speed as f32; + } + + let play_state = self.play_state(); + + // Update the last paused time if we are paused. + let timeline = *self.timeline_name(); + if let Some(state) = self.states.get_mut(&timeline) { + if let Some(fps) = blueprint_ctx.fps() { + state.fps = fps as f32; + } + + let bp_loop_section = blueprint_ctx.time_selection(); + // If we've switched timeline, use the new timeline's cached time selection. + if old_timeline == timeline { + state.time_selection = bp_loop_section.map(|r| r.into()); + } else { + match state.time_selection { + Some(selection) => blueprint_ctx.set_time_selection(selection.to_int()), + None => { + blueprint_ctx.clear_time_selection(); + } + } + } + + match play_state { + PlayState::Paused => { + state.last_paused_time = Some(state.time); + } + PlayState::Playing | PlayState::Following => {} + } + } + } + + /// Sets the current time. + /// + /// This will NOT update the blueprint! + pub fn set_time_ad_hoc(&mut self, time: TimeReal) { + self.set_time_cursor_ad_hoc(*self.timeline_name(), time); + } + + /// Sets the current time. + /// + /// This will NOT update the blueprint! + pub fn set_time_cursor_ad_hoc(&mut self, timeline: TimelineName, time: TimeReal) { + self.states + .entry(timeline) + .or_insert_with(|| TimeState::new(time)) + .time = time; + } + + /// Create [`TimeControlCommand`]s to move the time forward (if playing), and perhaps pause if + /// we've reached the end. + pub fn update( + &mut self, + db: &EntityDb, + params: &TimeControlUpdateParams, + blueprint_ctx: Option<&impl BlueprintContext>, + ) -> TimeControlResponse { + let TimeControlUpdateParams { + stable_dt, + more_data_is_streaming_in, + is_buffering, + should_diff_state, + } = *params; + + let just_interacted = std::mem::take(&mut self.just_interacted); + self.was_buffering = std::mem::take(&mut self.buffer_next_frame); + let is_buffering = self.was_buffering || is_buffering; + + // Swap highlight buffer: `highlighted_range_next_frame` (set by the + // command handler since the previous `update`) becomes readable via + // `highlighted_range` this frame. + self.highlighted_range = self.highlighted_range_next_frame.take(); + + let (old_playing, old_timeline, old_state) = ( + self.playing, + self.timeline().copied(), + self.states.get(self.timeline_name()).copied(), + ); + + if let Some(blueprint_ctx) = blueprint_ctx { + self.update_from_blueprint(blueprint_ctx, Some(db)); + } else { + self.select_valid_timeline(db); + } + + let Some(full_range) = db.time_range_for(self.timeline_name()) else { + return TimeControlResponse::no_repaint(); // we have no data on this timeline yet, so bail + }; + + let needs_repaint = if just_interacted { + NeedsRepaint::Yes + } else { + match self.play_state() { + PlayState::Paused => { + // It's possible that the playback is paused because e.g. it reached its end, but + // then the user decides to switch timelines. + // When they do so, it might be the case that they switch to a timeline they've + // never interacted with before, in which case we don't even have a time state yet. + let state = self.states.entry(*self.timeline_name()).or_insert_with(|| { + TimeState::new(if self.following { + full_range.max() + } else { + full_range.min() + }) + }); + + state.last_paused_time = Some(state.time); + NeedsRepaint::No + } + + PlayState::Playing => { + let state = self + .states + .entry(*self.timeline_name()) + .or_insert_with(|| TimeState::new(full_range.min())); + + if is_buffering { + // Do not move time cursor until we are done buffering + NeedsRepaint::No + } else { + let dt = stable_dt.min(0.1) * self.speed; + + if self.loop_mode == LoopMode::Off && full_range.max() <= state.time { + // We've reached the end of the data + self.set_time_ad_hoc(full_range.max().into()); + + if more_data_is_streaming_in { + // then let's wait for it without pausing! + } else { + self.pause(blueprint_ctx); + } + NeedsRepaint::No + } else { + let mut new_time = state.time; + + let loop_range = match self.loop_mode { + LoopMode::Off => None, + LoopMode::Selection => state.time_selection, + LoopMode::All => Some(full_range.into()), + }; + + match self.timeline.timeline().map(|t| t.typ()) { + Some(TimeType::Sequence) => { + new_time += TimeReal::from(state.fps * dt); + } + Some(TimeType::DurationNs | TimeType::TimestampNs) => { + new_time += TimeReal::from(Duration::from_secs(dt)); + } + None => {} + } + + if let Some(loop_range) = loop_range + && loop_range.max < new_time + { + new_time = loop_range.min; // loop! + } + + self.set_time_ad_hoc(new_time); + + NeedsRepaint::Yes + } + } + } + PlayState::Following => { + // Set the time to the max: + self.set_time_ad_hoc(full_range.max().into()); + + NeedsRepaint::No // no need for request_repaint - we already repaint when new data arrives + } + } + }; + + self.apply_state_diff_if_needed( + TimeControlResponse::new(needs_repaint), + should_diff_state, + db, + old_timeline, + old_playing, + old_state, + ) + } + + /// Apply state diff to response if needed. + #[expect(clippy::fn_params_excessive_bools)] // TODO(emilk): remove bool parameters + fn apply_state_diff_if_needed( + &mut self, + response: TimeControlResponse, + should_diff_state: bool, + db: &EntityDb, + old_timeline: Option, + old_playing: bool, + old_state: Option, + ) -> TimeControlResponse { + let mut response = response; + + if should_diff_state && db.time_range_for(self.timeline_name()).is_some() { + self.diff_with(&mut response, old_timeline, old_playing, old_state); + } + + response + } + + /// Handle updating last frame state and trigger callbacks on changes. + fn diff_with( + &mut self, + response: &mut TimeControlResponse, + old_timeline: Option, + old_playing: bool, + old_state: Option, + ) { + if old_playing != self.playing { + response.playing_change = Some(self.playing); + } + + if old_timeline != self.timeline().copied() { + let time = self + .time_for_timeline(*self.timeline_name()) + .unwrap_or(TimeReal::MIN); + + response.timeline_change = self.timeline().map(|t| (*t, time)); + } + + if let Some(state) = self.states.get_mut(self.timeline.name()) { + // TODO(jan): throttle? + if old_state.is_none_or(|old_state| old_state.time != state.time) { + response.time_change = Some(state.time); + } + } + } + + pub fn play_state(&self) -> PlayState { + if self.playing { + if self.following { + PlayState::Following + } else { + PlayState::Playing + } + } else { + PlayState::Paused + } + } + + pub fn loop_mode(&self) -> LoopMode { + if self.play_state() == PlayState::Following { + LoopMode::Off + } else { + self.loop_mode + } + } + + /// Updates the current play-state. + /// + /// If `blueprint_ctx` is specified this writes to the related + /// blueprint. + pub fn set_play_state( + &mut self, + db: Option<&EntityDb>, + play_state: PlayState, + blueprint_ctx: Option<&impl BlueprintContext>, + ) { + if let Some(blueprint_ctx) = blueprint_ctx + && Some(play_state) != blueprint_ctx.play_state() + { + blueprint_ctx.set_play_state(play_state); + } + + match play_state { + PlayState::Paused => { + self.playing = false; + // Clear `following` so a subsequent first-time TimeState insertion + // (see `update`'s Paused branch) lands at `full_range.min()` rather + // than `full_range.max()`. Without this, a Paused-from-blueprint + // transition out of the default `following: true` would place the + // cursor at the very end of the recording, and a subsequent drag + // would route through `exit_follow_mode` and resume playback. + self.following = false; + } + PlayState::Playing => { + self.playing = true; + self.following = false; + + // Start from beginning if we are at the end: + if let Some(db) = db + && let Some(range) = db.time_range_for(self.timeline_name()) + { + if let Some(state) = self.states.get_mut(self.timeline.name()) { + if range.max <= state.time { + state.time = range.min.into(); + } + } else { + self.states + .insert(*self.timeline_name(), TimeState::new(range.min)); + } + } + } + PlayState::Following => { + self.playing = true; + self.following = true; + + if let Some(db) = db + && let Some(range) = db.time_range_for(self.timeline_name()) + { + // Set the time to the max: + self.states + .entry(*self.timeline_name()) + .or_insert_with(|| TimeState::new(range.max)) + .time = range.max.into(); + } + } + } + } + + fn pause(&mut self, blueprint_ctx: Option<&impl BlueprintContext>) { + self.playing = false; + self.following = false; + if let Some(blueprint_ctx) = blueprint_ctx { + blueprint_ctx.set_play_state(PlayState::Paused); + } + if let Some(state) = self.states.get_mut(self.timeline.name()) { + state.last_paused_time = Some(state.time); + } + } + + /// Get the current [`re_entity_db::PrefetchTimeCursor`]. + /// + /// If the whole recording is looped the loop range is + /// `TimeInt::MIN..=TimeInt::MAX`. + pub fn time_cursor(&self) -> Option { + let typ = self.time_type()?; + let speed_if_unpaused = match typ { + TimeType::DurationNs | TimeType::TimestampNs => { + TimeInt::from_secs(1.0).as_f64() * self.speed as f64 + } + TimeType::Sequence => self.fps()? as f64 * self.speed as f64, + }; + + let loop_range = if self.loop_mode == LoopMode::All { + Some(AbsoluteTimeRange::new(TimeInt::MIN, TimeInt::MAX)) + } else { + self.active_loop_selection().map(|r| r.to_int()) + }; + + Some(re_entity_db::PrefetchTimeCursor { + time_cursor: re_log_types::TimelinePoint { + name: *self.timeline_name(), + typ, + time: self.time_int()?, + }, + speed_if_unpaused, + loop_range, + }) + } + + /// playback speed + pub fn speed(&self) -> f32 { + self.speed + } + + /// playback fps + pub fn fps(&self) -> Option { + self.states.get(self.timeline_name()).map(|state| state.fps) + } + + /// Make sure the selected timeline is a valid one + fn select_valid_timeline(&mut self, db: &EntityDb) { + let timelines = db.timelines(); + + let reset_timeline = match &self.timeline { + // If the timeline is auto refresh it every frame. + ActiveTimeline::Auto(_) => true, + // If it's user edited, refresh it if it's invalid. + ActiveTimeline::UserEdited(selected) => !timelines.contains_key(selected.name()), + // If it's pending never automatically refresh it. + ActiveTimeline::Pending(timeline_name) => { + // If the pending timeline is valid, it shouldn't be pending anymore. + if let Some(timeline) = timelines.get(timeline_name) { + self.timeline = ActiveTimeline::UserEdited(*timeline); + } + + false + } + }; + + if reset_timeline || matches!(self.timeline, ActiveTimeline::Auto(_)) { + self.timeline = + ActiveTimeline::Auto(Timeline::pick_best_timeline(timelines.values(), |t| { + db.num_temporal_rows_on_timeline(t.name()) + })); + } + } + + /// The currently selected timeline + #[inline] + pub fn timeline(&self) -> Option<&Timeline> { + self.timeline.timeline() + } + + pub fn timeline_name(&self) -> &TimelineName { + self.timeline.name() + } + + /// Time range that certain views must highlight. + pub fn highlighted_range(&self) -> Option<&TimeRangeHighlight> { + self.highlighted_range.as_ref() + } + + /// The time type of the currently selected timeline + pub fn time_type(&self) -> Option { + self.timeline().map(|t| t.typ()) + } + + /// The current time. + pub fn time(&self) -> Option { + self.states + .get(self.timeline_name()) + .map(|state| state.time) + } + + pub fn last_paused_time(&self) -> Option { + if matches!(self.play_state(), PlayState::Paused) { + self.time() + } else { + self.states + .get(self.timeline_name()) + .and_then(|state| state.last_paused_time) + } + } + + /// The current time & timeline. + pub fn time_cell(&self) -> Option { + let t = self.time()?; + Some(TimeCell::new(self.time_type()?, t.floor().as_i64())) + } + + /// The current time. + pub fn time_int(&self) -> Option { + self.time().map(|t| t.floor()) + } + + /// The current time. + pub fn time_i64(&self) -> Option { + self.time().map(|t| t.floor().as_i64()) + } + + /// Query for latest value at the currently selected time on the currently selected timeline. + pub fn current_query(&self) -> re_chunk_store::LatestAtQuery { + re_chunk_store::LatestAtQuery::new( + *self.timeline_name(), + self.time().map_or(TimeInt::MAX, |t| t.floor()), + ) + } + + /// The current loop range, if selection looping is turned on. + pub fn active_loop_selection(&self) -> Option { + if self.loop_mode == LoopMode::Selection { + self.states.get(self.timeline_name())?.time_selection + } else { + None + } + } + + /// The selected slice of time that is called the "loop selection". + /// + /// This can still return `Some` even if looping is currently off. + pub fn time_selection(&self) -> Option { + self.states.get(self.timeline_name())?.time_selection + } + + /// Is the current time in the selection range (if any), or at the current time mark? + pub fn is_time_selected(&self, timeline: &TimelineName, needle: TimeInt) -> bool { + if timeline != self.timeline_name() { + return false; + } + + if let Some(state) = self.states.get(self.timeline_name()) { + state.time.floor() == needle + } else { + false + } + } + + /// Is the active timeline pending resolution? + /// + /// When `true`, the requested timeline name hasn't been found in the data yet, + /// so [`Self::timeline()`] returns `None` and time-dependent queries may not work. + pub fn is_pending(&self) -> bool { + matches!(self.timeline, ActiveTimeline::Pending(_)) + } + + pub fn time_for_timeline(&self, timeline: TimelineName) -> Option { + self.states.get(&timeline).map(|state| state.time) + } + + /// The range of time we are currently zoomed in on. + pub fn time_view(&self) -> Option { + self.states + .get(self.timeline_name()) + .and_then(|state| state.view) + } +} diff --git a/crates/viewer/re_viewer_context/src/typed_entity_collections.rs b/crates/viewer/re_viewer_context/src/typed_entity_collections.rs index 0dd859ffa470..1ec9c96c2777 100644 --- a/crates/viewer/re_viewer_context/src/typed_entity_collections.rs +++ b/crates/viewer/re_viewer_context/src/typed_entity_collections.rs @@ -11,7 +11,7 @@ use re_types_core::ViewClassIdentifier; use crate::ViewSystemIdentifier; /// Types of matches when matching [`crate::VisualizabilityConstraints::SingleRequiredComponent`]. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, re_byte_size::SizeBytes)] pub enum DatatypeMatch { /// Only the physical datatype was matched, but semantics aren't the native ones. PhysicalDatatypeOnly { @@ -42,26 +42,6 @@ pub enum DatatypeMatch { }, } -impl re_byte_size::SizeBytes for DatatypeMatch { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::PhysicalDatatypeOnly { - arrow_datatype, - component_type, - selectors, - } => { - arrow_datatype.heap_size_bytes() - + component_type.heap_size_bytes() - + selectors.heap_size_bytes() - } - Self::NativeSemantics { - arrow_datatype, - component_type, - } => arrow_datatype.heap_size_bytes() + component_type.heap_size_bytes(), - } - } -} - impl DatatypeMatch { pub fn component_type(&self) -> Option<&re_chunk::ComponentType> { match self { @@ -79,7 +59,7 @@ impl DatatypeMatch { } /// [`crate::VisualizabilityConstraints::SingleRequiredComponent`] matched for this entity with the given components. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, re_byte_size::SizeBytes)] pub struct SingleRequiredComponentMatch { /// The component that needs to be mapped to one of the matches. pub target_component: ComponentIdentifier, @@ -94,7 +74,7 @@ pub struct SingleRequiredComponentMatch { /// /// Both a buffer component (matched by arrow datatype) and a format component /// (matched by arrow datatype AND semantic type) were found on the entity. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, re_byte_size::SizeBytes)] pub struct BufferAndFormatMatch { /// The buffer slot on the visualizer that needs to be mapped. pub buffer_target: ComponentIdentifier, @@ -113,33 +93,8 @@ pub struct BufferAndFormatMatch { pub format_matches: IntSet, } -impl re_byte_size::SizeBytes for SingleRequiredComponentMatch { - fn heap_size_bytes(&self) -> u64 { - let Self { - target_component, - matches, - } = self; - target_component.heap_size_bytes() + matches.heap_size_bytes() - } -} - -impl re_byte_size::SizeBytes for BufferAndFormatMatch { - fn heap_size_bytes(&self) -> u64 { - let Self { - buffer_target, - format_target, - buffer_matches, - format_matches, - } = self; - buffer_target.heap_size_bytes() - + format_target.heap_size_bytes() - + buffer_matches.heap_size_bytes() - + format_matches.heap_size_bytes() - } -} - /// Describes why a given entity was marked as visualizable. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, re_byte_size::SizeBytes)] pub enum VisualizableReason { /// The entity is visualizable because all entities are visualizable for this type. Always, @@ -154,16 +109,6 @@ pub enum VisualizableReason { BufferAndFormatMatch(BufferAndFormatMatch), } -impl re_byte_size::SizeBytes for VisualizableReason { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::Always | Self::ExactMatchAny => 0, - Self::SingleRequiredComponentMatch(m) => m.heap_size_bytes(), - Self::BufferAndFormatMatch(m) => m.heap_size_bytes(), - } - } -} - impl VisualizableReason { /// Returns true if this match reason is a perfect match for the given component identifier. pub fn full_native_match(&self, component_identifier: ComponentIdentifier) -> bool { @@ -199,15 +144,9 @@ impl VisualizableReason { /// We evaluate this filtering step entirely by store subscriber and provide a reason /// for why this entity was deemed visualizable. This in turn implies that this can /// *not* be influenced by individual view setups. -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone, Debug, re_byte_size::SizeBytes)] pub struct VisualizableEntities(pub IntMap); -impl re_byte_size::SizeBytes for VisualizableEntities { - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } -} - impl std::ops::Deref for VisualizableEntities { type Target = IntMap; @@ -221,7 +160,7 @@ impl std::ops::Deref for VisualizableEntities { /// /// In order to be a match the entity must have at some point in time on any timeline had any /// component that had an associated archetype as specified by the respective visualizer system. -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone, Debug, re_byte_size::SizeBytes)] pub struct IndicatedEntities(pub IntSet); impl std::ops::Deref for IndicatedEntities { @@ -237,7 +176,7 @@ impl std::ops::Deref for IndicatedEntities { /// /// Careful, if you're in the context of a view, this may contain visualizers that aren't relevant to the current view. /// Refer to [`PerVisualizerTypeInViewClass`] for a collection that is limited to visualizers active for a given view. -#[derive(Debug)] +#[derive(Debug, re_byte_size::SizeBytes)] pub struct PerVisualizerType(pub IntMap); impl std::ops::Deref for PerVisualizerType { @@ -270,15 +209,6 @@ impl PerVisualizerType { } } -impl re_byte_size::SizeBytes for PerVisualizerType -where - T: re_byte_size::SizeBytes, -{ - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } -} - /// Like [`PerVisualizerType`], but ensured that all visualizers are relevant for the given view class. #[derive(Debug)] pub struct PerVisualizerTypeInViewClass { @@ -311,7 +241,7 @@ impl std::ops::Deref for PerVisualizerTypeInViewClass { } /// List of elements per visualizer instruction id. -#[derive(Debug)] +#[derive(Debug, re_byte_size::SizeBytes)] pub struct PerVisualizerInstruction(pub HashMap); impl std::ops::Deref for PerVisualizerInstruction { @@ -342,12 +272,3 @@ impl Default for PerVisualizerInstruction { Self(HashMap::default()) } } - -impl re_byte_size::SizeBytes for PerVisualizerInstruction -where - T: re_byte_size::SizeBytes, -{ - fn heap_size_bytes(&self) -> u64 { - self.0.heap_size_bytes() - } -} diff --git a/crates/viewer/re_viewer_context/src/undo.rs b/crates/viewer/re_viewer_context/src/undo.rs index fc821287a9f3..bc38f87d9e80 100644 --- a/crates/viewer/re_viewer_context/src/undo.rs +++ b/crates/viewer/re_viewer_context/src/undo.rs @@ -15,7 +15,7 @@ const MAX_UNDOS: usize = 100; /// /// When undoing, we move back time, and redoing move it forward. /// When editing, we first drop all data after the current time. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, re_byte_size::SizeBytes)] pub struct BlueprintUndoState { /// The current blueprint time, used for latest-at. /// @@ -40,16 +40,6 @@ pub struct BlueprintUndoState { inflection_points: BTreeMap, } -impl re_byte_size::SizeBytes for BlueprintUndoState { - fn heap_size_bytes(&self) -> u64 { - let Self { - current_time: _, - inflection_points, - } = self; - inflection_points.heap_size_bytes() - } -} - // We don't restore undo-state when closing the viewer. // If you want to support this, make sure you replace the call to `cumulative_frame_nr` with something else, // (because that resets to zero on restart) and also make sure you test it properly! diff --git a/crates/viewer/re_viewer_context/src/view/mod.rs b/crates/viewer/re_viewer_context/src/view/mod.rs index 250db73dedd5..4d2a274a7533 100644 --- a/crates/viewer/re_viewer_context/src/view/mod.rs +++ b/crates/viewer/re_viewer_context/src/view/mod.rs @@ -43,7 +43,7 @@ pub use view_query::{ DataResult, RecommendedMappings, ViewQuery, VisualizerComponentMappings, VisualizerComponentSource, VisualizerInstruction, VisualizerInstructionsPerType, }; -pub use view_states::ViewStates; +pub use view_states::{PreviewState, ViewStates}; pub use visualizability_constraints::{ BufferAndFormatConstraint, SingleRequiredComponentConstraint, VisualizabilityConstraints, }; @@ -89,9 +89,6 @@ pub enum ViewSystemExecutionError { #[error(transparent)] ViewBuilderError(#[from] re_renderer::view_builder::ViewBuilderError), - - #[error("Missing output data for view system {0}.")] - MissingOutputData(ViewSystemIdentifier), } const _: () = assert!( diff --git a/crates/viewer/re_viewer_context/src/view/named_system.rs b/crates/viewer/re_viewer_context/src/view/named_system.rs index 6a8132d19908..bc6c8e15136a 100644 --- a/crates/viewer/re_viewer_context/src/view/named_system.rs +++ b/crates/viewer/re_viewer_context/src/view/named_system.rs @@ -2,17 +2,16 @@ use std::collections::{BTreeMap, BTreeSet}; use re_log_types::EntityPath; -re_string_interner::declare_new_type!( +re_string_interner::declare_new_type_nonempty!( /// Unique name for a system within a given [`crate::ViewClass`]. /// /// Note that this is *not* unique across the entire application. - #[derive(serde::Deserialize, serde::Serialize)] pub struct ViewSystemIdentifier; ); impl Default for ViewSystemIdentifier { fn default() -> Self { - "unknown".into() + re_string_interner::intern_static_nonempty!(ViewSystemIdentifier, "unknown") } } diff --git a/crates/viewer/re_viewer_context/src/view/system_execution_output.rs b/crates/viewer/re_viewer_context/src/view/system_execution_output.rs index 99080bd08d8f..c9480b659b42 100644 --- a/crates/viewer/re_viewer_context/src/view/system_execution_output.rs +++ b/crates/viewer/re_viewer_context/src/view/system_execution_output.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::{borrow::Cow, collections::BTreeMap}; use re_sdk_types::blueprint::components::VisualizerInstructionId; use vec1::Vec1; @@ -46,18 +46,35 @@ impl SystemExecutionOutput { } /// Get typed output data from a specific visualizer's execution result. + /// + /// Returns `None` when the visualizer was skipped because it had no active instructions in + /// the view. Consumers that want a present-but-empty value in that case should use + /// [`Self::visualizer_data_or_default`]. pub fn visualizer_data( &self, id: ViewSystemIdentifier, - ) -> Result<&T, ViewSystemExecutionError> { - self.visualizer_execution_output + ) -> Result, ViewSystemExecutionError> { + Ok(self + .visualizer_execution_output .per_visualizer .get(&id) .ok_or_else(|| ViewSystemExecutionError::VisualizerSystemNotFound(id.as_str()))? .as_ref() .map_err(|err| err.clone())? - .get_visualizer_data::() - .ok_or_else(|| ViewSystemExecutionError::MissingOutputData(id)) + .get_visualizer_data::()) + } + + /// Like [`Self::visualizer_data`], but substitutes `T::default()` when the visualizer was + /// skipped for having no active instructions, rather than returning an error. + pub fn visualizer_data_or_default( + &self, + id: ViewSystemIdentifier, + ) -> Result, ViewSystemExecutionError> { + match self.visualizer_data(id) { + Ok(Some(t)) => Ok(Cow::Borrowed(t)), + Ok(None) => Ok(Cow::Owned(T::default())), + Err(err) => Err(err), + } } /// Iterate over all visualizer output data that can be downcast to the given type. @@ -77,7 +94,7 @@ pub type VisualizerViewReport = BTreeMap>), } -impl re_byte_size::SizeBytes for VisualizerTypeReport { - fn heap_size_bytes(&self) -> u64 { - match self { - Self::OverallError(_err) => 0, // assume small and/or rare - Self::PerInstructionReport(reports) => reports.heap_size_bytes(), - } - } -} - impl VisualizerTypeReport { pub fn from_result( result: &Result, diff --git a/crates/viewer/re_viewer_context/src/view/view_class.rs b/crates/viewer/re_viewer_context/src/view/view_class.rs index dc9f3bde828a..e3c44a5532cd 100644 --- a/crates/viewer/re_viewer_context/src/view/view_class.rs +++ b/crates/viewer/re_viewer_context/src/view/view_class.rs @@ -3,15 +3,16 @@ use std::collections::BTreeMap; use itertools::Itertools as _; use nohash_hasher::IntSet; use re_chunk_store::MissingChunkReporter; -use re_log_types::EntityPath; +use re_log_types::{ComponentPath, EntityPath}; use re_sdk_types::ViewClassIdentifier; use vec1::Vec1; use super::ViewContext; use crate::{ - IndicatedEntities, PerVisualizerType, QueryRange, RecommendedMappings, SystemExecutionOutput, - ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, ViewSystemExecutionError, - ViewSystemIdentifier, ViewSystemRegistrator, ViewerContext, VisualizableReason, + DragAndDropFeedback, IndicatedEntities, PerVisualizerType, QueryRange, RecommendedMappings, + SystemExecutionOutput, ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, + ViewSystemExecutionError, ViewSystemIdentifier, ViewSystemRegistrator, ViewerContext, + VisualizableReason, }; #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq)] @@ -158,6 +159,17 @@ pub trait ViewClass: Send + Sync { /// Used for UI display. fn display_name(&self) -> &'static str; + // TODO(RR-4506): Remove this flag (and all sites that branch on it) once the Status view + // graduates from experimental. + /// Whether this view class is still experimental. + /// + /// Experimental views are shown in a separate "Experimental" section at the bottom of the + /// "add view" picker (with a warning icon), and surface an inline warning banner in the + /// selection panel when a view of this kind is selected. They are otherwise fully functional. + fn is_experimental(&self) -> bool { + false + } + /// Icon used to identify this view class. fn icon(&self) -> &'static re_ui::Icon { &re_ui::icons::VIEW_GENERIC @@ -298,6 +310,25 @@ pub trait ViewClass: Send + Sync { Ok(()) } + /// Handle components being dragged over a view of this class. + /// + /// This is the component-drop counterpart to the generic entity-drop handling done by the + /// viewport. The viewport calls this for every view tile hovered by a `Components` payload, + /// then uses the returned [`DragAndDropFeedback`] to drive the cursor and drop-target frame — + /// just like it does for entities. Implementors only decide acceptability and, when + /// `released` is `true`, perform the actual mutation. + /// + /// The default implementation ignores components (most views don't accept them). + fn handle_component_drop( + &self, + _ctx: &ViewerContext<'_>, + _view_id: ViewId, + _component_paths: &[ComponentPath], + _released: bool, + ) -> DragAndDropFeedback { + DragAndDropFeedback::Ignore + } + /// Draws the ui for this view class and handles ui events. /// /// The passed state is kept frame-to-frame. @@ -363,8 +394,15 @@ pub trait ViewState: std::any::Any + Sync + Send { /// Converts itself to a reference of [`std::any::Any`], which enables downcasting to concrete types. fn as_any_mut(&mut self) -> &mut dyn std::any::Any; - fn size_bytes(&self) -> u64 { - 0 // TODO(emilk): implement this for large view statses + /// How many bytes this state uses on the heap. + fn heap_size_bytes(&self) -> u64; +} + +/// Bridges `dyn ViewState` back to `SizeBytes`, so containers like `Box` can be sized. +impl re_byte_size::SizeBytes for dyn ViewState { + #[inline] + fn heap_size_bytes(&self) -> u64 { + ViewState::heap_size_bytes(self) } } @@ -377,6 +415,10 @@ impl ViewState for () { fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + + fn heap_size_bytes(&self) -> u64 { + 0 + } } pub trait ViewStateExt: ViewState { diff --git a/crates/viewer/re_viewer_context/src/view/view_class_placeholder.rs b/crates/viewer/re_viewer_context/src/view/view_class_placeholder.rs index fb64070d61f6..b2220ca28c96 100644 --- a/crates/viewer/re_viewer_context/src/view/view_class_placeholder.rs +++ b/crates/viewer/re_viewer_context/src/view/view_class_placeholder.rs @@ -14,7 +14,7 @@ pub struct ViewClassPlaceholder; impl ViewClass for ViewClassPlaceholder { fn identifier() -> ViewClassIdentifier { - "UnknownViewClass".into() + re_string_interner::intern_static_nonempty!(ViewClassIdentifier, "UnknownViewClass") } fn display_name(&self) -> &'static str { diff --git a/crates/viewer/re_viewer_context/src/view/view_context.rs b/crates/viewer/re_viewer_context/src/view/view_context.rs index 4f1bc06f432a..c67af356b5c1 100644 --- a/crates/viewer/re_viewer_context/src/view/view_context.rs +++ b/crates/viewer/re_viewer_context/src/view/view_context.rs @@ -5,8 +5,8 @@ use re_sdk_types::blueprint::components::VisualizerInstructionId; use re_sdk_types::{AsComponents, ComponentBatch, ComponentDescriptor, ViewClassIdentifier}; use super::VisualizerCollection; -use crate::{DataQueryResult, DataResult, QueryContext, ViewId}; -use crate::{ViewerContext, blueprint_helpers::BlueprintContext as _}; +use crate::ViewerContext; +use crate::{BlueprintContext, CommandSender, DataQueryResult, DataResult, QueryContext, ViewId}; /// The context associated with a view. /// @@ -28,6 +28,24 @@ pub struct ViewContext<'a> { pub query_result: &'a DataQueryResult, } +impl BlueprintContext for ViewContext<'_> { + fn command_sender(&self) -> &CommandSender { + self.viewer_ctx.command_sender() + } + + fn current_blueprint(&self) -> &re_entity_db::EntityDb { + self.viewer_ctx.current_blueprint() + } + + fn default_blueprint(&self) -> Option<&re_entity_db::EntityDb> { + self.viewer_ctx.default_blueprint() + } + + fn blueprint_query(&self) -> &LatestAtQuery { + self.viewer_ctx.blueprint_query() + } +} + impl<'a> ViewContext<'a> { #[inline] pub fn query_context( diff --git a/crates/viewer/re_viewer_context/src/view/view_query.rs b/crates/viewer/re_viewer_context/src/view/view_query.rs index ae44d97d7af1..b003e1593a8a 100644 --- a/crates/viewer/re_viewer_context/src/view/view_query.rs +++ b/crates/viewer/re_viewer_context/src/view/view_query.rs @@ -1,14 +1,16 @@ use std::collections::BTreeMap; -use itertools::Either; use nohash_hasher::IntSet; use re_chunk::{ComponentIdentifier, TimelineName}; use re_chunk_store::LatestAtQuery; use re_entity_db::{EntityPath, TimeInt}; -use re_sdk_types::blueprint::archetypes::{self as blueprint_archetypes, EntityBehavior}; use re_sdk_types::blueprint::components::VisualizerInstructionId; use re_sdk_types::blueprint::datatypes::{ComponentSourceKind, VisualizerComponentMapping}; +use re_sdk_types::{ + InvalidComponentIdentifierError, + blueprint::archetypes::{self as blueprint_archetypes, EntityBehavior}, +}; use crate::blueprint_helpers::BlueprintContext as _; use crate::{ @@ -32,7 +34,10 @@ pub enum VisualizerComponentSource { } impl VisualizerComponentSource { - pub fn from_blueprint_mapping(mapping: &VisualizerComponentMapping) -> Self { + /// Convert a [`VisualizerComponentMapping`] into a `VisualizerComponentSource`. + pub fn from_blueprint_mapping( + mapping: &VisualizerComponentMapping, + ) -> Result { let VisualizerComponentMapping { target, source_kind, @@ -40,20 +45,22 @@ impl VisualizerComponentSource { selector, } = mapping; - match source_kind { - ComponentSourceKind::SourceComponent => Self::SourceComponent { - source_component: source_component + Ok(match source_kind { + ComponentSourceKind::SourceComponent => { + let source_component = source_component .as_ref() .map(|c| c.as_str()) - .unwrap_or_else(|| target.as_str()) - .into(), - selector: selector.as_ref().map_or(String::new(), |s| s.to_string()), - }, + .unwrap_or_else(|| target.as_str()); + Self::SourceComponent { + source_component: ComponentIdentifier::try_new(source_component)?, + selector: selector.as_ref().map_or(String::new(), |s| s.to_string()), + } + } ComponentSourceKind::Override => Self::Override, ComponentSourceKind::Default => Self::Default, - } + }) } pub fn source_kind(&self) -> ComponentSourceKind { @@ -110,6 +117,11 @@ impl RecommendedMappings { } } + /// Creates a recommendation from a set of mandatory mappings. + pub fn from_mappings(mandatory_mappings: VisualizerComponentMappings) -> Self { + Self { mandatory_mappings } + } + /// Returns `true` if all mandatory mappings in this recommendation are already /// satisfied by the given existing component mappings. pub fn is_covered_by(&self, existing_mappings: &VisualizerComponentMappings) -> bool { @@ -153,6 +165,11 @@ impl RecommendedMappings { self.mandatory_mappings } + /// Returns the underlying component mappings. + pub fn mappings(&self) -> &VisualizerComponentMappings { + &self.mandatory_mappings + } + /// Human-readable display name derived from the first component source. pub fn display_name(&self) -> Option { self.mandatory_mappings @@ -492,14 +509,11 @@ impl<'s> ViewQuery<'s> { &self, visualizer: ViewSystemIdentifier, ) -> impl Iterator { - if let Some(instructions) = self - .active_visualizer_instructions_per_type + self.active_visualizer_instructions_per_type .get(&visualizer) - { - Either::Left(instructions.iter().copied()) - } else { - Either::Right(std::iter::empty()) - } + .into_iter() + .flatten() + .copied() } #[inline] diff --git a/crates/viewer/re_viewer_context/src/view/view_states.rs b/crates/viewer/re_viewer_context/src/view/view_states.rs index 0da995bdcd8c..d0b4efa6e919 100644 --- a/crates/viewer/re_viewer_context/src/view/view_states.rs +++ b/crates/viewer/re_viewer_context/src/view/view_states.rs @@ -5,10 +5,14 @@ use ahash::HashMap; +use re_byte_size::SizeBytes as _; use re_log_types::StoreId; use crate::view::system_execution_output::VisualizerViewReport; -use crate::{SystemExecutionOutput, ViewClass, ViewId, ViewState, VisualizerTypeReport}; +use crate::{ + AppBlueprintCtx, NeedsRepaint, SystemExecutionOutput, TimeControl, TimeControlUpdateParams, + ViewClass, ViewId, ViewState, VisualizerTypeReport, +}; /// Combined key of recording store id and view id. /// @@ -16,9 +20,104 @@ use crate::{SystemExecutionOutput, ViewClass, ViewId, ViewState, VisualizerTypeR /// view state between them since it may contain recording-specific data. type ViewStateKey = (StoreId, ViewId); +#[derive(re_byte_size::SizeBytes)] +pub struct ActivePreview { + pub time_control: TimeControl, +} + +impl Default for ActivePreview { + fn default() -> Self { + Self { + time_control: TimeControl::preview_time_control(), + } + } +} + +/// Shared playback state for all preview recordings shown in grid or table cards. +/// +/// All active previews have their own [`TimeControl`]. +#[derive(Default, re_byte_size::SizeBytes)] +pub struct PreviewState { + /// The previews that are currently active. + active_previews: ahash::HashMap, + + /// URIs that have already been requested. + pub requested_uris: ahash::HashSet, +} + +impl PreviewState { + /// Register a recording as an active preview clip. + /// + /// Called each frame by the view renderer when a preview is shown. + pub fn register_recording( + &mut self, + store_id: &StoreId, + store_bundle: &re_entity_db::StoreBundle, + ) { + self.active_previews.entry(store_id.clone()).or_default(); + + if let Some(db) = store_bundle.get(store_id) + && let Some(re_entity_db::LogSource::RedapGrpcStream { uri, .. }) = &db.data_source + { + // If we've successfully loaded a uri, we could possibly want to request + // it again later if it gets GC'ed. + self.requested_uris.remove(uri); + } + } + + /// Remove registrations for recordings that are no longer loaded. + pub fn cleanup_recordings(&mut self, is_loaded: impl Fn(&StoreId) -> bool) { + self.active_previews.retain(|id, _| is_loaded(id)); + } + + pub fn tick<'db>( + &mut self, + resolve: impl Fn(&StoreId) -> Option<&'db re_entity_db::EntityDb>, + stable_dt: f32, + ) -> NeedsRepaint { + let mut needs_repaint = NeedsRepaint::No; + + #[expect(clippy::iter_over_hash_type)] // Fine here, we're updating each one individually. + for (id, active_preview) in &mut self.active_previews { + let Some(db) = resolve(id) else { + continue; + }; + + let res = active_preview.time_control.update( + db, + &TimeControlUpdateParams { + stable_dt, + more_data_is_streaming_in: false, + is_buffering: db.is_buffering(), + should_diff_state: false, + }, + None::<&AppBlueprintCtx<'_>>, + ); + + needs_repaint = needs_repaint.or(res.needs_repaint); + } + + needs_repaint + } + + pub fn iter_active_previews(&self) -> impl Iterator { + self.active_previews.iter() + } + + pub fn recording_time_control(&self, store_id: &StoreId) -> Option<&TimeControl> { + self.active_previews.get(store_id).map(|p| &p.time_control) + } + + pub fn recording_time_control_mut(&mut self, store_id: &StoreId) -> Option<&mut TimeControl> { + self.active_previews + .get_mut(store_id) + .map(|p| &mut p.time_control) + } +} + /// State for the `View`s that persists across frames but otherwise /// is not saved. -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] pub struct ViewStates { states: HashMap>, @@ -28,19 +127,41 @@ pub struct ViewStates { // TODO(andreas): Would be nice to bundle this with `ViewState` by making `ViewState` a struct containing errors & generic data. // But at point of writing this causes too much needless churn. visualizer_reports: HashMap, + + // TODO(isse): Should we have one preview state per table/dataset? + /// Playback state shared across all preview recordings shown in grid/table cards. + pub preview_state: Option, } -impl re_byte_size::SizeBytes for ViewStates { - fn heap_size_bytes(&self) -> u64 { +impl re_byte_size::MemUsageTreeCapture for ViewStates { + fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree { let Self { states, - visualizer_reports: visualizer_errors, + visualizer_reports, + preview_state, } = self; - states + + let mut state_sizes = states .iter() - .map(|(key, state)| key.total_size_bytes() + state.size_bytes()) - .sum::() - + visualizer_errors.heap_size_bytes() + .map(|((store_id, view_id), state)| { + ( + format!("{store_id:?}/{view_id:?}"), + state.total_size_bytes(), + ) + }) + .collect::>(); + state_sizes.sort_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs)); + + let mut states_node = re_byte_size::MemUsageNode::default(); + for (name, size_bytes) in state_sizes { + states_node.add(name, size_bytes); + } + + let mut node = re_byte_size::MemUsageNode::default(); + node.add("states", states_node.into_tree()); + node.add("visualizer_reports", visualizer_reports.heap_size_bytes()); + node.add("preview", preview_state.heap_size_bytes()); + node.with_total_size_bytes(self.total_size_bytes()) } } diff --git a/crates/viewer/re_viewer_context/src/view/visualizability_constraints.rs b/crates/viewer/re_viewer_context/src/view/visualizability_constraints.rs index 623ca8e7e3e2..8400a2c74f32 100644 --- a/crates/viewer/re_viewer_context/src/view/visualizability_constraints.rs +++ b/crates/viewer/re_viewer_context/src/view/visualizability_constraints.rs @@ -147,13 +147,21 @@ impl SingleRequiredComponentConstraint { component_type: incoming_component_type, }), - (false, true) => { + (false, true) => extract_nested_fields(incoming_arrow_datatype, |dt| { + self.physical_types.contains(dt) + }) + .map(|selectors| DatatypeMatch::PhysicalDatatypeOnly { + arrow_datatype: incoming_arrow_datatype.clone(), + component_type: incoming_component_type, + selectors: selectors.into(), + }) + .or_else(|| { re_log::warn_once!( "Component {incoming_component:?} matched semantic type {:?} but none of the expected physical arrow types {incoming_arrow_datatype:?} for this semantic type.", self.semantic_type, ); None - } + }), } } } diff --git a/crates/viewer/re_viewer_context/src/view/visualizer_entity_subscriber.rs b/crates/viewer/re_viewer_context/src/view/visualizer_entity_subscriber.rs index a103b957d41f..63fade66238d 100644 --- a/crates/viewer/re_viewer_context/src/view/visualizer_entity_subscriber.rs +++ b/crates/viewer/re_viewer_context/src/view/visualizer_entity_subscriber.rs @@ -20,7 +20,8 @@ use crate::{ /// /// This is the immutable "template" stored in the [`crate::ViewClassRegistry`], /// extracted from a visualizer's query info at registration time. -#[derive(Clone)] // Cheap to clone; uses ref-counted data internally. +// We use Arc:s, so this is more or less amortized. +#[derive(Clone, re_byte_size::SizeBytes)] // Cheap to clone; uses ref-counted data internally. pub struct VisualizerEntityConfig { /// Visualizer type this config is associated with. pub visualizer: ViewSystemIdentifier, @@ -31,6 +32,7 @@ pub struct VisualizerEntityConfig { /// The mode for checking component requirements. /// /// See [`crate::VisualizerQueryInfo::constraints`] + #[size_bytes(ignore)] pub constraints: Arc, /// Lists all known builtin enums components. @@ -38,15 +40,10 @@ pub struct VisualizerEntityConfig { /// Used by [`VisualizabilityConstraints::SingleRequiredComponent`] to skip physical-only matches /// for enum types (which should only match via native semantics). // TODO(andreas): It would be great if we could just always access the latest reflection data, but this is really hard to pipe through to a store subscriber. + #[size_bytes(ignore)] pub known_builtin_enum_components: Arc>, } -impl re_byte_size::SizeBytes for VisualizerEntityConfig { - fn heap_size_bytes(&self) -> u64 { - 0 // We use Arc:s, so this is more or less amortized - } -} - impl VisualizerEntityConfig { /// Create a new [`VisualizerEntitySubscriber`] from this config with empty per-store data. pub fn create_subscriber(&self) -> VisualizerEntitySubscriber { @@ -67,39 +64,23 @@ impl VisualizerEntityConfig { /// "visualizable" is determined by the set of required components /// /// There's only a single entity subscriber per visualizer *type* per store. +#[derive(re_byte_size::SizeBytes)] +#[size_bytes(profile)] pub struct VisualizerEntitySubscriber { config: VisualizerEntityConfig, mapping: VisualizerEntityMapping, } -impl re_byte_size::SizeBytes for VisualizerEntitySubscriber { - fn heap_size_bytes(&self) -> u64 { - re_tracing::profile_function!(); - let Self { config, mapping } = self; - config.heap_size_bytes() + mapping.heap_size_bytes() - } -} - /// Per-entity state for a [`VisualizabilityConstraints::BufferAndFormat`] constraint. /// /// Buffer and format components may arrive in separate chunk store events, so we keep accumulating them here. -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] struct BufferAndFormatEntityState { all_buffer_matches: IntMap, all_formats_matches: IntSet, } -impl re_byte_size::SizeBytes for BufferAndFormatEntityState { - fn heap_size_bytes(&self) -> u64 { - let Self { - all_buffer_matches, - all_formats_matches, - } = self; - all_buffer_matches.heap_size_bytes() + all_formats_matches.heap_size_bytes() - } -} - -#[derive(Default)] +#[derive(Default, re_byte_size::SizeBytes)] struct VisualizerEntityMapping { /// Which entities the visualizer can be applied to. visualizable_entities: VisualizableEntities, @@ -116,19 +97,6 @@ struct VisualizerEntityMapping { buffer_and_format_state: IntMap, } -impl re_byte_size::SizeBytes for VisualizerEntityMapping { - fn heap_size_bytes(&self) -> u64 { - let Self { - visualizable_entities, - indicated_entities, - buffer_and_format_state, - } = self; - visualizable_entities.heap_size_bytes() - + indicated_entities.heap_size_bytes() - + buffer_and_format_state.heap_size_bytes() - } -} - impl VisualizerEntityMapping { /// Adds a visualizability reason for the given entity and combines it with an existing one if any. /// @@ -485,11 +453,14 @@ mod tests { } /// Build a `ComponentDescriptor` with the given component identifier and optional semantic type. - fn descriptor(component: &str, component_type: Option<&str>) -> ComponentDescriptor { + fn descriptor( + component: impl Into, + component_type: Option<&str>, + ) -> ComponentDescriptor { ComponentDescriptor { archetype: None, component: component.into(), - component_type: component_type.map(Into::into), + component_type: component_type.and_then(|s| ComponentType::try_new(s).ok()), } } diff --git a/crates/viewer/re_viewer_context/src/view/visualizer_system.rs b/crates/viewer/re_viewer_context/src/view/visualizer_system.rs index 52247fb74bce..95abc9803fa5 100644 --- a/crates/viewer/re_viewer_context/src/view/visualizer_system.rs +++ b/crates/viewer/re_viewer_context/src/view/visualizer_system.rs @@ -144,8 +144,14 @@ impl VisualizerQueryInfo { /// Severity level for visualizer diagnostics. /// /// Sorts from least concern to highest. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, re_byte_size::SizeBytes)] pub enum VisualizerReportSeverity { + /// A purely informational report for a component. + /// + /// For example if a component is not supported in some situations, but that's expected + /// (e.g. a colormap component that has no effect with non-grayscale image data). + Info, + /// Something went wrong on an optional component. /// /// We can often still show something using the default. @@ -161,7 +167,7 @@ pub enum VisualizerReportSeverity { } /// Contextual information about where/why a diagnostic occurred. -#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, re_byte_size::SizeBytes)] pub struct VisualizerReportContext { /// The component that caused the issue (if applicable). /// @@ -172,12 +178,6 @@ pub struct VisualizerReportContext { pub extra: Option, } -impl re_byte_size::SizeBytes for VisualizerReportContext { - fn heap_size_bytes(&self) -> u64 { - self.extra.heap_size_bytes() - } -} - /// A diagnostic message (error or warning) from a visualizer for a single instruction. /// /// Collected into [`crate::VisualizerTypeReport::PerInstructionReport`]. @@ -197,7 +197,7 @@ impl re_byte_size::SizeBytes for VisualizerReportContext { /// has an unexpected type, or is otherwise unusable. /// /// For a high-level failure handling overview, see the `re_viewer` crate documentation. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, re_byte_size::SizeBytes)] pub struct VisualizerInstructionReport { pub severity: VisualizerReportSeverity, pub context: VisualizerReportContext, @@ -209,14 +209,6 @@ pub struct VisualizerInstructionReport { pub details: Option, } -impl re_byte_size::SizeBytes for VisualizerInstructionReport { - fn heap_size_bytes(&self) -> u64 { - self.summary.heap_size_bytes() - + self.details.heap_size_bytes() - + self.context.heap_size_bytes() - } -} - /// Result of running [`VisualizerSystem::execute`]. /// /// Contains two kinds of output: @@ -366,6 +358,23 @@ pub trait VisualizerSystem: Send + Sync + std::any::Any { query: &ViewQuery<'_>, context_systems: &ViewContextCollection, ) -> Result; + + /// Optional custom UI shown in the selection panel for this visualizer instruction. + /// + /// Returns `true` if the custom UI replaces the default per-component value UI. + /// Visualizers that need source-mapping selectors render them themselves as part of + /// this UI. Returns `false` to fall back to the default per-component UI; the default + /// impl renders nothing and returns `false`. + fn selection_ui( + &self, + _ctx: &ViewContext<'_>, + _ui: &mut egui::Ui, + _data_result: &crate::DataResult, + _instruction: &crate::VisualizerInstruction, + _type_report: Option<&crate::VisualizerTypeReport>, + ) -> bool { + false + } } pub struct VisualizerCollection { diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index 78f2afe5772c..ee660acf6c5e 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -5,9 +5,7 @@ use re_entity_db::entity_db::EntityDb; use re_log_types::{EntryId, TableId}; use re_query::StorageEngineReadGuard; use re_sdk_types::ViewClassIdentifier; -use re_ui::list_item::ListItem; -use crate::command_sender::{SelectionSource, SetSelection}; use crate::query_context::DataQueryResult; use crate::time_control::TimeControlCommand; use crate::{ @@ -46,9 +44,6 @@ pub struct ViewerContext<'a> { /// The blueprint query used for resolving blueprint in this frame pub blueprint_query: &'a LatestAtQuery, - /// Where we are getting our data from. - pub connected_receivers: &'a re_log_channel::LogReceiverSet, - /// The active recording and blueprint. pub store_context: &'a ActiveStoreContext<'a>, } @@ -144,7 +139,7 @@ impl<'a> ViewerContext<'a> { app_ctx: &self.app_ctx, db: self.store_context.blueprint, time_ctrl: self.blueprint_time_ctrl, - caches: self.store_context.caches, // TODO(RR-3033): what cache to use here? + caches: self.store_context.caches, } } @@ -275,42 +270,8 @@ impl<'a> ViewerContext<'a> { response: &egui::Response, interacted_items: impl Into, ) { - let interacted_items = interacted_items - .into() - .into_mono_instance_path_items(self.recording(), &self.current_query()); - - // Focus -> Selection - - // We want the item to be selected if it was selected with arrow keys (in list_item) - // but not when focused using e.g. the tab key. - if ListItem::gained_focus_via_arrow_key(&response.ctx, response.id) { - self.command_sender() - .send_system(SystemCommand::SetSelection( - SetSelection::new(interacted_items.clone()) - .with_source(SelectionSource::ListItemNavigation), - )); - } - - // Selection -> Focus - - let single_selected = self.selection().single_item() == interacted_items.single_item(); - if single_selected { - // If selection changes, and a single item is selected, the selected item should - // receive egui focus. - // We don't do this if selection happened due to list item navigation to avoid - // a feedback loop. - let selection_changed = self - .selection_state() - .selection_changed() - .is_some_and(|source| source != SelectionSource::ListItemNavigation); - - // If there is a single selected item and nothing is focused, focus that item. - let nothing_focused = response.ctx.memory(|mem| mem.focused().is_none()); - - if selection_changed || nothing_focused { - response.request_focus(); - } - } + self.app_ctx + .handle_select_focus_sync(response, interacted_items); } /// Are we running inside the Safari browser? diff --git a/crates/viewer/re_viewer_context/src/visitor_flow_control.rs b/crates/viewer/re_viewer_context/src/visitor_flow_control.rs index 5d8b5602f109..fae4a122e9fb 100644 --- a/crates/viewer/re_viewer_context/src/visitor_flow_control.rs +++ b/crates/viewer/re_viewer_context/src/visitor_flow_control.rs @@ -15,7 +15,7 @@ pub enum VisitorControlFlow { } impl VisitorControlFlow { - /// Indicates whether we should visit the children of the current node—or entirely stop + /// Indicates whether we should visit the children of the current node — or entirely stop /// traversal. /// /// Returning a [`ControlFlow`] enables key ergonomics by allowing the use of the short circuit diff --git a/crates/viewer/re_viewer_context/tests/link_button_test.rs b/crates/viewer/re_viewer_context/tests/link_button_test.rs new file mode 100644 index 000000000000..d189f2d9b2d8 --- /dev/null +++ b/crates/viewer/re_viewer_context/tests/link_button_test.rs @@ -0,0 +1,166 @@ +//! Screenshot test for the "smart link" URL buttons produced by the real viewer decorator. +//! +//! The unit tests in `link_button.rs` only check the button *text*; this exercises the full +//! `make_url_decorator` → `re_ui` rendering path across the different built-in URL types, so we +//! catch regressions in icon/label/breadcrumb layout. + +#![cfg(not(target_arch = "wasm32"))] + +use std::sync::Arc; + +use re_log_types::{EntryId, EntryName}; +use re_ui::syntax_highlighting::SyntaxHighlightedBuilder; +use re_ui::{UiLayout, UrlDecorator}; +use re_viewer_context::{LinkKind, ResolvedEntry, UrlNameLookup, make_url_decorator}; + +const DATASET_TUID: &str = "1830B33B45B963E7774455beb91701ae"; +const DATASET_ENTRY_TUID: &str = "9a3c5e7b1d2f486a0b4c6d8e0f123456"; +const TABLE_ENTRY_TUID: &str = "182755B45B963E7774455beb91701aef"; + +/// All the built-in URL types we recognize, paired with a short description. +/// +/// Note: a bare local file path (e.g. `/path/to/file.rrd`) is intentionally absent — `data_label` +/// only treats text containing `://` as a URL, so the decorator is never invoked for it. +const URLS: &[(&str, &str)] = &[ + ( + "Dataset segment (resolved)", + "rerun://example.rerun.io/dataset/1830B33B45B963E7774455beb91701ae?segment_id=segment-abc-12", + ), + ( + "Dataset segment (unresolved)", + "rerun://example.rerun.io/dataset/abcdef0123456789abcdef0123456789?segment_id=seg99", + ), + ( + "Dataset entry (resolved)", + "rerun://example.rerun.io/entry/9a3c5e7b1d2f486a0b4c6d8e0f123456", + ), + ( + // Unresolved entries carry no kind, so this falls back to a dataset icon + short id — it + // renders identically to the "Table entry (unresolved)" row below. + "Dataset entry (unresolved)", + "rerun://example.rerun.io/entry/fedcba9876543210fedcba9876543210", + ), + ( + "Table entry (resolved)", + "rerun://example.rerun.io/entry/182755B45B963E7774455beb91701aef", + ), + ( + // Same id-only fallback as the unresolved dataset entry above. + "Table entry (unresolved)", + "rerun://example.rerun.io/entry/0011223344556677889900aabbccddee", + ), + ("Catalog", "rerun://example.rerun.io/catalog"), + ("Proxy", "rerun://example.rerun.io/proxy"), + ( + "Folder", + "rerun://example.rerun.io/folder/perception.detection", + ), + ("Intra-recording selection", "recording://camera/points"), + ("Local file (file:// URL)", "file:///recordings/data.rrd"), + ("Remote file", "https://example.com/recordings/data.rrd"), + ( + "Web-viewer share link", + "https://rerun.io/viewer?url=https://example.com/inner.rrd", + ), + ("Non-redap URL (plain link)", "https://rerun.io/"), +]; + +fn lookup() -> Arc { + let origin: re_uri::Origin = "rerun://example.rerun.io".parse().expect("valid origin"); + + let mut lookup = UrlNameLookup::default(); + lookup.insert( + ( + origin.clone(), + DATASET_TUID.parse::().expect("valid entry id"), + ), + ResolvedEntry { + name: EntryName::new("my-dataset").expect("valid entry name"), + kind: LinkKind::Dataset, + }, + ); + lookup.insert( + ( + origin.clone(), + DATASET_ENTRY_TUID + .parse::() + .expect("valid entry id"), + ), + ResolvedEntry { + name: EntryName::new("my-dataset-entry").expect("valid entry name"), + kind: LinkKind::Dataset, + }, + ); + lookup.insert( + ( + origin, + TABLE_ENTRY_TUID.parse::().expect("valid entry id"), + ), + ResolvedEntry { + name: EntryName::new("my-table").expect("valid entry name"), + kind: LinkKind::Table, + }, + ); + Arc::new(lookup) +} + +#[test] +fn link_buttons_match_snapshot() { + let mut snapshot_results = egui_kittest::SnapshotResults::new(); + + for (theme, suffix) in [(egui::Theme::Dark, "dark"), (egui::Theme::Light, "light")] { + let lookup = lookup(); + + let mut harness = + re_ui::testing::new_harness(re_ui::testing::TestOptions::Gui, [480.0, 360.0]) + .with_theme(theme) + .build_ui(move |ui| { + re_ui::apply_style_and_install_loaders(ui.ctx()); + UrlDecorator::set(ui.ctx(), make_url_decorator(lookup.clone(), theme)); + + egui::Grid::new("link_buttons") + .num_columns(2) + .show(ui, |ui| { + for (description, url) in URLS { + ui.label(*description); + UiLayout::List.data_label( + ui, + SyntaxHighlightedBuilder::new().with_string_value(url), + ); + ui.end_row(); + } + }); + }); + + harness.fit_contents(); + snapshot_results.add(harness.try_snapshot(format!("link_buttons_{suffix}"))); + } +} + +/// Hovering a button reveals the copy button: the button content yields space for it by truncating, +/// so the overall layout doesn't shift, and the button frame is painted behind the content only — +/// not behind the copy button. +#[test] +fn link_button_hovered_reveals_copy_button() { + let lookup = lookup(); + let url = "rerun://example.rerun.io/dataset/1830B33B45B963E7774455beb91701ae?segment_id=segment-abc-12"; + + let mut harness = re_ui::testing::new_harness(re_ui::testing::TestOptions::Gui, [320.0, 80.0]) + .build_ui(move |ui| { + re_ui::apply_style_and_install_loaders(ui.ctx()); + UrlDecorator::set( + ui.ctx(), + make_url_decorator(lookup.clone(), ui.ctx().theme()), + ); + + UiLayout::List.data_label(ui, SyntaxHighlightedBuilder::new().with_string_value(url)); + }); + + harness.hover_at(egui::pos2(30.0, 10.0)); + + // Run twice to ensure the tooltip is shown. + harness.run(); + harness.run(); + + harness.snapshot("link_button_hovered"); +} diff --git a/crates/viewer/re_viewer_context/tests/snapshots/link_button_hovered.png b/crates/viewer/re_viewer_context/tests/snapshots/link_button_hovered.png new file mode 100644 index 000000000000..cfb0c18c7258 --- /dev/null +++ b/crates/viewer/re_viewer_context/tests/snapshots/link_button_hovered.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cf154555a8f5e8fc8bb59052d69e1e32688ee6021fd1d425a134e1e94829f9b +size 13991 diff --git a/crates/viewer/re_viewer_context/tests/snapshots/link_buttons_dark.png b/crates/viewer/re_viewer_context/tests/snapshots/link_buttons_dark.png new file mode 100644 index 000000000000..3183b6b86956 --- /dev/null +++ b/crates/viewer/re_viewer_context/tests/snapshots/link_buttons_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a6b63bcc9aec51d4c4d4b48a3216ecda49382e9e583ae35501579b8264b8761 +size 72828 diff --git a/crates/viewer/re_viewer_context/tests/snapshots/link_buttons_light.png b/crates/viewer/re_viewer_context/tests/snapshots/link_buttons_light.png new file mode 100644 index 000000000000..23d28eb7807d --- /dev/null +++ b/crates/viewer/re_viewer_context/tests/snapshots/link_buttons_light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4908beadffafd26582d26cf6d029818d2086318e4ffeac6659b4ab6dd762c310 +size 71767 diff --git a/crates/viewer/re_viewer_mcp/Cargo.toml b/crates/viewer/re_viewer_mcp/Cargo.toml new file mode 100644 index 000000000000..c41f47f8fc54 --- /dev/null +++ b/crates/viewer/re_viewer_mcp/Cargo.toml @@ -0,0 +1,39 @@ +[package] +authors.workspace = true +description = "MCP server that allows llm agents to use the Rerun Viewer." +edition.workspace = true +homepage.workspace = true +license.workspace = true +include.workspace = true +name = "re_viewer_mcp" +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[[bin]] +name = "re-viewer-mcp" +path = "src/main.rs" + +[dependencies] +egui_inspection.workspace = true +egui_mcp.workspace = true +re_log = { workspace = true, features = ["setup"] } +re_protos.workspace = true + +anyhow.workspace = true +parking_lot.workspace = true +rmcp = { version = "1.7", features = ["server", "macros", "transport-io", "schemars"] } +schemars = "1.0" +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros", "sync"] } +tonic = { workspace = true, features = ["transport"] } + + +[dev-dependencies] +insta.workspace = true diff --git a/crates/viewer/re_viewer_mcp/README.md b/crates/viewer/re_viewer_mcp/README.md new file mode 100644 index 000000000000..262167af5350 --- /dev/null +++ b/crates/viewer/re_viewer_mcp/README.md @@ -0,0 +1,15 @@ +# re_viewer_mcp + +Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. + +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +MCP server for the Rerun Viewer. See the [docs](https://rerun.io/docs/reference/viewer/mcp) for more info. + +## Development + +There is a `.mcp.json` that Claude should pick up in the Rerun repository root. + +Use `cargo build -p re_viewer_mcp` to build the updated mcp server, and then within claude use `/mcp` and select `rerun` and +then reconnect, and it'll use the updated mcp (or reboot the cli). diff --git a/crates/viewer/re_viewer_mcp/src/lib.rs b/crates/viewer/re_viewer_mcp/src/lib.rs new file mode 100644 index 000000000000..833703e5152c --- /dev/null +++ b/crates/viewer/re_viewer_mcp/src/lib.rs @@ -0,0 +1,581 @@ +//! `re_viewer_mcp` — an MCP server that drives the Rerun viewer. +//! +//! It reuses the full `egui_mcp` UI tool set (`query_tree`, `screenshot`, `click`, …) but, instead of +//! dialing a local inspection socket, it drives the viewer over rerun's gRPC `ViewerControlService`. +//! +//! Each egui tool call becomes one `egui_inspection` request/response exchange, carried by a single +//! `Inspect` RPC. +//! +//! The server is exposed two ways — the standalone `re-viewer-mcp` binary and the `rerun viewer-mcp` +//! CLI subcommand. + +use std::sync::Arc; +use std::time::Duration; + +use parking_lot::Mutex; +use rmcp::{ + ErrorData as McpError, ServerHandler, ServiceExt as _, + handler::server::{router::tool::ToolRouter, tool::ToolCallContext, wrapper::Parameters}, + model::{ + CallToolRequestParams, CallToolResult, Content, Implementation, ListToolsResult, + PaginatedRequestParams, ServerCapabilities, ServerInfo, + }, + schemars, + service::{RequestContext, RoleServer}, + tool, tool_router, transport, +}; +use serde::{Deserialize, Serialize}; +use tonic::transport::Channel; + +use egui_inspection::protocol::{self, PROTOCOL_VERSION, Request, Response}; +use egui_mcp::{BoxFuture, Bridge, PeerInfo, Transport, UiServer}; +use re_protos::common::v1alpha1::{ + ApplicationId, StoreId, StoreKind, TimeRange, TimeType, Timeline, +}; +use re_protos::sdk_comms::v1alpha1::{ + GetViewerStateRequest, GetViewerStateResponse, InspectRequest, OpenUrlRequest, + SetTimeCursorRequest, SetTimeCursorResponse, TimeCursor, ViewerRecording, ViewerTimeline, + viewer_control_service_client::ViewerControlServiceClient, +}; + +const DEFAULT_VIEWER_ENDPOINT: &str = "http://127.0.0.1:9876"; + +/// Per-request RPC deadline. +const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + +/// An [`egui_mcp::Transport`] that carries each `egui_inspection` request/response over a unary +/// `Inspect` gRPC call to the running viewer. +#[derive(Clone)] +struct GrpcInspector { + client: ViewerControlServiceClient, +} + +impl Transport for GrpcInspector { + fn request(&self, req: Request) -> BoxFuture<'_, Result> { + Box::pin(async move { + let request = protocol::encode_body(&req).map_err(|err| err.to_string())?; + let mut client = self.client.clone(); + let response = client + .inspect(InspectRequest { request }) + .await + .map_err(|err| format!("inspect rpc failed: {err}"))? + .into_inner(); + protocol::decode_body(&response.response).map_err(|err| err.to_string()) + }) + } +} + +/// Dial the viewer once and build both the [`Bridge`] (which tunnels the egui tools over the +/// unary `Inspect` RPC) and the gRPC client the rerun-specific tools call through — sharing the +/// single connection between them. +async fn connect_grpc( + endpoint: &str, +) -> Result<(Bridge, ViewerControlServiceClient), String> { + let channel = tonic::transport::Endpoint::from_shared(endpoint.to_owned()) + .map_err(|err| err.to_string())? + .timeout(REQUEST_TIMEOUT) + .connect() + .await + .map_err(|err| err.to_string())?; + let client = ViewerControlServiceClient::new(channel); + let inspector = GrpcInspector { + client: client.clone(), + }; + + // Read the peer's label up front (also a liveness check), matching the TCP `attach` path. + let label = match inspector.request(Request::GetInfo).await? { + Response::Info { label, .. } => label, + Response::Error { message } => return Err(message), + _ => return Err("unexpected response to GetInfo".to_owned()), + }; + + let bridge = Bridge::with_transport( + inspector, + PeerInfo { + transport: endpoint.to_owned(), + protocol_version: PROTOCOL_VERSION, + label, + }, + ); + Ok((bridge, client)) +} + +/// The live connection to the viewer: the egui [`UiServer`] (which tunnels the egui tools over +/// the `Inspect` RPC) and the raw gRPC client (used by the rerun-specific tools). Both are +/// established together on `connect` and dropped together on `disconnect`, so they live behind a +/// single lock. +struct Connection { + ui: UiServer, + client: ViewerControlServiceClient, +} + +/// The `re_viewer_mcp` server: rerun-specific connection / state tools, plus the reusable `egui_mcp` +/// [`UiServer`], built on `connect` and dropped on `disconnect`, that drives the live viewer. +#[derive(Clone)] +struct ViewerMcpServer { + /// The active connection, `Some` while connected and `None` otherwise. Tool handlers clone + /// the `Arc` out of the lock (sync, so it can't be held across an await) and + /// then use it freely. + conn: Arc>>>, + + /// Router over the egui UI/inspection tools. Independent of the connection, so the tools + /// stay listed while disconnected; a call before `connect` returns `no app connected`. + ui_router: ToolRouter, + + /// Router for the rerun-specific tools layered on top of the egui ones. + tool_router: ToolRouter, +} + +#[derive(Debug, Default, Deserialize, schemars::JsonSchema)] +struct ConnectArgs { + /// gRPC endpoint of the running viewer's `ViewerControlService`. + /// Defaults to `http://127.0.0.1:9876`. + #[serde(default)] + endpoint: Option, +} + +#[derive(Debug, Default, Deserialize, schemars::JsonSchema)] +struct EmptyArgs {} + +#[derive(Debug, Default, Deserialize, schemars::JsonSchema)] +struct SetTimeArgs { + /// Recording to seek (see `viewer_state`). + /// Defaults to the active recording. + #[serde(default)] + store_id: Option, + + /// Timeline to seek on (see `viewer_state` for available timelines). + /// Defaults to the active timeline. + #[serde(default)] + timeline: Option, + + /// Time to seek to: a sequence index for sequence timelines, or nanoseconds for temporal timelines (see each timeline's `type` and `min`/`max` in `viewer_state`). + time: i64, + + /// If true, start playing the recording from the new time cursor position instead of just + /// moving the cursor and staying paused. Defaults to false. + #[serde(default)] + play: bool, +} + +/// JSON representation of a proto [`StoreId`], used both as agent-facing output (see +/// `viewer_state`) and as tool input identifying a recording to target. +#[derive(Debug, Default, Clone, Serialize, Deserialize, schemars::JsonSchema)] +struct StoreIdArg { + /// The kind of store: `"recording"`, `"blueprint"`, or `"unspecified"`. + kind: String, + + /// The recording id. + recording_id: String, + + /// The application id the recording belongs to. + application_id: String, +} + +impl From for StoreIdArg { + fn from(store_id: StoreId) -> Self { + let kind = match store_id.kind() { + StoreKind::Unspecified => "unspecified", + StoreKind::Recording => "recording", + StoreKind::Blueprint => "blueprint", + }; + Self { + kind: kind.to_owned(), + recording_id: store_id.recording_id, + application_id: store_id.application_id.unwrap_or_default().id, + } + } +} + +impl From for StoreId { + fn from(arg: StoreIdArg) -> Self { + let StoreIdArg { + kind, + recording_id, + application_id, + } = arg; + let kind = match kind.as_str() { + "blueprint" => StoreKind::Blueprint, + "recording" => StoreKind::Recording, + _ => StoreKind::Unspecified, + }; + Self { + kind: kind as i32, + recording_id, + application_id: (!application_id.is_empty()) + .then_some(ApplicationId { id: application_id }), + } + } +} + +#[derive(Debug, Default, Deserialize, schemars::JsonSchema)] +struct OpenUrlArgs { + /// The URL to open in the viewer: a recording/blueprint file URL, a `rerun://` dataset URI, a redap server/catalog URL, or an intra-recording link. + url: String, +} + +#[tool_router] +impl ViewerMcpServer { + fn new() -> Self { + Self { + conn: Arc::new(Mutex::new(None)), + ui_router: UiServer::router(), + tool_router: Self::tool_router(), + } + } + + /// The connected viewer's gRPC client, for the rerun-specific tools. Returns an owned clone + /// (tonic clients are cheap to clone and share the channel) so the RPC runs without holding + /// the lock, and errors with `not connected` when nothing is connected. + fn client(&self) -> ToolResult> { + self.conn + .lock() + .as_ref() + .map(|c| c.client.clone()) + .ok_or_else(|| "not connected — call `connect` first".to_owned()) + } + + /// Connect to a running Rerun viewer over gRPC. The other tools will be available once the connection is established. + /// `endpoint` defaults to `http://127.0.0.1:9876` (the viewer's default gRPC address). + /// Call `disconnect` to drop the connection. + #[tool] + async fn connect( + &self, + Parameters(args): Parameters, + ) -> ToolResult { + if self.conn.lock().is_some() { + return Err( + "already connected — call `disconnect` first to drop the current connection" + .to_owned(), + ); + } + let endpoint = args + .endpoint + .unwrap_or_else(|| DEFAULT_VIEWER_ENDPOINT.to_owned()); + let (bridge, client) = connect_grpc(&endpoint) + .await + .map_err(|err| format!("connect failed: {err}"))?; + let peer = bridge.peer_info.clone(); + *self.conn.lock() = Some(Arc::new(Connection { + ui: UiServer::new(bridge), + client, + })); + Ok(CallToolResult::structured(serde_json::json!({ + "ok": true, + "connected": endpoint, + "peer": peer, + }))) + } + + /// Disconnect from the viewer, dropping the gRPC-backed bridge. + /// The tools stop working until `connect` is called again. + #[tool] + async fn disconnect( + &self, + Parameters(_args): Parameters, + ) -> ToolResult { + if self.conn.lock().take().is_some() { + Ok(CallToolResult::structured( + serde_json::json!({ "ok": true }), + )) + } else { + Err("not connected".to_owned()) + } + } + + /// Report the current Rerun viewer state as JSON: the active recording, the current page URL, and every open recording (recording id, application id) with its timelines, their time ranges, and its current time cursor. + /// Use this to learn which recording/timeline to drive and what time values are valid before calling `set_time`. + /// Requires `connect`. + #[tool] + async fn viewer_state( + &self, + Parameters(_args): Parameters, + ) -> ToolResult { + let mut client = self.client()?; + let response = client + .get_viewer_state(GetViewerStateRequest {}) + .await + .map_err(|err| format!("viewer_state failed: {err}"))? + .into_inner(); + Ok(CallToolResult::success(vec![Content::text( + viewer_state_to_json(response).to_string(), + )])) + } + + /// Set the time cursor (timeline position) of a recording in the Rerun viewer. + /// `time` is a sequence index for sequence timelines or nanoseconds for temporal timelines (call `viewer_state` first for each timeline's type and valid range). + /// `store_id` and `timeline` default to the active recording / active timeline. + /// If `play` is unset or `false`, the recording will be paused. If `true`, the recording will play from the selected time. + /// Requires `connect`. + #[tool] + async fn set_time( + &self, + Parameters(args): Parameters, + ) -> ToolResult { + let mut client = self.client()?; + let response = client + .set_time_cursor(SetTimeCursorRequest { + store_id: args.store_id.map(StoreId::from), + timeline: args.timeline.map(|name| Timeline { name }), + time: Some(args.time.into()), + play: args.play, + }) + .await + .map_err(|err| format!("set_time failed: {err}"))? + .into_inner(); + Ok(CallToolResult::success(vec![Content::text( + set_time_to_json(response).to_string(), + )])) + } + + /// Open a URL in the Rerun viewer. + /// The URL can be a recording/blueprint file URL, a `rerun://` dataset URI, a redap server/catalog URL, or an intra-recording link. + /// Requires `connect`. + #[tool] + async fn open_url( + &self, + Parameters(args): Parameters, + ) -> ToolResult { + let mut client = self.client()?; + client + .open_url(OpenUrlRequest { + url: args.url.clone(), + }) + .await + .map_err(|err| format!("open_url failed: {err}"))?; + Ok(CallToolResult::structured(serde_json::json!({ + "ok": true, + "opened": args.url, + }))) + } +} + +/// Timeline name from a proto [`Timeline`]. +fn timeline_name(timeline: &Timeline) -> &str { + let Timeline { name } = timeline; + name +} + +/// Friendly name for a [`TimeType`] stored as its raw `i32` proto representation. +fn time_type_name(raw: i32) -> String { + TimeType::try_from(raw) + .unwrap_or(TimeType::Unspecified) + .to_string() +} + +/// Render a [`GetViewerStateResponse`] as the JSON object surfaced to the agent. +/// +/// Unfortunately our protos don't have a serde implementation so we manually convert it for now. +fn viewer_state_to_json(state: GetViewerStateResponse) -> serde_json::Value { + use serde_json::json; + let GetViewerStateResponse { + active_store_id, + url, + recordings, + } = state; + json!({ + "active_store_id": active_store_id.map(StoreIdArg::from), + "url": url, + "recordings": recordings + .into_iter() + .map(|ViewerRecording { + store_id, + timelines, + current_time, + }| { + json!({ + "store_id": store_id.map(StoreIdArg::from), + "timelines": timelines + .iter() + .map(|ViewerTimeline { + timeline, + time_type, + time_range, + }| { + let (min, max) = match time_range { + Some(TimeRange { start, end }) => (Some(*start), Some(*end)), + None => (None, None), + }; + json!({ + "timeline": timeline.as_ref().map(timeline_name), + "type": time_type_name(*time_type), + "min": min, + "max": max, + }) + }) + .collect::>(), + "current_time": current_time.as_ref().map(time_cursor_to_json), + }) + }) + .collect::>(), + }) +} + +/// Render a [`TimeCursor`] as the JSON object surfaced to the agent. +fn time_cursor_to_json(cursor: &TimeCursor) -> serde_json::Value { + let TimeCursor { + timeline, + time_type, + time, + } = cursor; + serde_json::json!({ + "timeline": timeline.as_ref().map(timeline_name), + "type": time_type.map(time_type_name), + "time": time.as_ref().map(|t| t.time), + }) +} + +/// Render a [`SetTimeCursorResponse`] as the JSON object surfaced to the agent (exhaustively +/// destructured, see [`viewer_state_to_json`]). +fn set_time_to_json(response: SetTimeCursorResponse) -> serde_json::Value { + let SetTimeCursorResponse { + store_id, + timeline, + time_type, + time, + } = response; + serde_json::json!({ + "store_id": store_id.map(StoreIdArg::from), + "timeline": timeline.as_ref().map(timeline_name), + "type": time_type_name(time_type), + "time": time.map(|t| t.time), + }) +} + +/// A recoverable tool failure (not connected, a bad endpoint, a bridge or gRPC error, …), carried +/// as a plain message string. +/// +/// It is *not* a JSON-RPC protocol error: a `String` already implements `rmcp`'s `IntoContents`, +/// so when a `#[tool]` method returns `Err(ToolError)`, `rmcp` renders it into a `CallToolResult` +/// with `isError: true` (per the MCP spec). `String` is also the [`Bridge`]'s error type, so the +/// handlers `?`-propagate bridge failures with no conversion. +type ToolError = String; + +/// The result of a tool handler — see [`ToolError`]. +type ToolResult = Result; + +/// Shape a recoverable failure as an `isError: true` tool result, for the `ServerHandler` +/// methods that return `Result` rather than a [`ToolResult`]. +fn text_error(msg: impl Into) -> CallToolResult { + CallToolResult::error(vec![Content::text(msg.into())]) +} + +/// Operating guidance sent to clients at initialize (the MCP `instructions` field). The per-tool +/// descriptions cover each command in isolation; this establishes the cross-cutting workflow — +/// `connect` first, then the observe→act→verify loop the egui tools share — that an agent +/// otherwise has to infer. +const INSTRUCTIONS: &str = r#"This MCP drives a live Rerun viewer: it reads the viewer's accessibility tree and synthesizes real input events. Work in an observe → act → verify loop. + +Getting oriented: +- Call `connect` first (it dials the viewer's gRPC server); every other tool errors until then. +- If no viewer is running, launch one. If the user tells you to work in the background, or no desktop is available, use `--headless`. +- Start most tasks with `query_tree` to discover widgets and their ids, and/or `screenshot` to see the rendered frame. + +Targeting widgets: +- Prefer locators — an `id` from `query_tree`, or `role`/`label_contains` — over a raw `pos`. Locators resolve to the widget's current position and survive layout changes; reach for `pos` only when nothing matches. + +Acting and verifying: +- After an action that changes the UI, confirm it landed: `query_tree` for the expected state, `screenshot` to look, or `wait_for` to poll until async or animated UI settles. +- Use `batch` to act and observe in one round trip (e.g. `click` then `screenshot`), avoiding an extra turn. +- To move through time, call `viewer_state` for the recordings/timelines and their valid ranges, then `set_time`. + +Conventions: +- Everything is in logical points, one shared coordinate frame: raw `pos`, `resize` dimensions, the `bounds` from `query_tree`/`get_node`, and a default (`pixels_per_point: 1.0`) `screenshot`. So a node's `bounds` center is exactly where to `click`, and a pixel in the screenshot is a logical point. There is no fixed screen size; use `resize` to set the viewport."#; + +impl ServerHandler for ViewerMcpServer { + fn get_info(&self) -> ServerInfo { + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + .with_server_info(Implementation::new("viewer-mcp", env!("CARGO_PKG_VERSION"))) + .with_instructions(INSTRUCTIONS) + } + + async fn list_tools( + &self, + _request: Option, + _context: RequestContext, + ) -> Result { + // The rerun-specific tools plus the reusable egui UI tools. The egui router is + // independent of the connection, so its tools stay listed even while disconnected. + let mut tools = self.tool_router.list_all(); + tools.extend(self.ui_router.list_all()); + Ok(ListToolsResult { + tools, + next_cursor: None, + meta: None, + }) + } + + async fn call_tool( + &self, + request: CallToolRequestParams, + context: RequestContext, + ) -> Result { + // Rerun-specific tools run on `self`; everything else is delegated to the attached UI + // server, which exists only while connected. + if self.tool_router.has_route(&request.name) { + return self + .tool_router + .call(ToolCallContext::new(self, request, context)) + .await; + } + let conn = self.conn.lock().clone(); + let Some(conn) = conn else { + return Ok(text_error("no app connected — call `connect` first")); + }; + conn.ui.dispatch(&self.ui_router, request, context).await + } +} + +/// Serve the MCP server over stdio until the client disconnects. +/// +/// Assumes the caller has already set up a Tokio runtime (this must run inside one) and logging. +/// Both the `rerun viewer-mcp` subcommand and the standalone `re-viewer-mcp` binary call this — each sets up +/// its own runtime and logging first. +pub async fn serve() -> anyhow::Result<()> { + let server = ViewerMcpServer::new(); + let running = server.serve(transport::stdio()).await?; + let _reason = running.waiting().await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fmt::Write as _; + + use rmcp::ServerHandler as _; + + use super::*; + + /// Snapshot of the documentation the llm will see when loading the mcp tools. + /// + /// It's useful to look at the snapshot output to check how much llm context the tool + /// definitions will use. + #[test] + fn agent_surface_snapshot() { + let server = ViewerMcpServer::new(); + + let mut surface = String::new(); + surface.push_str("# Server instructions\n\n"); + surface.push_str( + server + .get_info() + .instructions + .as_deref() + .unwrap_or("(none)"), + ); + surface.push_str("\n\n# Tools\n"); + + // The rerun-specific tools plus the reusable egui UI tools — the same set `list_tools` + // serves. + let mut tools = server.tool_router.list_all(); + tools.extend(server.ui_router.list_all()); + tools.sort_by(|a, b| a.name.cmp(&b.name)); + for tool in &tools { + write!(surface, "\n## {}\n\n", tool.name).unwrap(); + surface.push_str(&serde_json::to_string_pretty(tool).expect("serialize tool")); + surface.push('\n'); + } + + insta::assert_snapshot!("agent_surface", surface); + } +} diff --git a/crates/viewer/re_viewer_mcp/src/main.rs b/crates/viewer/re_viewer_mcp/src/main.rs new file mode 100644 index 000000000000..30ef8ef04895 --- /dev/null +++ b/crates/viewer/re_viewer_mcp/src/main.rs @@ -0,0 +1,11 @@ +//! `re-viewer-mcp` — the standalone binary for the [`re_viewer_mcp`] MCP server. +//! +//! Mostly useful for rerun developers. Usually it's recommended to use `rerun viewer-mcp` instead. + +fn main() -> anyhow::Result<()> { + re_log::setup_logging(); + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + rt.block_on(re_viewer_mcp::serve()) +} diff --git a/crates/viewer/re_viewer_mcp/src/snapshots/re_viewer_mcp__tests__agent_surface.snap b/crates/viewer/re_viewer_mcp/src/snapshots/re_viewer_mcp__tests__agent_surface.snap new file mode 100644 index 000000000000..13e5fcc36ac6 --- /dev/null +++ b/crates/viewer/re_viewer_mcp/src/snapshots/re_viewer_mcp__tests__agent_surface.snap @@ -0,0 +1,1144 @@ +--- +source: crates/viewer/re_viewer_mcp/src/lib.rs +expression: surface +--- +# Server instructions + +This MCP drives a live Rerun viewer: it reads the viewer's accessibility tree and synthesizes real input events. Work in an observe → act → verify loop. + +Getting oriented: +- Call `connect` first (it dials the viewer's gRPC server); every other tool errors until then. +- If no viewer is running, launch one. If the user tells you to work in the background, or no desktop is available, use `--headless`. +- Start most tasks with `query_tree` to discover widgets and their ids, and/or `screenshot` to see the rendered frame. + +Targeting widgets: +- Prefer locators — an `id` from `query_tree`, or `role`/`label_contains` — over a raw `pos`. Locators resolve to the widget's current position and survive layout changes; reach for `pos` only when nothing matches. + +Acting and verifying: +- After an action that changes the UI, confirm it landed: `query_tree` for the expected state, `screenshot` to look, or `wait_for` to poll until async or animated UI settles. +- Use `batch` to act and observe in one round trip (e.g. `click` then `screenshot`), avoiding an extra turn. +- To move through time, call `viewer_state` for the recordings/timelines and their valid ranges, then `set_time`. + +Conventions: +- Everything is in logical points, one shared coordinate frame: raw `pos`, `resize` dimensions, the `bounds` from `query_tree`/`get_node`, and a default (`pixels_per_point: 1.0`) `screenshot`. So a node's `bounds` center is exactly where to `click`, and a pixel in the screenshot is a logical point. There is no fixed screen size; use `resize` to set the viewport. + +# Tools + +## batch + +{ + "name": "batch", + "description": "Execute a sequence of app-driving tool calls in one round trip (the connection tools `attach`/`disconnect`/`status` are not available here).\nStops on the first error.\nResults are emitted in execution order, interleaved: each step contributes one JSON text item followed by any image items it produced (e.g. screenshots).\n`batch` cannot be nested.\nUse this to act and observe in one call, e.g. a `click` then a `query_tree` or `screenshot`.", + "inputSchema": { + "$defs": { + "BatchAction": { + "properties": { + "args": { + "additionalProperties": true, + "default": null, + "type": "object" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "actions": { + "items": { + "$ref": "#/$defs/BatchAction" + }, + "type": "array" + } + }, + "required": [ + "actions" + ], + "type": "object" + } +} + +## click + +{ + "name": "click", + "description": "Click the center of a node's bounding box, or a raw `pos` in logical points.\nSpecify either a locator (`id` from `query_tree`, `role`, or a text match — prefer `content_contains`, which matches `label` or `value`; `label_contains`/`value_contains` match just one field) or `pos: { x, y }`.\n`button` defaults to `primary` (accepts `primary`/`secondary`/`middle`/`extra1`/`extra2`, or aliases `left`/`right`).\n`count: 2` → double-click, `3` → triple.", + "inputSchema": { + "$defs": { + "PointerButtonArg": { + "description": "Which mouse button a `click`/`drag` uses. `left`/`right` are accepted as aliases for\n`primary`/`secondary`.", + "enum": [ + "primary", + "secondary", + "middle", + "extra1", + "extra2" + ], + "type": "string" + }, + "Pos2Lit": { + "properties": { + "x": { + "format": "float", + "type": "number" + }, + "y": { + "format": "float", + "type": "number" + } + }, + "required": [ + "x", + "y" + ], + "type": "object" + }, + "PressKeyModifiers": { + "properties": { + "alt": { + "default": false, + "type": "boolean" + }, + "command": { + "default": false, + "description": "= Cmd on Mac / Ctrl on Win+Linux.", + "type": "boolean" + }, + "ctrl": { + "default": false, + "type": "boolean" + }, + "mac_cmd": { + "default": false, + "type": "boolean" + }, + "shift": { + "default": false, + "type": "boolean" + } + }, + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "button": { + "$ref": "#/$defs/PointerButtonArg", + "default": "primary", + "description": "`primary`/`secondary`/`middle`/`extra1`/`extra2` (or aliases `left`/`right`). Defaults to `primary`." + }, + "content_contains": { + "description": "Case-insensitive substring match against *either* `label` or `value`. Prefer this when you\njust want \"the widget showing this text\" and don't care which field holds it — it's the\nmost robust choice across widget kinds.", + "type": [ + "string", + "null" + ] + }, + "count": { + "default": 1, + "description": "`2` → double-click; `3` → triple-click.", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "id": { + "default": null, + "description": "Node id from `query_tree`.", + "type": [ + "string", + "null" + ] + }, + "label_contains": { + "description": "Case-insensitive substring match against the node's `label` (its accessible name) only.\nNote that `Label`/monospace widgets carry their text in `value`, not `label` — for those,\nuse `content_contains` (or `value_contains`).", + "type": [ + "string", + "null" + ] + }, + "modifiers": { + "$ref": "#/$defs/PressKeyModifiers" + }, + "pos": { + "anyOf": [ + { + "$ref": "#/$defs/Pos2Lit" + }, + { + "type": "null" + } + ], + "description": "Raw position in logical points (use instead of locator fields)." + }, + "role": { + "description": "Role name, e.g. `Button`, `Label`, `TextInput` (case-insensitive).\nAn unrecognized role is rejected with an error that lists the roles present in the tree.", + "type": [ + "string", + "null" + ] + }, + "value_contains": { + "description": "Case-insensitive substring match against the node's `value` only (e.g. a text field's\ncontents, or a `Label`'s text).", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } +} + +## connect + +{ + "name": "connect", + "description": "Connect to a running Rerun viewer over gRPC. The other tools will be available once the connection is established.\n`endpoint` defaults to `http://127.0.0.1:9876` (the viewer's default gRPC address).\nCall `disconnect` to drop the connection.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "endpoint": { + "default": null, + "description": "gRPC endpoint of the running viewer's `ViewerControlService`.\nDefaults to `http://127.0.0.1:9876`.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } +} + +## disconnect + +{ + "name": "disconnect", + "description": "Disconnect from the viewer, dropping the gRPC-backed bridge.\nThe tools stop working until `connect` is called again.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" + } +} + +## drag + +{ + "name": "drag", + "description": "Primary-button drag from `start` to `end`.\nEach target accepts the same fields as `click`: locator (`id`/`content_contains`/`role`/`label_contains`/`value_contains`) or `pos: {x, y}`.\n`steps` controls how many intermediate pointer-move events are emitted between press and release.", + "inputSchema": { + "$defs": { + "Pos2Lit": { + "properties": { + "x": { + "format": "float", + "type": "number" + }, + "y": { + "format": "float", + "type": "number" + } + }, + "required": [ + "x", + "y" + ], + "type": "object" + }, + "PressKeyModifiers": { + "properties": { + "alt": { + "default": false, + "type": "boolean" + }, + "command": { + "default": false, + "description": "= Cmd on Mac / Ctrl on Win+Linux.", + "type": "boolean" + }, + "ctrl": { + "default": false, + "type": "boolean" + }, + "mac_cmd": { + "default": false, + "type": "boolean" + }, + "shift": { + "default": false, + "type": "boolean" + } + }, + "type": "object" + }, + "Target": { + "description": "The widget-matching constraints shared by `query_tree`'s filter and the action `Target`s.\n\nAn optional `role` plus up to one text predicate. All are case-insensitive and combined with\nlogical AND; an all-`None` `Query` matches every node.", + "properties": { + "content_contains": { + "description": "Case-insensitive substring match against *either* `label` or `value`. Prefer this when you\njust want \"the widget showing this text\" and don't care which field holds it — it's the\nmost robust choice across widget kinds.", + "type": [ + "string", + "null" + ] + }, + "id": { + "default": null, + "description": "Node id from `query_tree`.", + "type": [ + "string", + "null" + ] + }, + "label_contains": { + "description": "Case-insensitive substring match against the node's `label` (its accessible name) only.\nNote that `Label`/monospace widgets carry their text in `value`, not `label` — for those,\nuse `content_contains` (or `value_contains`).", + "type": [ + "string", + "null" + ] + }, + "pos": { + "anyOf": [ + { + "$ref": "#/$defs/Pos2Lit" + }, + { + "type": "null" + } + ], + "description": "Raw position in logical points (use instead of locator fields)." + }, + "role": { + "description": "Role name, e.g. `Button`, `Label`, `TextInput` (case-insensitive).\nAn unrecognized role is rejected with an error that lists the roles present in the tree.", + "type": [ + "string", + "null" + ] + }, + "value_contains": { + "description": "Case-insensitive substring match against the node's `value` only (e.g. a text field's\ncontents, or a `Label`'s text).", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "end": { + "$ref": "#/$defs/Target" + }, + "modifiers": { + "$ref": "#/$defs/PressKeyModifiers" + }, + "start": { + "$ref": "#/$defs/Target" + }, + "steps": { + "default": 8, + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "start", + "end" + ], + "type": "object" + } +} + +## get_node + +{ + "name": "get_node", + "description": "Return a single node by id (from `query_tree`).", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "outputSchema": { + "$defs": { + "NodeView": { + "properties": { + "bounds": { + "anyOf": [ + { + "$ref": "#/$defs/RectF" + }, + { + "type": "null" + } + ] + }, + "disabled": { + "type": "boolean" + }, + "focused": { + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "id": { + "description": "Node id, used with `click`, `type_text`, and `get_node`.", + "type": "string" + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "parent_id": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "value": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "role", + "focused", + "disabled", + "hidden" + ], + "type": "object" + }, + "RectF": { + "properties": { + "h": { + "format": "double", + "type": "number" + }, + "w": { + "format": "double", + "type": "number" + }, + "x": { + "format": "double", + "type": "number" + }, + "y": { + "format": "double", + "type": "number" + } + }, + "required": [ + "x", + "y", + "w", + "h" + ], + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "node": { + "anyOf": [ + { + "$ref": "#/$defs/NodeView" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + } +} + +## hover + +{ + "name": "hover", + "description": "Move the pointer over a node (or raw `pos`) without clicking.\nTooltips and hover popups only appear after a short delay — follow with `wait_for` (e.g. its `min_steps`) to let them settle before reading the tree or screenshotting.", + "inputSchema": { + "$defs": { + "Pos2Lit": { + "properties": { + "x": { + "format": "float", + "type": "number" + }, + "y": { + "format": "float", + "type": "number" + } + }, + "required": [ + "x", + "y" + ], + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "content_contains": { + "description": "Case-insensitive substring match against *either* `label` or `value`. Prefer this when you\njust want \"the widget showing this text\" and don't care which field holds it — it's the\nmost robust choice across widget kinds.", + "type": [ + "string", + "null" + ] + }, + "id": { + "default": null, + "description": "Node id from `query_tree`.", + "type": [ + "string", + "null" + ] + }, + "label_contains": { + "description": "Case-insensitive substring match against the node's `label` (its accessible name) only.\nNote that `Label`/monospace widgets carry their text in `value`, not `label` — for those,\nuse `content_contains` (or `value_contains`).", + "type": [ + "string", + "null" + ] + }, + "pos": { + "anyOf": [ + { + "$ref": "#/$defs/Pos2Lit" + }, + { + "type": "null" + } + ], + "description": "Raw position in logical points (use instead of locator fields)." + }, + "role": { + "description": "Role name, e.g. `Button`, `Label`, `TextInput` (case-insensitive).\nAn unrecognized role is rejected with an error that lists the roles present in the tree.", + "type": [ + "string", + "null" + ] + }, + "value_contains": { + "description": "Case-insensitive substring match against the node's `value` only (e.g. a text field's\ncontents, or a `Label`'s text).", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } +} + +## open_url + +{ + "name": "open_url", + "description": "Open a URL in the Rerun viewer.\nThe URL can be a recording/blueprint file URL, a `rerun://` dataset URI, a redap server/catalog URL, or an intra-recording link.\nRequires `connect`.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "url": { + "description": "The URL to open in the viewer: a recording/blueprint file URL, a `rerun://` dataset URI, a redap server/catalog URL, or an intra-recording link.", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" + } +} + +## press_key + +{ + "name": "press_key", + "description": "Send a key press (down + up) to the focused widget.\n`key` is an egui key name such as `Backspace`, `Delete`, `Enter`, `Tab`, `A`–`Z`, `ArrowLeft`, `ArrowRight`, `Home`, `End`, `Escape`.", + "inputSchema": { + "$defs": { + "PressKeyModifiers": { + "properties": { + "alt": { + "default": false, + "type": "boolean" + }, + "command": { + "default": false, + "description": "= Cmd on Mac / Ctrl on Win+Linux.", + "type": "boolean" + }, + "ctrl": { + "default": false, + "type": "boolean" + }, + "mac_cmd": { + "default": false, + "type": "boolean" + }, + "shift": { + "default": false, + "type": "boolean" + } + }, + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "key": { + "type": "string" + }, + "modifiers": { + "$ref": "#/$defs/PressKeyModifiers" + } + }, + "required": [ + "key" + ], + "type": "object" + } +} + +## query_tree + +{ + "name": "query_tree", + "description": "Walk the widget tree and return nodes matching the filter.\n`role`, if given, is a role name (e.g. `Button`, `Label`), matched case-insensitively; an unknown role errors with the roles present in the tree.\nUse the returned `id` with `click`, `type_text`, or `get_node`.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "content_contains": { + "description": "Case-insensitive substring match against *either* `label` or `value`. Prefer this when you\njust want \"the widget showing this text\" and don't care which field holds it — it's the\nmost robust choice across widget kinds.", + "type": [ + "string", + "null" + ] + }, + "label_contains": { + "description": "Case-insensitive substring match against the node's `label` (its accessible name) only.\nNote that `Label`/monospace widgets carry their text in `value`, not `label` — for those,\nuse `content_contains` (or `value_contains`).", + "type": [ + "string", + "null" + ] + }, + "limit": { + "default": 200, + "format": "uint", + "minimum": 0, + "type": "integer" + }, + "role": { + "description": "Role name, e.g. `Button`, `Label`, `TextInput` (case-insensitive).\nAn unrecognized role is rejected with an error that lists the roles present in the tree.", + "type": [ + "string", + "null" + ] + }, + "value_contains": { + "description": "Case-insensitive substring match against the node's `value` only (e.g. a text field's\ncontents, or a `Label`'s text).", + "type": [ + "string", + "null" + ] + }, + "visible_only": { + "default": true, + "type": "boolean" + } + }, + "type": "object" + }, + "outputSchema": { + "$defs": { + "NodeView": { + "properties": { + "bounds": { + "anyOf": [ + { + "$ref": "#/$defs/RectF" + }, + { + "type": "null" + } + ] + }, + "disabled": { + "type": "boolean" + }, + "focused": { + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "id": { + "description": "Node id, used with `click`, `type_text`, and `get_node`.", + "type": "string" + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "parent_id": { + "type": [ + "string", + "null" + ] + }, + "role": { + "type": "string" + }, + "value": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "role", + "focused", + "disabled", + "hidden" + ], + "type": "object" + }, + "RectF": { + "properties": { + "h": { + "format": "double", + "type": "number" + }, + "w": { + "format": "double", + "type": "number" + }, + "x": { + "format": "double", + "type": "number" + }, + "y": { + "format": "double", + "type": "number" + } + }, + "required": [ + "x", + "y", + "w", + "h" + ], + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "nodes": { + "items": { + "$ref": "#/$defs/NodeView" + }, + "type": "array" + } + }, + "required": [ + "nodes" + ], + "type": "object" + } +} + +## resize + +{ + "name": "resize", + "description": "Resize the app's viewport to the given logical-point dimensions.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "height": { + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "width": { + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "width", + "height" + ], + "type": "object" + } +} + +## screenshot + +{ + "name": "screenshot", + "description": "Capture the current frame as a PNG screenshot.\nDefaults to logical-point resolution (`pixels_per_point: 1.0`) so pixels align with `click`/`query_tree` coordinates; pass a higher `pixels_per_point` for detail, or `save_path` to also write it to disk.\nRequires the app window to be visible — a fully-occluded or minimized window can't render a frame to capture (notably on macOS), so the call times out; bring the window to the foreground first.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "pixels_per_point": { + "default": 1.0, + "description": "Output resolution in pixels per logical point.\nDefaults to `1.0`, which makes screenshot pixels line up 1:1 with the logical coordinates used by `click`/`query_tree`.\nHigher values give a sharper image, capped at the display's native scale (no upscaling).", + "format": "float", + "type": "number" + }, + "save_path": { + "default": null, + "description": "If set, also write the PNG to this path on the machine running the MCP server (in addition to returning it inline).", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } +} + +## scroll + +{ + "name": "scroll", + "description": "Send a mouse wheel scroll over a node (or raw `pos`).\n`delta` is in logical points: positive Y scrolls down (reveals content below); positive X scrolls right.", + "inputSchema": { + "$defs": { + "Pos2Lit": { + "properties": { + "x": { + "format": "float", + "type": "number" + }, + "y": { + "format": "float", + "type": "number" + } + }, + "required": [ + "x", + "y" + ], + "type": "object" + }, + "PressKeyModifiers": { + "properties": { + "alt": { + "default": false, + "type": "boolean" + }, + "command": { + "default": false, + "description": "= Cmd on Mac / Ctrl on Win+Linux.", + "type": "boolean" + }, + "ctrl": { + "default": false, + "type": "boolean" + }, + "mac_cmd": { + "default": false, + "type": "boolean" + }, + "shift": { + "default": false, + "type": "boolean" + } + }, + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "content_contains": { + "description": "Case-insensitive substring match against *either* `label` or `value`. Prefer this when you\njust want \"the widget showing this text\" and don't care which field holds it — it's the\nmost robust choice across widget kinds.", + "type": [ + "string", + "null" + ] + }, + "delta": { + "$ref": "#/$defs/Pos2Lit", + "description": "Logical points.\nPositive Y scrolls down (reveals content below); positive X scrolls right." + }, + "id": { + "default": null, + "description": "Node id from `query_tree`.", + "type": [ + "string", + "null" + ] + }, + "label_contains": { + "description": "Case-insensitive substring match against the node's `label` (its accessible name) only.\nNote that `Label`/monospace widgets carry their text in `value`, not `label` — for those,\nuse `content_contains` (or `value_contains`).", + "type": [ + "string", + "null" + ] + }, + "modifiers": { + "$ref": "#/$defs/PressKeyModifiers" + }, + "pos": { + "anyOf": [ + { + "$ref": "#/$defs/Pos2Lit" + }, + { + "type": "null" + } + ], + "description": "Raw position in logical points (use instead of locator fields)." + }, + "role": { + "description": "Role name, e.g. `Button`, `Label`, `TextInput` (case-insensitive).\nAn unrecognized role is rejected with an error that lists the roles present in the tree.", + "type": [ + "string", + "null" + ] + }, + "value_contains": { + "description": "Case-insensitive substring match against the node's `value` only (e.g. a text field's\ncontents, or a `Label`'s text).", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "delta" + ], + "type": "object" + } +} + +## set_time + +{ + "name": "set_time", + "description": "Set the time cursor (timeline position) of a recording in the Rerun viewer.\n`time` is a sequence index for sequence timelines or nanoseconds for temporal timelines (call `viewer_state` first for each timeline's type and valid range).\n`store_id` and `timeline` default to the active recording / active timeline.\nIf `play` is unset or `false`, the recording will be paused. If `true`, the recording will play from the selected time.\nRequires `connect`.", + "inputSchema": { + "$defs": { + "StoreIdArg": { + "description": "JSON representation of a proto [`StoreId`], used both as agent-facing output (see\n`viewer_state`) and as tool input identifying a recording to target.", + "properties": { + "application_id": { + "description": "The application id the recording belongs to.", + "type": "string" + }, + "kind": { + "description": "The kind of store: `\"recording\"`, `\"blueprint\"`, or `\"unspecified\"`.", + "type": "string" + }, + "recording_id": { + "description": "The recording id.", + "type": "string" + } + }, + "required": [ + "kind", + "recording_id", + "application_id" + ], + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "play": { + "default": false, + "description": "If true, start playing the recording from the new time cursor position instead of just\nmoving the cursor and staying paused. Defaults to false.", + "type": "boolean" + }, + "store_id": { + "anyOf": [ + { + "$ref": "#/$defs/StoreIdArg" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Recording to seek (see `viewer_state`).\nDefaults to the active recording." + }, + "time": { + "description": "Time to seek to: a sequence index for sequence timelines, or nanoseconds for temporal timelines (see each timeline's `type` and `min`/`max` in `viewer_state`).", + "format": "int64", + "type": "integer" + }, + "timeline": { + "default": null, + "description": "Timeline to seek on (see `viewer_state` for available timelines).\nDefaults to the active timeline.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "time" + ], + "type": "object" + } +} + +## type_text + +{ + "name": "type_text", + "description": "Type text into the currently focused widget.\nOptionally focus a node first (by `id`, `role`, or a text match — `content_contains`/`label_contains`/`value_contains`) — this uses an `AccessKit` focus request, not a click, so it won't move the cursor or clear an existing text selection.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "content_contains": { + "description": "Case-insensitive substring match against *either* `label` or `value`. Prefer this when you\njust want \"the widget showing this text\" and don't care which field holds it — it's the\nmost robust choice across widget kinds.", + "type": [ + "string", + "null" + ] + }, + "id": { + "default": null, + "description": "Optional focus target: node id from `query_tree` to focus before typing.\nOmit all locator fields to type into whatever is currently focused.", + "type": [ + "string", + "null" + ] + }, + "label_contains": { + "description": "Case-insensitive substring match against the node's `label` (its accessible name) only.\nNote that `Label`/monospace widgets carry their text in `value`, not `label` — for those,\nuse `content_contains` (or `value_contains`).", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role name, e.g. `Button`, `Label`, `TextInput` (case-insensitive).\nAn unrecognized role is rejected with an error that lists the roles present in the tree.", + "type": [ + "string", + "null" + ] + }, + "text": { + "type": "string" + }, + "value_contains": { + "description": "Case-insensitive substring match against the node's `value` only (e.g. a text field's\ncontents, or a `Label`'s text).", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "text" + ], + "type": "object" + } +} + +## viewer_state + +{ + "name": "viewer_state", + "description": "Report the current Rerun viewer state as JSON: the active recording, the current page URL, and every open recording (recording id, application id) with its timelines, their time ranges, and its current time cursor.\nUse this to learn which recording/timeline to drive and what time values are valid before calling `set_time`.\nRequires `connect`.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" + } +} + +## wait_for + +{ + "name": "wait_for", + "description": "Poll the widget tree until its conditions hold, or until `timeout_secs` elapses.\nWaits until at least `min_matches` visible nodes match the filter (when one is given) *and* at least `min_steps` frames have rendered since the call began.\nThe text filter is `role` and/or one of `content_contains` (matches `label` or `value` — prefer this; e.g. monospace/`Label` text lives in `value`), `label_contains`, `value_contains`.\nRequires a filter (`content_contains`/`role`/`label_contains`/`value_contains`) or a non-zero `min_steps`.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "content_contains": { + "description": "Case-insensitive substring match against *either* `label` or `value`. Prefer this when you\njust want \"the widget showing this text\" and don't care which field holds it — it's the\nmost robust choice across widget kinds.", + "type": [ + "string", + "null" + ] + }, + "label_contains": { + "description": "Case-insensitive substring match against the node's `label` (its accessible name) only.\nNote that `Label`/monospace widgets carry their text in `value`, not `label` — for those,\nuse `content_contains` (or `value_contains`).", + "type": [ + "string", + "null" + ] + }, + "min_matches": { + "default": 1, + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "min_steps": { + "default": 0, + "description": "Also wait until at least this many frames have rendered since the call began.\nUse it to let animations, tooltips, or other time/frame-driven UI settle (e.g. after a `hover`); `0` means don't wait for frames.", + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "role": { + "description": "Role name, e.g. `Button`, `Label`, `TextInput` (case-insensitive).\nAn unrecognized role is rejected with an error that lists the roles present in the tree.", + "type": [ + "string", + "null" + ] + }, + "timeout_secs": { + "default": 5, + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "value_contains": { + "description": "Case-insensitive substring match against the node's `value` only (e.g. a text field's\ncontents, or a `Label`'s text).", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } +} diff --git a/crates/viewer/re_viewport/Cargo.toml b/crates/viewer/re_viewport/Cargo.toml index 302e50f4003a..2b0087d59657 100644 --- a/crates/viewer/re_viewport/Cargo.toml +++ b/crates/viewer/re_viewport/Cargo.toml @@ -31,7 +31,6 @@ re_log.workspace = true re_renderer = { workspace = true, default-features = false, features = [ "import-gltf", "import-obj", - "serde", ] } re_tracing.workspace = true re_sdk_types.workspace = true diff --git a/crates/viewer/re_viewport/src/lib.rs b/crates/viewer/re_viewport/src/lib.rs index cfdf8a71fac3..527c9b189cce 100644 --- a/crates/viewer/re_viewport/src/lib.rs +++ b/crates/viewer/re_viewport/src/lib.rs @@ -6,8 +6,10 @@ mod system_execution; mod view_highlights; +mod view_loading_indicator; mod viewport_ui; +pub use view_loading_indicator::paint_view_loading_indicator; pub use viewport_ui::ViewportUi; pub mod external { diff --git a/crates/viewer/re_viewport/src/system_execution.rs b/crates/viewer/re_viewport/src/system_execution.rs index 98d56f4cc5fe..9add4ac6f90d 100644 --- a/crates/viewer/re_viewport/src/system_execution.rs +++ b/crates/viewer/re_viewport/src/system_execution.rs @@ -57,6 +57,17 @@ fn run_view_systems( .systems .par_iter() .map(|(name, vis_system)| { + // Skip execution when no entities in the view have instructions for this + // visualizer. + if !query + .active_visualizer_instructions_per_type + .contains_key(name) + { + let mut output = VisualizerExecutionOutput::default(); + output.affinity = vis_system.affinity(); + return (*name, Ok(output)); + } + re_tracing::profile_scope!("VisualizerSystem::execute", name.as_str()); let affinity = vis_system.affinity(); let result = vis_system @@ -104,7 +115,7 @@ pub fn new_view_query<'a>(ctx: &'a ViewerContext<'a>, view: &'a ViewBlueprint) - view_id: view.id, space_origin: &view.space_origin, active_visualizer_instructions_per_type, - timeline: current_query.timeline(), + timeline: *ctx.time_ctrl.timeline_name(), latest_at: current_query.at(), highlights, } diff --git a/crates/viewer/re_viewport/src/view_loading_indicator.rs b/crates/viewer/re_viewport/src/view_loading_indicator.rs new file mode 100644 index 000000000000..eb76ab178cea --- /dev/null +++ b/crates/viewer/re_viewport/src/view_loading_indicator.rs @@ -0,0 +1,33 @@ +/// Paint the standard loading indicator for views whose required data is still being fetched. +pub fn paint_view_loading_indicator( + ui: &mut egui::Ui, + id_salt: impl egui::AsIdSalt, + view_rect: egui::Rect, + any_missing_chunks: bool, + recording: &re_entity_db::EntityDb, +) { + let show_loading_indicator = (recording.is_downloading_manifest() || any_missing_chunks) + && recording.can_fetch_chunks_from_redap(); + + let loading_indicator_opacity = ui.ctx().animate_bool( + ui.id().with(("loading_indicator", id_salt)), + show_loading_indicator, + ); + + if 0.0 < loading_indicator_opacity { + let reason = if recording.is_downloading_manifest() { + "Downloading manifest from redap" + } else { + "Fetching chunks from redap" + }; + + re_ui::loading_indicator::paint_loading_indicator_inside( + ui, + egui::Align2::RIGHT_TOP, + view_rect, + loading_indicator_opacity, + None, + reason, + ); + } +} diff --git a/crates/viewer/re_viewport/src/viewport_ui.rs b/crates/viewer/re_viewport/src/viewport_ui.rs index a77f18b29741..0f794cabe327 100644 --- a/crates/viewer/re_viewport/src/viewport_ui.rs +++ b/crates/viewer/re_viewport/src/viewport_ui.rs @@ -24,10 +24,20 @@ use re_viewport_blueprint::{ use crate::system_execution::{execute_systems_for_all_views, execute_systems_for_view}; -/// Toggle the currently selected view to be maximized or not. +/// Shortcut for maximizing a view. +/// +/// Use [`is_toggle_maximize_view_pressed`] to check for this shortcut! // NOTE: we use CTRL and not COMMAND, because ⌘+M minimizes the whole window on macOS. const TOGGLE_MAXIMIZE_VIEW: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::M); +/// Checks if the keyboard shortcut for maximizing a view is pressed, +/// avoiding clashes with other similar shortcuts. +fn is_toggle_maximize_view_pressed(input: &mut egui::InputState) -> bool { + // Check if CTRL+SHIFT+M is pressed instead, which is used to open the dev panel. + // `consume_shortcut` intentionally ignores extra Shift and Alt modifiers. + !input.modifiers.shift && input.consume_shortcut(&TOGGLE_MAXIMIZE_VIEW) +} + /// Defines the UI and layout of the Viewport. pub struct ViewportUi { /// The blueprint that drives this viewport. @@ -123,13 +133,7 @@ impl ViewportUi { tree.ui(&mut egui_tiles_delegate, ui); let dragged_payload = egui::DragAndDrop::payload::(ui.ctx()); - let dragged_payload = dragged_payload.as_ref().and_then(|payload| { - if let DragAndDropPayload::Entities { entities } = payload.as_ref() { - Some(entities) - } else { - None - } - }); + let released = ui.input(|i| i.pointer.any_released()); let mut hover_rects = Vec::new(); let mut selection_rects = Vec::new(); @@ -157,9 +161,30 @@ impl ViewportUi { let should_display_drop_destination_frame = if pointer_in_rect && let Some(view_id) = contents.as_view_id() && let Some(view_blueprint) = self.blueprint.view(&view_id) - && let Some(dragged_payload) = dragged_payload + && let Some(payload) = dragged_payload.as_ref() { - Self::handle_drop_entities_to_view(ctx, view_blueprint, dragged_payload) + let feedback = match payload.as_ref() { + DragAndDropPayload::Entities { entities } => { + Self::handle_drop_entities_to_view( + ctx, + view_blueprint, + entities, + released, + ) + } + DragAndDropPayload::Components { component_paths } => view_blueprint + .class(ctx.view_class_registry()) + .handle_component_drop(ctx, view_id, component_paths, released), + DragAndDropPayload::Contents { .. } | DragAndDropPayload::Invalid => { + DragAndDropFeedback::Ignore + } + }; + + if feedback != DragAndDropFeedback::Ignore { + ctx.drag_and_drop_manager().set_feedback(feedback); + } + + feedback == DragAndDropFeedback::Accept } else { false }; @@ -250,8 +275,6 @@ impl ViewportUi { /// Handle the entities being dragged over a view. /// - /// Returns whether a "drop zone candidate" frame should be displayed to the user. - /// /// Design decisions: /// - We accept the drop only if at least one of the entities is visualizable and not already /// included. @@ -261,7 +284,8 @@ impl ViewportUi { ctx: &ViewerContext<'_>, view_blueprint: &ViewBlueprint, entities: &[EntityPath], - ) -> bool { + released: bool, + ) -> DragAndDropFeedback { let recording_engine = ctx.recording_engine(); let add_info = create_entity_add_info( ctx, @@ -279,19 +303,12 @@ impl ViewportUi { let any_is_visualizable = entities.iter().any(can_entity_be_added); - ctx.drag_and_drop_manager() - .set_feedback(if any_is_visualizable { - DragAndDropFeedback::Accept - } else { - DragAndDropFeedback::Reject - }); - if !any_is_visualizable { - return false; + return DragAndDropFeedback::Reject(None); } // drop incoming! - if ctx.egui_ctx().input(|i| i.pointer.any_released()) { + if released { egui::DragAndDrop::clear_payload(ctx.egui_ctx()); view_blueprint @@ -309,12 +326,9 @@ impl ViewportUi { ctx.command_sender() .send_system(SystemCommand::set_selection(Item::View(view_blueprint.id))); - - // drop is completed, no need for highlighting anymore - false - } else { - any_is_visualizable } + + DragAndDropFeedback::Accept } pub fn on_frame_start(&self, ctx: &ViewerContext<'_>) { @@ -434,26 +448,13 @@ impl<'a> egui_tiles::Behavior for TilesDelegate<'a, '_> { }); }); - { - let show_loading_indicator = missing_chunk_reporter.any_missing() - && self.ctx.recording().can_fetch_chunks_from_redap(); - - let loading_indicator_opacity = ui - .ctx() - .animate_bool(ui.id().with("loading_indicator"), show_loading_indicator); - - if 0.0 < loading_indicator_opacity { - let view_rect = response.response.rect; - re_ui::loading_indicator::paint_loading_indicator_inside( - ui, - egui::Align2::RIGHT_TOP, - view_rect, - loading_indicator_opacity, - None, - "Fetching chunks from redap", - ); - } - } + crate::paint_view_loading_indicator( + ui, + *view_id, + response.response.rect, + missing_chunk_reporter.any_missing(), + self.ctx.recording(), + ); response.response.widget_info(|| { let mut info = egui::WidgetInfo::new(egui::WidgetType::Panel); @@ -490,10 +491,14 @@ impl<'a> egui_tiles::Behavior for TilesDelegate<'a, '_> { .on_hover_cursor(egui::CursorIcon::Grab); let label = tab_widget.label.take(); + let active = tab_state.active; response.widget_info(|| { - let mut info = egui::WidgetInfo::new(egui::WidgetType::Label); - info.label = label.clone(); - info + egui::WidgetInfo::selected( + egui::WidgetType::SelectableLabel, + true, + active, + label.clone().unwrap_or_default(), + ) }); // Show a gap when dragged @@ -596,7 +601,7 @@ impl<'a> egui_tiles::Behavior for TilesDelegate<'a, '_> { .ui(ui); }) .clicked() - || ui.input_mut(|input| input.consume_shortcut(&TOGGLE_MAXIMIZE_VIEW)) + || ui.input_mut(is_toggle_maximize_view_pressed) { *self.maximized = None; MaximizeAnimationState::restore_view(ui.ctx(), view_id); @@ -605,8 +610,7 @@ impl<'a> egui_tiles::Behavior for TilesDelegate<'a, '_> { // Show maximize-button: let is_view_the_only_selected = self.ctx.selection().is_view_the_only_selected(&view_id); - let toggle = is_view_the_only_selected - && ui.input_mut(|input| input.consume_shortcut(&TOGGLE_MAXIMIZE_VIEW)); + let toggle = is_view_the_only_selected && ui.input_mut(is_toggle_maximize_view_pressed); if ui .small_icon_button(&re_ui::icons::MAXIMIZE, "Maximize view") .on_hover_ui(|ui| { @@ -766,10 +770,15 @@ impl TilesDelegate<'_, '_> { visualizer: Some(*instruction_id), }); for instruction_report in report.reports_for(instruction_id) { - grouped_reports - .entry(item.clone()) - .or_default() - .push(instruction_report.clone()); + // Only show a button for errors and warnings. + if instruction_report.severity + != re_viewer_context::VisualizerReportSeverity::Info + { + grouped_reports + .entry(item.clone()) + .or_default() + .push(instruction_report.clone()); + } } } } @@ -871,6 +880,7 @@ impl TilesDelegate<'_, '_> { re_viewer_context::VisualizerReportSeverity::Warning => { (&icons::WARNING, ui.tokens().alert_warning.icon) } + re_viewer_context::VisualizerReportSeverity::Info => continue, }; ui.horizontal_top(|ui| { diff --git a/crates/viewer/re_viewport_blueprint/benches/data_query.rs b/crates/viewer/re_viewport_blueprint/benches/data_query.rs index 3cc6dedc1084..2ea07edaf374 100644 --- a/crates/viewer/re_viewport_blueprint/benches/data_query.rs +++ b/crates/viewer/re_viewport_blueprint/benches/data_query.rs @@ -76,11 +76,13 @@ fn query_tree_many_entities(c: &mut Criterion) { let view_class_registry = ViewClassRegistry::default(); + let time_ctrl = re_viewer_context::TimeControl::default(); let ctx = ActiveStoreContext { blueprint: &blueprint, default_blueprint: None, recording: &recording, caches: &StoreCache::new(&view_class_registry, &recording), + time_ctrl: &time_ctrl, should_enable_heuristics: false, }; let blueprint_query = LatestAtQuery::latest(blueprint_timeline()); diff --git a/crates/viewer/re_viewport_blueprint/src/container.rs b/crates/viewer/re_viewport_blueprint/src/container.rs index 945a5f2b381f..b3f871fd217e 100644 --- a/crates/viewer/re_viewport_blueprint/src/container.rs +++ b/crates/viewer/re_viewport_blueprint/src/container.rs @@ -71,6 +71,7 @@ impl ContainerBlueprint { // ---- let results = blueprint_db.storage_engine().cache().latest_at( + re_chunk_store::ChunkTrackingMode::Report, query, &id.as_entity_path(), blueprint_archetypes::ContainerBlueprint::all_component_identifiers(), @@ -422,7 +423,7 @@ impl ContainerBlueprint { children.clone(), ); - for (share, id) in self.col_shares.iter().zip(children.iter()) { + for (share, id) in std::iter::zip(&self.col_shares, &children) { linear.shares.set_share(*id, *share); } @@ -434,7 +435,7 @@ impl ContainerBlueprint { children.clone(), ); - for (share, id) in self.row_shares.iter().zip(children.iter()) { + for (share, id) in std::iter::zip(&self.row_shares, &children) { linear.shares.set_share(*id, *share); } diff --git a/crates/viewer/re_viewport_blueprint/src/test_view_class.rs b/crates/viewer/re_viewport_blueprint/src/test_view_class.rs index 8978f6aee996..77dff20db0c2 100644 --- a/crates/viewer/re_viewport_blueprint/src/test_view_class.rs +++ b/crates/viewer/re_viewport_blueprint/src/test_view_class.rs @@ -15,7 +15,10 @@ pub struct TestVisualizer; impl IdentifiedViewSystem for TestVisualizer { fn identifier() -> ViewSystemIdentifier { - "TestVisualizer".into() + re_viewer_context::external::re_string_interner::intern_static!( + re_viewer_context::ViewSystemIdentifier, + "TestVisualizer" + ) } } diff --git a/crates/viewer/re_viewport_blueprint/src/ui/add_view_or_container_modal.rs b/crates/viewer/re_viewport_blueprint/src/ui/add_view_or_container_modal.rs index ef823f35250e..a79798440b7e 100644 --- a/crates/viewer/re_viewport_blueprint/src/ui/add_view_or_container_modal.rs +++ b/crates/viewer/re_viewport_blueprint/src/ui/add_view_or_container_modal.rs @@ -87,7 +87,7 @@ fn modal_ui( let resp = ui .add_enabled_ui(!disabled, |ui| { - row_ui(ui, icon_for_container_kind(&kind), title, subtitle) + row_ui(ui, icon_for_container_kind(&kind), title, subtitle, false) }) .inner .on_disabled_hover_text(format!( @@ -104,21 +104,36 @@ fn modal_ui( ui.full_span_separator(); - // view of any kind - for view in ctx + // Split views into stable / experimental groups. Experimental views go into a separate + // section at the bottom of the modal with a warning icon — they're fully functional, just + // marked clearly as in-flux. + let (stable_views, experimental_views): (Vec<_>, Vec<_>) = ctx .view_class_registry() .iter_registry() .map(|entry| ViewBlueprint::new_with_root_wildcard(entry.identifier)) - { + .partition(|view| !view.class(ctx.view_class_registry()).is_experimental()); + + let add_view_row = |ui: &mut egui::Ui, view: ViewBlueprint, is_experimental: bool| { let icon = view.class(ctx.view_class_registry()).icon(); let title = view.class(ctx.view_class_registry()).display_name(); let subtitle = format!("Create a new view to display {title} content."); - if row_ui(ui, icon, title, &subtitle).clicked() { + if row_ui(ui, icon, title, &subtitle, is_experimental).clicked() { viewport.add_views(std::iter::once(view), target_container, None); viewport.mark_user_interaction(ctx); ui.close(); } + }; + + for view in stable_views { + add_view_row(ui, view, false); + } + + if !experimental_views.is_empty() { + ui.full_span_separator(); + for view in experimental_views { + add_view_row(ui, view, true); + } } } @@ -142,7 +157,13 @@ fn modal_ui( /// │◀─────────────────────────────────────────────────▶│ /// clip_rect /// ``` -fn row_ui(ui: &mut egui::Ui, icon: &re_ui::Icon, title: &str, subtitle: &str) -> egui::Response { +fn row_ui( + ui: &mut egui::Ui, + icon: &re_ui::Icon, + title: &str, + subtitle: &str, + is_experimental: bool, +) -> egui::Response { //TODO(ab): use design tokens let row_space = 14.0; let row_height = 42.0; @@ -182,7 +203,22 @@ fn row_ui(ui: &mut egui::Ui, icon: &re_ui::Icon, title: &str, subtitle: &str) -> ui.vertical(|ui| { ui.strong(title); ui.add_space(-5.0); - ui.add(egui::Label::new(subtitle).wrap_mode(egui::TextWrapMode::Extend)); + if is_experimental { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 4.0; + ui.add( + re_ui::icons::WARNING + .as_image() + .tint(ui.tokens().alert_info.icon), + ); + ui.add( + egui::Label::new(format!("Experimental: {subtitle}").as_str()) + .wrap_mode(egui::TextWrapMode::Extend), + ); + }); + } else { + ui.add(egui::Label::new(subtitle).wrap_mode(egui::TextWrapMode::Extend)); + } }); let right_coord = ui.cursor().max.x; diff --git a/crates/viewer/re_viewport_blueprint/src/view.rs b/crates/viewer/re_viewport_blueprint/src/view.rs index 0d7484984b8d..fc8b8c3ef83c 100644 --- a/crates/viewer/re_viewport_blueprint/src/view.rs +++ b/crates/viewer/re_viewport_blueprint/src/view.rs @@ -141,6 +141,7 @@ impl ViewBlueprint { re_tracing::profile_function!(); let results = blueprint_db.storage_engine().cache().latest_at( + re_chunk_store::ChunkTrackingMode::Report, query, &id.as_entity_path(), blueprint_archetypes::ViewBlueprint::all_component_identifiers(), @@ -148,7 +149,8 @@ impl ViewBlueprint { // This is a required component. Note that when loading views we crawl the subtree and so // cleared empty views paths may exist transiently. The fact that they have an empty class_identifier - // is the marker that the have been cleared and not an error. + // is the marker that the have been cleared and not an error: an empty string is not a valid + // `ViewClassIdentifier`, so the `try_new` below turns it into a `None` and we skip the view. let class_identifier = results.component_mono::( blueprint_archetypes::ViewBlueprint::descriptor_class_identifier().component, )?; @@ -163,7 +165,7 @@ impl ViewBlueprint { ); let space_origin = space_origin.map_or_else(EntityPath::root, |origin| origin.0.into()); - let class_identifier: ViewClassIdentifier = class_identifier.0.as_str().into(); + let class_identifier = ViewClassIdentifier::try_new(class_identifier.0.as_str()).ok()?; let display_name = display_name.map(|v| v.0.to_string()); let space_env = EntityPathSubs::new_with_origin(&space_origin); @@ -251,11 +253,10 @@ impl ViewBlueprint { let bp_engine = blueprint.storage_engine(); if let Some(tree) = bp_engine.store().entity_tree().subtree(¤t_path) { tree.visit_children_recursively(|path| { - let sub_path: EntityPath = new_path - .iter() - .chain(&path[current_path.len()..]) - .cloned() - .collect(); + let sub_path: EntityPath = + std::iter::chain(new_path.iter(), &path[current_path.len()..]) + .cloned() + .collect(); let chunk = Chunk::builder(sub_path) .with_row( @@ -263,7 +264,7 @@ impl ViewBlueprint { store_context.blueprint_timepoint_for_writes(), blueprint_engine .store() - .all_components_on_timeline(&query.timeline(), path) + .all_components_on_timeline(query.timeline().as_ref(), path) .into_iter() .flat_map(|v| v.into_iter()) // It's important that we don't include the ViewBlueprint's components @@ -276,7 +277,7 @@ impl ViewBlueprint { .filter_map(|component| { let array = blueprint_engine .cache() - .latest_at(query, path, [component]) + .latest_at(re_chunk_store::ChunkTrackingMode::Report, query, path, [component]) .component_batch_raw(component)?; let descriptor = blueprint_engine.schema().entity_component_descriptor(path, component)?; Some((descriptor, array)) @@ -397,11 +398,9 @@ impl ViewBlueprint { // * can't be specified in the chunk store // Here, we query the visual time range that serves as the default for all entities in this space. - let property = ViewProperty::from_archetype::( - blueprint, - blueprint_query, - self.id, - ); + let property = ViewProperty::from_archetype_with_db::< + blueprint_archetypes::VisibleTimeRanges, + >(blueprint, blueprint_query, self.id); let ranges = property.component_array::( blueprint_archetypes::VisibleTimeRanges::descriptor_ranges().component, ); diff --git a/crates/viewer/re_viewport_blueprint/src/view_contents.rs b/crates/viewer/re_viewport_blueprint/src/view_contents.rs index a8a6b3bcae44..b05e6c976656 100644 --- a/crates/viewer/re_viewport_blueprint/src/view_contents.rs +++ b/crates/viewer/re_viewport_blueprint/src/view_contents.rs @@ -4,6 +4,7 @@ use arrow::array::AsArray as _; use nohash_hasher::{IntMap, IntSet}; use re_entity_db::external::re_chunk_store::LatestAtQuery; use re_entity_db::{EntityDb, EntityTree}; +use re_log::ResultExt as _; use re_log_types::path::RuleEffect; use re_log_types::{ EntityPath, EntityPathFilter, EntityPathHash, EntityPathSubs, ResolvedEntityPathFilter, @@ -119,7 +120,7 @@ impl ViewContents { view_class_identifier: ViewClassIdentifier, subst_env: &EntityPathSubs, ) -> Self { - let property = ViewProperty::from_archetype::( + let property = ViewProperty::from_archetype_with_db::( blueprint_db, query, view_id, @@ -238,9 +239,8 @@ impl ViewContents { /// Save the entity path filter. fn save_entity_path_filter_to_blueprint(&self, ctx: &ViewerContext<'_>) { - ViewProperty::from_archetype::( - ctx.blueprint_db(), - ctx.blueprint_query, + ViewProperty::from_archetype_for_view::( + ctx, self.view_id, ) .save_blueprint_component( @@ -260,7 +260,6 @@ impl ViewContents { /// Note that this result will not have any resolved overrides. Those can /// be added by separately calling `DataQueryPropertyResolver::update_overrides` on /// the result. - #[expect(clippy::too_many_arguments)] pub fn build_data_result_tree( &self, ctx: &re_viewer_context::ActiveStoreContext<'_>, @@ -469,7 +468,6 @@ impl DataQueryPropertyResolver<'_> { /// /// This will accumulate the recursive properties at each step down the tree, and then merge /// with individual overrides on each step. - #[expect(clippy::too_many_arguments)] #[expect(clippy::fn_params_excessive_bools)] // TODO(emilk): remove bool parameters fn update_overrides_recursive( &self, @@ -525,7 +523,10 @@ impl DataQueryPropertyResolver<'_> { .component_mono_quiet::( type_component, ) - .map_or_else(|| "No type specified".into(), |vt| vt.as_str().into()); + .and_then(|vt| { + ViewSystemIdentifier::try_new(vt.as_str()).ok_or_log_error_once() + }) + .unwrap_or_else(|| "No type specified".into()); VisualizerInstruction::new( instruction_id, @@ -637,7 +638,7 @@ impl DataQueryPropertyResolver<'_> { { if let Some(component_data) = blueprint_engine .cache() - .latest_at(blueprint_query, override_base_path, [component]) + .latest_at(re_chunk_store::ChunkTrackingMode::Report, blueprint_query, override_base_path, [component]) .component_batch_raw(component) && // We regard empty overrides as non-existent. This is important because there is no other way of doing component-clears. @@ -690,7 +691,7 @@ impl DataQueryPropertyResolver<'_> { { if let Some(component_data) = blueprint_engine .cache() - .latest_at(blueprint_query, &instruction.override_path, [component]) + .latest_at(re_chunk_store::ChunkTrackingMode::Report, blueprint_query, &instruction.override_path, [component]) .component_batch_raw(component) && // We regard empty overrides as non-existent. This is important because there is no other way of doing component-clears. !component_data.is_empty() @@ -712,13 +713,14 @@ impl DataQueryPropertyResolver<'_> { { instruction .component_mappings - .extend(mappings_from_store.into_iter().map(|mapping| { - ( - mapping.target.as_str().into(), + .extend(mappings_from_store.into_iter().filter_map(|mapping| { + let target = mapping.0.target_component().ok_or_log_error_once()?; + let source = re_viewer_context::VisualizerComponentSource::from_blueprint_mapping( &mapping.0, - ), - ) + ) + .ok_or_log_error()?; + Some((target, source)) })); } } @@ -850,11 +852,13 @@ mod tests { ) }); + let time_ctrl = re_viewer_context::TimeControl::default(); let ctx = ActiveStoreContext { blueprint: &blueprint, default_blueprint: None, recording: &recording, caches: &StoreCache::new(&view_class_registry, &recording), + time_ctrl: &time_ctrl, should_enable_heuristics: false, }; diff --git a/crates/viewer/re_viewport_blueprint/src/view_properties.rs b/crates/viewer/re_viewport_blueprint/src/view_properties.rs index a32200717380..f1af5877bfea 100644 --- a/crates/viewer/re_viewport_blueprint/src/view_properties.rs +++ b/crates/viewer/re_viewport_blueprint/src/view_properties.rs @@ -10,7 +10,7 @@ use re_sdk_types::{ }; use re_viewer_context::{ BlueprintContext, ComponentFallbackError, QueryContext, ViewContext, ViewId, - ViewSystemExecutionError, ViewerContext, + ViewSystemExecutionError, }; #[derive(thiserror::Error, Debug)] @@ -52,8 +52,21 @@ pub struct ViewProperty { } impl ViewProperty { + /// Query a specific view property for a view context. + pub fn from_archetype(ctx: &ViewContext<'_>) -> Self { + Self::from_archetype_for_view::(ctx.viewer_ctx, ctx.view_id) + } + /// Query a specific view property for a given view. - pub fn from_archetype( + pub fn from_archetype_for_view( + ctx: &impl BlueprintContext, + view_id: ViewId, + ) -> Self { + Self::from_archetype_with_db::(ctx.current_blueprint(), ctx.blueprint_query(), view_id) + } + + /// Query a specific view property from a blueprint database and query. + pub(crate) fn from_archetype_with_db( blueprint_db: &EntityDb, blueprint_query: &LatestAtQuery, view_id: ViewId, @@ -163,7 +176,7 @@ impl ViewProperty { } /// Returns `None` for empty arrays, which are written by - /// [`ViewerContext::clear_blueprint_component`] to represent an unset value. + /// [`BlueprintContext::clear_blueprint_component`] to represent an unset value. pub fn component_raw(&self, component: ComponentIdentifier) -> Option { self.query_results .get(component)? @@ -217,7 +230,7 @@ impl ViewProperty { /// Clears a blueprint component. pub fn clear_blueprint_component( &self, - ctx: &ViewerContext<'_>, + ctx: &impl BlueprintContext, component_descr: ComponentDescriptor, ) { ctx.clear_blueprint_component(self.blueprint_store_path.clone(), component_descr); @@ -226,14 +239,14 @@ impl ViewProperty { /// Resets a blueprint component to the value it had in the default blueprint. pub fn reset_blueprint_component( &self, - ctx: &ViewerContext<'_>, + ctx: &impl BlueprintContext, component_descr: ComponentDescriptor, ) { ctx.reset_blueprint_component(self.blueprint_store_path.clone(), component_descr); } /// Resets all components to the values they had in the default blueprint. - pub fn reset_all_components(&self, ctx: &ViewerContext<'_>) { + pub fn reset_all_components(&self, ctx: &impl BlueprintContext) { // Don't use `self.query_results.components.keys()` since it may already have some components missing since they didn't show up in the query. for component_descr in self.component_descrs.iter().cloned() { ctx.reset_blueprint_component(self.blueprint_store_path.clone(), component_descr); @@ -241,8 +254,8 @@ impl ViewProperty { } /// Resets all components to empty values, i.e. the fallback. - pub fn reset_all_components_to_empty(&self, ctx: &ViewerContext<'_>) { - let blueprint_storage_engine = ctx.blueprint_db().storage_engine(); + pub fn reset_all_components_to_empty(&self, ctx: &impl BlueprintContext) { + let blueprint_storage_engine = ctx.current_blueprint().storage_engine(); let blueprint_store = blueprint_storage_engine.store(); for component in self.query_results.components.keys().copied() { if let Some(component_descr) = blueprint_store diff --git a/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs b/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs index 476830668c4f..3a324e361cf5 100644 --- a/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs +++ b/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs @@ -84,6 +84,7 @@ impl ViewportBlueprint { let blueprint_engine = blueprint_db.storage_engine(); let results = blueprint_engine.cache().latest_at( + re_chunk_store::ChunkTrackingMode::Report, query, &VIEWPORT_PATH.into(), blueprint_archetypes::ViewportBlueprint::all_component_identifiers(), @@ -272,7 +273,7 @@ impl ViewportBlueprint { self.view(&data_result.view_id).is_some_and(|view| { let entity_path = &data_result.instance_path.entity_path; - // TODO(#5742): including any path that is—or descend from—the space origin is + // TODO(#5742): including any path that is — or descend from — the space origin is // necessary because such items may actually be displayed in the blueprint tree. entity_path == &view.space_origin || entity_path.is_descendant_of(&view.space_origin) @@ -356,11 +357,9 @@ impl ViewportBlueprint { // If now the user edits the view at `/**` to be `/points/**`, that does *not* // mean we should suddenly add `/camera/**` to the viewport. if !recommended_views.is_empty() { - let new_viewer_recommendation_hashes: Vec = self - .past_viewer_recommendations - .iter() - .cloned() - .chain( + let new_viewer_recommendation_hashes: Vec = + std::iter::chain( + self.past_viewer_recommendations.iter().cloned(), recommended_views .iter() .map(|recommendation| recommendation.recommendation_hash(class_id)), @@ -455,14 +454,12 @@ impl ViewportBlueprint { /// Returns an iterator over all the contents (views and containers) in the viewport. pub fn contents_iter(&self) -> impl Iterator + '_ { - self.views - .keys() - .map(|view_id| Contents::View(*view_id)) - .chain( - self.containers - .keys() - .map(|container_id| Contents::Container(*container_id)), - ) + std::iter::chain( + self.views.keys().map(|view_id| Contents::View(*view_id)), + self.containers + .keys() + .map(|container_id| Contents::Container(*container_id)), + ) } /// Walk the entire [`Contents`] tree, starting from the root container. diff --git a/crates/viewer/re_web_viewer_server/Cargo.toml b/crates/viewer/re_web_viewer_server/Cargo.toml index b76494efd28a..40bf29dc68c9 100644 --- a/crates/viewer/re_web_viewer_server/Cargo.toml +++ b/crates/viewer/re_web_viewer_server/Cargo.toml @@ -18,7 +18,7 @@ include = [ "Cargo.toml", # Matches the files in src/lib.rs: - "web_viewer/favicon.svg", + "web_viewer/apple-touch-icon.png", "web_viewer/favicon.ico", "web_viewer/index.html", "web_viewer/re_viewer_bg.wasm", diff --git a/crates/viewer/re_web_viewer_server/src/lib.rs b/crates/viewer/re_web_viewer_server/src/lib.rs index af612339c109..267d6b43c5be 100644 --- a/crates/viewer/re_web_viewer_server/src/lib.rs +++ b/crates/viewer/re_web_viewer_server/src/lib.rs @@ -27,7 +27,12 @@ mod data { #[inline] pub fn favicon() -> &'static [u8] { - include_bytes!("../web_viewer/favicon.svg") + include_bytes!("../web_viewer/favicon.ico") + } + + #[inline] + pub fn apple_touch_icon() -> &'static [u8] { + include_bytes!("../web_viewer/apple-touch-icon.png") } #[inline] @@ -192,6 +197,10 @@ impl WebViewerServer { format!("http://{local_addr}") } + pub fn bound_url(&self) -> String { + format!("http://{}", self.inner.server.server_addr()) + } + /// Blocks execution as long as the server is running. /// /// There's no way of shutting the server down from the outside right now. @@ -272,8 +281,8 @@ impl WebViewerServerInner { let (mime, bytes): (&str, &[u8]) = match path { "/" | "/index.html" => ("text/html", data::index_html()), - "/favicon.svg" => ("image/svg+xml", data::favicon()), "/favicon.ico" => ("image/x-icon", data::favicon()), + "/apple-touch-icon.png" => ("image/png", data::apple_touch_icon()), "/sw.js" => ("text/javascript", data::sw_js()), "/re_viewer.js" => ("text/javascript", data::viewer_js()), "/re_viewer_bg.wasm" => { @@ -310,3 +319,17 @@ impl WebViewerServerInner { request.respond(response) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unspecified_bind_address_has_distinct_bound_and_connect_urls() { + let server = WebViewerServer::new("0.0.0.0", WebViewerServerPort::AUTO).unwrap(); + let port = server.inner.server.server_addr().to_ip().unwrap().port(); + + assert_eq!(server.bound_url(), format!("http://0.0.0.0:{port}")); + assert_eq!(server.server_url(), format!("http://127.0.0.1:{port}")); + } +} diff --git a/crates/viewer/re_web_viewer_server/src/trailing_data.rs b/crates/viewer/re_web_viewer_server/src/trailing_data.rs index aee01c69bd55..2937529395fa 100644 --- a/crates/viewer/re_web_viewer_server/src/trailing_data.rs +++ b/crates/viewer/re_web_viewer_server/src/trailing_data.rs @@ -67,6 +67,7 @@ enum TrailingDataError { struct WebViewerData { index_html: Vec, favicon: Vec, + apple_touch_icon: Vec, sw_js: Vec, viewer_js: Vec, viewer_wasm: Vec, @@ -153,7 +154,8 @@ fn load_from_trailing_zip() -> Result { // Extract each file let index_html = extract_file(&mut zip, "index.html")?; - let favicon = extract_file(&mut zip, "favicon.svg")?; + let favicon = extract_file(&mut zip, "favicon.ico")?; + let apple_touch_icon = extract_file(&mut zip, "apple-touch-icon.png")?; let sw_js = extract_file(&mut zip, "sw.js")?; let viewer_js = extract_file(&mut zip, "re_viewer.js")?; let viewer_wasm = extract_file(&mut zip, "re_viewer_bg.wasm")?; @@ -163,6 +165,7 @@ fn load_from_trailing_zip() -> Result { Ok(WebViewerData { index_html, favicon, + apple_touch_icon, sw_js, viewer_js, viewer_wasm, @@ -211,6 +214,11 @@ pub fn favicon() -> &'static [u8] { &get_data().favicon } +#[inline] +pub fn apple_touch_icon() -> &'static [u8] { + &get_data().apple_touch_icon +} + #[inline] pub fn sw_js() -> &'static [u8] { &get_data().sw_js diff --git a/crates/viewer/re_web_viewer_server/web_viewer/apple-touch-icon.png b/crates/viewer/re_web_viewer_server/web_viewer/apple-touch-icon.png new file mode 100644 index 000000000000..fb24078b2ce0 Binary files /dev/null and b/crates/viewer/re_web_viewer_server/web_viewer/apple-touch-icon.png differ diff --git a/crates/viewer/re_web_viewer_server/web_viewer/favicon.ico b/crates/viewer/re_web_viewer_server/web_viewer/favicon.ico index 69db5a11687d..f80790a17383 100644 Binary files a/crates/viewer/re_web_viewer_server/web_viewer/favicon.ico and b/crates/viewer/re_web_viewer_server/web_viewer/favicon.ico differ diff --git a/crates/viewer/re_web_viewer_server/web_viewer/favicon.svg b/crates/viewer/re_web_viewer_server/web_viewer/favicon.svg deleted file mode 100644 index 4b48dd8c8c69..000000000000 --- a/crates/viewer/re_web_viewer_server/web_viewer/favicon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/crates/viewer/re_web_viewer_server/web_viewer/index.html b/crates/viewer/re_web_viewer_server/web_viewer/index.html index 7d41864b71a2..88279b517ddf 100644 --- a/crates/viewer/re_web_viewer_server/web_viewer/index.html +++ b/crates/viewer/re_web_viewer_server/web_viewer/index.html @@ -10,10 +10,8 @@ /> - - - - + + @@ -194,6 +192,43 @@ canvas_elem.classList.add("hidden"); } + // Feature-detect Wasm SIMD (`simd128`). The viewer .wasm is compiled with + // `-Ctarget-feature=+simd128`, so browsers without SIMD support will fail to + // instantiate the module with a cryptic error. We detect up-front and surface + // a friendly message instead. + // + // The probe is a tiny module that uses the `v128.any_true` instruction. + // Supported in: Chrome 91+, Firefox 89+, Safari 16.4+. + function has_wasm_simd() { + try { + return WebAssembly.validate( + new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, + 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11, + ]), + ); + } catch (e) { + return false; + } + } + + function show_unsupported_browser() { + show_center_html(` +

+ Your browser is too old to run the Rerun Viewer. +

+

+ The Viewer requires WebAssembly SIMD support, available in: +

+

+ Chrome 91+, Firefox 89+, Safari 16.4+, or any modern Chromium-based browser. +

+

+ Please update your browser and try again. +

+ `); + } + // On mobile platforms show a warning, but provide a link to try anyways if ( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( @@ -212,8 +247,14 @@ .querySelector("#try_anyways") .addEventListener("click", function (event) { event.preventDefault(); + if (!has_wasm_simd()) { + show_unsupported_browser(); + return; + } load_wasm(); }); + } else if (!has_wasm_simd()) { + show_unsupported_browser(); } else { load_wasm(); } @@ -339,6 +380,7 @@ enable_history: Option, notebook: Option, persist: Option, + theme: Option, */ const options = { url: query.getAll("url"), @@ -352,6 +394,7 @@ notebook: get_query_bool(query, "notebook", false), persist: get_query_bool(query, "persist", true), fallback_token: query.get("token"), + theme: query.get("theme"), login: { signed_in_url: location.origin + "/signed-in", signed_out_url: location.origin + "/signed-out", diff --git a/deny.toml b/deny.toml index 3af3cd7871fd..132c4efa911c 100644 --- a/deny.toml +++ b/deny.toml @@ -35,6 +35,10 @@ ignore = [ { id = "RUSTSEC-2024-0436", reason = "paste is unmaintained — https://github.com/dtolnay/paste" }, { id = "RUSTSEC-2024-0014", reason = "generational-arena is unmaintained" }, { id = "RUSTSEC-2025-0141", reason = "bincode is unmaintained — https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md" }, + { id = "RUSTSEC-2026-0176", reason = "RR-4865 can't update to pyo3 0.29 until numpy crate is updated" }, + { id = "RUSTSEC-2026-0177", reason = "RR-4865 can't update to pyo3 0.29 until numpy crate is updated" }, + { id = "RUSTSEC-2026-0194", reason = "quick-xml is pulled in by upstream crates that do not yet allow 0.41" }, + { id = "RUSTSEC-2026-0195", reason = "quick-xml is pulled in by upstream crates that do not yet allow 0.41" }, ] @@ -58,23 +62,20 @@ deny = [ #{ name = "insta", reason = "Only allowed as a dev-dependency for testing." }, ] skip = [ + { name = "accesskit_consumer" }, # `egui_mcp` uses a newer version than the viewer's `eframe`/`accesskit_winit`. { name = "base64" }, # Too popular { name = "bitflags" }, # core-graphics & png uses an older version. { name = "block2" }, # Old version via rfd { name = "console" }, # smallish { name = "core-foundation" }, # Currently, e.g. `webbrowser` and `winit` use different versions. - { name = "downcast-rs" }, # eco-system is transitioning from 1 to 2 - { name = "float-cmp" }, - { name = "gimli" }, # wasm-bindgen + { name = "getrandom" }, # ring uses 0.2, ahash/arrow/datafusion use 0.3 { name = "hashbrown" }, # Old version used by polar-rs { name = "itertools" }, # Too popular { name = "kurbo" }, # Different versions in egui_extras/resvg and epaint/vello_cpu { name = "libloading" }, # datafusion-ffi needs an older version than wgpu { name = "linux-raw-sys" }, # because of two rustix versions. - { name = "lru" }, # because of lance { name = "lz4_flex" }, # the Arrow ecosystem is a bit behind, but it's fine, this is a very tiny, flat dependency { name = "memmap2" }, # because of walkers - { name = "nom" }, # lance { name = "objc2-app-kit" }, # `accesskit_macos` uses a different version than `arboard` { name = "objc2-foundation" }, # `accesskit_macos` uses a different version than `arboard` { name = "objc2" }, # `accesskit_macos` uses a different version than `arboard` @@ -83,20 +84,23 @@ skip = [ { name = "redox_syscall" }, # Plenty of versions in the wild { name = "rustc-hash" }, # numpy with compatible pyo3 requires different version than wgpu { name = "rustix" }, # tantivy uses an old version. + { name = "socket2" }, # `egui_mcp` pulls `tokio` (→ socket2 0.6) into the non-dev graph; the viewer otherwise uses 0.5 via `hyper-util`. { name = "unicode-width" }, # walkers depends on 0.1 via http-cache-request / cacache { name = "ureq" }, # duplicated by `protoc-prebuilt`, which uses an outdated version. + { name = "webpki-roots" }, # need to update ureq { name = "zip" }, # duplicated by `protoc-prebuilt`, which uses an ancient version. ] skip-tree = [ + # PREFER `skip` (above): `skip-tree` exempts the crate AND its whole transitive subtree from the + # duplicate check, so it can silently hide unrelated new duplicates. Only use `skip-tree` when an + # outdated root drags in a self-contained cluster of old deps that would be noise to list one-by-one. + { name = "darling" }, # enumset_derive uses 0.21, serde_with_macros (via lance-namespace) uses 0.23 { name = "petgraph" }, # b/c lance { name = "phf" }, # b/c mime_guess2 - { name = "rand_distr" }, # from ndarray-rand { name = "thiserror" }, # Waiting for eco-system to switch to 2.0 { name = "toml" }, # b/c cargo_metadata - { name = "webpki-roots" }, # need to update ureq { name = "windows-sys" }, # Impossible { name = "zerocopy" }, # Need to update re_rav1d - # NOTE: `skip-tree` skips the whole tree! Consider adding to just `skip` instead (scroll up!) ] [licenses] diff --git a/dimos/Cargo.toml b/dimos/Cargo.toml index 2a085820c9a1..69271719a96b 100644 --- a/dimos/Cargo.toml +++ b/dimos/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "dimos-viewer" -version = "0.32.0-alpha.2" +version = "0.35.0-alpha.1" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false description = "DimOS Interactive Viewer — custom Rerun viewer with LCM click-to-navigate" diff --git a/dimos/pyproject.toml b/dimos/pyproject.toml index 6c682bdb2a89..db67d71398dc 100644 --- a/dimos/pyproject.toml +++ b/dimos/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "dimos-viewer" -version = "0.32.0a2" +version = "0.35.0a1" description = "Interactive Rerun viewer for DimOS with click-to-navigate support" readme = "README.md" requires-python = ">=3.10" diff --git a/docs/content/_redirects.yaml b/docs/content/_redirects.yaml index dc5a2b41fc24..6eac06cb63f4 100644 --- a/docs/content/_redirects.yaml +++ b/docs/content/_redirects.yaml @@ -18,19 +18,28 @@ # defined in this file. Remove one or the other to resolve conflicts. # Overview section -overview/installing-viewer: overview/installing-rerun +overview/installing-viewer: getting-started/install-rerun +overview/installing-rerun: getting-started/install-rerun +overview/installing-rerun/python: getting-started/install-rerun/python +overview/installing-rerun/cpp: getting-started/install-rerun/cpp +overview/installing-rerun/rust: getting-started/install-rerun/rust +overview/installing-rerun/viewer: getting-started/install-rerun/viewer +overview/installing-rerun/troubleshooting: getting-started/install-rerun/troubleshooting # Getting Started section getting-started/what-is-rerun: overview/what-is-rerun getting-started/navigating-the-viewer: getting-started/configure-the-viewer/navigating-the-viewer -getting-started/troubleshooting: overview/installing-rerun/troubleshooting -getting-started/installing-viewer: overview/installing-rerun/viewer +getting-started/troubleshooting: getting-started/install-rerun/troubleshooting +getting-started/installing-viewer: getting-started/install-rerun/viewer getting-started/data-out/analyze-and-log: getting-started/data-out/analyze-and-send getting-started/data-out/query-data: getting-started/data-out getting-started/quick-start: getting-started -getting-started/quick-start/cpp: getting-started/data-in/cpp -getting-started/quick-start/python: getting-started/data-in/python -getting-started/quick-start/rust: getting-started/data-in/rust +getting-started/quick-start/cpp: getting-started/data-in +getting-started/quick-start/python: getting-started/data-in +getting-started/quick-start/rust: getting-started/data-in +getting-started/data-in/cpp: getting-started/data-in +getting-started/data-in/python: getting-started/data-in +getting-started/data-in/rust: getting-started/data-in # Concepts section (old paths moved to subfolders) concepts/app-model: concepts/how-does-rerun-work @@ -72,16 +81,15 @@ reference/mcap/layers-explained: concepts/logging-and-ingestion/mcap/decoders-ex reference/mcap/decoders-explained: concepts/logging-and-ingestion/mcap/decoders-explained reference/mcap/message-formats: concepts/logging-and-ingestion/mcap/message-formats reference/migration-0-9: reference/migration/migration-0-9 -reference/sdk: reference/sdk/logging-controls reference/sdk-micro-batching: reference/sdk/micro-batching reference/sdk-operating-modes: reference/sdk/operating-modes reference/video: concepts/logging-and-ingestion/video -reference/viewer: reference/viewer/overview # Howto section howto/build-a-blueprint-programmatically: howto/visualization/build-a-blueprint-programmatically howto/callbacks: howto/visualization/callbacks howto/configure-viewer-through-code: getting-started/configure-the-viewer/navigating-the-viewer#programmatic-blueprints +howto/logging-and-ingestion/convert-existing-data: concepts/logging-and-ingestion/chunk-processing-api howto/dataframe-api: howto/query-and-transform/get-data-out howto/embed-rerun-viewer: howto/integrations/embed-web howto/fixed-window-plot: howto/visualization/fixed-window-plot @@ -109,3 +117,11 @@ howto/logging/shared-recordings: howto/logging-and-ingestion/shared-recordings # Howto - extend subfolder howto/extend/custom-data: howto/logging-and-ingestion/custom-data howto/extend/extend-ui: howto/visualization/extend-ui + +# Howto - train section moves +howto/integrations/dataloader: howto/train/dataloader +howto/query-and-transform/lerobot_export: howto/train/lerobot_export + +# Status renamed to State (8d4bb20945) +reference/types/archetypes/status: reference/types/archetypes/state_change +reference/types/views/status_view: reference/types/views/state_timeline_view diff --git a/docs/content/concepts.md b/docs/content/concepts.md index 4a8be1f57966..30c4c0e3c143 100644 --- a/docs/content/concepts.md +++ b/docs/content/concepts.md @@ -10,3 +10,4 @@ For a deeper understanding of how your logging data can be organized and visuali - [Visualization](./concepts/visualization.md) - how data is displayed in the Viewer - [Query and transform](./concepts/query-and-transform.md) - how to query and transform data - [Lenses](./concepts/query-and-transform/lenses.md) - extract, reshape, and reroute component data +- [Train](./concepts/train.md) - how to use Rerun data for training diff --git a/docs/content/concepts/how-does-rerun-work.md b/docs/content/concepts/how-does-rerun-work.md index 9d4ef2132bdd..18ea0bfad0a8 100644 --- a/docs/content/concepts/how-does-rerun-work.md +++ b/docs/content/concepts/how-does-rerun-work.md @@ -9,7 +9,7 @@ Rerun has several components manage multimodal data across its lifetime. This pa ### Logging SDK -The Logging SDK is how you get data into Rerun. Available for Python, Rust, and C++, it runs inside your application and logs data using [archetypes](logging-and-ingestion/entity-component.md)—structured types like `Points3D`, `Image`, or `Transform3D`. +The Logging SDK is how you get data into Rerun. Available for Python, Rust, and C++, it runs inside your application and logs data using [archetypes](logging-and-ingestion/entity-component.md) — structured types like `Points3D`, `Image`, or `Transform3D`. Data can be streamed directly to the Viewer, saved to `.rrd` files, or both. @@ -26,60 +26,30 @@ The Web Viewer has performance limitations compared to the native viewer. It run Both viewers can be extended: the Native Viewer through its [Rust API](../howto/visualization/extend-ui.md), and the Web Viewer can be [embedded in web applications](../howto/integrations/embed-web.md) or [Jupyter notebooks](../howto/integrations/embed-notebooks.md). -### Data platform +### Catalog server -The Data Platform provides persistent storage and indexing for large-scale data. It organizes data into: +The catalog server provides persistent storage and indexing for large-scale data. It organizes data into: - **Datasets**: Named collections of related recordings - **Segments**: Individual `.rrd` files registered to a dataset Data is served via the **redap** protocol (**Re**run **Da**ta **P**rotocol). -The Data Platform is available as: +The catalog server is available as: - Open-source server for local development (`rerun server`) -- Managed offering for production deployments +- **Rerun Hub**, our managed offering for production deployments ### Catalog SDK -The Catalog SDK (`rerun.catalog`) is a Python library for querying and manipulating the data stored on the Data Platform. Combined with the managed Data Platform, it allows building complex data transformation pipelines. +The Catalog SDK (`rerun.catalog`) is a Python library for querying and manipulating the data stored on a catalog server. Combined with Rerun Hub, it allows building complex data transformation pipelines. ## How they connect -```d2 -direction: down -horizontal-gap: 0 -vertical-gap: 0 - -Logging SDK - -".rrd files" - -Viewer: { - label.near: bottom-center - - gRPC endpoint - Chunk Store - Renderer -} - -Viewer.gRPC endpoint -> Viewer.Chunk Store -Viewer.Chunk Store -> Viewer.Renderer - -Data Platform: { - label.near: bottom-center - Datasets -} - -Catalog SDK - -Logging SDK -> Viewer.gRPC endpoint: stream -Logging SDK -> ".rrd files": save -".rrd files" -> Viewer.Chunk Store: load -".rrd files" -> Data Platform: register -Data Platform -> Viewer.Chunk Store: redap -Data Platform -> Catalog SDK: redap -``` +
+ + +
## What ships where? @@ -93,7 +63,7 @@ It's a great place to start exploring the examples. The `rerun` binary bundles multiple tools in one: - **Native Viewer** for visualization -- **OSS Data Platform** server (via `rerun server`) +- **OSS catalog server** (via `rerun server`) - **RRD tools** for file manipulation - **Web Viewer** (via `rerun --serve-web`) @@ -110,19 +80,19 @@ The Python SDK includes: - **Catalog SDK** - **CLI**, including the Viewer (the `rerun` CLI is made available by installing the `rerun-sdk` Python package) -See: Python SDK [installation instructions](../overview/installing-rerun/python.md) and [quick start guide](../getting-started/data-in/python.md) +See: Python SDK [installation instructions](../getting-started/install-rerun/python.md) and [quick start guide](../getting-started/data-in.md) ### Rust SDK The Logging SDK as a Rust crate. -See: Rust SDK [installation instructions](../overview/installing-rerun/rust.md) and [quick start guide](../getting-started/data-in/rust.md) +See: Rust SDK [installation instructions](../getting-started/install-rerun/rust.md) and [quick start guide](../getting-started/data-in.md) ### C++ SDK The Logging SDK for C++ projects. -See: C++ SDK [installation instructions](../overview/installing-rerun/cpp.md) and [quick start guide](../getting-started/data-in/cpp.md) +See: C++ SDK [installation instructions](../getting-started/install-rerun/cpp.md) and [quick start guide](../getting-started/data-in.md) ### The `web-viewer` and `web-viewer-react` NPM packages @@ -136,10 +106,10 @@ See: the `web-viewer` package [reference](../reference/npm.md) The simplest workflow: stream data directly from your code to the Viewer for live visualization. -```d2 -direction: right -Logging SDK -> Viewer: stream -``` +
+ + +
Minimal example: @@ -153,11 +123,10 @@ Best for: development, debugging, real-time monitoring. Log data to `.rrd` files, then open them in the Viewer whenever needed. Files can be loaded from disk or URLs. -```d2 -direction: right -Logging SDK -> ".rrd": save -".rrd" -> Viewer: load -``` +
+ + +
Minimal example: @@ -173,16 +142,14 @@ Best for: sharing recordings, offline analysis, archiving. -### Store on data platform +### Store on a catalog server -Register `.rrd` files with the Data Platform for persistent, indexed storage. Query and visualize on demand. +Register `.rrd` files with a catalog server for persistent, indexed storage. Query and visualize on demand. -```d2 -direction: right -".rrd" -> Data Platform: register -Data Platform -> Viewer: redap -Data Platform -> Catalog SDK: redap -``` +
+ + +
Minimal example of creating a dataset and registering files: @@ -201,14 +168,12 @@ Best for: large datasets, team collaboration, production pipelines. ### Query and transform data -Use the Catalog SDK to query data from the Data Platform, process it, and write results back. Visualization is available at any time. +Use the Catalog SDK to query data from a catalog server, process it, and write results back. Visualization is available at any time. -```d2 -direction: right -Data Platform -> Catalog SDK: redap -Data Platform <- Catalog SDK: redap -Data Platform -> Viewer: redap -``` +
+ + +
Minimal example of querying a dataset: diff --git a/docs/content/concepts/logging-and-ingestion/batches.md b/docs/content/concepts/logging-and-ingestion/batches.md index 1ae2f704ea00..832e23f4c6b2 100644 --- a/docs/content/concepts/logging-and-ingestion/batches.md +++ b/docs/content/concepts/logging-and-ingestion/batches.md @@ -3,7 +3,7 @@ title: Component Batches order: 900 --- -In the Rerun data model, the value of a given component at a given point in time is always itself a list—or a _batch_—of values. +In the Rerun data model, the value of a given component at a given point in time is always itself a list — or a _batch_ — of values. Consider this example: diff --git a/docs/content/concepts/logging-and-ingestion/chunk-processing-api.md b/docs/content/concepts/logging-and-ingestion/chunk-processing-api.md new file mode 100644 index 000000000000..fe0cb22c174e --- /dev/null +++ b/docs/content/concepts/logging-and-ingestion/chunk-processing-api.md @@ -0,0 +1,201 @@ +--- +title: Chunk Processing API +order: 750 +--- + +The Chunk Processing API is a flexible, [chunk](chunks.md)-centric API for data ingestion, transformation, and conversion pipelines. +It covers I/O from common robotics file formats, powerful declarative data wrangling primitives, and a multithreaded, native engine for pipeline execution. +The API is designed to support distributed execution in the future. + +> [!NOTE] +> The Chunk Processing API is currently experimental and may change in future releases. It is available in the Python SDK under `rerun.experimental`. + +## Building blocks + +The Chunk Processing API is built from three kinds of primitives — readers, stores, and lazy streams — that compose into a pipeline executed by a terminal call: + +
+ + +
+ +### Readers + +Readers produce [`Chunk`](chunks.md)s from external sources such as files, or datasets hosted on a catalog server. + +In some cases, readers are classes provided by the Chunk Processing API, such as [`RrdReader`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.RrdReader) and [`McapReader`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.McapReader). +The reader functionality can also be provided by classes from other parts of the Rerun SDK. +For example, [`DatasetEntry`](https://ref.rerun.io/docs/python/stable/catalog/#rerun.catalog.DatasetEntry) has a [`segment_store`](https://ref.rerun.io/docs/python/stable/catalog/#rerun.catalog.DatasetEntry.segment_store) method which returns a [`LazyStore`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.LazyStore) for the corresponding segment (see the [catalog object model](../query-and-transform/catalog-object-model.md) for more information on datasets). +[`UrdfTree`](https://ref.rerun.io/docs/python/stable/urdf/#rerun.urdf.UrdfTree) is another example of a class that offers reader functionality in addition to a larger feature set. + +There are two ways in which a reader may provide chunks. +All readers can sequentially stream all their source's chunks, typically via the `stream()` method. +Internally, such readers typically parse the source file, convert data to chunks as it is extracted, and yield those chunks as they are produced. + +Some readers, called [`IndexedReader`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.IndexedReader), can also provide indexed, random access to chunks via a [`LazyStore`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.LazyStore). +This is typically implemented on top of an existing chunk index, and is currently available for the following readers: +- `RrdReader` (relies on the RRD footer index) +- `DatasetEntry.segment_store()` (relies on the chunk index maintained by the catalog server) + +Processing chunks through a `LazyStore` is beneficial for pipelines where only a subset of chunks is needed, avoiding the I/O cost of loading unnecessary ones. + +> [!NOTE] +> Filter pushdown to `LazyStore` (e.g. `lazy_store.stream().filter(content="/my/entity")`) is planned but not yet implemented; today the filter runs after the chunks have been loaded. + +In all cases, readers typically act as the root of a processing pipeline and provide a `LazyChunkStream` object to refine and execute it — see [Lazy stream](#lazy-stream) below. + + +### Stores + +A store is a collection of chunks and comes in two complementary flavors: + +- **[`LazyStore`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.LazyStore)** — index-based, on-demand. Returned by indexed loaders such as `RrdReader(path).store()` and `DatasetEntry.segment_store()`. +- **[`ChunkStore`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.ChunkStore)** — fully materialized, all chunks held in memory. Build one with `ChunkStore.from_chunks([...])`, or materialize a stream via `stream.collect()`. + + +The previous section already hinted at the perks of `LazyStore`. Being index-based, it is cheap to create and takes limited amounts of memory. +Also, it unlocks performance speed-ups by only loading chunks that are relevant to the given processing pipeline. +On the other hand, `ChunkStore` is fully materialized: its memory footprint scales with the recording size. +This is a major exception in the chunk processing API, which generally leans on lazy loading and streaming execution to allow processing large datasets with bounded memory. + +Both kinds of stores share a common API surface, including: +- extracting the underlying [`Schema`](https://ref.rerun.io/docs/python/stable/catalog/#rerun.catalog.Schema) of the store; +- turning the store back into a pipeline with `.stream()`; +- exposing various statistics and content summaries. + + +One common reason to materialize a `ChunkStore` is to run chunk optimization; see [Optimize chunk count](../../howto/logging-and-ingestion/optimize-chunks.md#compacting-chunks-with-the-chunk-processing-api) for details. + +A materialized `ChunkStore` can also be queried directly as a dataframe with [`ChunkStore.reader`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.ChunkStore.reader), without spinning up a catalog server. +The returned [DataFusion](https://datafusion.apache.org/) dataframe is data-equivalent to loading the same chunks into a dataset and calling its `reader()`, so the full [dataframe query API](../query-and-transform/dataframe-queries.md) applies — modulo the `rerun_segment_id` column (see the [catalog object model](../query-and-transform/catalog-object-model.md) for more information about datasets and segments). + +For example, first materialize a store (here built a single chunk, for illustration): + +snippet: concepts/chunk_processing_query[build_store] + +Then the `ChunkStore` can be queried directly: + +snippet: concepts/chunk_processing_query[query] + + +### Lazy stream + +The `LazyChunkStream` is the central abstraction: a deferred, single-pass iterator of chunks with operators for filtering (`filter` / `drop`), branching (`split`), fan-in (`merge`), reshaping (`lenses`), and arbitrary per-chunk manipulation (`map` / `flat_map`). + +The key design is that a lazy stream is not a materialized collection or actual streaming process. +A `LazyChunkStream` instance can be thought of as a leaf node in a pipeline-description [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph). +By composition, it allows building up the DAG to represent the intended pipeline. + +For example, this creates a basic pipeline that does nothing but read an MCAP file: + +snippet: concepts/chunk_processing_intro[read] + +This pipeline can be extended using the lazy stream's methods. +For example, we can add a filter operation: + +snippet: concepts/chunk_processing_intro[filter] + +Up to this point, no data has actually been read or processed. +This happens when a terminal operation is called, for example: + +snippet: concepts/chunk_processing_intro[terminal] + +This exact call triggers the pipeline execution, including reading the source MCAP, performing the filter operation, and writing the output RRD. + +#### Pipeline execution + +To recap: + +- A pipeline is a DAG rooted at one or more readers or stores and ending at a leaf node represented by a lazy stream. +- Composition is cheap: building the DAG is metadata only, regardless of input size. This is done through `LazyChunkStream`'s APIs. +- The actual execution of the pipeline is triggered by calling a terminal method of the lazy stream, for example `.write_rrd()`. Terminal calls are blocking, but execution is multithreaded and essentially GIL-free. +- Memory cost is bounded by what flows through a chunk at a time, not by the total recording size. + +#### Move semantics + +To better express the DAG composition process, `LazyChunkStream` instances exhibit Rust-like move semantics to avoid accidental reuse: + +- `stream.filter(...)` moves `stream` into the new pipeline. Reusing `stream` afterwards raises `ValueError: already been consumed`. +- `stream.split(...)` returns two branches and consumes the parent. Each branch is itself a stream that can only be consumed once. +- `LazyChunkStream.merge(a, b, ...)` consumes every input. + +Terminal calls, however, do not consume the stream — a lazy stream can be executed multiple times against different destinations: + +```python +chunk_list = stream.to_chunks() +stream.write_rrd(path=..., application_id=..., recording_id=...) +``` + +Note that doing so executes the entire pipeline twice, which may not be desirable for complex pipelines. In that case, collect the stream to an intermediate `ChunkStore` to trade memory for re-computation. + +## Complete example + +The rest of this page walks through a single end-to-end pipeline that reads a robot-arm MCAP recording, fans the protobuf joint-state column out into per-joint `Scalars` series in degrees, tags the result with a static `/metadata` chunk built from scratch, and writes a new `.rrd`. + +Full source: [Python](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/chunk_processing.py). + +### Setup + +snippet: concepts/chunk_processing[setup] + +- Imports the experimental entry points: readers (`McapReader`), chunk and stream types (`Chunk`, `LazyChunkStream`), lens primitives (`DeriveLens`, `Selector`). +- Locates the input MCAP relative to the repo root and picks a CWD-relative output path. Nothing here touches Rerun yet. + +### Reading + +snippet: concepts/chunk_processing[reading] + +- `McapReader(MCAP).stream()` is the only line that touches the source — and even that is lazy: no MCAP bytes are decoded yet. +- The returned `LazyChunkStream` is the root of the DAG. + +### Processing + +snippet: concepts/chunk_processing[processing] + +- `drop(content="/video_raw/**")` is a no-op against this MCAP (the path does not exist) but illustrates content-based pruning. +- `fan(side)` builds six `DeriveLens` instances, one per joint, each extracting `.joint_positions[i]` (via `Selector(...).pipe(...)`), converting radians to degrees with `pyarrow.compute`, and routing the result to `/joints_deg//` as a `Scalars` column. +- Two scoped `.lenses(...)` calls apply the per-side fan only to chunks under `/robot_left/**` and `/robot_right/**` respectively. The same component name (`schemas.proto.JointState:message`) lives on both sides; scoping by `content=` is what disambiguates them. With `forward_unmatched`, every chunk outside the scope passes through untouched. + +### Merging + +snippet: concepts/chunk_processing[merging] + +- `Chunk.from_columns("/metadata", indexes=[], columns=rr.AnyValues.columns(…))` builds a single static chunk from scratch — `indexes=[]` makes it static. Any archetype's `.columns(…)` helper works here. +- `LazyChunkStream.from_iter([metadata])` lifts that one chunk into a one-element stream so it can participate in the pipeline. +- `LazyChunkStream.merge(processed, ...)` is fan-in: the two inputs become one stream. Order is preserved per-input, not globally. + +### Writing + +snippet: concepts/chunk_processing[write] + +- `write_rrd(...)` is the terminal: this is where the DAG actually executes. The whole pipeline runs in a single streaming pass. +- `application_id` and `recording_id` identify the resulting recording; a fresh `uuid.uuid4()` makes each invocation produce a distinct recording. + +## Relationship to the logging APIs + +Both the logging APIs (`rr.log`, `rr.send_columns`, `RecordingStream`) and the Chunk Processing API target the same underlying data model, but they differ in several ways: + +| | Logging API | Chunk processing API | +|------------------------|------------------------------------------|---------------------------------------------------------------------------------------------------------| +| Direction | logging call → sink | chunk source → transform → chunk sink | +| Granularity | single rows or columns of data | whole chunks | +| Execution model | continuous, as logging calls are emitted | lazy, upon stream execution | +| Where chunks come from | built by the logging API's batcher | already exist (from a reader) or built explicitly with `Chunk.from_columns` / `Chunk.from_record_batch` | +| Typical use | realtime data logging | ingestion, conversion, post-processing pipelines | + +The two are interoperable: +- **Logging → chunk processing:** save a `RecordingStream` to an `.rrd`, then re-open it with `RrdReader` to get a `LazyChunkStream`. + + > [!NOTE] + > This roundtrip-via-file will be smoothed out in the future for better ergonomics and performance. +- **Chunk processing → logging:** `rerun.experimental.send_chunks(chunks, recording=...)` feeds chunks into an active `RecordingStream` (useful for streaming to a viewer, for example). +- **Building chunks by hand:** `Chunk.from_columns` mirrors `rr.send_columns` and accepts the same `rr..columns(...)` helpers, so any data that can be logged with `rr.send_columns` can also be packaged as a `Chunk` and injected into a processing pipeline. + Likewise, `Chunk.from_record_batch` (for a single `RecordBatch`) and `Chunk.from_dataframe` (a multi-batch `Table`, `RecordBatchReader`, or `datafusion.DataFrame`) mirrors `rr.send_record_batch` and `rr.send_dataframe`. + See [Chunks](chunks.md) for details. + + +## See also + +- [Chunks](chunks.md): the underlying data model. +- [Lenses](../query-and-transform/lenses.md): the reshaping primitives used here. +- [`robot_data_preprocessing`](https://github.com/rerun-io/rerun/tree/main/examples/python/robot_data_preprocessing): a practical example showing how to apply the chunk processing API to robot data. diff --git a/docs/content/concepts/logging-and-ingestion/chunks.md b/docs/content/concepts/logging-and-ingestion/chunks.md index ec7f4d524814..a9a2ec12ba27 100644 --- a/docs/content/concepts/logging-and-ingestion/chunks.md +++ b/docs/content/concepts/logging-and-ingestion/chunks.md @@ -3,8 +3,6 @@ title: Chunks order: 700 --- - - A *Chunk* is the core datastructure at the heart of Rerun: it dictates how data gets logged, injected, stored, and queried. A basic understanding of chunks is important in order to understand why and how Rerun and its APIs work the way they work. @@ -62,14 +60,14 @@ You can learn more about chunks and how they came to be in [this blog post](http ## Getting chunks into Rerun -If you've used the Rerun SDK before, you know it doesn't actually force to manually craft these chunks byte by byte - that would be rather cumbersome! +If you've used the Rerun SDK before, you know it doesn't actually force you to craft these chunks manually, which would be rather cumbersome! How does one create and store chunks in Rerun, then? -### The row-oriented way: `log` +### The row-oriented logging: `log` -The `log` API is generally [what we show in the getting-started guides](https://rerun.io/docs/getting-started/data-in/python#logging-your-own-data) since it's the easiest to use: +The `log` API is generally [what we show in the getting-started guides](https://rerun.io/docs/getting-started/data-in#logging-our-first-points) since it's the easiest to use: snippet: archetypes/scalars_row_updates @@ -89,9 +87,9 @@ But if you're handing a bunch of rows of data over to Rerun, how does it end up Before logging data, you can use the `rr.set_time_` APIs to update the SDK's time context with timestamps for custom timelines. For example, `rr.set_time("frame", sequence=42)` will set the "frame" timeline's current value to 42 in the time context. -When you later call `rr.log`, the SDK will generate a row id and values for the built-in timelines `log_time` and `log_tick`. +When you later call `rr.log`, the SDK will generate a row id and a value for the built-in `log_time` timeline (enabled by default), as well as `log_tick` if you have opted in to it. It will also grab the current values for any custom timelines from the time context. -Any data passed to `rr.log` or `rr.log_components` becomes component batches. +Any data passed to `rr.log` becomes component batches. A diagram showing how a row gets created in Rerun @@ -116,20 +114,77 @@ The current chunk is then sent to its destination, either periodically or as soo Building up small column chunks before sending from the SDK trades off a small amount of latency and memory use in favor of more efficient transfer and ingestion. You can read about how to configure the batcher [here](../../reference/sdk/micro-batching.md). -### The column-oriented way: `send_columns` +### The column-oriented logging: `send_columns` The `log` API showcased above is designed to extract data from your running code as it's being generated. It is, by nature, *row-oriented*. If you already have data stored in something more *column-oriented*, it can be both a lot easier and more efficient to send it to Rerun in that form directly. This is what the `send_columns` API is for: it lets you efficiently update the state of an entity over time, sending data for multiple index and component columns in a single operation. -> ⚠️ `send_columns` API bypasses the time context and [micro-batcher](../../reference/sdk/micro-batching.md) ⚠️ +> [!WARNING] +> `send_columns` API bypasses the time context and [micro-batcher](../../reference/sdk/micro-batching.md). > > In contrast to the `log` API, `send_columns` does NOT add any other timelines to the data. Neither the built-in timelines `log_time` and `log_tick`, nor any [user timelines](timelines.md). Only the timelines explicitly included in the call to `send_columns` will be included. snippet: archetypes/scalars_column_updates -See also the reference: -* [🐍 Python `send_columns`](https://ref.rerun.io/docs/python/0.21.0/common/columnar_api/#rerun.send_columns) +Reference: +* [🐍 Python `send_columns`](https://ref.rerun.io/docs/python/stable/common/columnar_api/#rerun.send_columns) * [🦀 Rust `send_columns`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.send_columns) * [🌊 C++ `send_columns`](https://ref.rerun.io/docs/cpp/stable/classrerun_1_1RecordingStream.html#a7e326526d1473c02fcb2ed94afe6da69) + + +### Sending actual chunks: `send_chunks` + +The `Chunk` data structure described above is also exposed as a Python class. +You can build a chunk from, e.g., time/component columns, inspect or transform existing chunks, and forward chunks to a recording stream with `send_chunks`: + +snippet: concepts/build_chunk + +Alternatively, chunks can be created from an existing Arrow [`RecordBatch`](https://arrow.apache.org/docs/python/generated/pyarrow.RecordBatch.html) using [`Chunk.from_record_batch`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.Chunk.from_record_batch): + +snippet: concepts/build_chunk_from_record_batch[body] + +`send_chunks` also accepts iterables of chunks, as well as instances of [`LazyChunkStream`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.LazyChunkStream), [`ChunkStore`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.ChunkStore), and [`LazyStore`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.LazyStore). +For example, to forward every chunk of an existing RRD into a new recording stream: + +snippet: concepts/send_chunks + +Like `send_columns`, this path bypasses the time context and the [micro-batcher](../../reference/sdk/micro-batching.md): chunks are forwarded as-is, with whatever timelines they were built with. +See the [Chunk Processing API](chunk-processing-api.md) for building ingestion, transformation, and conversion pipelines out of these primitives. + +Reference: +* [🐍 Python `Chunk`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.Chunk) +* [🐍 Python `Chunk.from_columns`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.Chunk.from_columns) +* [🐍 Python `Chunk.from_record_batch`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.Chunk.from_record_batch) +* [🐍 Python `send_chunks`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.send_chunks) + + +### Dataframe logging: `Chunk.from_dataframe` and `send_dataframe` + +[`rr.send_dataframe`](https://ref.rerun.io/docs/python/stable/common/columnar_api/#rerun.send_dataframe) and the related [`Chunk.from_dataframe`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.Chunk.from_dataframe) extend the single record batch equivalent and accept a full PyArrow [`Table`](https://arrow.apache.org/docs/python/generated/pyarrow.Table.html) (or a [`RecordBatchReader`](https://arrow.apache.org/docs/python/generated/pyarrow.RecordBatchReader.html), or any Arrow-C-stream object such as a `datafusion.DataFrame`) carrying any number of entities, and yields the chunks of each record batch in turn. + +To map columns of the dataframe to Rerun timelines and components, the dataframe must carry the same `rerun:*` metadata as above. +For example, here we hand-craft a dataframe containing a Points3D entity: + +snippet: concepts/send_dataframe[build_table] + +`Chunk.from_dataframe` then interprets that metadata and yields one chunk per entity path: + +snippet: concepts/send_dataframe[from_dataframe] + +`rr.send_dataframe` is a thin logging convenience wrapper over `Chunk.from_dataframe`: it builds those same chunks and forwards them to the active recording stream in one call. + +snippet: concepts/send_dataframe[send_dataframe] + +Like `send_columns`, it bypasses the time context and the [micro-batcher](../../reference/sdk/micro-batching.md) — only timelines explicitly present in the table are added. + +Manually crafting the required metadata is obviously inconvenient. +This API is instead designed to compose with [dataframe queries](../query-and-transform/dataframe-queries.md), which produce dataframes already populated with metadata derived from the originally queried data. + +Reference: +* [🐍 Python `Chunk.from_dataframe`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.Chunk.from_dataframe) +* [🐍 Python `send_dataframe`](https://ref.rerun.io/docs/python/stable/common/columnar_api/#rerun.send_dataframe) +* [🐍 Python `send_record_batch`](https://ref.rerun.io/docs/python/stable/common/columnar_api/#rerun.send_record_batch) + + diff --git a/docs/content/concepts/logging-and-ingestion/entity-path.md b/docs/content/concepts/logging-and-ingestion/entity-path.md index 610b7e1f5e84..419d3d0a4484 100644 --- a/docs/content/concepts/logging-and-ingestion/entity-path.md +++ b/docs/content/concepts/logging-and-ingestion/entity-path.md @@ -47,7 +47,8 @@ You can insert an arbitrary unicode code point into an entity path using `\u{262 So for instance, `world/3D/My\ Image.jpg/detection` is a valid path (note the escaped space!). -⚠️ NOTE: even though entity paths are somewhat analogous to file paths, they are NOT the same. `..` does not mean "parent folder", and you are NOT intended to pass a file path as an entity path (especially not on Windows, which use `\` as a path separator). +> [!WARNING] +> Even though entity paths are somewhat analogous to file paths, they are NOT the same. `..` does not mean "parent folder", and you are NOT intended to pass a file path as an entity path (especially not on Windows, which use `\` as a path separator). ### Path hierarchy functions Path hierarchy plays an important role in a number of different functions within Rerun: diff --git a/docs/content/concepts/logging-and-ingestion/importers.md b/docs/content/concepts/logging-and-ingestion/importers.md index e51c4db0c18a..4fb5b67d2dcb 100644 --- a/docs/content/concepts/logging-and-ingestion/importers.md +++ b/docs/content/concepts/logging-and-ingestion/importers.md @@ -1,8 +1,5 @@ --- title: Importers order: 800 +redirect: concepts/logging-and-ingestion/importers/overview --- - -Extending Rerun's file loading capabilities with custom importers. - - diff --git a/docs/content/concepts/logging-and-ingestion/importers/overview.md b/docs/content/concepts/logging-and-ingestion/importers/overview.md index e62c47639b47..e2c57773b36b 100644 --- a/docs/content/concepts/logging-and-ingestion/importers/overview.md +++ b/docs/content/concepts/logging-and-ingestion/importers/overview.md @@ -3,7 +3,7 @@ title: Overview order: 50 --- -Internally, the [`Importer`](https://docs.rs/re_importer/latest/re_importer/trait.Importer.html?speculative-link) trait takes care of loading files into the Viewer and/or SDK. +Internally, the [`Importer`](https://docs.rs/re_importer/latest/re_importer/trait.Importer.html) trait takes care of loading files into the Viewer and/or SDK. There are 3 broad kinds of `Importer`s: _builtin_, _external_ and _custom_. _External_ and _custom_ are the two ways of extending the file loading system that we'll describe below. @@ -11,7 +11,7 @@ _External_ and _custom_ are the two ways of extending the file loading system th When a user attempts to open a file in the Viewer/SDK, **all** known `Importer`s are notified of the path to be opened, unconditionally. This gives `Importer`s maximum flexibility to decide what files they are interested in, as opposed to e.g. only being able to look at a file's extension. -Once notified, an `Importer` can return an [`ImporterError::Incompatible`](https://docs.rs/re_importer/latest/re_importer/enum.ImporterError.html?speculative-link#variant.Incompatible) error to indicate that it doesn't support a given file type. +Once notified, an `Importer` can return an [`ImporterError::Incompatible`](https://docs.rs/re_importer/latest/re_importer/enum.ImporterError.html#variant.Incompatible) error to indicate that it doesn't support a given file type. If, and only if, all importers known to the Viewer/SDK return an `Incompatible` error code, then an error message is shown to the user indicating that this file type is not (_yet_) supported. In these instances of unsupported files, we expose two ways of implementing and registering your `Importer`s, explained below. diff --git a/docs/content/concepts/logging-and-ingestion/mcap/cli-reference.md b/docs/content/concepts/logging-and-ingestion/mcap/cli-reference.md index 6973773de9bd..6782bfabf29c 100644 --- a/docs/content/concepts/logging-and-ingestion/mcap/cli-reference.md +++ b/docs/content/concepts/logging-and-ingestion/mcap/cli-reference.md @@ -62,10 +62,11 @@ rerun mcap convert input.mcap -d ros2msg -d raw -d recording_info -o output.rrd Decoding: - **`raw`**: Preserve original message bytes - **`schema`**: Extract metadata and schema information -- **`stats`**: Compute file and channel statistics -- **`metadata`**: Extract metadata records into RRD `__properties`, if present +- **`stats`**: Compute file and channel statistics into RRD `__mcap_properties` +- **`metadata`**: Extract metadata records into RRD `__mcap_metadata`, if present +- **`attachments`**: Extract MCAP attachment records into static data under `__mcap_attachments` - **`protobuf`**: Decode protobuf messages using into generic Arrow data without Rerun visualization components -- **`recording_info`**: Extract recording session metadata +- **`recording_info`**: Extract recording session metadata into RRD `__mcap_properties` - **`urdf`**: Use Rerun's built-in URDF loader when a ROS 2 `/robot_description` topic is present Semantic: @@ -83,6 +84,7 @@ rerun mcap convert input.mcap -o output.rrd rerun mcap convert input.mcap \ -d raw \ + -d attachments \ -d schema \ -d stats \ -d metadata \ diff --git a/docs/content/concepts/logging-and-ingestion/mcap/decoders-explained.md b/docs/content/concepts/logging-and-ingestion/mcap/decoders-explained.md index e2c0759ae222..c196f8097034 100644 --- a/docs/content/concepts/logging-and-ingestion/mcap/decoders-explained.md +++ b/docs/content/concepts/logging-and-ingestion/mcap/decoders-explained.md @@ -9,7 +9,7 @@ You can specify which decoders to use during conversion, allowing you to extract ## Understanding decoders with an example -When multiple decoders are enabled, they each process the same messages independently, creating different component types on identical entity paths. This can result in data duplication—for instance, enabling both `raw` and `protobuf` decoders stores the same message as both structured field data and raw binary blobs. +When multiple decoders are enabled, they each process the same messages independently, creating different component types on identical entity paths. This can result in data duplication — for instance, enabling both `raw` and `protobuf` decoders stores the same message as both structured field data and raw binary blobs. Consider an MCAP file from a ROS2 robot containing sensor data on the topic `/robot/camera/image_raw` with ROS2 `sensor_msgs/msg/Image` messages: diff --git a/docs/content/concepts/logging-and-ingestion/mcap/message-formats.md b/docs/content/concepts/logging-and-ingestion/mcap/message-formats.md index c153a28d514f..044b6fdd25ea 100644 --- a/docs/content/concepts/logging-and-ingestion/mcap/message-formats.md +++ b/docs/content/concepts/logging-and-ingestion/mcap/message-formats.md @@ -30,6 +30,7 @@ We are continually adding support for more standard message types. | Text | `std_msgs/String` | - | [TextDocument](../../../reference/types/archetypes/text_document.md) | | Log messages | `rcl_interfaces/Log` | `Log` | [TextLog](../../../reference/types/archetypes/text_log.md) | | 2D grid map | `nav_msgs/OccupancyGrid` | - | [GridMap](../../../reference/types/archetypes/grid_map.md) | +| 3D voxel grid map | `nav2_msgs/VoxelGrid` | `VoxelGrid` | [VoxelGridMap](../../../reference/types/archetypes/voxel_grid_map.md) | ### Timelines @@ -115,8 +116,18 @@ You can see this also in the selection panel: ## ROS1 message types -ROS1 messages are currently not supported for semantic interpretation through any layer. -The `raw` and `schema` layers are able to preserve the original bytes and structure of the messages. +ROS 1 data is not supported for semantic interpretation through any decoder. +The `raw` and `schema` decoders are able to preserve the original bytes and structure of ROS 1 messages in MCAP files, but Rerun will not convert them to visualization archetypes. + +We don't plan to add support for ROS 1 in Rerun, as it has reached [end-of-life](https://www.ros.org/blog/noetic-eol/) in May 2025. +But if you have legacy ROS 1 data and want to migrate it to modern formats, we recommend to try external tools like [`rosbags`](https://ternaris.gitlab.io/rosbags/). +For example, this command converts a ROS 1 `.bag` to a ROS 2 CDR-encoded `.mcap` that Rerun can import like any other supported ROS 2 recording: +```bash +rosbags-convert --src my_data_ros1.bag --dst my_data_ros2 --dst-storage mcap + +rerun my_data_ros2/my_data_ros2.mcap +``` +Please refer to the `rosbags` documentation for further information. ## Adding support for new types diff --git a/docs/content/concepts/logging-and-ingestion/recordings.md b/docs/content/concepts/logging-and-ingestion/recordings.md index 96c78bf402ec..21e1150a97ab 100644 --- a/docs/content/concepts/logging-and-ingestion/recordings.md +++ b/docs/content/concepts/logging-and-ingestion/recordings.md @@ -25,9 +25,9 @@ In particular, they share the same [blueprint](../visualization/blueprints.md). -### Recordings on the Data Platform +### Recordings on a catalog server -The Data Platform has a slightly different object model, which you can read more about in [Catalog object model](../query-and-transform/catalog-object-model.md). +A catalog server has a slightly different object model, which you can read more about in [Catalog object model](../query-and-transform/catalog-object-model.md). Datasets are top-level objects that group semantically related episodes of data, which we call _segments_. For example, it can be multiple recordings of the same robotic task. @@ -42,9 +42,9 @@ This again allows pooling multiple physical recordings into a single (logical) s ### Distributed recordings -Both the Viewer's implicit merging semantics and the Data Platform's layer system enable distributed logging workflows. Multiple processes or machines can produce separate `.rrd` files that share the same recording ID and application ID. +Both the Viewer's implicit merging semantics and the catalog server's layer system enable distributed logging workflows. Multiple processes or machines can produce separate `.rrd` files that share the same recording ID and application ID. -When these files are loaded into the Viewer, they are treated as a single logical recording. Alternatively, when using the Data Platform, these files can be registered to separate layers. This enables workflows where data collection is distributed across multiple sources but visualized as a unified set of data. +When these files are loaded into the Viewer, they are treated as a single logical recording. Alternatively, when using a catalog server, these files can be registered to separate layers. This enables workflows where data collection is distributed across multiple sources but visualized as a unified set of data. You can learn more about this in the [shared recordings guide](../../howto/logging-and-ingestion/shared-recordings.md). @@ -64,13 +64,13 @@ snippet: tutorials/custom-application-id ### When application IDs matter -Application IDs are used by the Viewer when loading recordings directly (not via the Data Platform): +Application IDs are used by the Viewer when loading recordings directly (not via a catalog server): - The Viewer stores blueprints per application ID - Different recordings share the same blueprint if they share the same application ID - Recordings are grouped by application ID in the Viewer UI -As stated above, application IDs are discarded when registering recordings to the Data Platform. See [Recordings on the Data Platform](#recordings-on-the-data-platform) above. +As stated above, application IDs are discarded when registering recordings to a catalog server. See [Recordings on a catalog server](#recordings-on-a-catalog-server) above. Check out the API to learn more about SDK initialization: - [🐍 Python](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.init) diff --git a/docs/content/concepts/logging-and-ingestion/rrd-format.md b/docs/content/concepts/logging-and-ingestion/rrd-format.md new file mode 100644 index 000000000000..f6e3328611c7 --- /dev/null +++ b/docs/content/concepts/logging-and-ingestion/rrd-format.md @@ -0,0 +1,378 @@ +--- +title: RRD format +order: 725 +--- + +An RRD is the file format Rerun uses to persist recordings and blueprints. At the lowest level it is a linear sequence of framed messages — store announcements and chunks of data — optionally followed by a footer index that makes random access cheap. This page covers the envelope around chunks and how they are serialized; the chunk data model itself is described in [Chunks](chunks.md). + +## Stores + +Logical groupings of chunks form so-called stores. +They come in two flavors: [recording](recordings.md) and [blueprint](../visualization/blueprints.md). +Both are structurally identical and distinguished only by a flag (store kind). + +A single RRD can hold any number of stores. +The file extension is either `.rrd` or `.rbl`. +Both refer to the exact same on-disk format and are used conventionally: +- `.rrd` files hold any combination of recording and blueprint stores; +- `.rbl` files hold a single blueprint store. + + +## Message kinds (`LogMsg`) + +The body of an RRD is a sequence of `LogMsg`s. There are three variants: + +- **`SetStoreInfo`** announces a new store and carries its [`StoreInfo`](#store-metadata-storeinfo). + It must appear before any data for that store. + There can be more than one `SetStoreInfo` for the same store in a single stream — for example, when a `RecordingStream` is created and later attached to a `FileSink` — and the latest one wins. +- **`ArrowMsg`** carries the actual data: an [Apache Arrow IPC](https://arrow.apache.org/docs/format/Columnar.html#ipc-streaming-format) payload encoding a single chunk, tagged with the `StoreId` it belongs to. + This is what makes up the bulk of every RRD. +- **`BlueprintActivationCommand`** is the only non-data control message. + It is emitted after a blueprint's chunks have been sent, and lets the producer atomically activate the blueprint via the [`make_active` / `make_default`](https://ref.rerun.io/docs/python/stable/blueprint/) flags. + It exists so that the Viewer never sees a half-loaded blueprint, and so the application can decide whether to apply the blueprint as the current one or the default. + + +> [!NOTE] +> At the wire level there is also an `End` message kind that frames the optional footer described [below](#footer). It is not a `LogMsg` variant in the application-level type system — it is an envelope reserved for the footer payload — but it shares the same framing as the three `LogMsg`s above. + + +## Chunks (`ArrowMsg` payload) + +Every `ArrowMsg` carries a single **chunk** — an Apache Arrow `RecordBatch` with Rerun-specific schema metadata. A chunk belongs to one entity path and holds a contiguous run of rows for that entity, with one column per timeline and one column per component. See [Chunks](chunks.md) for the conceptual deep-dive (how chunks are built, batched, sorted, compacted); this section just shows what a chunk looks like when you crack one open. + +The schema is laid out per **Sorbet**, Rerun's object-model spec — it defines how chunks, archetypes, components, and timelines map onto Arrow column names, types, and metadata. The easiest way to see it concretely is to save a recording and reopen it with [`RrdReader`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.RrdReader). + +First, let's create an RRD file with some content: + +snippet: concepts/rrd_format[write] + +Then we can inspect the first chunk it contains: + +snippet: concepts/rrd_format[inspect] + +> [!NOTE] +> By default, `chunk.format()` trims metadata keys to keep the representation concise. +> Using `trim_metadata_keys=False` disables this behavior, so the typical `rerun:` / `sorbet:` prefixes are visible here. + +This prints a chunk together with its schema. A typical output looks like: + +```text +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * rerun:entity_path: /points │ +│ * rerun:id: chunk_18B0AA9FA7B7B1A61d23c55ca87b18b4 │ +│ * sorbet:version: 0.1.3 │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌─────────────────────────────────────┬─────────────────────────┬────────────────────────────┬────────────────────────────┬──────────────────────────────────┬─────────────────────────────────────┐ │ +│ │ RowId ┆ frame ┆ log_tick ┆ log_time ┆ Points3D:colors ┆ Points3D:positions │ │ +│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ +│ │ type: non-null FixedSizeBinary(16) ┆ type: Int64 ┆ type: Int64 ┆ type: Timestamp(ns) ┆ type: List(UInt32) ┆ type: List(FixedSizeList(3 x │ │ +│ │ ARROW:extension:metadata: ┆ rerun:index_name: frame ┆ rerun:index_name: log_tick ┆ rerun:index_name: log_time ┆ rerun:archetype: Points3D ┆ non-null Float32)) │ │ +│ │ {"namespace":"row"} ┆ rerun:is_sorted: true ┆ rerun:is_sorted: true ┆ rerun:is_sorted: true ┆ rerun:component: Points3D:colors ┆ rerun:archetype: Points3D │ │ +│ │ ARROW:extension:name: TUID ┆ rerun:kind: index ┆ rerun:kind: index ┆ rerun:kind: index ┆ rerun:component_type: Color ┆ rerun:component: Points3D:positions │ │ +│ │ rerun:is_sorted: true ┆ ┆ ┆ ┆ rerun:kind: data ┆ rerun:component_type: Position3D │ │ +│ │ rerun:kind: control ┆ ┆ ┆ ┆ ┆ rerun:kind: data │ │ +│ ╞═════════════════════════════════════╪═════════════════════════╪════════════════════════════╪════════════════════════════╪══════════════════════════════════╪═════════════════════════════════════╡ │ +│ │ row_18B0AA9FA79D51886952b7c6bb9f6ed ┆ 0 ┆ 0 ┆ 2026-05-18T13:04:15.500740 ┆ [4278190335, 16711935] ┆ [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]] │ │ +│ │ 4 ┆ ┆ ┆ ┆ ┆ │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ row_18B0AA9FA7B6D7E06952b7c6bb9f6ed ┆ 1 ┆ 1 ┆ 2026-05-18T13:04:15.501658 ┆ [65535] ┆ [[2.0, 2.0, 2.0]] │ │ +│ │ 5 ┆ ┆ ┆ ┆ ┆ │ │ +│ └─────────────────────────────────────┴─────────────────────────┴────────────────────────────┴────────────────────────────┴──────────────────────────────────┴─────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +What to notice: + +- All Rerun-specific metadata keys are prefixed with `rerun:` (`rerun:entity_path`, `rerun:id`, `rerun:kind`, `rerun:index_name`, …). Sorbet's own metadata uses the `sorbet:` prefix (`sorbet:version`). +- The chunk-level **metadata** identifies the entity path the chunk belongs to and the chunk's id. +- The **`RowId`** column is the row identity column (`rerun:kind: control`). +- Each timeline contributes one **index column** (`frame`, `log_tick`, `log_time`) — `log_time` is auto-populated by the logging API (and `log_tick` if opted in), `frame` is the user-defined timeline. +- Each component contributes one **data column** (`Points3D:colors`, `Points3D:positions`) carrying the per-row values. + + +## Store metadata (`StoreInfo`) + +Every store in an RRD is identified by a `StoreId` and described by a `StoreInfo`: + +- **`StoreId`** combines: + - **`kind`** — `Recording` or `Blueprint`. The on-disk format treats both identically; the kind is just a flag. What differs is the *expected content*: recordings hold user-logged data on user-defined entity paths, blueprints hold `rr.blueprint.*` objects on Viewer-reserved paths. The Viewer dispatches on the kind — recordings populate the data store, blueprints populate the Viewer's UI/layout state. + - **`application_id`** — a user-chosen identifier for the application that produced the recording (see [Recordings](recordings.md) for the conventions, including the relationship with segment and dataset IDs in the remote/catalog context). + - **`recording_id`** — a UUID or user-chosen string that distinguishes runs of the same application (catalog servers use this as the segment ID — see the [catalog object model](../query-and-transform/catalog-object-model.md)). +- **`StoreInfo`** wraps the `StoreId` and adds: + - **`cloned_from`** — for stores that originated as a clone of another (typically the active blueprint is derived from a default blueprint). + - **`store_source`** — where the store came from (`PythonSdk`, `RustSdk`, `CppSdk`, or a file source such as CLI / drag-drop). + - **`store_version`** — the Rerun version that produced the data. + +Matching `application_id` and `recording_id` is how the Viewer merges multiple `.rrd` files (or multiple stores within one file) into a single logical recording. + +`.rbl` is just an RRD whose store happens to have `kind = Blueprint` — nothing in the bytes makes it special. +The convention of using `.rbl` for blueprints instead of `.rrd` is purely a filename hint to the Viewer and users. + +When an RRD holds multiple [stores](#stores) each store begins with its own `SetStoreInfo`, and every subsequent `ArrowMsg` is tagged with its store's `StoreId`. +Messages from different stores may be interleaved or grouped. +The [footer](#footer) indexes each store separately, so readers can enumerate stores and select the ones they want without scanning chunk bytes. + + +## Footer + +The footer is an optional manifest appended at the end of an RRD that enables random access into the file. +For each chunk in the RRD, the manifest carries chunk-level metadata (id, byte offset in the file, byte size — compressed and uncompressed) along with per-component and per-timeline statistics and the chunk's schema hash. +Like all data in Rerun, the manifest is internally stored as an Arrow `RecordBatch`, with one row per chunk. + +With the footer, a reader can enumerate stores in a handful of seeks and pull only the chunks it actually needs — for example, by entity path or by time range — without reading any chunk it does not care about. +This is what enables [`RrdReader`](chunk-processing-api.md) to be cheap to use on large files, and the OSS catalog server to "load" large datasets quickly and with little memory overhead. + +All tooling included in recent versions of the Rerun SDK emit footers by default. +An RRD may still miss a footer for a variety of reasons — for example, when a stream is not shut down cleanly, or legacy RRDs written before footers existed. +In those cases, readers fall back to a linear scan, which is semantically equivalent — just slower for partial reads. + +For illustration, let's see what a footer looks like in an RRD. +This can be done with the following command: + +```sh +rerun rrd print --footers --footers-lod 2 my.rrd +``` + +Here we use `--footers-lod 2` to see the entire table, which happen to be very wide. Here is the result for the recording produced by the snippet above: + +```text +Showing data after migration to latest Rerun version +StoreInfo { + store_id: StoreId( + Recording, + "rerun_example_rrd_format", + "example", + ), + cloned_from: None, + store_source: PythonSdk( + 3.11.13, + ), + store_version: Some( + CrateVersion { + major: 0, + minor: 33, + patch: 0, + meta: Some( + DevAlpha { + alpha: 1, + commit: None, + }, + ), + }, + ), +} +StoreInfo { + store_id: StoreId( + Recording, + "rerun_example_rrd_format", + "example", + ), + cloned_from: None, + store_source: PythonSdk( + 3.11.13, + ), + store_version: Some( + CrateVersion { + major: 0, + minor: 33, + patch: 0, + meta: Some( + DevAlpha { + alpha: 1, + commit: None, + }, + ), + }, + ), +} +Chunk(chunk_18B0AA9F967A41276952b7c6bb9f6ed2) with 1 rows (632 B) - /__properties - data columns: [RecordingInfo:start_time] +Chunk(chunk_18B0AA9FA7B7B1A61d23c55ca87b18b4) with 2 rows (1.2 KiB) - /points - data columns: [Points3D:colors Points3D:positions] +┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * source: "/tmp/rrd_format_doc.rrd" │ +│ * schema_sha_256: 03bea0095483cf5d32a3d28fc28f0433d917cdeacf88a07fcf02b97a778f492b │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌─────────────────────┬────────────────────────────────────┬────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┬──────────────────────────────┬─────────────────────────────────┬────────────────────────────────────┬──────────────────────────────────────────┬──────────────┬──────────────┬─────────────────┬─────────────────┬────────────────────────────┬────────────────────────────┬─────────────────────────────┬────────────────────────────┬────────────────────────────────┬────────────────────────────────┬───────────────────────────────┬───────────────────────────────────┬────────────────────────────────┬──────────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────────────────────────┬──────────────────────────────────────┬────────────────────────────────┬──────────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────────────────────────┬──────────────────────────────────────┐ │ +│ │ chunk_entity_path ┆ chunk_id ┆ chunk_is_static ┆ chunk_num_rows ┆ chunk_byte_offset ┆ chunk_byte_size ┆ chunk_byte_size_uncompressed ┆ Points3D:colors:has_static_data ┆ Points3D:positions:has_static_data ┆ RecordingInfo:start_time:has_static_data ┆ frame:start ┆ frame:end ┆ log_tick:start ┆ log_tick:end ┆ log_time:start ┆ log_time:end ┆ frame:Points3D:colors:start ┆ frame:Points3D:colors:end ┆ frame:Points3D:colors:num_rows ┆ frame:Points3D:positions:start ┆ frame:Points3D:positions:end ┆ frame:Points3D:positions:num_rows ┆ log_tick:Points3D:colors:start ┆ log_tick:Points3D:colors:end ┆ log_tick:Points3D:colors:num_rows ┆ log_tick:Points3D:positions:start ┆ log_tick:Points3D:positions:end ┆ log_tick:Points3D:positions:num_rows ┆ log_time:Points3D:colors:start ┆ log_time:Points3D:colors:end ┆ log_time:Points3D:colors:num_rows ┆ log_time:Points3D:positions:start ┆ log_time:Points3D:positions:end ┆ log_time:Points3D:positions:num_rows │ │ +│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ +│ │ type: non-null Utf8 ┆ type: non-null FixedSizeBinary(16) ┆ type: non-null Boolean ┆ type: non-null UInt64 ┆ type: non-null UInt64 ┆ type: non-null UInt64 ┆ type: non-null UInt64 ┆ type: non-null Boolean ┆ type: non-null Boolean ┆ type: non-null Boolean ┆ type: Int64 ┆ type: Int64 ┆ type: Int64 ┆ type: Int64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) ┆ type: Int64 ┆ type: Int64 ┆ type: UInt64 ┆ type: Int64 ┆ type: Int64 ┆ type: UInt64 ┆ type: Int64 ┆ type: Int64 ┆ type: UInt64 ┆ type: Int64 ┆ type: Int64 ┆ type: UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) ┆ type: UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) ┆ type: UInt64 │ │ +│ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: RecordingInfo ┆ index: frame ┆ index: frame ┆ index: log_tick ┆ index: log_tick ┆ index: log_time ┆ index: log_time ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D ┆ archetype: Points3D │ │ +│ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ component: Points3D:colors ┆ component: Points3D:positions ┆ component: RecordingInfo:start_time ┆ ┆ ┆ ┆ ┆ ┆ ┆ component: Points3D:colors ┆ component: Points3D:colors ┆ component: Points3D:colors ┆ component: Points3D:positions ┆ component: Points3D:positions ┆ component: Points3D:positions ┆ component: Points3D:colors ┆ component: Points3D:colors ┆ component: Points3D:colors ┆ component: Points3D:positions ┆ component: Points3D:positions ┆ component: Points3D:positions ┆ component: Points3D:colors ┆ component: Points3D:colors ┆ component: Points3D:colors ┆ component: Points3D:positions ┆ component: Points3D:positions ┆ component: Points3D:positions │ │ +│ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ component_type: Color ┆ component_type: Position3D ┆ component_type: Timestamp ┆ ┆ ┆ ┆ ┆ ┆ ┆ component_type: Color ┆ component_type: Color ┆ component_type: Color ┆ component_type: Position3D ┆ component_type: Position3D ┆ component_type: Position3D ┆ component_type: Color ┆ component_type: Color ┆ component_type: Color ┆ component_type: Position3D ┆ component_type: Position3D ┆ component_type: Position3D ┆ component_type: Color ┆ component_type: Color ┆ component_type: Color ┆ component_type: Position3D ┆ component_type: Position3D ┆ component_type: Position3D │ │ +│ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ index: rerun:static ┆ index: rerun:static ┆ index: rerun:static ┆ ┆ ┆ ┆ ┆ ┆ ┆ index: frame ┆ index: frame ┆ index: frame ┆ index: frame ┆ index: frame ┆ index: frame ┆ index: log_tick ┆ index: log_tick ┆ index: log_tick ┆ index: log_tick ┆ index: log_tick ┆ index: log_tick ┆ index: log_time ┆ index: log_time ┆ index: log_time ┆ index: log_time ┆ index: log_time ┆ index: log_time │ │ +│ ╞═════════════════════╪════════════════════════════════════╪════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╪══════════════════════════════╪═════════════════════════════════╪════════════════════════════════════╪══════════════════════════════════════════╪══════════════╪══════════════╪═════════════════╪═════════════════╪════════════════════════════╪════════════════════════════╪═════════════════════════════╪════════════════════════════╪════════════════════════════════╪════════════════════════════════╪═══════════════════════════════╪═══════════════════════════════════╪════════════════════════════════╪══════════════════════════════╪═══════════════════════════════════╪═══════════════════════════════════╪═════════════════════════════════╪══════════════════════════════════════╪════════════════════════════════╪══════════════════════════════╪═══════════════════════════════════╪═══════════════════════════════════╪═════════════════════════════════╪══════════════════════════════════════╡ │ +│ │ /__properties ┆ 18b0aa9f967a41276952b7c6bb9f6ed2 ┆ true ┆ 1 ┆ 240 ┆ 986 ┆ 1736 ┆ false ┆ false ┆ true ┆ null ┆ null ┆ null ┆ null ┆ null ┆ null ┆ null ┆ null ┆ 0 ┆ null ┆ null ┆ 0 ┆ null ┆ null ┆ 0 ┆ null ┆ null ┆ 0 ┆ null ┆ null ┆ 0 ┆ null ┆ null ┆ 0 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ /points ┆ 18b0aa9fa7b7b1a61d23c55ca87b18b4 ┆ false ┆ 2 ┆ 1242 ┆ 1564 ┆ 3656 ┆ false ┆ false ┆ false ┆ 0 ┆ 1 ┆ 0 ┆ 1 ┆ 2026-05-18T13:04:15.500740 ┆ 2026-05-18T13:04:15.501658 ┆ 0 ┆ 1 ┆ 2 ┆ 0 ┆ 1 ┆ 2 ┆ 0 ┆ 1 ┆ 2 ┆ 0 ┆ 1 ┆ 2 ┆ 2026-05-18T13:04:15.500740 ┆ 2026-05-18T13:04:15.501658 ┆ 2 ┆ 2026-05-18T13:04:15.500740 ┆ 2026-05-18T13:04:15.501658 ┆ 2 │ │ +│ └─────────────────────┴────────────────────────────────────┴────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┴──────────────────────────────┴─────────────────────────────────┴────────────────────────────────────┴──────────────────────────────────────────┴──────────────┴──────────────┴─────────────────┴─────────────────┴────────────────────────────┴────────────────────────────┴─────────────────────────────┴────────────────────────────┴────────────────────────────────┴────────────────────────────────┴───────────────────────────────┴───────────────────────────────────┴────────────────────────────────┴──────────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────────────────────────┴──────────────────────────────────────┴────────────────────────────────┴──────────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────────────────────────┴──────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +The first lines decode the `SetStoreInfo` messages and the `ArrowMsg` payloads found in the stream. +The wide table that follows is the manifest itself, with one row per chunk. +The chunk-level columns (entity path, id, sortedness, row count, byte span, uncompressed size) are followed by per-component and per-timeline statistics — global timeline ranges (`frame:start`/`end`, `log_tick:*`, `log_time:*`) and per-component-per-timeline statistics (`frame:Points3D:positions:start`/`end`/`num_rows`, …) — that let an indexed reader skip components within a timeline range without reading their payloads. + +## File layout + +This section gives a byte-level walkthrough of the framing. All multibyte integers are little-endian. + +The high-level shape of any RRD is: + +``` +┌─────────────────────────────────────────────────────────┐ +│ StreamHeader 12 bytes │ +├─────────────────────────────────────────────────────────┤ +│ Message₁ : MessageHeader (16 B) + payload (N₁ B) │ +│ Message₂ : MessageHeader (16 B) + payload (N₂ B) │ +│ … │ +├─────────────────────────────────────────────────────────┤ +│ End msg : MessageHeader (16 B) + RrdFooter payload │ ┐ +│ │ │ optional +│ StreamFooter 32 bytes (typ.) │ │ footer +└─────────────────────────────────────────────────────────┘ ┘ +``` + +The three building blocks are detailed below. + +### Stream header + +Every RRD opens with the same fixed 12 bytes: + +``` + StreamHeader — 12 bytes +┌────────────┬────────────┬─────────────────────┐ +│ FourCC │ Version │ EncodingOptions │ +│ 4 bytes │ 4 bytes │ 4 bytes │ +└────────────┴────────────┴─────────────────────┘ +0 4 8 12 +``` + +| offset | field | value | +|------------|-------------|-------------------------------------------------------------| +| `[0..4)` | FourCC | `b"RRF2"` for the current format | +| `[4..8)` | Version | 4-byte encoded Rerun crate version | +| `[8]` | compression | `0` = Off, `1` = LZ4 | +| `[9]` | serializer | `2` = Protobuf (`1` once meant MsgPack and is now rejected) | +| `[10..12)` | reserved | `0x00 0x00` | + +- Older `"RRF0"` / `"RRF1"` FourCCs are recognized but rejected with `OldRrdVersion` — there is no in-place reader for them, you have to migrate through an older Rerun release. +- The historical bit-pattern `[0, 0, 0, 0]` for `Version` is interpreted as `0.2.0` (pre-2023-02-27 files); any encoded version older than `0.23` is rejected outright. +- `EncodingOptions` describes how the *payloads* of subsequent messages are encoded. In practice these flags are mostly advisory today — the values that matter ride alongside each individual message — but the bytes are still part of the format and the two reserved bytes must be zero. + +> [!NOTE] +> The header format exposes legacy details that are no longer supported and may require an older Rerun SDK version to migrate. +> However, RRDs created by Rerun SDK 0.23 and later are guaranteed to be migrated, and this guarantee holds for future releases — see the next section. + +### Message framing + +After the header, the file is a sequence of framed messages. Each message is a 16-byte header followed by an opaque payload: + +``` + MessageHeader — 16 bytes +┌───────────────────────────┬───────────────────────────┐ +│ kind │ payload_len │ +│ u64 LE │ u64 LE │ +└───────────────────────────┴───────────────────────────┘ +0 8 16 + +╔════════════════════════════════╗ +║ payload — payload_len bytes ║ +║ protobuf ║ +╚════════════════════════════════╝ +``` + +| offset | field | value | +|-----------|---------------|------------------------------------------------------| +| `[0..8)` | `kind` | `MessageKind` discriminant (see table below) | +| `[8..16)` | `payload_len` | byte length of the protobuf payload that follows | + +The `kind` field tells the decoder how to interpret the payload: + +| Value | `MessageKind` | Payload | +|------:|------------------------------|---------------------------------------------------------------------------------------| +| `0` | `End` | An `RrdFooter` protobuf (the optional footer — see below) | +| `1` | `SetStoreInfo` | A `SetStoreInfo` protobuf (announces a store) | +| `2` | `ArrowMsg` | An `ArrowMsg` protobuf wrapping a chunk's Arrow IPC bytes (optionally LZ4-compressed) | +| `3` | `BlueprintActivationCommand` | A `BlueprintActivationCommand` protobuf | + +The outer payload bytes are always plain protobuf. Of the four kinds, only `ArrowMsg` can carry compressed data: its `compression` field tracks whether the wrapped Arrow IPC bytes are LZ4-compressed, so different `ArrowMsg`s in the same file can mix compressed and uncompressed Arrow IPC payloads. +The `compression` byte in the `StreamHeader`'s `EncodingOptions` is advisory only — per-message decoding does not consult it. + +### Stream footer + +The footer is written in two parts. + +The first part is a regular framed message: an `End`-kind `MessageHeader` followed by the `RrdFooter` protobuf payload described in the [Footer](#footer) section. +It lives somewhere in the message stream — usually right before the file is closed — and is no different from any other framed message structurally. + +The second part is the **`StreamFooter` trailer** at EOF. It is *not* a framed message: it is a raw structure whose job is to let readers jump straight to the `RrdFooter`(s) from the end of the file. +The trailer is a variable-length entry table — one 20-byte `StreamFooterEntry` per `RrdFooter` in the stream — followed by a fixed 12-byte tail that always sits at the very end of the file: + +``` +StreamFooter = num_entries × StreamFooterEntry + 12-byte static tail + +┌──────────────────────────────────────────────┬─────────────────────────┐ +│ entries[0..num_entries) — 20·num_entries B │ static tail — 12 B │ +└──────────────────────────────────────────────┴─────────────────────────┘ +EOF − 12 − 20·num_entries EOF − 12 EOF +``` + +Each `StreamFooterEntry` is 20 bytes: + +| offset | field | value | +|------------|---------|--------------------------------------------------------------------------------| +| `[0..8)` | `start` | u64 LE — byte offset of the `RrdFooter` payload (after its own MessageHeader) | +| `[8..16)` | `len` | u64 LE — length of the `RrdFooter` payload | +| `[16..20)` | `crc32` | u32 LE — `xxh32(payload)` with the fixed seed `7850921` (`"RERUN"` in base-26) | + +The static tail is the part with a known offset from EOF: + +| offset (from EOF) | field | value | +|---------------------|---------------|------------------------------------| +| `[-12..-8)` | FourCC | `b"RRF2"` | +| `[-8..-4)` | identifier | `b"FOOT"` | +| `[-4..0)` | `num_entries` | u32 LE — number of entries above | + +The CRC only covers the `RrdFooter` payload, not the surrounding `MessageHeader`, so it can be checked independently of message framing. + +Reading the footer therefore boils down to: + +``` +1. seek EOF − 12 → read FourCC, identifier, num_entries +2. seek EOF − 12 − 20·num_entries → read num_entries entries +3. for each entry: + seek entry.start, read entry.len bytes + check xxh32(bytes, seed=7850921) == entry.crc32 + decode protobuf RrdFooter +``` + +A file can legally have more than one trailer — that happens when streams are simply concatenated (`cat a.rrd b.rrd > both.rrd`), but tools like `rerun rrd merge` collapse them back into a single trailer with a single entry. +Files written without a footer (streaming sinks, legacy producers) skip the `End` message and the trailer entirely; readers detect their absence by the missing `FOOT` identifier and fall back to a linear scan. + + +## Stability + +The format is split into two layers with different stability stories. + +### Binary format + +This concerns the binary structure of the RRD file. +The framing described in [File layout](#file-layout) is considered **stable** and we have no plans to change it. +Legacy RRDs whose `Version` field is older than `0.23` are currently rejected. +That's the cut-off below which we do not attempt to migrate at load time; a manual hop through an older Rerun SDK release is required. +The same is true for RRDs whose FourCC is `RRF0` or `RRF1`. + +Should we need to break framing compatibility again, the FourCC will bump (`RRF3`, …) and load-time auto-migration will be provided. +The `rerun rrd migrate` CLI will also be available for offline batch conversion. + +### Sorbet + +We refer to the high-level data model specification as Sorbet. +Its reference implementation lives in the Rust `re_sorbet` crate. +This includes the chunk and footer schemas, as well as the high-level data model (timelines, archetypes, components, etc. — see [Entities and Components](entity-component.md)). + +Sorbet is versioned and **subject to change**, but `re_sorbet` performs in-memory migration to the current Sorbet version as chunks (and the footer manifest) are loaded. +Any CLI tool that rewrites an RRD (`rerun rrd merge`, `rerun rrd optimize`, `rerun rrd migrate`, …) emits chunks in the current Sorbet version, so a round-trip through any of these is also a migration. +Future changes to Sorbet will be auto-migrated in the same way. + diff --git a/docs/content/concepts/logging-and-ingestion/timelines.md b/docs/content/concepts/logging-and-ingestion/timelines.md index e75d4def010a..6a409ae4ba48 100644 --- a/docs/content/concepts/logging-and-ingestion/timelines.md +++ b/docs/content/concepts/logging-and-ingestion/timelines.md @@ -7,15 +7,15 @@ order: 500 Each piece of logged data is associated with one or more timelines. -The logging SDK always creates two timelines for you: -* `log_tick` - a sequence timeline with the sequence number of the log call -* `log_time` - a temporal timeline with the time of the log call +The logging SDK can automatically create two timelines for you: +* `log_time` - a temporal timeline with the time of the log call. Enabled by default; opt-out via the `RERUN_LOG_TIME` environment variable or `set_log_time_enabled`. +* `log_tick` - a sequence timeline with the sequence number of the log call. Disabled by default; opt-in via the `RERUN_LOG_TICK` environment variable or `set_log_tick_enabled`. You can use the `set_time` function (Python reference: [set_time](https://ref.rerun.io/docs/python/stable/common/logging_functions/#rerun.set_time)) to associate logs with other timestamps on other timelines. For example: snippet: tutorials/timelines_example -This will add the logged points to the timelines `frame_idx` and `sensor_time`, as well as the automatic timelines `log_tick` and `log_time`. +This will add the logged points to the timelines `frame_idx` and `sensor_time`, as well as the automatic `log_time` timeline (and `log_tick`, if you opted in). You can then choose which timeline you want to organize your data along in the expanded timeline view in the bottom of the Rerun Viewer. ### How to log precise times diff --git a/docs/content/concepts/logging-and-ingestion/transforms.md b/docs/content/concepts/logging-and-ingestion/transforms.md index f31723ab51ba..61beb102d316 100644 --- a/docs/content/concepts/logging-and-ingestion/transforms.md +++ b/docs/content/concepts/logging-and-ingestion/transforms.md @@ -44,9 +44,11 @@ with `child_frame` and `parent_frame` parameters set to their respective names. snippet: concepts/transform3d_hierarchy_named_frames Note that unlike in ROS, you can log your transform relationship on _any_ entity. -**Note:** A current limitation to this is that once a `Transform3D` (or `Pinhole`) relating two frames has been logged to an entity, this particular relation may no longer be logged on any other entity. -An exception to this rule is [static data](static.md): if you log a frame to frame relationship on an entity with static time, you can later on use a different entity for temporal information. -This is useful to specify "default" transforms without yet knowing what timeline and paths are going to be used for temporal transforms. + +> [!NOTE] +> A current limitation to this is that once a `Transform3D` (or `Pinhole`) relating two frames has been logged to an entity, this particular relation may no longer be logged on any other entity. +> An exception to this rule is [static data](static.md): if you log a frame to frame relationship on an entity with static time, you can later on use a different entity for temporal information. +> This is useful to specify "default" transforms without yet knowing what timeline and paths are going to be used for temporal transforms. Named transform frames have several advantages over entity path based hierarchies: @@ -168,7 +170,8 @@ Note that in this example the archetype is logged at the root path, this will ma [Pinholes](https://rerun.io/docs/reference/types/archetypes/view_coordinates) have a view coordinates field integrated as a shortcut. The default coordinate system for pinhole entities is `RDF` (X=Right, Y=Down, Z=Forward). -> ⚠️ Unlike in 3D views where `rr.ViewCoordinates` only impacts how the rendered scene is oriented, applying `rr.ViewCoordinates` to a pinhole-camera will actually influence the projection transform chain. Under the hood this value inserts a hidden transform that re-orients the axis of projection. Different world-content will be projected into your camera with different orientations depending on how you choose this value. See for instance the [`open_photogrammetry_format`](https://rerun.io/examples/3d-reconstruction/open_photogrammetry_format) example. +> [!WARNING] +> Unlike in 3D views where `rr.ViewCoordinates` only impacts how the rendered scene is oriented, applying `rr.ViewCoordinates` to a pinhole-camera will actually influence the projection transform chain. Under the hood this value inserts a hidden transform that re-orients the axis of projection. Different world-content will be projected into your camera with different orientations depending on how you choose this value. See for instance the [`open_photogrammetry_format`](https://rerun.io/examples/3d-reconstruction/open_photogrammetry_format) example. For 2D spaces and other entities, view coordinates currently have currently no effect ([#1387](https://github.com/rerun-io/rerun/issues/1387)). diff --git a/docs/content/concepts/logging-and-ingestion/video.md b/docs/content/concepts/logging-and-ingestion/video.md index 93645ea4e017..98ef970d5f01 100644 --- a/docs/content/concepts/logging-and-ingestion/video.md +++ b/docs/content/concepts/logging-and-ingestion/video.md @@ -24,9 +24,10 @@ There are two options to choose from: * Raw video frames [`VideoStream`](../../reference/types/archetypes/video_stream.md) * Video files using [`AssetVideo`](../../reference/types/archetypes/asset_video.md) -⚠️ Do not use compressed video if you need accurate pixel replication: -this is not only due to the obvious detail loss on encoding, -but also since the exact _display_ of the same video is not consistent across platforms and decoder versions. +> [!WARNING] +> Do not use compressed video if you need accurate pixel replication: +> this is not only due to the obvious detail loss on encoding, +> but also since the exact _display_ of the same video is not consistent across platforms and decoder versions. ## Streaming video / raw encoded video frames @@ -49,14 +50,12 @@ For more details on how to query and decode video streams from Rerun, see our [q Current limitations of `VideoStream`: * [#9815](https://github.com/rerun-io/rerun/issues/9815): Decoding on native is generally slower than decoding in the browser right now. This can cause increased latency and in some cases may even stop video playback. -* [#10186](https://github.com/rerun-io/rerun/issues/10186): [`VideoStream`](../../reference/types/archetypes/video_stream.md) only supports H.264, H.265, AV1 at this point. * [#10090](https://github.com/rerun-io/rerun/issues/10090): B-frames are not yet supported for [`VideoStream`](../../reference/types/archetypes/video_stream.md). * [#10422](https://github.com/rerun-io/rerun/issues/10422): [`VideoFrameReference`](../../reference/types/archetypes/video_frame_reference.md) does not yet work with [`VideoStream`](../../reference/types/archetypes/video_stream.md). @@ -94,16 +93,11 @@ Codec support varies in the web & native viewer: | | Browser | Native | | ---------- | ------- | ------ | -| AV1 | ✅ | 🟧 | -| H.264/avc | ✅ | ✅ | -| H.265/hevc | 🟧 | ✅ | -| VP9 | ✅ | ❌ | - - +| AV1 | ✅ | 🟧 | +| H.264/avc | ✅ | ✅ | +| H.265/hevc | 🟧 | ✅ | +| VP8 | ✅ | ✅ | +| VP9 | ✅ | ✅ | Details see below. @@ -127,9 +121,9 @@ Discoverable for scripts/zombie_todos.py: TODO(#7755): fix above if ticket is outdated. --> -#### H.264/avc & H.265/hevc +#### H.264/avc, H.265/hevc, VP8 & VP9 -H.264/avc and H.265/hevc are supported via a separately installed `FFmpeg` binary, requiring a minimum version of `5.1`. +H.264/avc, H.265/hevc, VP8, and VP9 are supported via a separately installed `FFmpeg` binary, requiring a minimum version of `5.1`. The viewer does intentionally not come bundled with `FFmpeg` to avoid licensing issues. By default rerun will look for a system installed `FFmpeg` installation in `PATH`, @@ -158,11 +152,13 @@ We tested the following codecs in more detail: | AV1 | ✅ | ✅ | ✅ | ✅ | 🚧[^3] | ✅ | ✅ | | H.264/avc | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | H.265/hevc | ❌ | ❌ | ❌ | ✅ | 🚧[^4] | ❌ | 🚧[^5] | +| VP8 | ✅ | ✅ | ✅ | ✅ | ❌ | | | +| VP9 | ✅ | ✅ | ✅ | ✅ | ❌ | | | [^1]: Any Chromium-based browser should work, but we don't test all of them. -[^2]: Chrome on Windows has been observed to stutter on playback. It can be mitigated by [using software decoding](https://rerun.io/docs/overview/installing-rerun/troubleshooting#video-stuttering), but this may lead to high memory usage. See [#7595](https://github.com/rerun-io/rerun/issues/7595). +[^2]: Chrome on Windows has been observed to stutter on playback. It can be mitigated by [using software decoding](../../getting-started/install-rerun/troubleshooting.md#video-stuttering), but this may lead to high memory usage. See [#7595](https://github.com/rerun-io/rerun/issues/7595). [^3]: Safari/WebKit does not support AV1 decoding except on [Apple Silicon devices with hardware support](https://webkit.org/blog/14445/webkit-features-in-safari-17-0/). -[^4]: Safari/WebKit has been observed suttering when playing `hvc1` but working fine with `hevc1`. Despite support being advertised Safari 16.5 has been observed not support H.265 decoding. +[^4]: Safari/WebKit has been observed stuttering when playing `hvc1` but working fine with `hevc1`. Despite support being advertised Safari 16.5 has been observed not support H.265 decoding. [^5]: Only supported if hardware encoding is available. Therefore always affected by Windows stuttering issues, see above. Beyond this, for best compatibility we recommend: diff --git a/docs/content/concepts/query-and-transform/catalog-object-model.md b/docs/content/concepts/query-and-transform/catalog-object-model.md index 9ec40afa86e6..bef31004a65b 100644 --- a/docs/content/concepts/query-and-transform/catalog-object-model.md +++ b/docs/content/concepts/query-and-transform/catalog-object-model.md @@ -3,12 +3,12 @@ title: Catalog object model order: 100 --- -This page covers the Data Platform's object model. For logging and recording basics, see [Recordings](../logging-and-ingestion/recordings.md). For API details, see the [Catalog SDK reference](https://ref.rerun.io/docs/python/stable/common/catalog/). +This page covers the catalog server's object model. For logging and recording basics, see [Recordings](../logging-and-ingestion/recordings.md). For API details, see the [Catalog SDK reference](https://ref.rerun.io/docs/python/stable/common/catalog/). ## Catalog -We refer to the contents stored in a given instance of the Data Platform as the _catalog_. +We refer to the contents stored in a given catalog server as the _catalog_. The catalog contains top-level objects called _entries_. There are currently two types of entries: **tables** and **datasets**. @@ -18,8 +18,30 @@ Entries share a few common properties: - **id**: a globally unique identifier - **name**: a user-provided name, which must be unique within the catalog -The id is immutable, but the name can be changed provided it remains unique. +### Renaming a catalog entry +The id of a catalog entry is immutable, but the name can be changed provided it remains unique. +In Python, call `set_name()` on an entry to rename it on the catalog server, for example: + +```python +client = rr.catalog.CatalogClient(…) +dataset = client.get_dataset("old_name") +dataset.set_name("new_name") +``` + +### Structuring datasets + +When working with larger amounts of data, it can be useful to organize catalog entries in a directory-like structure. +This can be done by using `.` delimiters in the names. +The screenshot below is an example of a dot-delimited dataset name showing up as a directory tree in the viewer's data source browser: + + + + + + + + ## Table entries @@ -47,57 +69,10 @@ By default, the `"base"` layer name is used. Registering two `.rrd` files with the same recording ID (that is, with the same segment ID) to the same dataset, and using the same layer name, will result in the second `.rrd` overwriting the first. Additive registration can be achieved by using different layer names for different `.rrd`s with the same recording ID/segment ID. -```d2 -direction: left - -Catalog: { - shape: cylinder - - my_dataset: { - label: "my_dataset" - - segment_a: { - label: "segment_a" - - base: { - label: "layer\n\"base\"" - shape: parallelogram - } - } - - segment_b: { - label: "segment_b" - - base: { - label: "layer\n\"base\"" - shape: parallelogram - } - annotations: { - label: "layer\n\"extra\"" - shape: parallelogram - } - } - } -} - -Object Store: { - shape: cylinder - - "recording_a.rrd": { - shape: page - } - "recording_b.rrd": { - shape: page - } - "extra_b.rrd": { - shape: page - } -} - -Object Store."recording_a.rrd" -> Catalog.my_dataset.segment_a.base -Object Store."recording_b.rrd" -> Catalog.my_dataset.segment_b.base -Object Store."extra_b.rrd" -> Catalog.my_dataset.segment_b.annotations -``` +
+ + +
Layers are immutable and can only be overwritten by registering a new `.rrd` file. In other words, datasets support the following mutation operations: - _create segment_: by registering a `.rrd` with a "new" recording ID @@ -115,19 +90,10 @@ This differs from the table model, where the schema is defined upfront (_schema- In this context, the schema of a dataset is the union of schemas of its segments, which themselves are the union of the schemas of their layers. -```d2 -grid-rows: 3 -grid-gap: 10 - -"my_dataset schema": { width: 400; style.fill: "${d2-config.theme-overrides.B5}" } - -"segment_a schema".width: 200 -"segment_b schema".width: 200 - -base1: "base\nschema" { width: 95; style.fill: "${d2-config.theme-overrides.N5}" } -extra: "extra\nschema" { width: 95; style.fill: "${d2-config.theme-overrides.N5}" } -base2: "base\nschema" { width: 200; style.fill: "${d2-config.theme-overrides.N5}" } -``` +
+ + +
Datasets maintain a minimal level of schema self-consistency. Registering a `.rrd` whose schema is incompatible with the current dataset schema will result in an error. diff --git a/docs/content/concepts/query-and-transform/dataframe-queries.md b/docs/content/concepts/query-and-transform/dataframe-queries.md index 9665f32cc2ab..e883263f51b4 100644 --- a/docs/content/concepts/query-and-transform/dataframe-queries.md +++ b/docs/content/concepts/query-and-transform/dataframe-queries.md @@ -24,13 +24,13 @@ Dataframe queries can be used in two contexts: Let's use an example to illustrate how dataframe queries work. -Dataframe queries run against datasets stored on a [Data Platform](../how-does-rerun-work.md#data-platform). +Dataframe queries run against datasets stored on a [catalog server](../how-does-rerun-work.md#catalog-server). We can create a demo recording and load it into a temporary local catalog using the following code: snippet: concepts/query-and-transform/dataframe_query_example[setup] -We can then perform a dataframe query (against the local open-source Data Platform included in Rerun): +We can then perform a dataframe query (against the local open-source catalog server included in Rerun): snippet: concepts/query-and-transform/dataframe_query_example[query] @@ -63,38 +63,16 @@ This should produce an output similar to: ``` Let's unpack what happened here: -- **Catalog required**: We use `rr.server.Server()` to spin up a temporary local catalog. In production, you might connect to a Rerun Data Platform deployment instead. We then obtain the dataset to be queried from the catalog. +- **Catalog required**: We use `rr.server.Server()` to spin up a temporary local catalog. In production, you might connect to a Rerun Hub deployment instead. We then obtain the dataset to be queried from the catalog. - **Content filtering**: The `filter_contents()` method restricts the scope of the query to specific entities. This affects which columns are returned, but may also change which rows are returned since rows are only produced where at least one filtered column has data (see [How are rows produced?](#how-are-rows-produced-by-dataframe-queries)). - **Reader produces a lazy dataframe**: The `reader(index=…)` method returns a [DataFusion](https://datafusion.apache.org/) dataframe. The `index` parameter specifies which timeline drives row generation: a row is produced for each unique value of this index where data exists. The returned dataframe doesn't execute until it is collected. - **Filtering/aggregation/joining/etc.**: The standard suite of dataframe operations is provided by DataFusion. Here we use `filter()` to filter rows based on the data. Again, these are lazy operations that only build a query plan. - **Execution**: The `print(df)` implicitly executes the dataframe's query plan and returns the final result. The same would happen when converting to dataframe for other frameworks (Pandas, Polars, PyArrow, etc.). -```d2 -direction: down - -Dataset: { - shape: cylinder -} - -view: { - label: "Dataset view" -} - -DataFrame: { - label: "DataFusion DataFrame" -} - -Result: { - label: "Materialized rows\n(Arrow RecordBatch)" - shape: page -} - - -Dataset -> view: "filter_contents()\nfilter_segments()" -Dataset -> DataFrame: "reader()" -view -> DataFrame: "reader()" -DataFrame -> Result: "collect()" -``` +
+ + +
## FAQ diff --git a/docs/content/concepts/query-and-transform/lenses.md b/docs/content/concepts/query-and-transform/lenses.md index fbef1f951615..76e4e70e76ee 100644 --- a/docs/content/concepts/query-and-transform/lenses.md +++ b/docs/content/concepts/query-and-transform/lenses.md @@ -3,10 +3,11 @@ title: Lenses order: 400 --- -> **Note:** The Lenses API is currently experimental and may change in future releases. +> [!NOTE] +> The Lenses API is currently experimental and may change in future releases. -Lenses transform chunk data by extracting, reshaping, and rerouting components. -They operate on individual chunks and produce one or more output chunks with new component columns, entity paths, or timelines. +Lenses transform data by extracting, reshaping, and rerouting components. +They produce new component columns, entity paths, or timelines from existing data. ## Motivation @@ -20,45 +21,34 @@ Using an expressive API, Lenses allow you to: 3. Wrangle the values stored in individual components Lenses are available in the Rust SDK using `LensesSink` or directly on a `Chunk` via the `ChunkExt` trait. - In Python, Lenses can be applied to chunks directly or as a pipeline step in the `ChunkStream` API. Internally, Rerun uses lenses to implement large parts of our data importers, the MCAP importer is one example of this. -## Operational model +## Example data -Lenses operate on (component) columns and generally consist of these steps: - -1. Select an input column using a `ComponentIdentifier`. -2. Choose a target `ComponentDescriptor` to describe the semantics of the resulting column. -3. The transform operations that are performed on the input as a `Selector`. - -## Example - -Here is an example of what this looks like in our SDKs. -Let's assume we have data that was logged like the following: +The examples below all operate on the same input chunk, logged to `/sensor/imu` with `frame` as a timeline and two component columns `Imu:accel` and `Imu:status`: snippet: concepts/lenses[log_data] -This produces a chunk on `/sensor/imu`, with `frame` as a timeline and two component columns `Imu:accel` and `Imu:status`: - | `frame` | `Imu:accel` | `Imu:status` | |------:|-----------|------------| | 0 | `[{x: 1.0, y: 4.0, elapsed: 0}]` | `["ok"]` | | 1 | `[{x: 2.0, y: 5.0, elapsed: 10000000}]` | `["ok"]` | | 2 | `[{x: 3.0, y: 6.0, elapsed: 20000000}]` | `["warn"]` | -We can now define a lens for this data, which extracts the `.y` field from the struct as a component, extracts the `.elapsed` field as a timeline, tags it as a Rerun [`Scalar`](../../reference/types/archetypes/scalars.md), and moves the result to the new entity `/new_entity/accel_y`. -In code, the lens will look like this: +## Derive lenses + +A derive lens creates **new** component columns from an input component. +It selects an input column, extracts data using a `Selector`, and writes the results as new columns (optionally at a different entity and with additional timelines). -snippet: concepts/lenses[lens_definition] +The following lens extracts the `.y` field from the struct as a [`Scalar`](../../reference/types/archetypes/scalars.md), extracts the `.elapsed` field as a new timeline, and writes both to the entity `/new_entity/accel_y`: -See the full examples in [Rust](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/lenses.rs?speculative-link) and [Python](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/lenses.py?speculative-link). +snippet: concepts/lenses[derive_lens] - - +See the full examples in [Rust](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/lenses.rs) and [Python](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/lenses.py). -When we apply the `extract_y` lens, we get the following resulting components. +When we apply the `extract_y` lens, we get the following resulting chunks. On `/sensor/imu`, the unmodified `Imu:status` column remains: @@ -78,14 +68,33 @@ On `/new_entity/accel_y`, we get the extracted [`Scalar`](../../reference/types/ Note that the original `frame` timeline is present for all entities with the correct values. -### Output modes +## Mutate lenses -In chunk streaming scenarios, users can specify what should happen with matched and unmatched chunks. -Lenses support the following output modes: +A mutate lens modifies an existing component column by applying a selector to it. +Unlike derive lenses, no new columns are created. +The input column is transformed and stays at the same entity. -* `ForwardUnmatched` will forward unmatched chunks as well as a residual chunk that contains all components that are not targeted by any Lens. -* `ForwardAll` will forward all chunks unconditionally. This will lead to data duplication but can be helpful, for example for debugging. -* `DropUnmatched` all chunks and components that are not targeted by any lens will be discarded. +The following lens simplifies the `Imu:accel` struct to just its `.x` field: + +snippet: concepts/lenses[mutate_lens] + +After applying the `simplify_accel` lens, `/sensor/imu` looks like this: + +| `frame` | `Imu:accel` | `Imu:status` | +|------:|-----------|------------| +| 0 | `[1.0]` | `["ok"]` | +| 1 | `[2.0]` | `["ok"]` | +| 2 | `[3.0]` | `["warn"]` | + +The struct has been replaced by the extracted float values, while `Imu:status` remains unchanged. + +## Output modes + +When streaming data through lenses, the output mode controls which components are forwarded: + +* `ForwardUnmatched` forwards original components that are not consumed by any lens, alongside any lens-produced outputs. +* `ForwardAll` forwards all original components alongside lens-produced outputs. This leads to data duplication but can be helpful for debugging. +* `DropUnmatched` only forwards lens-produced outputs, dropping all other components. ## Selectors @@ -95,11 +104,12 @@ Because a lot of user-defined types are hierarchically nested message definition The basic syntax elements are: -* `.` — identity, selects the current value -* `.field` — access a named field (e.g. `.my.nested.struct.field`) -* `.sequence[]` — iterate over all elements in a sequence -* `.sequence[].x` — access a field on each element of a sequence -* `.optional_field?` — access an optional field, skipping missing values +* `.` - identity, selects the current value +* `.field` - access a named field (e.g. `.my.nested.struct.field`) +* `.sequence[]` - iterate over all elements in a sequence +* `.sequence[].x` - access a field on each element of a sequence +* `.optional_field?` - access an optional field, skipping missing values +* `pack(.x, .y, .z)` - pack several same-typed fields into a fixed-size list (see below) These can be composed using pipes (`|`) as described below. @@ -113,3 +123,11 @@ This is useful for value transformations that go beyond path navigation, like un For example, the following lens extracts the `.x` field and scales it by `9.81`: snippet: concepts/lenses[pipe_example] + +### Packing fields into fixed-size lists + +Many Rerun components are based on Arrow fixed-size lists. +For example, `Position3D` is a `FixedSizeList[3]`. +`pack(...)` assembles a fixed-size list from several paths that resolve to the same datatype, e.g. `pack(.x, .y, .z)`. + +If a field is nullable, acknowledge it with `!` (for example `pack(.x!, .y!, .z!)`); a null in any field will null the corresponding row in the resulting array, potentially shadowing non-null data in other fields. diff --git a/docs/content/concepts/query-and-transform/properties-and-segments.md b/docs/content/concepts/query-and-transform/properties-and-segments.md index 3ecfd69774a0..8335a71fda17 100644 --- a/docs/content/concepts/query-and-transform/properties-and-segments.md +++ b/docs/content/concepts/query-and-transform/properties-and-segments.md @@ -9,7 +9,7 @@ Common use cases for properties include tagging recordings with capture location ## Understanding properties -Let's use an example to illustrate how properties work and how they can be retrieved and queried using the Rerun Data Platform. +Let's use an example to illustrate how properties work and how they can be retrieved and queried using a catalog server. First, we create a few recordings with some properties: @@ -25,7 +25,7 @@ Internally, properties are logged under a reserved `/__properties` entity path a ## Querying the segment table Once recordings are registered to a [dataset](catalog-object-model.md#datasets), their properties become visible and queryable through the segment table. -Here we use the local open-source Data Platform included with Rerun to illustrate this: +Here we use the local open-source catalog server included with Rerun to illustrate this: snippet: concepts/query-and-transform/segment_properties[segment_table] diff --git a/docs/content/concepts/train.md b/docs/content/concepts/train.md new file mode 100644 index 000000000000..0c9374f9a820 --- /dev/null +++ b/docs/content/concepts/train.md @@ -0,0 +1,48 @@ +--- +title: Train +order: 400 +--- + +A Rerun [catalog](query-and-transform/catalog-object-model.md) can feed training pipelines two ways: export recordings to a standard format, or stream them directly into a PyTorch `DataLoader`. + +## Export to a training format + +The catalog exposes recordings as queryable DataFrames via [DataFusion](https://datafusion.apache.org/python/). +Multi-rate sensor streams can be time-aligned and columns of interest extracted, with the result written to whatever format a training pipeline expects. + +See [Export recordings to LeRobot datasets](../howto/train/lerobot_export.md) for a worked example. + +## Train directly from the catalog + +The experimental [`rerun.experimental.dataloader`](https://ref.rerun.io/docs/python/stable/experimental_dataloader/) module wraps a catalog as iterable or map-style PyTorch datasets, with no intermediate export step. + +### Sample space + +Three things describe a dataset (see [reference](https://ref.rerun.io/docs/python/stable/experimental_dataloader/)): + +- **[`DataSource`](https://ref.rerun.io/docs/python/stable/experimental_dataloader/#rerun.experimental.dataloader.DataSource)** — a catalog `DatasetEntry` with an optional segment filter; each registered RRD is one *segment*, typically one episode or trajectory +- **`index`** — the timeline that defines what "one sample" means (e.g. `"frame_index"` or `"real_time"`) +- **`fields`** — a dict of [`Field`](https://ref.rerun.io/docs/python/stable/experimental_dataloader/#rerun.experimental.dataloader.Field)s, each mapping a source column (an `entity:Archetype:component` triple) to a decoder + +[`SampleIndex`](https://ref.rerun.io/docs/python/stable/experimental_dataloader/#rerun.experimental.dataloader.SampleIndex) pre-computes the full sample space from lightweight per-segment index-range metadata — one query per segment, not a scan of the data. +For timestamp timelines, `FixedRateSampling` defines the sampling grid and the server handles drift between grid and real row positions via `fill_latest_at`. + +### Decoders + +Each `Field` has a `ColumnDecoder` ([`_decoders.py`](https://github.com/rerun-io/rerun/blob/main/rerun_py/rerun_sdk/rerun/experimental/dataloader/_decoders.py)) that converts a raw Arrow column to a `torch.Tensor`: + +- [`NumericDecoder`](https://ref.rerun.io/docs/python/stable/experimental_dataloader/#rerun.experimental.dataloader.NumericDecoder) — scalars and numeric lists +- [`ImageDecoder`](https://ref.rerun.io/docs/python/stable/experimental_dataloader/#rerun.experimental.dataloader.ImageDecoder) — JPEG/PNG blobs +- [`VideoFrameDecoder`](https://ref.rerun.io/docs/python/stable/experimental_dataloader/#rerun.experimental.dataloader.VideoFrameDecoder) — compressed video (`h264`/`h265`/`av1`) + +### Windows + +`Field(window=(start, end))` returns a slice of values across an inclusive range relative to the current sample rather than a single value. +This is how action chunks and observation history are expressed. + +### Dataset styles + +- `RerunIterableDataset` — streaming with automatic shuffling and cross-worker and DDP partitioning +- `RerunMapDataset` — random access by global index; works with PyTorch samplers like `DistributedSampler` and `WeightedRandomSampler` + +See [Train PyTorch models with Rerun](../howto/train/dataloader.md) for usage. diff --git a/docs/content/concepts/visualization/customize-views.md b/docs/content/concepts/visualization/customize-views.md index 4e45cb001e9e..a79cdc034d1d 100644 --- a/docs/content/concepts/visualization/customize-views.md +++ b/docs/content/concepts/visualization/customize-views.md @@ -27,7 +27,7 @@ Views rely on visualizers to display each of their entities. For example, [3D views](../../reference/types/views/spatial3d_view.md) use the `Points3D` visualizer to display 3D point clouds, and [time series views](../../reference/types/views/time_series_view.md) use the `SeriesLines` visualizer to display time series line plots. Which visualizers are available is highly dependent on the specific kind of view. -For example, the `SeriesLines` visualizer only exists for time series views—not, e.g., for 3D views. +For example, the `SeriesLines` visualizer only exists for time series views — not, e.g., for 3D views. For a given view, each entity's components determine which visualizers are available. By default, visualizers are selected for entities logged with a corresponding [archetype](../../reference/types/archetypes.md). @@ -105,6 +105,7 @@ A powerful mechanism that is built into visualizers is the option to source comp Within a view, a visualizer can pick up any component that has the same datatype as the builtin type that it expects. For example, the `SeriesLines` and `SeriesPoints` visualizers can pick up any numerical data for their [`Scalar`](../../reference/types/components/scalar.md) component. The same holds for String-like components that can be selected for [`Name`](../../reference/types/components/name.md). +Likewise, the state timeline view's visualizer can source its `StateChange:state` input from any string, boolean, or numeric component (see [Visualize state changes](../../howto/visualization/state-timeline.md)). Such data often comes from MCAP data that has user-defined message types, or from components that were flexibly logged via [`AnyValues`](https://ref.rerun.io/docs/python/main/common/custom_data/#rerun.AnyValues) or [`DynamicArchetype`](https://ref.rerun.io/docs/python/main/common/custom_data/#rerun.DynamicArchetype). The Viewer can even look for data with compatible datatypes in nested fields of Arrow [`StructArrays`](https://docs.rs/arrow/latest/arrow/array/struct.StructArray.html). @@ -118,7 +119,7 @@ Suitable components show up in the source dropdown: -> #12661: Currently, only the time series view allows remapping of required components (scalars). All other visualizers require matching Rerun semantics (correct archetype & type metadata) for their required fields. +> #12661: Currently, only the time series view (scalars) and the state timeline view (state values) allow remapping of required components. All other visualizers require matching Rerun semantics (correct archetype & type metadata) for their required fields. As always, component mappings can be set via the blueprint APIs: diff --git a/docs/content/development/roadmap.md b/docs/content/development/roadmap.md index cf6e129de196..f7f5e55481ce 100644 --- a/docs/content/development/roadmap.md +++ b/docs/content/development/roadmap.md @@ -14,7 +14,7 @@ This page is meant to give an high level overview of ongoing and planned work. T - Performance improvements - UX & DX improvements - Supporting more data types -- Rerun Cloud features (commercial) +- Rerun Hub features (commercial) - Get in touch on hi@rerun.io if you're interested in becoming a design partner ## Roadmap of major feature areas diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md index 3d5063f9ea8c..3288a580bd33 100644 --- a/docs/content/getting-started.md +++ b/docs/content/getting-started.md @@ -3,20 +3,76 @@ title: Getting Started order: 1 --- -Rerun helps robotics and Physical AI teams iterate faster by providing unified infrastructure for working with multimodal data and time series data. +Rerun helps robotics and Physical AI teams iterate faster: log from any sensor, visualize in the Viewer, query with dataframes, and train with a dataloader tailored to robotic learning — across one recording or many. -With Rerun, you can log data from any sensor, visualize it in our interactive viewer, and query it using dataframes. You can do this with a simple SDK that understands time and natively handles multi-rate data. +## Installation -## Choose your task +`pip install rerun-sdk[dataplatform, dataloader]` bundles the **SDK** (log/query from code) and the **Viewer** (visualizer app). The optional dependencies support queries and training below. +For Rust, C++, see [Install Rerun](./getting-started/install-rerun.md) and [Set up a project](./getting-started/project-setup.md). -Walking through a practical example is the best way to get up to speed with Rerun. +## Open the Viewer -- [Log and Ingest](./getting-started/data-in.md) -- [Visualize](./getting-started/configure-the-viewer.md) -- [Query and Transform](./getting-started/data-out.md) +`rerun` launches the Viewer. +Pass a file to open it directly: -## If you're having problems +```bash +rerun path/to/recording.rrd +``` -- Checkout out our [troubleshooting guide](./overview/installing-rerun/troubleshooting.md). -- [open an issue](https://github.com/rerun-io/rerun/issues/new/choose). -- Or [join the Discord server](https://discord.gg/PXtCgFBSmH). +Supports `.rrd`, `.mcap`, and [more](./getting-started/data-in/open-any-file.md). +Also available in-browser at [rerun.io/viewer](https://rerun.io/viewer). + + +## Scale across many recordings + +Rerun's catalog organizes recordings as queryable [**segments**](./concepts/query-and-transform/catalog-object-model.md). +The workflow: log (or convert) data to an `.rrd`, start a catalog server (or connect to an existing one if using the commercial Rerun Hub), register the `.rrd` as a segment, then visualize and query across recordings. + +### Log + +Save data to an `.rrd` see [Log and Ingest](./getting-started/data-in.md) for more details. +If you already have data in another format see our [how-to](https://rerun.io/docs/howto/logging-and-ingestion) for various examples converting to `.rrd`. + +snippet: tutorials/getting_started_log +### Start a catalog server + +`rerun server` starts a local catalog on port `51234` (use Rerun Hub for persistent, multi-user storage), then connect from your code: + +```bash +rerun server +``` + +snippet: tutorials/getting_started[setup] + +### Ingest + +Register an `.rrd` with a dataset so it shows up as a queryable segment. + +snippet: tutorials/getting_started[ingest] + +### Visualize + +Point the Viewer at your server to browse every recording in the catalog. +See [Configure the Viewer](./getting-started/configure-the-viewer.md). + +```bash +rerun rerun+http://127.0.0.1:51234 +``` + +### Query + +Query the catalog into a [DataFusion](https://datafusion.apache.org/) DataFrame. See [Query and Transform](./getting-started/data-out.md). + +snippet: tutorials/getting_started[query] + +### Train + +Connect a Dataloader to the server to generate training batches. See [Train](./getting-started/train.md). + +snippet: tutorials/getting_started[train] + +## If you're stuck + +- Check the [troubleshooting guide](./getting-started/install-rerun/troubleshooting.md). +- [Open an issue](https://github.com/rerun-io/rerun/issues/new/choose). +- [Join the Discord server](https://discord.gg/PXtCgFBSmH). diff --git a/docs/content/getting-started/configure-the-viewer.md b/docs/content/getting-started/configure-the-viewer.md index 2d6123fdb54f..cae1d599c064 100644 --- a/docs/content/getting-started/configure-the-viewer.md +++ b/docs/content/getting-started/configure-the-viewer.md @@ -36,7 +36,7 @@ to take the images. Although the Rerun SDK is available in both Python and Rust, this walkthrough makes use the Python installation. Even if you plan to use Rerun with Rust, we still recommend having a Rerun Python environment available for quick -experimentation and working with examples. You can either follow the [Python Quickstart](data-in/python.md) or simply run: +experimentation and working with examples. You can either follow the [Log and Ingest tutorial](data-in.md) or simply run: ```bash pip install rerun-sdk diff --git a/docs/content/getting-started/configure-the-viewer/navigating-the-viewer.md b/docs/content/getting-started/configure-the-viewer/navigating-the-viewer.md index a046f65628c8..75fb7ccc10ec 100644 --- a/docs/content/getting-started/configure-the-viewer/navigating-the-viewer.md +++ b/docs/content/getting-started/configure-the-viewer/navigating-the-viewer.md @@ -190,7 +190,8 @@ Blueprint files are small, portable, and can be version-controlled alongside you Load a blueprint file using "Open…" from the file menu, or simply drag and drop the `.rbl` file into the Viewer. -**Important:** The blueprint's Application ID must match the Application ID of your recording. Blueprints are bound to specific Application IDs to ensure they work with compatible data structures. See [Application IDs](../../concepts/visualization/blueprints.md#application-ids-binding-blueprints-to-data) for more details. +> [!IMPORTANT] +> The blueprint's Application ID must match the Application ID of your recording. Blueprints are bound to specific Application IDs to ensure they work with compatible data structures. See [Application IDs](../../concepts/visualization/blueprints.md#application-ids-binding-blueprints-to-data) for more details. ### Sharing blueprints diff --git a/docs/content/getting-started/data-in.md b/docs/content/getting-started/data-in.md index a1c69ddbc4d7..f2ff6ed8a391 100644 --- a/docs/content/getting-started/data-in.md +++ b/docs/content/getting-started/data-in.md @@ -3,10 +3,238 @@ title: Log and Ingest order: 400 --- -This section shows you how to send data to Rerun from either your running applications or existing files. +In this section we'll log and visualize our first non-trivial dataset, putting many of Rerun's core concepts and features to use. -- Sending data from your code - - [C++](./data-in/cpp.md) - - [Python](./data-in/python.md) - - [Rust](./data-in/rust.md) -- [Opening files](./data-in/open-any-file.md) +In a few lines of code, we'll go from a blank sheet to something you don't see every day: an animated, interactive, DNA-shaped abacus: + + + +This guide aims to go wide instead of deep. +There are links to other doc pages where you can learn more about specific topics. + +The complete code listings for this tutorial live alongside the Rerun source tree: +[Python](https://github.com/rerun-io/rerun/tree/latest/examples/python/dna/dna.py), +[Rust](https://github.com/rerun-io/rerun/tree/latest/examples/rust/dna/src/main.rs), +[C++](https://github.com/rerun-io/rerun/tree/latest/examples/cpp/dna/main.cpp). + +## Prerequisites + +Before starting, make sure you've [installed the SDK](./install-rerun.md) and [set up a project](./project-setup.md) for your language of choice. + +## Initializing the SDK + +Create a new file (or project), import the relevant utilities from your language's SDK, and initialize a recording. Initialization names the recording with a stable [`ApplicationId`](../concepts/logging-and-ingestion/recordings.md), then spawns a [Rerun Viewer](../reference/viewer/overview.md) and connects the recording to it: + +snippet: tutorials/dna[imports] + +snippet: tutorials/dna[init] + +A stable `ApplicationId` will make the Viewer retain its UI state across runs for this specific dataset, which makes our lives much easier as we iterate. + +By default, `spawn` will start a Viewer in another process and automatically pipe the data through. There are other ways to send data to a Viewer (covered at the end of this section), but the spawn default works great as we experiment. + + + + + + + + + +## Logging our first points + +The core structure of our DNA-looking shape can easily be described using two point clouds shaped like spirals: + +snippet: tutorials/dna[first_points] + +Run your program and you should now see this scene in the viewer. +If the Viewer was still running, Rerun will simply connect to this existing session and replace the data with this new [_recording_](../concepts/logging-and-ingestion/recordings.md). + + + + + + + + + +_This is a good time to make yourself familiar with the viewer: try interacting with the scene and exploring the different menus._ +_Checkout the [Viewer Walkthrough](configure-the-viewer/navigating-the-viewer.md) and [viewer reference](../reference/viewer/overview.md) for a complete tour of the viewer's capabilities._ + +## Under the hood + +This tiny snippet of code actually holds much more than meets the eye… + +### Archetypes + +The easiest way to log geometric primitives is to use the SDK's `log` method with one of the built-in archetype classes (such as `Points3D` here). Archetypes take care of building batches of components that are recognized and correctly displayed by the Rerun viewer. + +### Components + +Under the hood, the Rerun SDK logs individual _components_ like positions, colors, and radii. Archetypes are just one high-level, convenient way of building such collections of components. For advanced use cases, it's possible to add custom components to archetypes, or even log entirely custom sets of components, bypassing archetypes altogether. + +For more information on how the Rerun data model works, refer to our section on [Entities and Components](../concepts/logging-and-ingestion/entity-component.md). For supplying your own components, see [Use custom data](../howto/logging-and-ingestion/custom-data.md). + +### Entities & hierarchies + +Note the two strings we're passing in: `"dna/structure/left"` & `"dna/structure/right"`. + +These are [_entity paths_](../concepts/logging-and-ingestion/entity-component.md), which uniquely identify each entity in our scene. Every entity is made up of a path and one or more components. +[Entity paths typically form a hierarchy](../concepts/logging-and-ingestion/entity-path.md) which plays an important role in how data is visualized and transformed (as we shall soon see). + +### Component batches + +One final observation: notice how we're logging a whole batch of points and colors all at once. +[Component batches](../concepts/logging-and-ingestion/batches.md) are first-class citizens in Rerun and come with all sorts of performance benefits and dedicated features. +You're looking at one of these dedicated features right now: notice how we're only logging a single radius for all these points, yet somehow it applies to all of them. We call this _clamping_. + +--- + +A _lot_ is happening in these two simple function calls. +Good news is: once you've digested all of the above, logging any other entity will simply be more of the same. In fact, let's go ahead and log everything else in the scene now. + +## Adding the missing pieces + +We can represent the scaffolding using a batch of 3D line strips: + +snippet: tutorials/dna[scaffolding] + +Which only leaves the beads: + +snippet: tutorials/dna[beads] + +Once again, although we are getting fancier with our array manipulations, there is nothing new here: it's all about populating archetypes and feeding them to the Rerun API. + + + + + + + + + +## Animating the beads + +### Introducing time + +Up until this point, we've completely set aside one of the core concepts of Rerun: [Time and Timelines](../concepts/logging-and-ingestion/timelines.md). + +Even so, if you look at your [Timeline View](../reference/viewer/timeline.md) right now, you'll notice that Rerun has kept track of time on your behalf anyway by memorizing when each log call occurred. + + + + + + + screenshot of the beads with the timeline + + +Unfortunately, the logging time isn't particularly helpful to us in this case: we can't have our beads animate depending on the logging time, else they would move at different speeds depending on the performance of the logging process! +For that, we need to introduce our own custom timeline that uses a deterministic clock which we control. + +Rerun has rich support for time: whether you want concurrent or disjoint timelines, out-of-order insertions or even data that lives _outside_ the timeline(s). You will find a lot of flexibility in there. + +Replace the section that logs the beads with a loop that logs them at different timestamps: + +snippet: tutorials/dna[time_loop] + +A call to `set_time` (or `set_duration_secs` in Rust / `set_time_duration` in C++) creates our new `Timeline` and makes sure that any logging calls that follow get assigned that time. +You can add as many timelines and timestamps as you want when logging data. + +> [!WARNING] +> If you run this code as is, the result will be… surprising: the beads are animating as expected, but everything we've logged until that point is gone! + + + + + + + screenshot of the surprising situation + + +Enter… + +### Latest-at semantics + +That's because the Rerun Viewer has switched to displaying your custom timeline by default, but the original data was only logged to the _default_ timeline (called `log_time`). +To fix this, set the custom timeline to time zero before logging the original structure: + +snippet: tutorials/dna[latest_at_fix] + + + + + + + screenshot after using latest-at + + +This fix actually introduces yet another very important concept in Rerun: "latest-at" semantics. +Notice how entities `"dna/structure/left"` & `"dna/structure/right"` have only ever been logged at time zero, and yet they are still visible when querying times far beyond that point. + +_Rerun always reasons in terms of "latest" data: for a given entity, it retrieves all of its most recent components at a given time._ + +## Transforming space + +There's only one thing left: our original scene had the abacus rotate along its principal axis. + +As was the case with time, (hierarchical) space transformations are first-class citizens in Rerun. +Now it's just a matter of combining the two: we need to log the transform of the scaffolding at each timestamp. + +Either expand the previous loop to include logging transforms or simply add a second loop like this: + +snippet: tutorials/dna[transform_loop] + +Voila! + + + +## Other ways of logging & visualizing data + +`spawn` is great when you're experimenting on a single machine like we did in this tutorial, but what if the logging happens on, for example, a headless computer? + +Rerun offers several solutions for such use cases. + +### Logging data over the network + +At any time, you can start a Rerun Viewer by running `rerun`. This Viewer is in fact a server that's ready to accept data over gRPC (it's listening on `0.0.0.0:9876` by default). + +On the logger side, replace the `spawn` call from above with a `connect_grpc` call to send data to any gRPC address: + +snippet: tutorials/dna_connect_grpc + +Run `rerun --help` for more options. + +### Saving & loading to/from RRD files + +Sometimes, sending data over the network is not an option. Maybe you'd like to share the data, attach it to a bug report, etc. + +Rerun has you covered: each SDK exposes a `save` method (Python: [`rr.save`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.save), Rust: [`RecordingStream::save`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.save), C++: [`RecordingStream::save`](https://ref.rerun.io/docs/cpp/stable/classrerun_1_1RecordingStream.html#a555a7940a076c93d951de5b139d14918)) that streams all logged data to disk. View the resulting file with `rerun path/to/recording.rrd`. + +You can also save a recording (or a portion of it) as you're visualizing it, directly from the viewer. + +### RRD file backwards compatibility + +RRD files saved with Rerun 0.23 or later can be opened with a newer Rerun version. +For more details and potential limitations, please refer to [our blog post](https://rerun.io/blog/release-0.23). + +> [!WARNING] +> At the moment, we only guarantee compatibility across adjacent minor versions (e.g. Rerun 0.24 can open RRDs from 0.23). + +### Rust-only: showing the Viewer in-process + +The Rust SDK can host the Viewer directly inside your application via [`rerun::native_viewer::show`](https://docs.rs/rerun/latest/rerun/native_viewer/fn.show.html), which expects a complete recording from memory rather than a live stream. This requires enabling the `native_viewer` feature in `Cargo.toml`. The Viewer blocks the main thread until closed; see the Rust API docs for details. + +## Closing + +This closes our whirlwind tour of logging with Rerun. We've barely scratched the surface of what's possible, but this should have hopefully given you plenty of pointers to start experimenting. + +As a next step, browse through our [example gallery](https://rerun.io/examples) for some more realistic example use-cases, browse the [Types](../reference/types.md) section for more simple examples of how to use the main datatypes, or dig deeper into [querying your logged data](data-out.md). + +## Opening files + +You can also open existing files (RRD, MCAP, images, video, point clouds, etc.) directly with the Viewer — see [Opening files](data-in/open-any-file.md). diff --git a/docs/content/getting-started/data-in/cpp.md b/docs/content/getting-started/data-in/cpp.md deleted file mode 100644 index e5275c7814f8..000000000000 --- a/docs/content/getting-started/data-in/cpp.md +++ /dev/null @@ -1,411 +0,0 @@ ---- -title: Send from C++ -order: 1 ---- - -In this section we'll log and visualize our first non-trivial dataset, putting many of Rerun's core concepts and features to use. - -In a few lines of code, we'll go from a blank sheet to something you don't see every day: an animated, interactive, DNA-shaped abacus: - - - -This guide aims to go wide instead of deep. -There are links to other doc pages where you can learn more about specific topics. - -At any time, you can checkout the complete code listing for this tutorial [here](https://github.com/rerun-io/rerun/tree/latest/examples/cpp/dna/main.cpp) to better keep track of the overall picture. -To build the example from the repository, run: - -```bash -cd examples/cpp/dna -cmake -B build -cmake --build build -j -``` - -And then to run it on Linux/Mac: - -``` -./build/example_dna -``` - -and Windows respectively: - -``` -build\Debug\example_dna.exe -``` - -## Prerequisites - -You should have already [installed the viewer](../../overview/installing-rerun.md). - -We assume you have a working C++ toolchain and are using `CMake` to build your project. For this example -we will let Rerun download build [Apache Arrow](https://arrow.apache.org/)'s C++ library itself. -To learn more about how Rerun's CMake script can be configured, see [CMake Setup in Detail](https://ref.rerun.io/docs/cpp/stable/md__2home_2runner_2work_2rerun_2rerun_2rerun__cpp_2cmake__setup__in__detail.html) in the C++ reference documentation. - -## Setting up your CMakeLists.txt - -A minimal CMakeLists.txt for this example looks like this: - -```cmake -cmake_minimum_required(VERSION 3.16...3.27) -project(example_dna LANGUAGES CXX) - -add_executable(example_dna main.cpp) - -# Download the rerun_sdk -include(FetchContent) -FetchContent_Declare(rerun_sdk URL - https://github.com/rerun-io/rerun/releases/latest/download/rerun_cpp_sdk.zip) -FetchContent_MakeAvailable(rerun_sdk) - -# Link against rerun_sdk. -target_link_libraries(example_dna PRIVATE rerun_sdk) -``` - -Note that Rerun requires at least C++17. Depending on the sdk will automatically ensure that C++17 or newer is enabled. - -## Includes - -To use Rerun all you need to include is `rerun.hpp`, however for this example we will pull in a few extra headers. - -Starting our `main.cpp`: - -```cpp -#include -#include - -#include // std::generate -#include -#include - -using namespace rerun::demo; -using namespace std::chrono_literals; - -static constexpr size_t NUM_POINTS = 100; -``` - -## Initializing the SDK - -To get going we want to create a [`RecordingStream`](https://github.com/rerun-io/rerun/blob/latest/rerun_cpp/src/rerun/recording_stream.hpp), which is the main interface for sending data to Rerun. -When creating the `RecordingStream` we also need to specify the name of the application we're working on -by setting it's `ApplicationId`. - -We then use the stream to spawn a new Rerun Viewer via [`spawn`](https://github.com/rerun-io/rerun/blob/d962b34b07775bbacf14883d683cca6746852b6a/rerun_cpp/src/rerun/recording_stream.hpp#L151). - -Add our initial `main` to `main.cpp`: - -```cpp -int main() { - auto rec = rerun::RecordingStream("rerun_example_dna_abacus"); - rec.spawn().exit_on_failure(); -} -``` - -Among other things, a stable `ApplicationId` will make it so the [Rerun Viewer](../../reference/viewer/overview.md) retains its UI state across runs for this specific dataset, which will make our lives much easier as we iterate. - -Check out the reference to learn more about how Rerun deals with [recordings and datasets](../../concepts/logging-and-ingestion/recordings.md). - -## Testing our app - -Even though we haven't logged any data yet this is a good time to verify everything is working. - -```bash -cmake -B build -cmake --build build -j -./build/example_dna -``` - -When everything finishes compiling, an empty Rerun Viewer should be spawned: - - - - - - - - - -## Logging our first points - -Now let's add some data to the viewer. - -The core structure of our DNA looking shape can easily be described using two point clouds shaped like spirals. -Add the following to your `main` function: - -```cpp -std::vector points1, points2; -std::vector colors1, colors2; -color_spiral(NUM_POINTS, 2.0f, 0.02f, 0.0f, 0.1f, points1, colors1); -color_spiral(NUM_POINTS, 2.0f, 0.02f, TAU * 0.5f, 0.1f, points2, colors2); - -rec.log( - "dna/structure/left", - rerun::Points3D(points1).with_colors(colors1).with_radii({0.08f}) -); -rec.log( - "dna/structure/right", - rerun::Points3D(points2).with_colors(colors2).with_radii({0.08f}) -); -``` - -Re-compile and run your program again: - -```bash -cmake --build build -j -./build/example_dna -``` - -and now you should now see this scene in the viewer: - - - - - - - - - -_This is a good time to make yourself familiar with the viewer: try interacting with the scene and exploring the different menus._ -_Checkout the [Viewer Walkthrough](../configure-the-viewer/navigating-the-viewer.md) and [viewer reference](../../reference/viewer/overview.md) for a complete tour of the viewer's capabilities._ - -## Under the hood - -This tiny snippet of code actually holds much more than meets the eye… - -### Archetypes - -The easiest way to log geometric primitives is the use the [`RecordingStream::log`](https://ref.rerun.io/docs/cpp/stable/classrerun_1_1RecordingStream.html#a7badac918d44d66e04e948f38818ff11) method with one of the built-in archetype class, such as [`Points3D`](https://github.com/rerun-io/rerun/blob/latest/rerun_cpp/src/rerun/archetypes/points3d.hpp). Archetypes take care of building batches of components that are recognized and correctly displayed by the Rerun viewer. - -### Components - -Under the hood, the Rerun C++ SDK logs individual _components_ like positions, colors, -and radii. Archetypes are just one high-level, convenient way of building such collections of components. For advanced use -cases, it's possible to add custom components to archetypes, or even log entirely custom sets of components, bypassing -archetypes altogether. -For more information on how the Rerun data model works, refer to our section on [Entities and Components](../../concepts/logging-and-ingestion/entity-component.md). - -Notably, the [`RecordingStream::log`](https://ref.rerun.io/docs/cpp/stable/classrerun_1_1RecordingStream.html#a7badac918d44d66e04e948f38818ff11) method -will handle any data type that implements the [`AsComponents`](https://github.com/rerun-io/rerun/blob/latest/rerun_cpp/src/rerun/as_components.hpp) trait, making it easy to add your own data. -For more information on how to supply your own components see [Use custom data](../../howto/logging-and-ingestion/custom-data.md). - -### Entities & hierarchies - -Note the two strings we're passing in: `"dna/structure/left"` and `"dna/structure/right"`. - -These are [_entity paths_](../../concepts/logging-and-ingestion/entity-component.md), which uniquely identify each entity in our scene. Every entity is made up of a path and one or more components. -[Entity paths typically form a hierarchy](../../concepts/logging-and-ingestion/entity-path.md) which plays an important role in how data is visualized and transformed (as we shall soon see). - -### Component batches - -One final observation: notice how we're logging a whole batch of points and colors all at once here. -[Component batches](../../concepts/logging-and-ingestion/batches.md) are first-class citizens in Rerun and come with all sorts of performance benefits and dedicated features. -You're looking at one of these dedicated features right now in fact: notice how we're only logging a single radius for all these points, yet somehow it applies to all of them. We call this _clamping_. - ---- - -A _lot_ is happening in these two simple function calls. -Good news is: once you've digested all of the above, logging any other entity will simply be more of the same. In fact, let's go ahead and log everything else in the scene now. - -## Adding the missing pieces - -We can represent the scaffolding using a batch of 3D line segments: - -```cpp -std::vector lines; -for (size_t i = 0; i < points1.size(); ++i) { - lines.emplace_back(rerun::LineStrip3D({points1[i].xyz, points2[i].xyz})); -} - -rec.log( - "dna/structure/scaffolding", - rerun::LineStrips3D(lines).with_colors(rerun::Color(128, 128, 128)) -); -``` - -Which only leaves the beads: - -```cpp -std::default_random_engine gen; -std::uniform_real_distribution dist(0.0f, 1.0f); -std::vector offsets(NUM_POINTS); -std::generate(offsets.begin(), offsets.end(), [&] { return dist(gen); }); - -std::vector beads_positions(lines.size()); -std::vector beads_colors(lines.size()); - -for (size_t i = 0; i < lines.size(); ++i) { - float offset = offsets[i]; - auto c = static_cast(bounce_lerp(80.0f, 230.0f, offset * 2.0f)); - - beads_positions[i] = rerun::Position3D( - bounce_lerp(lines[i].points[0].x(), lines[i].points[1].x(), offset), - bounce_lerp(lines[i].points[0].y(), lines[i].points[1].y(), offset), - bounce_lerp(lines[i].points[0].z(), lines[i].points[1].z(), offset) - ); - beads_colors[i] = rerun::Color(c, c, c); -} - -rec.log( - "dna/structure/scaffolding/beads", - rerun::Points3D(beads_positions).with_colors(beads_colors).with_radii({0.06f}) -); -``` - -Once again, although we are getting fancier and fancier with our iterator mappings, there is nothing new here: it's all about populating archetypes and feeding them to the Rerun API. - - - - - - - - - -## Animating the beads - -### Introducing time - -Up until this point, we've completely set aside one of the core concepts of Rerun: [Time and Timelines](../../concepts/logging-and-ingestion/timelines.md). - -Even so, if you look at your [Timeline View](../../reference/viewer/timeline.md) right now, you'll notice that Rerun has kept track of time on your behalf anyway by memorizing when each log call occurred. - - - - - - - screenshot of the beads with the timeline - - -Unfortunately, the logging time isn't particularly helpful to us in this case: we can't have our beads animate depending on the logging time, else they would move at different speeds depending on the performance of the logging process! -For that, we need to introduce our own custom timeline that uses a deterministic clock which we control. - -Rerun has rich support for time: whether you want concurrent or disjoint timelines, out-of-order insertions or even data that lives _outside_ the timeline(s). You will find a lot of flexibility in there. - -Let's add our custom timeline. - -Replace the section that logs the beads with a loop that logs the beads at different timestamps: - -```cpp -for (int t = 0; t < 400; t++) { - auto time = std::chrono::duration(t) * 0.01f; - - rec.set_time_duration("stable_time", time); - - for (size_t i = 0; i < lines.size(); ++i) { - float time_offset = time.count() + offsets[i]; - auto c = static_cast(bounce_lerp(80.0f, 230.0f, time_offset * 2.0f)); - - beads_positions[i] = rerun::Position3D( - bounce_lerp(lines[i].points[0].x(), lines[i].points[1].x(), time_offset), - bounce_lerp(lines[i].points[0].y(), lines[i].points[1].y(), time_offset), - bounce_lerp(lines[i].points[0].z(), lines[i].points[1].z(), time_offset) - ); - beads_colors[i] = rerun::Color(c, c, c); - } - - rec.log( - "dna/structure/scaffolding/beads", - rerun::Points3D(beads_positions).with_colors(beads_colors).with_radii({0.06f}) - ); -} -``` - -First we use [`RecordingStream::set_time_secs`](https://ref.rerun.io/docs/cpp/stable/classrerun_1_1RecordingStream.html#ad735156502aea8eecd0a5eb2f6678d55) to declare our own custom `Timeline` and set the current timestamp. -You can add as many timelines and timestamps as you want when logging data. - -⚠️ If you run this code as is, the result will be.. surprising: the beads are animating as expected, but everything we've logged until that point is gone! ⚠️ - -![logging data - wat](https://static.rerun.io/a396c8aae1cbd717a3f35472594f789e4829b1ae_logging_data7_wat.png) - -Enter… - -### Latest-at semantics - -That's because the Rerun Viewer has switched to displaying your custom timeline by default, but the original data was only logged to the _default_ timeline (called `log_time`). -To fix this, go back to the top of your main and initialize your timeline before logging the initial structure: - -```cpp -rec.set_time_duration_secs("stable_time", 0.0f); - -rec.log( - "dna/structure/left", - rerun::Points3D(points1).with_colors(colors1).with_radii({0.08f}) -); -rec.log( - "dna/structure/right", - rerun::Points3D(points2).with_colors(colors2).with_radii({0.08f}) -); -``` - - - - - - - screenshot after using latest-at - - -This fix actually introduces yet another very important concept in Rerun: "latest-at" semantics. -Notice how entities `"dna/structure/left"` & `"dna/structure/right"` have only ever been logged at time zero, and yet they are still visible when querying times far beyond that point. - -_Rerun always reasons in terms of "latest" data: for a given entity, it retrieves all of its most recent components at a given time._ - -## Transforming space - -There's only one thing left: our original scene had the abacus rotate along its principal axis. - -As was the case with time, (hierarchical) space transformations are first class-citizens in Rerun. -Now it's just a matter of combining the two: we need to log the transform of the scaffolding at each timestamp. - -Either expand the previous loop to include logging transforms or -simply add a second loop like this: - -```cpp -for (int t = 0; t < 400; t++) { - auto time = std::chrono::duration(t) * 0.01f; - - rec.set_time_duration("stable_time", time); - - rec.log( - "dna/structure", - rerun::archetypes::Transform3D(rerun::RotationAxisAngle( - {0.0f, 0.0f, 1.0f}, - rerun::Angle::radians(time.count() / 4.0f * TAU) - )) - ); -} -``` - -Voila! - - - -## Other ways of logging & visualizing data - -### Saving & loading to/from RRD files - -Sometimes, sending the data over the network is not an option. Maybe you'd like to share the data, attach it to a bug report, etc. - -Rerun has you covered: - -- Use [`RecordingStream::save`](https://ref.rerun.io/docs/cpp/stable/classrerun_1_1RecordingStream.html#a555a7940a076c93d951de5b139d14918) to stream all logging data to disk. -- Visualize it via `rerun path/to/recording.rrd` - -You can also save a recording (or a portion of it) as you're visualizing it, directly from the viewer. - -### RRD file backwards compatibility - -RRD files saved with Rerun 0.23 or later can be opened with a newer Rerun version. -For more details and potential limitations, please refer to [our blog post](https://rerun.io/blog/release-0.23). - -⚠️ At the moment, we only guarantee compatibility across adjacent minor versions (e.g. Rerun 0.24 can open RRDs from 0.23). - -### Closing - -This closes our whirlwind tour of logging with Rerun. We've barely scratched the surface of what's possible, but this should have hopefully given you plenty pointers to start experimenting. - -As a next step, browse through our [example gallery](https://rerun.io/examples) for some more realistic example use-cases, browse the [Types](../../reference/types.md) section for more simple examples of how to use the main data types, or dig deeper into [querying your logged data](../data-out.md). diff --git a/docs/content/getting-started/data-in/open-any-file.md b/docs/content/getting-started/data-in/open-any-file.md index 1b69d2ca055a..335280fce8fe 100644 --- a/docs/content/getting-started/data-in/open-any-file.md +++ b/docs/content/getting-started/data-in/open-any-file.md @@ -3,7 +3,7 @@ title: Opening files order: 4 --- -The Rerun Viewer and SDK have built-in support for opening many kinds of files, and can be extended to support any other file type without needing to modify the Rerun codebase itself. +The Rerun Viewer and SDK have built-in support for opening many kinds of files, and can be [extended](../../concepts/logging-and-ingestion/importers/overview.md) to support any other file type without needing to modify the Rerun codebase itself. The Viewer can load files in 3 different ways: @@ -13,7 +13,8 @@ The Viewer can load files in 3 different ways: All these file loading methods support loading a single file, many files at once (e.g. `rerun myfiles/*`), or even folders. -⚠ Drag-and-drop of folders does [not yet work](https://github.com/rerun-io/rerun/issues/4528) on the web version of the Rerun Viewer ⚠ +> [!WARNING] +> Drag-and-drop of folders does [not yet work](https://github.com/rerun-io/rerun/issues/4528) on the web version of the Rerun Viewer. The following data types have built-in support in the Rerun Viewer and SDK: @@ -30,6 +31,7 @@ With the exception of `rrd` files that can be streamed from an HTTP URL (e.g. `r To log the contents of a file from the SDK you can use the `log_file_from_path` and `log_file_from_contents` methods ([C++](https://ref.rerun.io/docs/cpp/stable/classrerun_1_1RecordingStream.html#a8f253422a7adc2a19b89d1538c05bcac), [Python](https://ref.rerun.io/docs/python/stable/common/other_classes_and_functions/#rerun.log_file_from_path), [Rust](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.log_file_from_path)) and the associated examples ([C++](https://github.com/rerun-io/rerun/blob/main/examples/cpp/log_file/main.cpp), [Python](https://github.com/rerun-io/rerun/blob/main/examples/python/log_file/log_file.py), [Rust](https://github.com/rerun-io/rerun/blob/main/examples/rust/log_file/src/main.rs)). -Note: when calling these APIs from the SDK, the data will be loaded by the process running the SDK, not the Viewer! +> [!NOTE] +> When calling these APIs from the SDK, the data will be loaded by the process running the SDK, not the Viewer! snippet: tutorials/log-file diff --git a/docs/content/getting-started/data-in/python.md b/docs/content/getting-started/data-in/python.md deleted file mode 100644 index 60a7182b3e30..000000000000 --- a/docs/content/getting-started/data-in/python.md +++ /dev/null @@ -1,314 +0,0 @@ ---- -title: Send from Python -order: 2 ---- - -In this section we'll log and visualize our first non-trivial dataset, putting many of Rerun's core concepts and features to use. - -In a few lines of code, we'll go from a blank sheet to something you don't see every day: an animated, interactive, DNA-shaped abacus: - - - -This guide aims to go wide instead of deep. -There are links to other doc pages where you can learn more about specific topics. - -At any time, you can checkout the complete code listing for this tutorial [here](https://github.com/rerun-io/rerun/tree/latest/examples/python/dna/dna.py) to better keep track of the overall picture. - -## Prerequisites - -We assume you have working Python and `rerun-sdk` installations. If not, check out [installing python](../../overview/installing-rerun/python.md). - -## Initializing the SDK - -Start by opening your editor of choice and creating a new file called `dna_example.py`. - -The first thing we need to do is to import `rerun` and initialize the SDK by calling [`rr.init`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.init). This init call is required prior to using any of the global -logging calls, and allows us to name our recording using an `ApplicationId`. - -We also import some other utilities we will use later in the example. - -```python -import rerun as rr - -from math import tau -import numpy as np -from rerun.utilities import build_color_spiral -from rerun.utilities import bounce_lerp - -rr.init("rerun_example_dna_abacus") -``` - -A stable [`ApplicationId`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.init) will make it so the [Rerun Viewer](../../reference/viewer/overview.md) retains its UI state across runs for this specific dataset, which will make our lives much easier as we iterate. - -Check out the reference to learn more about how Rerun deals with [applications and recordings](../../concepts/logging-and-ingestion/recordings.md). - -## Starting the Viewer - -Next up, we want to spawn the [Rerun Viewer](../../reference/viewer/overview.md) itself. - -To do this, you can add the line: - -```python -rr.spawn() -``` - -Now you can run your application just as you would any other Python script: - -``` -(venv) $ python dna_example.py -``` - -And with that, we're ready to start sending out data: - - - - - - - - - -By default, the SDK will start a Viewer in another process and automatically pipe the data through. -There are other means of sending data to a Viewer as we'll see at the end of this section, but for now this default will work great as we experiment. - -## Logging our first points - -The core structure of our DNA looking shape can easily be described using two point clouds shaped like spirals. -Add the following to your file: - -```python -NUM_POINTS = 100 - -# Points and colors are both np.array((NUM_POINTS, 3)) -points1, colors1 = build_color_spiral(NUM_POINTS) -points2, colors2 = build_color_spiral(NUM_POINTS, angular_offset=tau * 0.5) - -rr.log("dna/structure/left", rr.Points3D(points1, colors=colors1, radii=0.08)) -rr.log("dna/structure/right", rr.Points3D(points2, colors=colors2, radii=0.08)) -``` - -Run your script once again and you should now see this scene in the viewer. -Note that if the Viewer was still running, Rerun will simply connect to this existing session and replace the data with this new [_recording_](../../concepts/logging-and-ingestion/recordings.md). - - - - - - - - - -_This is a good time to make yourself familiar with the viewer: try interacting with the scene and exploring the different menus._ -_Checkout the [Viewer Walkthrough](../configure-the-viewer/navigating-the-viewer.md) and [viewer reference](../../reference/viewer/overview.md) for a complete tour of the viewer's capabilities._ - -## Under the hood - -This tiny snippet of code actually holds much more than meets the eye… - -### Archetypes - -The easiest way to log geometric primitives is the use the [`rr.log`](https://ref.rerun.io/docs/python/stable/common/logging_functions/#rerun.log) function with one of the built-in archetype classes, such as [`rr.Points3D`](https://ref.rerun.io/docs/python/stable/common/archetypes/#rerun.archetypes.Points3D). Archetypes take care of building batches -of components that are recognized and correctly displayed by the Rerun viewer. - -### Components - -Under the hood, the Rerun [Python SDK](https://ref.rerun.io/docs/python) logs individual _components_ like positions, colors, -and radii. Archetypes are just one high-level, convenient way of building such collections of components. For advanced use -cases, it's possible to add custom components to archetypes, or even log entirely custom sets of components, bypassing -archetypes altogether. - -For more information on how the Rerun data model works, refer to our section on [Entities and Components](../../concepts/logging-and-ingestion/entity-component.md). - -Our [Python SDK](https://ref.rerun.io/docs/python) integrates with the rest of the Python ecosystem: the points and colors returned by [`build_color_spiral`](https://ref.rerun.io/docs/python/stable/common/utilities/#rerun.utilities.build_color_spiral) in this example are vanilla `numpy` arrays. -Rerun takes care of mapping those arrays to actual Rerun components depending on the context (e.g. we're calling [`rr.Points3D`](https://ref.rerun.io/docs/python/stable/common/archetypes/#rerun.archetypes.Points3D) in this case). - -### Entities & hierarchies - -Note the two strings we're passing in: `"dna/structure/left"` & `"dna/structure/right"`. - -These are [_entity paths_](../../concepts/logging-and-ingestion/entity-component.md), which uniquely identify each entity in our scene. Every entity is made up of a path and one or more components. -[Entity paths typically form a hierarchy](../../concepts/logging-and-ingestion/entity-path.md) which plays an important role in how data is visualized and transformed (as we shall soon see). - -### Component batches - -One final observation: notice how we're logging a whole batch of points and colors all at once here. -[Component batches](../../concepts/logging-and-ingestion/batches.md) are first-class citizens in Rerun and come with all sorts of performance benefits and dedicated features. -You're looking at one of these dedicated features right now in fact: notice how we're only logging a single radius for all these points, yet somehow it applies to all of them. We call this _clamping_. - ---- - -A _lot_ is happening in these two simple function calls. -Good news is: once you've digested all of the above, logging any other entity will simply be more of the same. In fact, let's go ahead and log everything else in the scene now. - -## Adding the missing pieces - -We can represent the scaffolding using a batch of 3D line strips: - -```python -rr.log("dna/structure/scaffolding", rr.LineStrips3D(np.stack((points1, points2), axis=1), colors=[128, 128, 128])) -``` - -Which only leaves the beads: - -```python -offsets = np.random.rand(NUM_POINTS) -beads = [bounce_lerp(points1[n], points2[n], offsets[n]) for n in range(NUM_POINTS)] -colors = [[int(bounce_lerp(80, 230, offsets[n] * 2))] for n in range(NUM_POINTS)] -rr.log( - "dna/structure/scaffolding/beads", - rr.Points3D(beads, radii=0.06, colors=np.repeat(colors, 3, axis=-1)), -) -``` - -Once again, although we are getting fancier and fancier with our [`numpy` incantations](https://ref.rerun.io/docs/python/stable/common/utilities/#rerun.utilities.util.bounce_lerp), -there is nothing new here: it's all about building out `numpy` arrays and feeding them to the Rerun API. - - - - - - - - - -## Animating the beads - -### Introducing time - -Up until this point, we've completely set aside one of the core concepts of Rerun: [Time and Timelines](../../concepts/logging-and-ingestion/timelines.md). - -Even so, if you look at your [Timeline View](../../reference/viewer/timeline.md) right now, you'll notice that Rerun has kept track of time on your behalf anyway by memorizing when each log call occurred. - - - - - - - screenshot of the beads with the timeline - - -Unfortunately, the logging time isn't particularly helpful to us in this case: we can't have our beads animate depending on the logging time, else they would move at different speeds depending on the performance of the logging process! -For that, we need to introduce our own custom timeline that uses a deterministic clock which we control. - -Rerun has rich support for time: whether you want concurrent or disjoint timelines, out-of-order insertions or even data that lives _outside_ the timeline(s). You will find a lot of flexibility in there. - -Let's add our custom timeline: - -```python -time_offsets = np.random.rand(NUM_POINTS) - -for i in range(400): - time = i * 0.01 - rr.set_time("stable_time", duration=time) - - times = np.repeat(time, NUM_POINTS) + time_offsets - beads = [bounce_lerp(points1[n], points2[n], times[n]) for n in range(NUM_POINTS)] - colors = [[int(bounce_lerp(80, 230, times[n] * 2))] for n in range(NUM_POINTS)] - rr.log( - "dna/structure/scaffolding/beads", - rr.Points3D(beads, radii=0.06, colors=np.repeat(colors, 3, axis=-1)), - ) -``` - -A call to [`set_time`](https://ref.rerun.io/docs/python/stable/common/logging_functions/#rerun.set_time) will create our new `Timeline` and make sure that any logging calls that follow gets assigned that time. - -⚠️ If you run this code as is, the result will be… surprising: the beads are animating as expected, but everything we've logged until that point is gone! ⚠️ - - - - - - - screenshot of the surprising situation - - -Enter… - -### Latest-at semantics - -That's because the Rerun Viewer has switched to displaying your custom timeline by default, but the original data was only logged to the _default_ timeline (called `log_time`). -To fix this, go back to the top of the file and add: - -```python -rr.spawn() -rr.set_time("stable_time", duration=0) -``` - - - - - - - screenshot after using latest-at - - -This fix actually introduces yet another very important concept in Rerun: "latest-at" semantics. -Notice how entities `"dna/structure/left"` & `"dna/structure/right"` have only ever been logged at time zero, and yet they are still visible when querying times far beyond that point. - -_Rerun always reasons in terms of "latest" data: for a given entity, it retrieves all of its most recent components at a given time._ - -## Transforming space - -There's only one thing left: our original scene had the abacus rotate along its principal axis. - -As was the case with time, (hierarchical) space transformations are first class-citizens in Rerun. -Now it's just a matter of combining the two: we need to log the transform of the scaffolding at each timestamp. - -Either expand the previous loop to include logging transforms or -simply add a second loop like this: - -```python -for i in range(400): - time = i * 0.01 - rr.set_time("stable_time", duration=time) - rr.log( - "dna/structure", - rr.Transform3D(rotation=rr.RotationAxisAngle(axis=[0, 0, 1], radians=time / 4.0 * tau)), - ) -``` - -Voila! - - - -## Other ways of logging & visualizing data - -[`rr.spawn`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.spawn) is great when you're experimenting on a single machine like we did in this tutorial, but what if the logging happens on, for example, a headless computer? - -Rerun offers several solutions for such use cases. - -### Logging data over the network - -At any time, you can start a Rerun Viewer by running `rerun`. This Viewer is in fact a server that's ready to accept data over gRPC (it's listening on `0.0.0.0:9876` by default). - -On the logger side, simply use [`rr.connect_grpc`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.connect_grpc) instead of [`rr.spawn`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.spawn) to start sending the data over to any gRPC address. - -Checkout `rerun --help` for more options. - -### Saving & loading to/from RRD files - -Sometimes, sending the data over the network is not an option. Maybe you'd like to share the data, attach it to a bug report, etc. - -Rerun has you covered: - -- Use [`rr.save`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.save) to stream all logged data to disk. -- View it with `rerun path/to/recording.rrd` - -You can also save a recording (or a portion of it) as you're visualizing it, directly from the viewer. - -### RRD file backwards compatibility - -RRD files saved with Rerun 0.23 or later can be opened with a newer Rerun version. -For more details and potential limitations, please refer to [our blog post](https://rerun.io/blog/release-0.23). - -⚠️ At the moment, we only guarantee compatibility across adjacent minor versions (e.g. Rerun 0.24 can open RRDs from 0.23). - -## Closing - -This closes our whirlwind tour of logging with Rerun. We've barely scratched the surface of what's possible, but this should have hopefully given you plenty pointers to start experimenting. - -As a next step, browse through our [example gallery](https://rerun.io/examples) for some more realistic example use-cases, browse the [Types](../../reference/types.md) section for more simple examples of how to use the main datatypes, or dig deeper into [querying your logged data](../data-out.md). diff --git a/docs/content/getting-started/data-in/rust.md b/docs/content/getting-started/data-in/rust.md deleted file mode 100644 index a74757397079..000000000000 --- a/docs/content/getting-started/data-in/rust.md +++ /dev/null @@ -1,394 +0,0 @@ ---- -title: Send from Rust -order: 3 ---- - -In this section we'll log and visualize our first non-trivial dataset, putting many of Rerun's core concepts and features to use. - -In a few lines of code, we'll go from a blank sheet to something you don't see every day: an animated, interactive, DNA-shaped abacus: - - - -This guide aims to go wide instead of deep. -There are links to other doc pages where you can learn more about specific topics. - -At any time, you can checkout the complete code listing for this tutorial [here](https://github.com/rerun-io/rerun/tree/latest/examples/rust/dna/src/main.rs) to better keep track of the overall picture. -To run the example from the repository, run `cargo run -p dna`. - -## Prerequisites - -We assume you have a working Rust environment and have started a new project with the `rerun` dependency. If not, check out the [installing rust](../../overview/installing-rerun/rust.md). - -For this example in particular, we're going to need all of these: - -```toml -[dependencies] -rerun = "0.23" -itertools = "0.14" -rand = "0.8" -``` - -While we're at it, let's get imports out of the way: - -```rust -use std::f32::consts::TAU; - -use itertools::Itertools as _; -use rand::Rng as _; -use rerun::{ - demo_util::{bounce_lerp, color_spiral}, - external::glam, -}; -``` - -## Starting the Viewer - -Just run `rerun` to start the [Rerun Viewer](../../reference/viewer/overview.md). It will wait for your application to log some data to it. This Viewer is in fact a server that's ready to accept data over gRPC (it's listening on `0.0.0.0:9876` by default). - -Checkout `rerun --help` for more options. - - - - - - - - - -## Initializing the SDK - -To get going we want to create a [`RecordingStream`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html): -We can do all of this with the [`rerun::RecordingStreamBuilder::new`](https://docs.rs/rerun/latest/rerun/struct.RecordingStreamBuilder.html#method.new) function which allows us to name the dataset we're working on by setting its [`ApplicationId`](https://docs.rs/rerun/latest/rerun/struct.ApplicationId.html). -We then connect it to the already running Viewer via [`connect_grpc`](https://docs.rs/rerun/latest/rerun/struct.RecordingStreamBuilder.html#method.connect_grpc), returning the `RecordingStream` upon success. - -```rust -fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_dna_abacus") - .connect_grpc()?; - - Ok(()) -} -``` - -Among other things, a stable [`ApplicationId`](https://docs.rs/rerun/latest/rerun/struct.ApplicationId.html) will make it so the [Rerun Viewer](../../reference/viewer/overview.md) retains its UI state across runs for this specific dataset, which will make our lives much easier as we iterate. - -Check out the reference to learn more about how Rerun deals with [recordings and datasets](../../concepts/logging-and-ingestion/recordings.md). - -## Logging our first points - -The core structure of our DNA looking shape can easily be described using two point clouds shaped like spirals. -Add the following to your `main` function: - -```rust -const NUM_POINTS: usize = 100; - -let (points1, colors1) = color_spiral(NUM_POINTS, 2.0, 0.02, 0.0, 0.1); -let (points2, colors2) = color_spiral(NUM_POINTS, 2.0, 0.02, TAU * 0.5, 0.1); - -rec.log( - "dna/structure/left", - &rerun::Points3D::new(points1.iter().copied()) - .with_colors(colors1) - .with_radii([0.08]), -)?; -rec.log( - "dna/structure/right", - &rerun::Points3D::new(points2.iter().copied()) - .with_colors(colors2) - .with_radii([0.08]), -)?; -``` - -Run your program with `cargo run` and you should now see this scene in the viewer: - - - - - - - - - -_This is a good time to make yourself familiar with the viewer: try interacting with the scene and exploring the different menus._ -_Checkout the [Viewer Walkthrough](../configure-the-viewer/navigating-the-viewer.md) and [viewer reference](../../reference/viewer/overview.md) for a complete tour of the viewer's capabilities._ - -## Under the hood - -This tiny snippet of code actually holds much more than meets the eye… - -### Archetypes - - - -The easiest way to log geometric primitives is the use the [`RecordingStream::log`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.log) method with one of the built-in archetype class, such as [`Points3D`](https://docs.rs/rerun/latest/struct.Points3D.html). Archetypes take care of building batches -of components that are recognized and correctly displayed by the Rerun viewer. - -### Components - -Under the hood, the Rerun [Rust SDK](https://docs.rs/rerun) logs individual _components_ like positions, colors, -and radii. Archetypes are just one high-level, convenient way of building such collections of components. For advanced use -cases, it's possible to add custom components to archetypes, or even log entirely custom sets of components, bypassing -archetypes altogether. -For more information on how the Rerun data model works, refer to our section on [Entities and Components](../../concepts/logging-and-ingestion/entity-component.md). - -Notably, the [`RecordingStream::log`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.log) method - - - -will handle any data type that implements the [`AsComponents`](https://docs.rs/rerun/latest/rerun/trait.AsComponents.html) trait, making it easy to add your own data. -For more information on how to supply your own components see [Use custom data](../../howto/logging-and-ingestion/custom-data.md). - -### Entities & hierarchies - -Note the two strings we're passing in: `"dna/structure/left"` and `"dna/structure/right"`. - -These are [_entity paths_](../../concepts/logging-and-ingestion/entity-component.md), which uniquely identify each entity in our scene. Every entity is made up of a path and one or more components. -[Entity paths typically form a hierarchy](../../concepts/logging-and-ingestion/entity-path.md) which plays an important role in how data is visualized and transformed (as we shall soon see). - -### Component batches - -One final observation: notice how we're logging a whole batch of points and colors all at once here. -[Component batches](../../concepts/logging-and-ingestion/batches.md) are first-class citizens in Rerun and come with all sorts of performance benefits and dedicated features. -You're looking at one of these dedicated features right now in fact: notice how we're only logging a single radius for all these points, yet somehow it applies to all of them. We call this _clamping_. - ---- - -A _lot_ is happening in these two simple function calls. -Good news is: once you've digested all of the above, logging any other entity will simply be more of the same. In fact, let's go ahead and log everything else in the scene now. - -## Adding the missing pieces - -We can represent the scaffolding using a batch of 3D line segments: - -```rust -let lines: Vec<[glam::Vec3; 2]> = points1 - .iter() - .zip(&points2) - .map(|(&p1, &p2)| (p1, p2).into()) - .collect_vec(); - -rec.log( - "dna/structure/scaffolding", - &rerun::LineStrips3D::new(lines.iter().cloned()) - .with_colors([rerun::Color::from_rgb(128, 128, 128)]), -)?; -``` - -Which only leaves the beads: - -```rust -let mut rng = rand::rng(); -let offsets = (0..NUM_POINTS).map(|_| rng.random::()).collect_vec(); - -let beads = lines - .iter() - .zip(&offsets) - .map(|(&[p1, p2], &offset)| bounce_lerp(p1, p2, offset)) - .collect_vec(); -let colors = offsets - .iter() - .map(|&offset| bounce_lerp(80.0, 230.0, offset * 2.0) as u8) - .map(|c| rerun::Color::from_rgb(c, c, c)) - .collect_vec(); - -rec.log( - "dna/structure/scaffolding/beads", - &rerun::Points3D::new(beads) - .with_colors(colors) - .with_radii([0.06]), -)?; -``` - -Once again, although we are getting fancier and fancier with our iterator mappings, there is nothing new here: it's all about populating archetypes and feeding them to the Rerun API. - - - - - - - - - -## Animating the beads - -### Introducing time - -Up until this point, we've completely set aside one of the core concepts of Rerun: [Time and Timelines](../../concepts/logging-and-ingestion/timelines.md). - -Even so, if you look at your [Timeline View](../../reference/viewer/timeline.md) right now, you'll notice that Rerun has kept track of time on your behalf anyway by memorizing when each log call occurred. - - - - - - - screenshot of the beads with the timeline - - -Unfortunately, the logging time isn't particularly helpful to us in this case: we can't have our beads animate depending on the logging time, else they would move at different speeds depending on the performance of the logging process! -For that, we need to introduce our own custom timeline that uses a deterministic clock which we control. - -Rerun has rich support for time: whether you want concurrent or disjoint timelines, out-of-order insertions or even data that lives _outside_ the timeline(s). You will find a lot of flexibility in there. - -Let's add our custom timeline: - -```rust -for i in 0..400 { - let time = i as f32 * 0.01; - - rec.set_duration_secs("stable_time", time); - - let times = offsets.iter().map(|offset| time + offset).collect_vec(); - let beads = lines - .iter() - .zip(×) - .map(|(&[p1, p2], &time)| bounce_lerp(p1, p2, time)) - .collect_vec(); - let colors = times - .iter() - .map(|time| bounce_lerp(80.0, 230.0, time * 2.0) as u8) - .map(|c| rerun::Color::from_rgb(c, c, c)) - .collect_vec(); - - rec.log( - "dna/structure/scaffolding/beads", - &rerun::Points3D::new(beads) - .with_colors(colors) - .with_radii([0.06]), - )?; -} -``` - -First we use [`RecordingStream::set_time_seconds`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.set_time_seconds) to declare our own custom `Timeline` and set the current timestamp. -You can add as many timelines and timestamps as you want when logging data. - -⚠️ If you run this code as is, the result will be.. surprising: the beads are animating as expected, but everything we've logged until that point is gone! ⚠️ - -![logging data - wat](https://static.rerun.io/a396c8aae1cbd717a3f35472594f789e4829b1ae_logging_data7_wat.png) - -Enter… - -### Latest-at semantics - -That's because the Rerun Viewer has switched to displaying your custom timeline by default, but the original data was only logged to the _default_ timeline (called `log_time`). -To fix this, add this at the beginning of the main function: - -```rust -rec.set_duration_secs("stable_time", 0.0); -``` - - - - - - - screenshot after using latest-at - - -This fix actually introduces yet another very important concept in Rerun: "latest-at" semantics. -Notice how entities `"dna/structure/left"` & `"dna/structure/right"` have only ever been logged at time zero, and yet they are still visible when querying times far beyond that point. - -_Rerun always reasons in terms of "latest" data: for a given entity, it retrieves all of its most recent components at a given time._ - -## Transforming space - -There's only one thing left: our original scene had the abacus rotate along its principal axis. - -As was the case with time, (hierarchical) space transformations are first class-citizens in Rerun. -Now it's just a matter of combining the two: we need to log the transform of the scaffolding at each timestamp. - -Either expand the previous loop to include logging transforms or -simply add a second loop like this: - -```rust -for i in 0..400 { - let time = i as f32 * 0.01; - - rec.set_duration_secs("stable_time", time); - - rec.log( - "dna/structure", - &rerun::archetypes::Transform3D::from_rotation(rerun::RotationAxisAngle::new( - glam::Vec3::Z, - rerun::Angle::from_radians(time / 4.0 * TAU), - )), - )?; -} -``` - -Voila! - - - -## Other ways of logging & visualizing data - -### Saving & loading to/from RRD files - -Sometimes, sending the data over the network is not an option. Maybe you'd like to share the data, attach it to a bug report, etc. - -Rerun has you covered: - -- Use [`RecordingStream::save`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.save) to stream all logging data to disk. -- Visualize it via `rerun path/to/recording.rrd` - -You can also save a recording (or a portion of it) as you're visualizing it, directly from the viewer. - -### RRD file backwards compatibility - -RRD files saved with Rerun 0.23 or later can be opened with a newer Rerun version. -For more details and potential limitations, please refer to [our blog post](https://rerun.io/blog/release-0.23). - -⚠️ At the moment, we only guarantee compatibility across adjacent minor versions (e.g. Rerun 0.24 can open RRDs from 0.23). - -### Spawning the Viewer from your process - -If the Rerun Viewer is [installed](../../overview/installing-rerun.md) and available in your `PATH`, you can use [`RecordingStream::spawn`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.spawn) to automatically start a Viewer in a new process and connect to it over gRPC. -If an external Viewer was already running, `spawn` will connect to that one instead of spawning a new one. - -```rust -fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_dna_abacus") - .spawn()?; - - // … log data to `rec` … - - Ok(()) -} -``` - -Alternatively, you can use [`rerun::native_viewer::show`](https://docs.rs/rerun/latest/rerun/native_viewer/fn.show.html) to start a Viewer on the main thread (for platform-compatibility reasons) and feed it data from memory. -This requires the `native_viewer` feature to be enabled in `Cargo.toml`: - -```toml -rerun = { version = "0.9", features = ["native_viewer"] } -``` - -Doing so means you're building the Rerun Viewer itself as part of your project, meaning compilation will take a bit longer the first time. - -Unlike `spawn` however, this expects a complete recording instead of being fed in real-time: - -```rust -let (rec, storage) = rerun::RecordingStreamBuilder::new("rerun_example_dna_abacus").memory()?; - -// … log data to `rec` … - -// Blocks until the viewer is closed. -// For more customizations, refer to `re_viewer::run_native_app`. -rerun::show( - // Show has to be called on the main thread. - rerun::MainThreadToken::i_promise_i_am_on_the_main_thread(), - storage.take(), -)?; -``` - -The Viewer will block the main thread until it is closed. - -### Closing - -This closes our whirlwind tour of Rerun. We've barely scratched the surface of what's possible, but this should have hopefully given you plenty pointers to start experimenting. - -As a next step, browse through our [example gallery](https://rerun.io/examples) for some more realistic example use-cases, browse the [Types](../../reference/types.md) section for more simple examples of how to use the main data types, or dig deeper into [querying your logged data](../data-out.md). diff --git a/docs/content/getting-started/data-out.md b/docs/content/getting-started/data-out.md index 88afb8c2f337..d4243d70096c 100644 --- a/docs/content/getting-started/data-out.md +++ b/docs/content/getting-started/data-out.md @@ -11,6 +11,7 @@ In this three-part guide, we explore a query workflow by implementing an "open j 2. [Export the dataframe](data-out/export-dataframe.md) 3. [Analyze the data and send back the results](data-out/analyze-and-send.md) -Note: this guide uses the popular [Pandas](https://pandas.pydata.org) dataframe package. The same concept however applies for alternative dataframe packages such as [Polars](https://pola.rs) or using [Datafusion](https://datafusion.apache.org/python/) directly. +> [!NOTE] +> This guide uses the popular [Pandas](https://pandas.pydata.org) dataframe package. The same concept however applies for alternative dataframe packages such as [Polars](https://pola.rs) or using [Datafusion](https://datafusion.apache.org/python/) directly. If you just want to see the final result, jump to the [complete script](data-out/analyze-and-send.md#complete-script) at the end of the third section. diff --git a/docs/content/getting-started/data-out/analyze-and-send.md b/docs/content/getting-started/data-out/analyze-and-send.md index b3f74080182d..4be95899a93f 100644 --- a/docs/content/getting-started/data-out/analyze-and-send.md +++ b/docs/content/getting-started/data-out/analyze-and-send.md @@ -24,7 +24,8 @@ snippet: tutorials/data_out[connect_viewer] -_Note_: When automating data analysis, it is typically preferable to log the results to an distinct RRD file next to the source RRD (using `rr.save()`). In such a situation, it is also valid to use the same app ID and recording ID. This allows opening both the source and result RRDs in the viewer, which will display data from both files under the same recording. +> [!NOTE] +> When automating data analysis, it is typically preferable to log the results to an distinct RRD file next to the source RRD (using `rr.save()`). In such a situation, it is also valid to use the same app ID and recording ID. This allows opening both the source and result RRDs in the viewer, which will display data from both files under the same recording. We will send our jaw open state data in two forms: diff --git a/docs/content/getting-started/data-out/export-dataframe.md b/docs/content/getting-started/data-out/export-dataframe.md index e642c7f71714..47a36cb0bb72 100644 --- a/docs/content/getting-started/data-out/export-dataframe.md +++ b/docs/content/getting-started/data-out/export-dataframe.md @@ -147,7 +147,8 @@ Name: jawOpen, dtype: float64 This confirms that the newly created `"jawOpen"` column now contains regular, 64-bit float numbers, and missing values are represented by NaNs. -_Note_: should you want to filter out the NaNs, you may use the [`dropna()`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.dropna.html) method. +> [!NOTE] +> Should you want to filter out the NaNs, you may use the [`dropna()`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.dropna.html) method. ## Next steps diff --git a/docs/content/getting-started/install-rerun.md b/docs/content/getting-started/install-rerun.md new file mode 100644 index 000000000000..9d22d15b39bb --- /dev/null +++ b/docs/content/getting-started/install-rerun.md @@ -0,0 +1,14 @@ +--- +title: Install Rerun +order: 350 +--- + +Choose what you want to install: + + +- [Python](./install-rerun/python.md) — the Python SDK (includes the Viewer) +- [C++](./install-rerun/cpp.md) — the C++ SDK +- [Rust](./install-rerun/rust.md) — the Rust SDK +- [Viewer](./install-rerun/viewer.md) — the standalone Rerun Viewer application + +If you run into any issues, check the [Troubleshooting](./install-rerun/troubleshooting.md) guide. diff --git a/docs/content/overview/installing-rerun/cpp.md b/docs/content/getting-started/install-rerun/cpp.md similarity index 77% rename from docs/content/overview/installing-rerun/cpp.md rename to docs/content/getting-started/install-rerun/cpp.md index 1720fdf302bf..8a93ef59c55e 100644 --- a/docs/content/overview/installing-rerun/cpp.md +++ b/docs/content/getting-started/install-rerun/cpp.md @@ -18,4 +18,4 @@ You'll additionally need to install the [Viewer](./viewer.md). ## Next steps -To start getting your own data streamed to the viewer, check out the [C++ quick start guide](../../getting-started/data-in/cpp.md). +[Set up a C++ project](../../getting-started/project-setup/cpp.md), then walk through the [Log and Ingest](../../getting-started/data-in.md) tutorial. diff --git a/docs/content/overview/installing-rerun/python.md b/docs/content/getting-started/install-rerun/python.md similarity index 60% rename from docs/content/overview/installing-rerun/python.md rename to docs/content/getting-started/install-rerun/python.md index ce1767d847c8..2b81dd837891 100644 --- a/docs/content/overview/installing-rerun/python.md +++ b/docs/content/getting-started/install-rerun/python.md @@ -8,14 +8,11 @@ The Python SDK includes both the SDK and the Viewer, so you're ready to go with - `pip install rerun-sdk` via pip - `conda install -c conda-forge rerun-sdk` via Conda - - Conda always comes with support for all features but if using pip you may need to specify optional features: - `pip install rerun-sdk[notebook]` for the embedded notebook tools -- `pip install rerun-sdk[dataplatform]` for the query api tools - - +- `pip install rerun-sdk[catalog]` for the query api tools +- `pip install rerun-sdk[dataloader]` for model training tools ## Next steps -To start getting your own data streamed to the viewer, check out the [Python quick start guide](../../getting-started/data-in/python.md). +[Set up a Python project](../../getting-started/project-setup/python.md), then walk through the [Log and Ingest](../../getting-started/data-in.md) tutorial. diff --git a/docs/content/overview/installing-rerun/rust.md b/docs/content/getting-started/install-rerun/rust.md similarity index 56% rename from docs/content/overview/installing-rerun/rust.md rename to docs/content/getting-started/install-rerun/rust.md index bc939a04efbe..e9119d05dad4 100644 --- a/docs/content/overview/installing-rerun/rust.md +++ b/docs/content/getting-started/install-rerun/rust.md @@ -9,4 +9,4 @@ You'll additionally need to install the [Viewer](./viewer.md). ## Next steps -To start getting your own data streamed to the viewer, check out the [Rust quick start guide](../../getting-started/data-in/rust.md). +[Set up a Rust project](../../getting-started/project-setup/rust.md), then walk through the [Log and Ingest](../../getting-started/data-in.md) tutorial. diff --git a/docs/content/overview/installing-rerun/troubleshooting.md b/docs/content/getting-started/install-rerun/troubleshooting.md similarity index 97% rename from docs/content/overview/installing-rerun/troubleshooting.md rename to docs/content/getting-started/install-rerun/troubleshooting.md index fd32981eaf01..2116aef7b562 100644 --- a/docs/content/overview/installing-rerun/troubleshooting.md +++ b/docs/content/getting-started/install-rerun/troubleshooting.md @@ -77,7 +77,9 @@ ERROR: No matching distribution found for rerun-sdk Then this is likely because you're running a version of pip that is too old. You can check the version of pip with `pip --version`. If you're running a version of pip 20 or older, you should upgrade it with `pip install --upgrade pip`. -⚠️ depending on your system configuration this may upgrade the pip installation aliased by `pip3` instead of `pip`. + +> [!WARNING] +> Depending on your system configuration this may upgrade the pip installation aliased by `pip3` instead of `pip`. ## Startup issues diff --git a/docs/content/overview/installing-rerun/viewer.md b/docs/content/getting-started/install-rerun/viewer.md similarity index 98% rename from docs/content/overview/installing-rerun/viewer.md rename to docs/content/getting-started/install-rerun/viewer.md index 306f3f9fc081..2fbb88eacd79 100644 --- a/docs/content/overview/installing-rerun/viewer.md +++ b/docs/content/getting-started/install-rerun/viewer.md @@ -11,7 +11,7 @@ There are many ways to install the viewer. Please pick whatever works best for y - Download `rerun-cli` for your platform from the [GitHub Release artifacts](https://github.com/rerun-io/rerun/releases/latest/). - Via Cargo - `cargo binstall rerun-cli` - download binaries via [`cargo binstall`](https://github.com/cargo-bins/cargo-binstall) - - `cargo install rerun-cli --locked` - build it from source (this requires Rust 1.92+) + - `cargo install rerun-cli --locked` - build it from source (this requires Rust 1.95+) - Via Snap (_community maintained_) - `snap install rerun` - download the viewer from the [Store](https://snapcraft.io/rerun). - Together with the Rerun [Python SDK](./python.md): @@ -21,6 +21,7 @@ There are many ways to install the viewer. Please pick whatever works best for y In any case you should be able to run `rerun` afterwards to start the Viewer. You'll be welcomed by an overview page that allows you to jump into some examples. + If you're facing any difficulties, don't hesitate to [open an issue](https://github.com/rerun-io/rerun/issues/new/choose) or [join the Discord server](https://discord.gg/PXtCgFBSmH). The Rerun Viewer has built-in support for opening many kinds of files, and can be [extended to open any other file type](../../getting-started/data-in/open-any-file.md) without needing to modify the Rerun codebase itself. diff --git a/docs/content/getting-started/project-setup.md b/docs/content/getting-started/project-setup.md new file mode 100644 index 000000000000..19b4c8a13b0c --- /dev/null +++ b/docs/content/getting-started/project-setup.md @@ -0,0 +1,12 @@ +--- +title: Set up a project +order: 375 +--- + +After [installing the SDK](./install-rerun.md) for your language, set up a project that depends on Rerun. Pick your language: + +- [Python](./project-setup/python.md) +- [C++](./project-setup/cpp.md) +- [Rust](./project-setup/rust.md) + +Once your project is set up, the [Log and Ingest](./data-in.md) tutorial walks through your first non-trivial recording. diff --git a/docs/content/getting-started/project-setup/cpp.md b/docs/content/getting-started/project-setup/cpp.md new file mode 100644 index 000000000000..d4e221eb7730 --- /dev/null +++ b/docs/content/getting-started/project-setup/cpp.md @@ -0,0 +1,50 @@ +--- +title: Set up a C++ project +order: 200 +--- + +You should have already [installed the C++ SDK](../install-rerun/cpp.md). + +We assume you have a working C++ toolchain and are using CMake to build your project. +For this project we will let Rerun download and build [Apache Arrow](https://arrow.apache.org/)'s C++ library itself. +To learn more about how Rerun's CMake script can be configured, see [CMake Setup in Detail](https://ref.rerun.io/docs/cpp/stable/md__2home_2runner_2work_2rerun_2rerun_2rerun__cpp_2cmake__setup__in__detail.html) in the C++ reference documentation. + +## Setting up your CMakeLists.txt + +A minimal `CMakeLists.txt` looks like this: + +```cmake +cmake_minimum_required(VERSION 3.16...3.27) +project(example_project LANGUAGES CXX) + +add_executable(example_project main.cpp) + +# Download the rerun_sdk +include(FetchContent) +FetchContent_Declare(rerun_sdk URL + https://github.com/rerun-io/rerun/releases/latest/download/rerun_cpp_sdk.zip) +FetchContent_MakeAvailable(rerun_sdk) + +# Link against rerun_sdk. +target_link_libraries(example_project PRIVATE rerun_sdk) +``` + +Note that Rerun requires at least C++17. Depending on the SDK will automatically ensure that C++17 or newer is enabled. + +## Includes + +To use Rerun all you need to include is `rerun.hpp`: + +```cpp +#include +``` + +## Building + +```bash +cmake -B build +cmake --build build -j +./build/example_project +``` + +You're now ready to follow the [Log and Ingest](../data-in.md) tutorial. diff --git a/docs/content/getting-started/project-setup/python.md b/docs/content/getting-started/project-setup/python.md new file mode 100644 index 000000000000..50c6edfb8269 --- /dev/null +++ b/docs/content/getting-started/project-setup/python.md @@ -0,0 +1,15 @@ +--- +title: Set up a Python project +order: 100 +--- + +You should have already [installed the Python SDK](../install-rerun/python.md). + +A Python project doesn't require any setup beyond having `rerun-sdk` available. +Open your editor of choice, create a new file, and import Rerun: + +```python +import rerun as rr +``` + +You're now ready to follow the [Log and Ingest](../data-in.md) tutorial. diff --git a/docs/content/getting-started/project-setup/rust.md b/docs/content/getting-started/project-setup/rust.md new file mode 100644 index 000000000000..5acb0c25a223 --- /dev/null +++ b/docs/content/getting-started/project-setup/rust.md @@ -0,0 +1,16 @@ +--- +title: Set up a Rust project +order: 300 +--- + +You should have already [installed the Rust SDK](../install-rerun/rust.md). + +If you haven't already, start a new project with `cargo new` and add the `rerun` dependency: + +```bash +cargo new my_project +cd my_project +cargo add rerun +``` + +You're now ready to follow the [Log and Ingest](../data-in.md) tutorial. diff --git a/docs/content/getting-started/train.md b/docs/content/getting-started/train.md new file mode 100644 index 000000000000..87151b81931d --- /dev/null +++ b/docs/content/getting-started/train.md @@ -0,0 +1,71 @@ +--- +title: Train +order: 475 +--- + +This page walks through streaming Rerun recordings directly into a PyTorch `DataLoader`, without an intermediate export step, using the bundled [LeRobot ACT training example](https://github.com/rerun-io/rerun/tree/main/examples/python/dataloader) end-to-end. +For an explanation of the dataloader API itself — windowed action chunks, GOP-aware video decoding, DDP partitioning — see [Train PyTorch models with Rerun](../howto/train.md). + +> [!NOTE] +> The `rerun.experimental.dataloader` module is provisional and will change between releases. + +## Run the example + +The example trains a [LeRobot ACT](https://tonyzhaozh.github.io/aloha/) policy on the [`rerun/so101-pick-and-place`](https://huggingface.co/datasets/rerun/so101-pick-and-place) dataset from HuggingFace. + +### 1. Grab the example + +Sparse-checkout just the example directory, without the rest of the Rerun repo: + +```bash +git clone --filter=blob:none --sparse https://github.com/rerun-io/rerun.git +cd rerun +git sparse-checkout set examples/python/dataloader +cd examples/python/dataloader +``` + +### 2. Install + +The example has its own `uv` project because LeRobot pins an incompatible `rerun-sdk`. +The additional arguments to uv sync allow you to run just this example without the full rerun repo setup. + +```bash +uv sync --no-sources --no-dev +``` + +If you have the full Rerun monorepo checked out and want to develop against your local Rerun build, run instead: + +```bash +RERUN_ALLOW_MISSING_BIN=1 uv sync +uv pip install ../../../rerun_py/rerun_dev_fixup +``` + +### 3. Start a catalog server + +In a separate terminal: + +```bash +rerun server +``` + +### 4. Prepare and register the dataset + +Downloads the dataset from HuggingFace, splits it into per-episode RRDs, and registers them with the catalog: + +```bash +uv run python prepare_dataset.py +``` + +### 5. Train + +```bash +uv run python train.py +``` + +The script streams batches from the catalog, trains an ACT policy for a few epochs, and saves a checkpoint to `act_checkpoint/`. + +## References + +- [Example source](https://github.com/rerun-io/rerun/tree/main/examples/python/dataloader) — `prepare_dataset.py` and `train.py` +- [`rerun/so101-pick-and-place`](https://huggingface.co/datasets/rerun/so101-pick-and-place) — LeRobot dataset on HuggingFace +- [Train PyTorch models with Rerun](../howto/train.md) — full how-to: windowing, video decoding, iterable vs. map style, DDP diff --git a/docs/content/howto.md b/docs/content/howto.md index 56d471770541..cf75bc2ef8b8 100644 --- a/docs/content/howto.md +++ b/docs/content/howto.md @@ -1,6 +1,6 @@ --- title: How-to -order: 2 +order: 3 --- Guides for using Rerun in more advanced ways. @@ -8,4 +8,5 @@ Guides for using Rerun in more advanced ways. - [Logging and ingestion](./howto/logging-and-ingestion.md) - sending data to Rerun - [Visualization](./howto/visualization.md) - displaying data in the Viewer - [Query and transform](./howto/query-and-transform.md) - querying and transforming data +- [Train](./howto/train.md) - using Rerun data for training - [Integrations](./howto/integrations.md) - integrating Rerun with other tools diff --git a/docs/content/howto/integrations/embed-web.md b/docs/content/howto/integrations/embed-web.md index 3c8aa143dce2..13b98ffe2800 100644 --- a/docs/content/howto/integrations/embed-web.md +++ b/docs/content/howto/integrations/embed-web.md @@ -25,6 +25,16 @@ For instance: ``` +### Matching the host page's theme + +By default, the embedded viewer follows the user's OS theme (`prefers-color-scheme`). If your host page has its own theme toggle, you can pin the viewer to match by passing `theme=dark`, `theme=light`, or `theme=system`: + +```html + +``` + +This is useful for sites whose theme can differ from the OS preference — without it, a user on a light-mode OS visiting your dark-mode page would see a bright viewer panel against a dark background. + ## Using the JavaScript package We offer JavaScript bindings to the Rerun Viewer via NPM. This method provides control over the Viewer but requires a JavaScript web application setup with a bundler. @@ -33,7 +43,8 @@ Various packages are available: - [@rerun-io/web-viewer](https://www.npmjs.com/package/@rerun-io/web-viewer): Suitable for JS apps without a framework or frameworks without dedicated packages. - [@rerun-io/web-viewer-react](https://www.npmjs.com/package/@rerun-io/web-viewer-react): Designed specifically for React apps. -> ℹ️ Note: The stability of the `rrd` format is still evolving, so the package version corresponds to the supported Rerun SDK version. Therefore, `@rerun-io/web-viewer@0.10.0` can only connect to a data source (`.rrd` file, gRPC connection, etc.) originating from a Rerun SDK with version `0.10.0`! +> [!NOTE] +> The stability of the `rrd` format is still evolving, so the package version corresponds to the supported Rerun SDK version. Therefore, `@rerun-io/web-viewer@0.10.0` can only connect to a data source (`.rrd` file, gRPC connection, etc.) originating from a Rerun SDK with version `0.10.0`! ### Basic example @@ -43,7 +54,8 @@ To begin, install the package ([@rerun-io/web-viewer](https://www.npmjs.com/pack npm i @rerun-io/web-viewer ``` -> ℹ Note: This package is compatible only with recent browser versions. If your target browser lacks support for Wasm imports or top-level await, additional plugins may be required for your bundler setup. For instance, if you're using [Vite](https://vitejs.dev/), you'll need to install [vite-plugin-wasm](https://www.npmjs.com/package/vite-plugin-wasm) and [vite-plugin-top-level-await](https://www.npmjs.com/package/vite-plugin-top-level-await) and integrate them into your `vite.config.js`. +> [!NOTE] +> This package is compatible only with recent browser versions. If your target browser lacks support for Wasm imports or top-level await, additional plugins may be required for your bundler setup. For instance, if you're using [Vite](https://vitejs.dev/), you'll need to install [vite-plugin-wasm](https://www.npmjs.com/package/vite-plugin-wasm) and [vite-plugin-top-level-await](https://www.npmjs.com/package/vite-plugin-top-level-await) and integrate them into your `vite.config.js`. Once installed and configured, import and use it within your application: diff --git a/docs/content/howto/integrations/ros2-nav-turtlebot.md b/docs/content/howto/integrations/ros2-nav-turtlebot.md index c3f12fa4dff7..dc5a74573bec 100644 --- a/docs/content/howto/integrations/ros2-nav-turtlebot.md +++ b/docs/content/howto/integrations/ros2-nav-turtlebot.md @@ -21,18 +21,18 @@ All of the code for this guide can be found on GitHub in [rerun/examples/python/ros_node](https://github.com/rerun-io/rerun/blob/main/examples/python/ros_node/). - Rerun viewer showing data streamed from the example ROS node - - - - + Rerun viewer showing data streamed from the example ROS node + + + + --- Other relevant tutorials: -- [Python SDK Tutorial](../../getting-started/data-in/python.md) +- [Log and Ingest Tutorial](../../getting-started/data-in.md) - [Viewer Walkthrough](../../getting-started/configure-the-viewer/navigating-the-viewer.md) - [Transforms & Coordinate Frames](../../concepts/logging-and-ingestion/transforms.md) - [Loading URDF models](../../howto/logging-and-ingestion/urdf.md) @@ -174,6 +174,15 @@ def scan_callback(self, scan: LaserScan) -> None: rr.log("scan", rr.CoordinateFrame(frame=scan.header.frame_id)) ``` +### OccupancyGrid to rr.GridMap + +ROS [`nav_msgs/OccupancyGrid`](https://docs.ros2.org/latest/api/nav_msgs/msg/OccupancyGrid.html) messages map directly to Rerun's [`GridMap`](../../reference/types/archetypes/grid_map.md) archetype. +This example subscribes to the static map and the local & global costmap topics, logging them with Rerun's RViz-compatible `RvizMap` and `RvizCostmap` colormaps and with draw-order values for defined layering. + +Most fields are a 1:1 mapping: the occupancy data becomes the `GridMap` image data, `info.resolution` becomes the cell size, and `info.origin` defines the map pose. +The main caveat is row order: ROS occupancy grids start at the map's bottom-left cell, while regular image buffers as used by Rerun's `GridMap` are top-row first. +The example therefore flips the rows before logging the grid data. + ### Camera info and images ROS Images can also be mapped to Rerun very easily, using the `cv_bridge` package. diff --git a/docs/content/howto/logging-and-ingestion/convert-existing-data.md b/docs/content/howto/logging-and-ingestion/convert-existing-data.md deleted file mode 100644 index ff23eb8b309c..000000000000 --- a/docs/content/howto/logging-and-ingestion/convert-existing-data.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Convert existing data to Rerun -order: 200 -description: How to convert custom data formats to Rerun using row-oriented or columnar APIs ---- - -There are a variety of ways to convert data into an `RRD`. -When filetypes are opened in the viewer they go through our [importers](../../concepts/logging-and-ingestion/importers.md). - -For example, there's a built-in importer for [MCAP files](../../concepts/logging-and-ingestion/mcap.md) and we also have a few [command line options](../../concepts/logging-and-ingestion/mcap/cli-reference.md) for converting MCAP data directly into an `RRD`. -This works great for message types that are supported by the built-in importer - however, the most general solution to support arbitrary message types is the logging API. - ---- - -Other relevant tutorials: - -- [Log and Ingest](../../getting-started/data-in.md) -- [Send entire columns at once](send-columns.md) -- [Working with MCAP](../../howto/logging-and-ingestion/mcap.md) - -## Converting existing data to RRD -This guide covers the two recommended approaches: `recording.log` (row-oriented) and `recording.send_columns` (columnar). Both produce identical `.rrd` output. - -## Quick comparison - -Rerun offers two APIs that we will use for conversion. Both produce identical `.rrd` files: - -| | `recording.log` | `recording.send_columns` | -|---|---|---| -| **API style** | Row-oriented: one entity per call | Columnar: many timestamps per call | -| **Best for** | Live streaming, prototyping, simple conversions | Batch conversion of large datasets | -| **Performance** | Lower throughput, no batch latency | ~3–10x faster for batch workloads | -| **Typical use cases** | Sensor streams, simple scripts | Bulk data conversion | -| **Language support** | Python, Rust, C++ | Python, Rust, C++ | - - -## When to use which - -**Use `recording.log` when:** - -* Your dataset is small and performance isn't critical -* Implementation simplicity is the priority - -**Use `recording.send_columns` when:** - -* You're doing batch conversion of large recorded datasets -* You have high-frequency signals (transforms, IMU, joint states) - -Here are timings from a real-world MCAP conversion with custom Protobuf messages (~21k messages total): - -| | `recording.log` | `recording.send_columns` | -|---|---|---| -| Video frames (2,363 msgs) | 0.12s | 0.01s | -| Transforms (16,505 msgs) | 0.84s | 0.08s | -| Other messages (2,354 msgs) | 0.09s | 0.01s | -| **Total Rerun logging time** | **1.33s** | **0.10s** | - -> **Note:** These are example timings from a specific dataset. Actual performance will vary. The relative speedup (10-13x here) is typical for the Rerun logging step of batch conversions. - -## Map to archetypes - -Regardless of which API you use, the goal is to map your custom data into Rerun [archetypes](../../reference/types/archetypes.md). - -When writing your converter, the first question for each message type is: **What is the proper Rerun archetype?** - -* For example, transforms and poses map to [`Transform3D`](../../reference/types/archetypes/transform3d.md) and [`InstancePoses3D`](../../reference/types/archetypes/instance_poses3d.md), an image to [`Image`](../../reference/types/archetypes/image.md), point clouds to [`Points3D`](../../reference/types/archetypes/points3d.md). -* For data that does not map cleanly to existing Archetypes, you can use [`AnyValues`](custom-data.md) for simple key-value pairs, or [`DynamicArchetype`](custom-data.md) when you want to group related fields under a named archetype. -Both appear in the dataframe view and are queryable, but don't specify visual qualities as explicitly. - -## Converter structure with `recording.log` - -**Full working example:** [Converting MCAP Protobuf data using `recording.log`](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf.py) - -Here is an example of how you could build a converter using `log` calls. -We use handler functions for each message type we want to convert. -Each handler sets timestamps and logs directly. - -First, we add an utility to manage logging timestamps: - -snippet: howto/convert_mcap_protobuf[set_message_times] - -Then we specify how to convert specific kinds of messages: - -snippet: howto/convert_mcap_protobuf[compressed_video] - -Finally, we loop over all messages and log them: - -snippet: howto/convert_mcap_protobuf[conversion_loop] - - -## Converter structure with `recording.send_columns` - -**Full working example:** [Converting MCAP Protobuf data using `recording.send_columns`](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py) - -Our example for `send_columns` works differently because it sends data in batches instead of single log calls. -For this purpose, our handlers first extract data into collector utilities. -These collectors first accumulate data during iteration and then send it in bulk after the loop. - -**Note:** the `ColumnCollector` used below is a user-defined helper class (not part of the Rerun SDK) that accumulates time-indexed data and sends it via `send_columns`. -See the full example for its implementation. - -snippet: howto/convert_mcap_protobuf_send_column[conversion_loop] - diff --git a/docs/content/howto/logging-and-ingestion/custom-data.md b/docs/content/howto/logging-and-ingestion/custom-data.md index 152958c1cc5a..9b3553e1dbc0 100644 --- a/docs/content/howto/logging-and-ingestion/custom-data.md +++ b/docs/content/howto/logging-and-ingestion/custom-data.md @@ -62,10 +62,7 @@ You can also define and log your own custom archetypes and components completely In this example we extend the Rerun Points3D archetype with a custom confidence component and user-defined archetype. -⚠️ NOTE: Due to the component descriptor changes in `v0.24` it is currently not possible for custom data to be picked up by visualizers. -We are currently investigating approaches to bring that functionality back. - -However, your custom data will still show up in the dataframe view, as shown below. +This is what it looks like in the in the dataframe view: snippet: tutorials/custom_data @@ -76,3 +73,14 @@ snippet: tutorials/custom_data
+ + +## Creating/augmenting visualizations from custom data + +All components can be mapped to arbitrary slots of visualizers. +For a general information on component mapping see [component mappings](../visualization/component-mappings.md), +for the common case of plotting see [plot any scalar](../visualization/plot-any-scalar.md) + +> [!INFO] +> Complex mappings e.g. from scalars to colors are not yet possible, but will be supported in future versions +> by exposing more functionality from [Lenses](../../concepts/query-and-transform/lenses.md) directly in the Viewer. diff --git a/docs/content/howto/logging-and-ingestion/layers.md b/docs/content/howto/logging-and-ingestion/layers.md index 7dbabb6bec21..44891500da1d 100644 --- a/docs/content/howto/logging-and-ingestion/layers.md +++ b/docs/content/howto/logging-and-ingestion/layers.md @@ -9,7 +9,8 @@ In the [catalog object model](../../concepts/query-and-transform/catalog-object- Layers are immutable, but data can be added to segments by registering other layers with the same recording id but a different layer name. This how-to page provides examples for two ways data can be added to existing datasets through layers. -Note: layers should not be confused with [MCAP decoders](../../concepts/logging-and-ingestion/mcap/decoders-explained.md), which serve a different purpose in the context of MCAP file ingestion. +> [!NOTE] +> Layers should not be confused with [MCAP decoders](../../concepts/logging-and-ingestion/mcap/decoders-explained.md), which serve a different purpose in the context of MCAP file ingestion. ## Adding data to existing segments using layers @@ -166,39 +167,26 @@ When you register a recording without specifying a `layer_name`, it is assigned ### Is it possible to obtain a dataframe with a list of all layers in a dataset? Yes. -The [`DatasetEntry.manifest()`](https://ref.rerun.io/docs/python/stable/common/catalog/#rerun.catalog.DatasetEntry.manifest) method returns a DataFusion DataFrame containing the full dataset manifest, which includes layer information for each segment: +The [`DatasetEntry.segment_table()`](https://ref.rerun.io/docs/python/stable/common/catalog/#rerun.catalog.DatasetEntry.segment_table) method returns a DataFusion DataFrame with one row per segment and a `rerun_layer_names` column listing the layers of each segment: -snippet: howto/layers[manifest] +snippet: howto/layers[list_layers] Output: ``` -┌───────────────────────────────────────┬──────────────────┬────────────────────────────────────┐ -│ rerun_segment_id ┆ rerun_layer_name ┆ property:quality:tracking_good │ -│ --- ┆ --- ┆ --- │ -│ type: Utf8 ┆ type: Utf8 ┆ type: nullable List[nullable bool] │ -│ ┆ ┆ component: tracking_good │ -│ ┆ ┆ entity_path: /__properties/quality │ -│ ┆ ┆ kind: data │ -╞═══════════════════════════════════════╪══════════════════╪════════════════════════════════════╡ -│ ILIAD_50aee79f_2023_07_12_20h_55m_08s ┆ base ┆ null │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_50aee79f_2023_07_12_20h_55m_08s ┆ quality ┆ [false] │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_50aee79f_2023_07_12_20h_55m_08s ┆ tracking_error ┆ null │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_5e938e3b_2023_07_20_10h_40m_10s ┆ base ┆ null │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_5e938e3b_2023_07_20_10h_40m_10s ┆ quality ┆ [false] │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_5e938e3b_2023_07_20_10h_40m_10s ┆ tracking_error ┆ null │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_5e938e3b_2023_07_28_11h_25m_26s ┆ base ┆ null │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_5e938e3b_2023_07_28_11h_25m_26s ┆ quality ┆ [true] │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_5e938e3b_2023_07_28_11h_25m_26s ┆ tracking_error ┆ null │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ILIAD_j807b3f8_2023_06_15_13h_42m_56s ┆ base ┆ null │ -└───────────────────────────────────────┴──────────────────┴────────────────────────────────────┘ +┌───────────────────────────────────────┬─────────────────────────────────┐ +│ rerun_segment_id ┆ rerun_layer_names │ +│ --- ┆ --- │ +│ type: Utf8 ┆ type: List[Utf8] │ +╞═══════════════════════════════════════╪═════════════════════════════════╡ +│ ILIAD_50aee79f_2023_07_12_20h_55m_08s ┆ [base, tracking_error, quality] │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ILIAD_5e938e3b_2023_07_20_10h_40m_10s ┆ [base, tracking_error, quality] │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ILIAD_5e938e3b_2023_07_28_11h_25m_26s ┆ [base, tracking_error, quality] │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ILIAD_j807b3f8_2023_06_15_13h_42m_56s ┆ [base, tracking_error, quality] │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ILIAD_sbd7d2c6_2023_12_24_16h_20m_37s ┆ [base, tracking_error, quality] │ +└───────────────────────────────────────┴─────────────────────────────────┘ ``` diff --git a/docs/content/howto/logging-and-ingestion/mcap.md b/docs/content/howto/logging-and-ingestion/mcap.md index 3fca982f23cd..ce248d21f327 100644 --- a/docs/content/howto/logging-and-ingestion/mcap.md +++ b/docs/content/howto/logging-and-ingestion/mcap.md @@ -60,12 +60,13 @@ Each layer extracts different types of information from the MCAP source and each - **`raw`**: Logs the unprocessed message bytes as Rerun blobs without any interpretation - **`schema`**: Extracts metadata about channels, topics, and schemas -- **`stats`**: Extracts file-level metrics like message counts, time ranges, and channel statistics -- **`metadata`** Extracts metadata records (if present) into the `__properties` of the RRD +- **`stats`**: Extracts file-level metrics like message counts, time ranges, and channel statistics into `__mcap_properties` in the RRD +- **`metadata`** Extracts metadata records (if present) into `__mcap_metadata` in the RRD +- **`attachments`**: Extracts MCAP attachment records (if present) as static data under `__mcap_attachments` - **`protobuf`**: Automatically decodes protobuf-encoded messages using reflection - **`ros2msg`**: Provides semantic conversion of common ROS2 message types into Rerun's visualization components - **`ros2_reflection`**: Automatically decodes ROS2 messages using reflection -- **`recording_info`**: Extracts recording metadata such as message counts, start time, and session information +- **`recording_info`**: Extracts recording metadata such as message counts, start time, and session information into `__mcap_properties` in the RRD - **`urdf`**: Uses Rerun's built-in URDF loader when a ROS 2 `/robot_description` string topic is present By default, Rerun analyzes an MCAP file to determine which decoders are active to provide the most comprehensive view of your data, while avoiding duplication. diff --git a/docs/content/howto/logging-and-ingestion/optimize-chunks.md b/docs/content/howto/logging-and-ingestion/optimize-chunks.md index d77848d943fd..81019c50ffc9 100644 --- a/docs/content/howto/logging-and-ingestion/optimize-chunks.md +++ b/docs/content/howto/logging-and-ingestion/optimize-chunks.md @@ -96,7 +96,7 @@ ipc_size_bytes_p999 = 568 KiB If a file contains many small chunks, run [`rerun rrd optimize`](../../reference/cli.md#rerun-rrd-optimize) to rewrite it with fewer, larger chunks. For example: ```sh -$ rerun rrd optimize --max-rows 4096 --max-bytes 1048576 -o nuscenes_compacted.rrd <(curl 'https://app.rerun.io/version/latest/examples/nuscenes_dataset.rrd') +$ rerun rrd optimize --max-size 2MiB -o nuscenes_compacted.rrd <(curl 'https://app.rerun.io/version/latest/examples/nuscenes_dataset.rrd') merge/compaction finished srcs=["/dev/fd/63"] time=2.51217062s num_chunks_before=576 num_chunks_after=217 num_chunks_reduction="-62.326%" srcs_size_bytes=90.0 MiB dst_size_bytes=89.6 MiB size_reduction="-0.474%" $ rrd stats nuscenes_compacted.rrd @@ -132,11 +132,19 @@ ipc_size_bytes_p999 = 1.0 MiB # … truncated … ``` -This produces a new file where chunks have been merged up to ~4096 rows or 1 MiB each (the defaults). This significantly reduces viewer-side load and improves performance for future queries and visualization. +This produces a new file where chunks have been merged up to the size and row thresholds of the selected optimization profile (see below) (further capped by `--max-size 2MiB` in the example above). This significantly reduces viewer-side load and improves performance for future queries and visualization. Because it runs offline, the CLI compactor has full access to the dataset and no real-time constraints, making it the most effective tool for optimal compaction. It's a good idea to compact files ahead of time if they’ll be queried or visualized repeatedly. -> ⚠️ `rerun rrd optimize` will automatically migrate the data to the latest version of the RRD protocol, if needed. ⚠️ +> [!WARNING] +> `rerun rrd optimize` will automatically migrate the data to the latest version of the RRD protocol, if needed. + +Note that `rerun rrd optimize` ships two preset profiles, selected with `--profile`, that set sensible thresholds for two common targets: + +* `object-store` *(default)* — large chunks (up to ~65k rows, ~2 MiB), tuned for object-store-backed datasets stored on catalog servers, where query throughput and network streaming matter most. +* `live` — small chunks (up to ~4096 rows, ~384 KiB), tuned for the live-Viewer workflow where the time panel benefits from finer-grained resolution. + +Per-knob flags (`--max-rows`, `--max-size`, …) and the `RERUN_CHUNK_MAX_*` environment variables override the profile's values. Constraints: * Runs: standalone CLI tool @@ -144,11 +152,25 @@ Constraints: * Operational limits: none -- runs fully offline +## Compacting chunks with the chunk processing API + +The same compaction logic that powers `rerun rrd optimize` is exposed in the [Chunk Processing API](../../concepts/logging-and-ingestion/chunk-processing-api.md), so you can fold optimization into a Python ingestion or conversion pipeline rather than running it as a separate CLI step: + +snippet: howto/optimize_chunks[optimize] + +[`LazyChunkStream.collect()`](https://ref.rerun.io/docs/python/stable/experimental/#rerun.experimental.LazyChunkStream) materializes the pipeline into a `ChunkStore`; passing an `OptimizationProfile` runs extra compaction passes tuned for a specific target. The two presets mirror the CLI's `--profile` values: + +* `OptimizationProfile.OBJECT_STORE` (corresponds to `--profile object-store`, the CLI default) — large chunks for object-store-backed datasets; +* `OptimizationProfile.LIVE` (corresponds to `--profile live`) — small chunks for the live-Viewer workflow. + +* **Note:** `collect()` materializes the entire pipeline into an in-memory `ChunkStore` before writing, so the full recording must fit in RAM. + + ## Conclusion * Compaction isn’t a minor optimization — it can and frequently yields massive performance gains depending on your workload. * Rerun applies micro-batching and compaction by default, but optimal settings vary per use case. * Compaction can (and should) happen at multiple stages, each with different tradeoffs, operating under very different constraints. -* The Rerun CLI is your best tool to: - * Understand chunk-related performance issues (`rerun rrd stats`) - * Preemptively optimize data (`rerun rrd optimize`) +* Once data has been recorded, two complementary tools let you preemptively optimize it for downstream use: + * The Rerun CLI: `rerun rrd stats` to diagnose, `rerun rrd optimize` for one-shot offline compaction. + * The [Chunk Processing API](../../concepts/logging-and-ingestion/chunk-processing-api.md): same compaction logic, exposed in-process so you can fold it into a Python ingestion or conversion pipeline via `collect(optimize=OptimizationProfile.…)`. diff --git a/docs/content/howto/logging-and-ingestion/send-columns.md b/docs/content/howto/logging-and-ingestion/send-columns.md index c5a0c8ad964c..45a3d9a0eb66 100644 --- a/docs/content/howto/logging-and-ingestion/send-columns.md +++ b/docs/content/howto/logging-and-ingestion/send-columns.md @@ -4,12 +4,13 @@ order: 100 description: How to use the Rerun SDK to log big chunks of data in one call --- -The [`log` API](../../getting-started/data-in/python.md#logging-our-first-points) is designed to extract data from your running code as it's being generated. It is, by nature, *row-oriented*. +The [`log` API](../../getting-started/data-in.md#logging-our-first-points) is designed to extract data from your running code as it's being generated. It is, by nature, *row-oriented*. If you already have data stored in something more *column-oriented*, it can be both a lot easier and more efficient to send it to Rerun in that form directly. This is what the `send_columns` API is for: it lets you efficiently update the state of an entity over time, sending data for multiple index and component columns in a single operation. -> ⚠️ `send_columns` API bypasses the time context and [micro-batcher](../../reference/sdk/micro-batching.md) ⚠️ +> [!WARNING] +> `send_columns` API bypasses the time context and [micro-batcher](../../reference/sdk/micro-batching.md). > > In contrast to the `log` API, `send_columns` does NOT add any other timelines to the data. Neither the built-in timelines `log_time` and `log_tick`, nor any [user timelines](../../concepts/logging-and-ingestion/timelines.md). Only the timelines explicitly included in the call to `send_columns` will be included. diff --git a/docs/content/howto/logging-and-ingestion/send-partial-updates.md b/docs/content/howto/logging-and-ingestion/send-partial-updates.md index 186a3abd9ff3..2914688394d9 100644 --- a/docs/content/howto/logging-and-ingestion/send-partial-updates.md +++ b/docs/content/howto/logging-and-ingestion/send-partial-updates.md @@ -1,6 +1,6 @@ --- title: Send partial updates over time -order: 200 +order: 250 description: How to use the Rerun SDK to send partial data updates over time --- diff --git a/docs/content/howto/logging-and-ingestion/send-table.md b/docs/content/howto/logging-and-ingestion/send-table.md index 376e40e8957c..cbca37e6802b 100644 --- a/docs/content/howto/logging-and-ingestion/send-table.md +++ b/docs/content/howto/logging-and-ingestion/send-table.md @@ -4,7 +4,8 @@ order: 300 description: Shows how to send tables as dataframes to the Rerun viewer. --- -> **Note:** The `send_table` API is currently experimental and may change in future releases. +> [!NOTE] +> The `send_table` API is currently experimental and may change in future releases. Rerun now supports sending tabular data to the Rerun Viewer! This feature allows you to visualize and interact with dataframes (encoded as Arrow record batches) directly in the Rerun Viewer environment. @@ -44,7 +45,7 @@ pip install rerun-sdk[notebook] pyarrow pandas numpy from rerun.experimental import ViewerClient # Connect to a running Rerun Viewer -client = ViewerClient(addr="rerun+http://0.0.0.0:9876/proxy") +client = ViewerClient.connect(url="rerun+http://127.0.0.1:9876/proxy") ``` ### Sending a simple table @@ -108,7 +109,7 @@ You can also use the native viewer instead of the inline viewer: os.environ["RERUN_NOTEBOOK_ASSET"] = "serve-local" # Connect to a running Rerun Viewer -client = ViewerClient(addr="rerun+http://0.0.0.0:9876/proxy") +client = ViewerClient.connect(url="rerun+http://127.0.0.1:9876/proxy") ``` ## Current limitations diff --git a/docs/content/howto/logging-and-ingestion/urdf.md b/docs/content/howto/logging-and-ingestion/urdf.md index 107045379361..8d4774b6a446 100644 --- a/docs/content/howto/logging-and-ingestion/urdf.md +++ b/docs/content/howto/logging-and-ingestion/urdf.md @@ -3,7 +3,7 @@ title: Loading URDF models order: 900 --- -Rerun features a built-in [importer](https://rerun.io/docs/concepts/logging-and-ingestion/importers/overview?speculative-link) for [URDF](https://en.wikipedia.org/wiki/URDF) files. +Rerun features a built-in [importer](https://rerun.io/docs/concepts/logging-and-ingestion/importers/overview) for [URDF](https://en.wikipedia.org/wiki/URDF) files. A robot model loaded from an URDF file visualized in Rerun. @@ -22,7 +22,8 @@ This will automatically invoke the importer, which will take care of: Once that is done, the joints can be updated by sending [`Transform3D`](../../reference/types/archetypes/transform3d.md)s, where you have to set the `parent_frame` and `child_frame` fields explicitly to each joint's specific frame IDs. -> ⚠️ Note: previous versions (< 0.28) required you to send transforms with _implicit_ frame IDs, i.e. having to send each joint transform on a specific entity path. +> [!NOTE] +> Previous versions (< 0.28) required you to send transforms with _implicit_ frame IDs, i.e. having to send each joint transform on a specific entity path. > This was dropped in favor of _named_ frame IDs, which is more in line with ROS and allows you to send all transform updates on one entity (e.g. a `transforms` entity like in the example below). ## Example diff --git a/docs/content/howto/query-and-transform/dataframe_operations.md b/docs/content/howto/query-and-transform/dataframe_operations.md index df68cb5c291e..924ed662db9d 100644 --- a/docs/content/howto/query-and-transform/dataframe_operations.md +++ b/docs/content/howto/query-and-transform/dataframe_operations.md @@ -28,10 +28,11 @@ snippet: howto/dataframe_operations[group_by] Some of our columns start much later than others. Find out how often this delay exceeds some threshold. -⚠️ **Performance warning:** -Even though datafusion pulls data lazily, we don't currently decouple our payload from its timeline. -E.g. in this example this means that we have to pull the full camera data to inspect their min/max timestamps. -This works quickly when the data is already local and in memory, but can be a bottleneck on cloud at scale. +> [!WARNING] +> **Performance warning:** +> Even though datafusion pulls data lazily, we don't currently decouple our payload from its timeline. +> E.g. in this example this means that we have to pull the full camera data to inspect their min/max timestamps. +> This works quickly when the data is already local and in memory, but can be a bottleneck on cloud at scale. snippet: howto/dataframe_operations[join_query] diff --git a/docs/content/howto/query-and-transform/dataset_resampling.md b/docs/content/howto/query-and-transform/dataset_resampling.md index cfc4fcc1012a..aa3d4d5b1def 100644 --- a/docs/content/howto/query-and-transform/dataset_resampling.md +++ b/docs/content/howto/query-and-transform/dataset_resampling.md @@ -1,6 +1,6 @@ --- title: Dataset Resampling -order: 110 +order: 105 --- This snippet demonstrates how to resample a dataset based on the time index of one component diff --git a/docs/content/howto/query-and-transform/get-data-out.md b/docs/content/howto/query-and-transform/get-data-out.md index 26260360f2e2..b5776ca1d610 100644 --- a/docs/content/howto/query-and-transform/get-data-out.md +++ b/docs/content/howto/query-and-transform/get-data-out.md @@ -7,7 +7,9 @@ Rerun comes with the ability to get data out of Rerun from code. This page provi ## Starting a server with recordings -The first step to query data is to start a server and load it with a dataset containing your recording. +The first step to query data is to start a catalog server and load it with a dataset containing your recording. + +See the [catalog object model](../../concepts/query-and-transform/catalog-object-model.md) docs for more details on how datasets are organized in Rerun. ```python import rerun as rr diff --git a/docs/content/howto/query-and-transform/overview.md b/docs/content/howto/query-and-transform/overview.md index e6fc2500233b..b5f6dcac840a 100644 --- a/docs/content/howto/query-and-transform/overview.md +++ b/docs/content/howto/query-and-transform/overview.md @@ -3,10 +3,10 @@ title: Overview order: 10 --- -Rerun is a Data Platform for Physical Data. -The open source SDK connects to the cloud Data Platform, which allows you to store, retrieve, and query over large amounts of data, and integrates with the SDK so you can browse and inspect the data visually. +Rerun is the Unified Data Layer for Physical AI. +The Rerun SDK connects to a catalog server, which allows you to store, retrieve, and query over large amounts of data, and integrates with the SDK so you can browse and inspect the data visually. -The open source Rerun SDK includes a simplified Data Platform server, which is API compatible with the cloud platform. -The open source server loads everything into memory, which makes it fast and simple to operate for very small datasets, which in turn makes it perfect for quick testing and local experimentation. +The Rerun SDK includes a simplified open-source catalog server that is API compatible with Rerun Hub, our managed offering. +The open-source server loads everything into memory, which makes it fast and simple to operate for very small datasets, which in turn makes it perfect for quick testing and local experimentation. -See the [how-to guide for the open source server](get-data-out.md) for more details on launching and connecting to the server. +See the [how-to guide for the open-source server](get-data-out.md) for more details on launching and connecting to the server. diff --git a/docs/content/howto/query-and-transform/query_images.md b/docs/content/howto/query-and-transform/query_images.md index c760c1310c54..7e6152fb95c8 100644 --- a/docs/content/howto/query-and-transform/query_images.md +++ b/docs/content/howto/query-and-transform/query_images.md @@ -4,7 +4,7 @@ order: 60 --- Images are incredibly useful, however there are many ways to store and manipulate them. -This example focuses on querying image frames from the Rerun Data Platform. +This example focuses on querying image frames from a catalog server. The dependencies in this example require `rerun-sdk[all]`. diff --git a/docs/content/howto/query-and-transform/query_performance_tuning.md b/docs/content/howto/query-and-transform/query_performance_tuning.md index aed7b6871661..30c1ae73504e 100644 --- a/docs/content/howto/query-and-transform/query_performance_tuning.md +++ b/docs/content/howto/query-and-transform/query_performance_tuning.md @@ -35,7 +35,7 @@ snippet: howto/dataframe_performance[cache] ## Leverage sparsity to minimize scans In a write once, read many paradigm adding an additional sparse column can enable cheap access to data of interest via filtering. -The Rerun Data Platform has the ability to "push down" filters to greatly reduce the amount of data returned, improving query performance. +The catalog server has the ability to "push down" filters to greatly reduce the amount of data returned, improving query performance. In this example we take advantage of this fact by filtering based on a sparse marker we have intentionally inserted into the recording. snippet: howto/dataframe_performance[sparsity] diff --git a/docs/content/howto/query-and-transform/query_videos.md b/docs/content/howto/query-and-transform/query_videos.md index 5f71a0603564..ce7ad81d50a0 100644 --- a/docs/content/howto/query-and-transform/query_videos.md +++ b/docs/content/howto/query-and-transform/query_videos.md @@ -3,10 +3,10 @@ title: Query video streams order: 70 --- -Video streams provide the best compression ratio for camera feeds, but require special handling when querying data back from the Data Platform. +Video streams provide the best compression ratio for camera feeds, but require special handling when querying data back from a catalog server. For more details about the different video types we support see our [video reference](../../concepts/logging-and-ingestion/video.md). -This guide focuses on querying [`VideoStream`](../../reference/types/archetypes/video_stream.md) data from the Rerun Data Platform, +This guide focuses on querying [`VideoStream`](../../reference/types/archetypes/video_stream.md) data from a catalog server, including how to decode individual frames and how to export entire streams to MP4 files. The dependencies in this example require `rerun-sdk[all]` and `av` for video decoding. diff --git a/docs/content/howto/query-and-transform/segment_url.md b/docs/content/howto/query-and-transform/segment_url.md index d1e89ebfb0c4..1a82c0dd83d1 100644 --- a/docs/content/howto/query-and-transform/segment_url.md +++ b/docs/content/howto/query-and-transform/segment_url.md @@ -8,7 +8,7 @@ The generated URLs can optionally seek to a timestamp, select a time range, or s ## Setup -We start by loading sample data in a local Data Platform instance and creating a table with some segment metadata. +We start by loading sample data in a local catalog server instance and creating a table with some segment metadata. snippet: howto/query-and-transform/segment_url[setup] diff --git a/docs/content/howto/query-and-transform/time_alignment.md b/docs/content/howto/query-and-transform/time_alignment.md index cf03084427d2..457990e7b6b7 100644 --- a/docs/content/howto/query-and-transform/time_alignment.md +++ b/docs/content/howto/query-and-transform/time_alignment.md @@ -1,6 +1,6 @@ --- title: Time-align data -order: 80 +order: 75 --- Real-world data is usually not time-aligned. diff --git a/docs/content/howto/query-and-transform/view_operations.md b/docs/content/howto/query-and-transform/view_operations.md index 09b510d938ff..c3e0487a5055 100644 --- a/docs/content/howto/query-and-transform/view_operations.md +++ b/docs/content/howto/query-and-transform/view_operations.md @@ -1,6 +1,6 @@ --- title: View Operations -order: 70 +order: 65 --- Robotics data has many sensors and many columns. diff --git a/docs/content/howto/train.md b/docs/content/howto/train.md new file mode 100644 index 000000000000..798bc5830f3b --- /dev/null +++ b/docs/content/howto/train.md @@ -0,0 +1,5 @@ +--- +title: Train +order: 350 +redirect: howto/train/dataloader +--- diff --git a/docs/content/howto/train/dataloader.md b/docs/content/howto/train/dataloader.md new file mode 100644 index 000000000000..497263a38f92 --- /dev/null +++ b/docs/content/howto/train/dataloader.md @@ -0,0 +1,124 @@ +--- +title: Train PyTorch models with Rerun +order: 200 +description: Stream Rerun recordings into a PyTorch DataLoader for model training, without an intermediate export step. +--- + +Train PyTorch models directly from a Rerun server. + +The experimental [`dataloader`](https://github.com/rerun-io/rerun/tree/main/rerun_py/rerun_sdk/rerun/experimental/dataloader) module exposes Rerun recordings as iterable or map-style PyTorch datasets, decoding compressed video (`h264`/`h265`/`av1`), images, and scalars on the fly. Random access, multi-worker prefetching, and [DDP](https://docs.pytorch.org/tutorials/beginner/ddp_series_theory.html) partitioning all work out of the box. + +> [!WARNING] +> **Experimental.** The API is provisional and will change between releases. For large-scale training, [Rerun Hub](https://rerun.io) offers a higher-performance backend than the OSS catalog. + +The full code for this guide lives in [`examples/python/dataloader/`](https://github.com/rerun-io/rerun/tree/main/examples/python/dataloader), which trains a [LeRobot ACT](https://tonyzhaozh.github.io/aloha/) policy from a HuggingFace dataset. + + + + + + + + + +## Training sample construction + +A [vision-language-action policy](https://en.wikipedia.org/wiki/Vision-language-action_model) is trained on samples that align several columns of multimodal data at the same instant in time: + + + A single training sample for a VLA model with camera, task, state, and action columns aligned at the current row + + +The dataloader assembles those samples on demand from the per-recording [chunks](https://rerun.io/docs/concepts/logging-and-ingestion/chunks) in a Rerun [catalog](https://rerun.io/docs/concepts/query-and-transform/catalog-object-model#catalog), while the PyTorch `DataLoader` drives batching, shuffling, and worker parallelism. + +## How to use it + +### Register data with a catalog + +The dataloader reads from a Rerun catalog, so you must first register [RRDs](https://rerun.io/docs/concepts/logging-and-ingestion/recordings/#storage-formats). Start the OSS server in a separate terminal: + +```bash +rerun server +``` + +Then register your recordings. Each registered RRD becomes a *segment* in the dataset, typically one episode or trajectory per RRD: + +snippet: howto/dataloader[register] + +The example's [`prepare_dataset.py`](https://github.com/rerun-io/rerun/blob/main/examples/python/dataloader/prepare_dataset.py) shows the full flow for converting a HuggingFace LeRobot dataset into per-episode RRDs and registering them. + +### Describe a sample + +A Rerun dataset is built from three things: + +- a `DataSource`: the catalog dataset and an optional segment filter +- an `index`: the timeline that defines what "one sample" means (e.g. `"real_time"` or `"frame_index"`) +- a dict of `Field`s: what each sample should contain + +snippet: howto/dataloader[describe_sample] + +Each `Field.path` is a column name from the dataset's catalog schema. The decoder turns that column into a tensor: + +- `NumericDecoder()` for scalar and list-of-scalar columns +- `ImageDecoder()` for encoded image blobs (JPEG/PNG) +- `VideoFrameDecoder(codec=…)` for compressed video (`h264`/`h265`/`av1`) + +The dict keys (`"state"`, `"action"`, …) in `fields` become the keys of each sample dict that the dataset yields. When the `index` is a timestamp timeline (like `"real_time"` above), pass `timeline_sampling=FixedRateSampling(rate_hz=…)` so the dataloader knows how to lay out the sampling grid. + +#### Action chunks and history via `window` + +`Field(window=(start, end))` returns a *slice* of values across that inclusive range relative to the current index, instead of a single value: + + + Sample with non-uniform history showing the current row plus a windowed slice of preceding rows + + +snippet: howto/dataloader[window] + +The example uses this to feed 50-step action chunks into the ACT policy. + +#### Video decoding is GOP-aware + +A `VideoFrameDecoder` looks like a regular field from the outside, but decoding any one frame of compressed video requires running the codec from the previous keyframe forward through the target frame. The chain of frames the codec has to walk through is bounded by the [GOP](https://en.wikipedia.org/wiki/Group_of_pictures) length: + + + Sample construction for a VLA model: each video frame requires decoding from the preceding keyframe forward + + +snippet: howto/dataloader[video_decoder] + +The dataloader handles this transparently. `VideoFrameDecoder.context_range` asks the prefetcher for a window of preceding samples ending at the target, sized to be guaranteed to span at least one keyframe; the codec runs over the fetched packets in order and returns the frame at the target index. You only need to pass `keyframe_interval`, which must be greater than or equal to the actual GOP length; for timestamp timelines, also pass an `fps_estimate` that approximates the true frame rate. + +### Iterable vs. Map-style + +The dataloader provides both PyTorch dataset styles: + +- `RerunIterableDataset`: streaming iteration with internal shuffling (on by default) and cross-worker partitioning. Good default. Call `ds.set_epoch(epoch)` to reseed the shuffle between epochs. +- `RerunMapDataset`: random access by global index, plugs into PyTorch's sampler ecosystem (`DistributedSampler`, `WeightedRandomSampler`, `SubsetRandomSampler`, …). + +Wrap either in `torch.utils.data.DataLoader`: + +snippet: howto/dataloader[dataloader] + +For DDP, the iterable dataset partitions the index list across ranks automatically. With the map dataset, swap in `sampler=DistributedSampler(ds)` and call `sampler.set_epoch(epoch)` each epoch. + +### Train + +From there, the training loop is standard PyTorch: + +snippet: howto/dataloader[train] + +The full [LeRobot ACT example](https://github.com/rerun-io/rerun/tree/main/examples/python/dataloader) wires this up against three camera streams plus state and action chunks, and trains the policy end-to-end. + +## Limitations + +The module is **experimental**: expect breaking changes between releases as we iterate on the design. + +For large-scale training (hundreds of recordings, multi-node), consider [Rerun Hub](https://rerun.io), which offers a higher-performance backend than the OSS catalog. + +## References + +- [LeRobot ACT training example](https://github.com/rerun-io/rerun/tree/main/examples/python/dataloader) +- [`rerun.experimental.dataloader`](https://github.com/rerun-io/rerun/tree/main/rerun_py/rerun_sdk/rerun/experimental/dataloader) module source +- [The data layer tax in robot learning](https://rerun.io/blog/data-layer-tax) (figures used in this guide) +- [Export recordings to LeRobot datasets](lerobot_export.md) (inverse: Rerun → LeRobot dataset) diff --git a/docs/content/howto/query-and-transform/lerobot_export.md b/docs/content/howto/train/lerobot_export.md similarity index 77% rename from docs/content/howto/query-and-transform/lerobot_export.md rename to docs/content/howto/train/lerobot_export.md index 4e879eff4d2f..18acfbfc6236 100644 --- a/docs/content/howto/query-and-transform/lerobot_export.md +++ b/docs/content/howto/train/lerobot_export.md @@ -8,19 +8,20 @@ This guide demonstrates how to use the OSS Rerun server to query recordings, ali ## Prerequisites -This example requires the `rerun_export` package from the Rerun repository: +The conversion code lives in the [`rerun-lerobot`](https://github.com/rerun-io/rerun-lerobot) package. +Install it from PyPI: ```bash -pip install -e examples/python/rerun_export +pip install rerun-lerobot ``` -This will install the necessary dependencies including LeRobot, DataFusion, and PyArrow. +See the [`rerun-lerobot` repository](https://github.com/rerun-io/rerun-lerobot) for source and development instructions. ## Time alignment and resampling By default, the export uses the frame rate specified in the config to create evenly spaced samples (a LeRobot requirement). -For more details on time alignment, see [Time-align data](time_alignment.md). +For more details on time alignment, see [Time-align data](../query-and-transform/time_alignment.md). ## Setup @@ -29,12 +30,12 @@ Each recording becomes a segment in the dataset, and each unique segment id beco snippet: howto/lerobot_export[setup] -See [Catalog object model](../../concepts/query-and-transform/catalog-object-model.md) for how recordings are represented on the Data Platform. +See [Catalog object model](../../concepts/query-and-transform/catalog-object-model.md) for how recordings are represented on a catalog server. ### Filter data for training Robot recordings often contain more data than needed for training. -Filter the dataset to include only the relevant entity paths and components that will map to LeRobot’s standardized format. +Filter the dataset to include only the relevant entity paths and components that will map to LeRobot's standardized format. For example, you might include joint position commands as actions, joint states and end-effector pose as observations, RGB camera streams as video inputs, and a language instruction as the task description. Other signals such as debug visualizations, intermediate computations, or unused sensors can be excluded. @@ -79,7 +80,7 @@ Convert the filtered data into a LeRobot episode. This is the core transformatio snippet: howto/lerobot_export[export_episode] The `convert_dataframe_to_episode` function performs time alignment and resamples the dataframe to the target frame rate. It generates a sequence of evenly spaced timestamps at the target frame rate and treats these as the canonical timesteps for the episode. -For each timestep, it queries the most recent available value of every selected component using Rerun’s [`latest-at`](../../concepts/logging-and-ingestion/latest-at.md) semantics. If a stream has no sample exactly at that time, its last observed value is forward-filled. +For each timestep, it queries the most recent available value of every selected component using Rerun's [`latest-at`](../../concepts/logging-and-ingestion/latest-at.md) semantics. If a stream has no sample exactly at that time, its last observed value is forward-filled. The `finalize()` call completes the dataset by writing metadata and closing all files. @@ -103,10 +104,10 @@ dataset.push_to_hub(repo_id="your-username/your-dataset-name") ## Command-line interface -The `rerun_export` package includes a CLI that implements this workflow for batch processing: +The `rerun-lerobot` package includes a CLI that implements this workflow for batch processing: ```bash -rerun_export \ +rerun-lerobot \ --rrd-dir ./tests/assets/rrd/sample_5 \ --output ./lerobot_dataset \ --dataset-name rerun-example-droid \ @@ -114,9 +115,9 @@ rerun_export \ --action /action/joint_positions:Scalars:scalars \ --state /observation/joint_positions:Scalars:scalars \ --task /language_instruction:TextDocument:text \ - --video ext1:/camera/ext1 \ - --video ext2:/camera/ext2 \ - --video wrist:/camera/wrist + --video ext1:/camera/ext1:VideoStream:sample \ + --video ext2:/camera/ext2:VideoStream:sample \ + --video wrist:/camera/wrist:VideoStream:sample ``` -See the [rerun_export example](https://rerun.io/examples/python/rerun_export) for the complete implementation. +See the [`rerun-lerobot` repository](https://github.com/rerun-io/rerun-lerobot) for the complete implementation. diff --git a/docs/content/howto/visualization/build-a-blueprint-programmatically.md b/docs/content/howto/visualization/build-a-blueprint-programmatically.md index 26fb80df2bea..22f83b6672bf 100644 --- a/docs/content/howto/visualization/build-a-blueprint-programmatically.md +++ b/docs/content/howto/visualization/build-a-blueprint-programmatically.md @@ -350,7 +350,7 @@ This is particularly useful when using Rust or C++ SDKs, since the blueprint API snippet: howto/visualization/load_blueprint -This works using the `log_file_from_path` API, which allows you to log any file that contains data that Rerun understands—in this case, blueprint data. +This works using the `log_file_from_path` API, which allows you to log any file that contains data that Rerun understands — in this case, blueprint data. API reference: diff --git a/docs/content/howto/visualization/extend-ui.md b/docs/content/howto/visualization/extend-ui.md index 8b5c3cd7562d..4a4b33eb8787 100644 --- a/docs/content/howto/visualization/extend-ui.md +++ b/docs/content/howto/visualization/extend-ui.md @@ -7,7 +7,8 @@ description: How to extend the Rerun Viewer UI using Rust and egui There are three ways to extend the Rerun Viewer with custom Rust code, depending on how deep you need to go: embedding custom UI panels alongside the Viewer, adding a custom visualizer to a built-in view, or implementing an entirely new view class. -**⚠️ Note that the interfaces for extending the Viewer are not yet stable.** Expect code implementing custom extensions to break with every release of Rerun. +> [!WARNING] +> The interfaces for extending the Viewer are not yet stable. Expect code implementing custom extensions to break with every release of Rerun. ## Embedding custom UI in the Viewer diff --git a/docs/content/howto/visualization/fixed-window-plot.md b/docs/content/howto/visualization/fixed-window-plot.md index 87120f962800..1e374072d0e4 100644 --- a/docs/content/howto/visualization/fixed-window-plot.md +++ b/docs/content/howto/visualization/fixed-window-plot.md @@ -1,6 +1,6 @@ --- title: Visualize fixed-window plots -order: 200 +order: 225 --- As of Rerun 0.16, the [TimeSeriesView](../../reference/types/views/time_series_view.md) now supports direct diff --git a/docs/content/howto/visualization/geospatial-data.md b/docs/content/howto/visualization/geospatial-data.md index 82630ed8cee3..d4d676ea0526 100644 --- a/docs/content/howto/visualization/geospatial-data.md +++ b/docs/content/howto/visualization/geospatial-data.md @@ -4,7 +4,7 @@ order: 300 --- Rerun 0.20 introduced a new [map view](../../reference/types/views/map_view.md). -This guide provides a short overview on how to use it to visualise geospatial data. +This guide provides a short overview on how to use it to visualize geospatial data. ## Coordinate system @@ -35,7 +35,8 @@ Rerun currently supports two types of geometries: - [`GeoPoints`](../../reference/types/archetypes/geo_points.md): batch of individual points, with optional [radius](../../reference/types/components/radius.md) and [color](../../reference/types/components/color.md) - [`GeoLineStrings`](../../reference/types/archetypes/geo_line_strings.md): batch of line strings, with optional [radius](../../reference/types/components/radius.md) and [color](../../reference/types/components/color.md) -*Note*: polygons are planned but are not supported yet (see [this issue](https://github.com/rerun-io/rerun/issues/8066)). +> [!NOTE] +> Polygons are planned but are not supported yet (see [this issue](https://github.com/rerun-io/rerun/issues/8066)). As in other views, radii may be expressed either as UI points (negative values) or scene units (positive values). For the latter case, the map view uses meters are scene units. diff --git a/docs/content/howto/visualization/multiple-viewers.md b/docs/content/howto/visualization/multiple-viewers.md index b158b97bc527..66c776cc1ce7 100644 --- a/docs/content/howto/visualization/multiple-viewers.md +++ b/docs/content/howto/visualization/multiple-viewers.md @@ -20,7 +20,7 @@ To open multiple viewer windows, use different ports with the `--port` flag. # Start a viewer on the default port (9876) $ rerun & -# This does nothing—a viewer is already running on :9876 +# This does nothing — a viewer is already running on :9876 $ rerun & # Start a second viewer on port 6789 @@ -50,6 +50,6 @@ rr.connect_grpc("rerun+http://127.0.0.1:6789") ## Tips -- Use `spawn()` to automatically start a new viewer if needed—it will reuse an existing viewer on the default port if one is running +- Use `spawn()` to automatically start a new viewer if needed — it will reuse an existing viewer on the default port if one is running - Each viewer maintains its own Chunk Store, so data sent to different viewers is independent -- The Web Viewer doesn't use gRPC ports the same way—it connects via WebSocket when served locally +- The Web Viewer doesn't use gRPC ports the same way — it connects via WebSocket when served locally diff --git a/docs/content/howto/visualization/plot-any-scalar.md b/docs/content/howto/visualization/plot-any-scalar.md index 4ff101dad985..449f35e647df 100644 --- a/docs/content/howto/visualization/plot-any-scalar.md +++ b/docs/content/howto/visualization/plot-any-scalar.md @@ -48,6 +48,10 @@ The following remaps the `Scalars:scalars` input to read from `custom:my_custom_ snippet: howto/component_mapping[source_mapping] +### Add data by dragging components + +You can set up this mapping interactively instead of via the blueprint API: drag a component from the streams tree onto a time series view. If the component has a compatible (numeric) datatype, a new `SeriesLines` visualizer is added that remaps `Scalars:scalars` from it. Non-numeric components (e.g. a string) are rejected, as is dropping a component that the view already plots. + ## Selectors for nested data When your data lives inside an Arrow `StructArray`, use a _selector_ to extract a specific field. diff --git a/docs/content/howto/visualization/state-timeline.md b/docs/content/howto/visualization/state-timeline.md new file mode 100644 index 000000000000..7f4e267f5f23 --- /dev/null +++ b/docs/content/howto/visualization/state-timeline.md @@ -0,0 +1,79 @@ +--- +title: Visualize state changes +order: 700 +--- + +The [StateTimelineView](../../reference/types/views/state_timeline_view.md) shows how entities transition between discrete states over time. Each entity becomes a horizontal lane, and each logged state is rendered as a colored band that runs until the next change. This is a good fit for state machines, mode transitions, sensor health, or any other piece of data that's better described as "what state am I in right now?" than as a numerical value. + +## Logging state changes + +Use [`StateChange`](../../reference/types/archetypes/state_change.md) to log a transition. Each call marks the start of a new state at the current time; the previous state implicitly ends. The state value is a string, so you can use any label that's meaningful for your application. + +snippet: howto/state_timeline[log_changes] + + + + + + + + + +### Notes +- The view groups state changes by entity path, so logging to `/door` and `/window` produces two separate lanes. +- Logging the same state value twice in a row is a no-op for visualization, only transitions to a different value start a new phase. +- Each phase runs from its `StateChange` time to the next `StateChange` time on the same entity. The final phase extends indefinitely. + +## Customizing labels, colors, and visibility + +To override the default styling, log a [`StateConfiguration`](../../reference/types/archetypes/state_configuration.md) to the same entity. `values`, `labels`, `colors`, and `visible` are parallel arrays — index `i` of each describes the same state value. Anything you don't provide falls back to the default (raw value as label, hashed color, visible). + +It is usually best to log `StateConfiguration` as static, since it describes how to display values rather than a moment in time. + +snippet: howto/state_timeline[state_config] + + + + + + + + + +## Visualize any component as state + +You don't have to log [`StateChange`](../../reference/types/archetypes/state_change.md) to use this view. Any component whose data is string-, boolean-, or number-like can drive a lane by **remapping** the visualizer's `StateChange:state` input to read from it instead. This lets you separate how you _model_ your data from how you _visualize_ it. For example, visualizing a robot mode that you logged as a plain string via `AnyValues` or `DynamicArchetype` (the same idea as [Plot any scalar](plot-any-scalar.md), applied to the state slot). + +The supported source data types are: + +- `Utf8` and `LargeUtf8` (rendered as string states) +- `Boolean` (rendered as two states) +- `Int8`, `Int16`, `Int32`, `Int64`, `UInt8`, `UInt16`, `UInt32`, `UInt64`, `Float16`, `Float32`, and `Float64` (rendered as numeric states) + +For background on how visualizers resolve their inputs, see [Component mappings](component-mappings.md) and [Customize views](../../concepts/visualization/customize-views.md). + +For example, log a robot mode as a plain string component: + +snippet: howto/state_remapping[custom_data] + +Then point the state-timeline visualizer at it by remapping `StateChange:state`: + +snippet: howto/state_remapping[blueprint] + +### Add data by dragging components + +You can set up the same mapping interactively: drag a component from the streams tree onto a State Timeline view. If the component is a compatible source (string, boolean, or numeric), a new lane is added that remaps `StateChange:state` from it. Incompatible components (e.g. a blob or tensor) are rejected, as is dropping a component that the view already visualizes. + +## Setting up the view via blueprint + +The State Timeline view is also created automatically when `StateChange` data is present, but you can also configure it explicitly via the blueprint API: + +snippet: howto/state_timeline[blueprint] + + + + + + + + diff --git a/docs/content/overview/installing-rerun.md b/docs/content/overview/installing-rerun.md deleted file mode 100644 index b7cad84369f2..000000000000 --- a/docs/content/overview/installing-rerun.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Installing Rerun -order: 300 ---- - -Choose what you want to install: - - -- [Python](./installing-rerun/python.md) – the Python SDK (includes the Viewer) -- [C++](./installing-rerun/cpp.md) – the C++ SDK -- [Rust](./installing-rerun/rust.md) – the Rust SDK -- [Viewer](./installing-rerun/viewer.md) – the standalone Rerun Viewer application - -If you run into any issues, check the [Troubleshooting](./installing-rerun/troubleshooting.md) guide. diff --git a/docs/content/overview/resources.md b/docs/content/overview/resources.md index 7a71fadd67aa..9ab6a392daa3 100644 --- a/docs/content/overview/resources.md +++ b/docs/content/overview/resources.md @@ -3,110 +3,29 @@ title: Docs Guide order: 400 --- -This page provides an overview of how the Rerun documentation is organized to help you find what you need. +## How the docs are organized -## Overview +### Using Rerun -High-level introduction to Rerun: +| You want to… | Where to look | +| --- | --- | +| Learn by doing | [Getting Started](../getting-started.md) | +| Dive into a specific task | [How-to](../howto.md) | +| Understand how Rerun works | [Concepts](../concepts.md) | -- **[What is Rerun?](what-is-rerun.md)** - Learn about Rerun's data platform for Physical AI -- **[Installing the Viewer](installing-rerun/viewer.md)** - Get Rerun installed on your system +### Even more details: [reference](../reference.md) -## Getting started +- [Types](../reference/types.md) +- [Per language API documentation](../reference.md) +- [CLI flags](../reference/cli.md) +- [Migration notes](../reference/migration.md) -Step-by-step guides to get up and running quickly: +### Contributing to Rerun -- **[Log and Ingest](../getting-started/data-in.md)** - Learn how to log data to Rerun from your code -- **[Visualize](../getting-started/configure-the-viewer.md)** - Customize the visualization to your needs -- **[Query and Transform](../getting-started/data-out.md)** - Query and export data from Rerun recordings -- **[Troubleshooting](../overview/installing-rerun/troubleshooting.md)** - Solutions to common issues +**[Developing Rerun](../development.md)** is for people working *on* Rerun, not just *with* it. -## Concepts +## Beyond the docs -Understanding the foundational concepts behind Rerun: - -- **[How Does Rerun Work](../concepts/how-does-rerun-work.md)** - How Rerun applications are structured -- **[Entity Component System](../concepts/logging-and-ingestion/entity-component.md)** - Rerun's data model -- **[Entity Paths](../concepts/logging-and-ingestion/entity-path.md)** - Organizing your data hierarchically -- **[Spaces and Transforms](../concepts/logging-and-ingestion/transforms.md)** - Working with coordinate systems -- **[Timelines](../concepts/logging-and-ingestion/timelines.md)** - Managing temporal data -- **[Blueprints](../concepts/visualization/blueprints.md)** - Configuring visualization layouts -- **[Batches](../concepts/logging-and-ingestion/batches.md)** - Efficiently logging collections of data -- **[Static Data](../concepts/logging-and-ingestion/static.md)** - Data that exists across all timelines -- **[Query Semantics](../concepts/logging-and-ingestion/latest-at.md)** - How Rerun resolves data queries -- **[Annotation Context](../concepts/visualization/annotation-context.md)** - Shared styling and labels -- **[Recordings](../concepts/logging-and-ingestion/recordings.md)** - Managing recordings, application IDs, and the Data Platform -- **[Visualizers and Overrides](../concepts/visualization/customize-views.md)** - Customizing rendering -- **[Chunks](../concepts/logging-and-ingestion/chunks.md)** - Internal storage mechanism (advanced) - -## How-to guides - -Practical guides for specific tasks and advanced features: - -### Logging data -- **[Logging](../howto/logging-and-ingestion.md)** - Advanced logging techniques -- **[Send Columns](../howto/logging-and-ingestion/send-columns.md)** - Efficiently log columnar data -- **[Using Native Loggers](../howto/integrations/integrate-host-loggers.md)** - Integrate with existing logging systems -- **[Short-lived Entities](../howto/logging-and-ingestion/clears.md)** - Handling temporary data - -### Visualization -- **[Visualization](../howto/visualization.md)** - Advanced visualization techniques -- **[Configure Viewer Through Code](../getting-started/configure-the-viewer/navigating-the-viewer.md#programmatic-blueprints)** - Programmatic viewer configuration -- **[Fixed Window Plots](../howto/visualization/fixed-window-plot.md)** - Creating time-windowed plots - -### Data management -- **[DataFrame API](../howto/query-and-transform/get-data-out.md)** - Query recordings programmatically -- **[Get Data Out](../howto/query-and-transform/get-data-out.md)** - Export data from Rerun -- **[MCAP Integration](../howto/logging-and-ingestion/mcap.md)** - Working with MCAP files -- **[Shared Recordings](../howto/logging-and-ingestion/shared-recordings.md)** - Collaborate with recordings - -### Integration & deployment -- **[Integrations](../howto/integrations.md)** - Integrate Rerun with other tools -- **[Embed Rerun Viewer](../howto/integrations/embed-web.md)** - Embed the viewer in your application -- **[Jupyter Notebooks](../howto/integrations/embed-notebooks.md)** - Use Rerun in notebooks -- **[Callbacks](../howto/visualization/callbacks.md)** - Respond to viewer events - -### Performance & optimization -- **[Limit RAM Usage](../howto/visualization/limit-ram.md)** - Control memory consumption -- **[Optimize Chunks](../howto/logging-and-ingestion/optimize-chunks.md)** - Fine-tune data storage - -### Extending Rerun -- **[Extend](../howto/extend.md)** - Add custom types and visualizations - -### Examples -- **[ROS2 Nav Turtlebot](../howto/integrations/ros2-nav-turtlebot.md)** - Complete robotics example - -## Reference - -Detailed API documentation and technical specifications: - -### Types -- **[Archetypes](../reference/types/archetypes.md)** - Bundles of components with first-class viewer support -- **[Components](../reference/types/components.md)** - Individual data components used by archetypes -- **[Datatypes](../reference/types/datatypes.md)** - Fundamental data structures -- **[Views](../reference/types/views.md)** - Available visualization view types - -### SDKs -- **[Python APIs](https://ref.rerun.io/docs/python)** - Python SDK reference -- **[Rust APIs](https://docs.rs/rerun/)** - Rust SDK reference -- **[C++ APIs](https://ref.rerun.io/docs/cpp)** - C++ SDK reference -- **[Web Viewer API](https://ref.rerun.io/docs/js/)** - JavaScript/TypeScript web viewer API - -### Viewer & CLI -- **[Viewer](../reference/viewer/overview.md)** - Viewer UI overview and features -- **[CLI Manual](../reference/cli.md)** - Command-line interface reference - -### Migration guides -- **[Migration](../reference/migration.md)** - Guides for upgrading between Rerun versions - -## Development - -Contributing to Rerun: - -- **[Developing Rerun](../development.md)** - How to contribute to the Rerun project - -## Community - -- [Rerun Discord](https://discord.gg/PXtCgFBSmH) - Join the community -- [GitHub Repository](https://github.com/rerun-io/rerun) - Source code and issue tracking -- [Examples Gallery](https://rerun.io/examples) - See Rerun in action +- [Discord](https://discord.gg/PXtCgFBSmH) — ask questions and chat with other Rerun users +- [GitHub](https://github.com/rerun-io/rerun) — source code and issue tracker +- [Examples](https://rerun.io/examples) — see Rerun in action diff --git a/docs/content/overview/what-is-rerun.md b/docs/content/overview/what-is-rerun.md index ebb8035e2c46..991f9533132d 100644 --- a/docs/content/overview/what-is-rerun.md +++ b/docs/content/overview/what-is-rerun.md @@ -1,11 +1,12 @@ --- -title: What is Rerun? +title: The Data Layer for Physical AI order: 0 --- -Rerun is a data platform for Physical AI that helps you understand and improve complex processes involving rich multimodal data like 2D, 3D, text, time series, and tensors. +Rerun covers the whole journey from raw recordings to training, on a single unified data layer for multi-rate, multimodal robotics data. -It combines simple and flexible data logging with a powerful visualizer and query engine, designed specifically for domains like robotics, spatial computing, embodied AI, computer vision, simulation, and any system involving sensors and signals that evolve over time. +It's comprised of **Rerun SDK**: an open source library and tools for logging, storing, querying, visualizing, and training on multi-rate, multimodal data; and +**Rerun Hub**: a data catalog and backend for large scale storage, access, and streaming of robotics data from object storage. ## The problem @@ -18,18 +19,6 @@ Building intelligent physical systems requires rapid iteration on both data and The best robotics teams minimize their time from new data to training. Rerun gives you the unified infrastructure to make that happen. -## The Rerun Data Platform - -Rerun provides an integrated solution for working with multimodal temporal data: - -**Time-aware data model:** At its core is an [Entity Component System (ECS)](../concepts/logging-and-ingestion/entity-component.md) designed for robotics and XR applications. This model understands both [spatial relationships](../concepts/logging-and-ingestion/transforms.md) and [temporal evolution](../concepts/logging-and-ingestion/timelines.md), making it natural to work with sensor data, transforms, and time-series information. - -**Built-in visualization:** A fast, embeddable visualizer lets you see your data as 3D scenes, images, plots, and text—all synchronized and explorable through time. Build [layouts and customize visualizations](../getting-started/configure-the-viewer.md) interactively or [programmatically](../concepts/visualization/blueprints.md). - -**Query and export:** Extract clean [dataframes](../howto/query-and-transform/get-data-out.md) for analysis in Pandas, Polars, or DuckDB. Use recordings to create datasets for training and evaluating your models. - -**Flexible ingestion:** Load data from your code via the [SDK](../getting-started/data-in.md), from storage formats like [MCAP](../howto/logging-and-ingestion/mcap.md), or from proprietary log formats. [Extend Rerun](../howto/extend.md) when you need custom types or visualizations. - ## Who is Rerun for? Rerun is built for teams developing intelligent physical systems: @@ -39,34 +28,60 @@ Rerun is built for teams developing intelligent physical systems: - **ML engineers** preparing datasets and understanding model behavior - **Autonomy teams** developing and testing decision-making systems -If you're working with robots, drones, autonomous vehicles, spatial AI, or any system with sensors that evolve over time, Rerun helps you move faster. - -## What Rerun is not - -To set clear expectations: - -- **Not a training platform**: Use Rerun with PyTorch, TensorFlow, JAX, etc. We prepare your data; you train your models. -- **Not a deployment tool**: Rerun helps you develop and understand your systems, not deploy them to production. -- **Not a robot operating system**: Rerun works with ROS, ROS2, or any other robotics stack. -- **Not a general visualization tool**: We're specialized for physical, multimodal, time-series data. +If you're working with robots, drones, autonomous vehicles, spatial AI, or any system with data that evolves over time, Rerun helps you move faster. ## How do you use it? - - - - - - - - -1. Use the [Rerun SDK](../getting-started/data-in.md) to [log multimodal data](../getting-started/data-in.md) from your code or load it from storage -2. View live or recorded data in the standalone viewer or [embedded in your app](../howto/integrations/embed-web.md) -3. Build layouts and [customize visualizations](../getting-started/configure-the-viewer.md) interactively in the UI or through the SDK -4. [Query recordings](../getting-started/data-out.md) to get clean dataframes into tools like Pandas, Polars, or DuckDB -5. [Extend Rerun](../howto/extend.md) when you need to - -We also offer a commercial data platform for teams that need collaborative dataset management, version control, and cloud storage. [Learn more](https://rerun.io/pricing). +### Log and ingest +Use the [logging API](../getting-started/data-in.md) to log multimodal data from your code, or [the chunk processing API](../concepts/logging-and-ingestion/chunk-processing-api.md) to convert your existing data to the [.rrd](../concepts/logging-and-ingestion/rrd-format.md) file format to later visualize or query. +
+ + +
+ +### Visualize +Rerun provides an open source pre-built [viewer](../reference/viewer/overview.md) that is [adjustable](../getting-started/configure-the-viewer.md) and [extensible](../howto/extend.md). +You can log directly to the viewer, [open](../getting-started/data-in/open-any-file.md) a range of file formats to get data into the viewer, or even connect the viewer to a Rerun [catalog](../concepts/query-and-transform/catalog-object-model.md). + +
+ + +
+ +### Query and transform +The Rerun file format supports both high performance visualization and querying over the same data source. + +You can use the open source [catalog](../concepts/query-and-transform/catalog-object-model.md) server for running local [laptop scale examples](../getting-started/data-out). +We also offer **Rerun Hub**, a scalable catalog for robotic data, for teams that need collaborative dataset management, version control, and cloud storage ([reach out](https://5li7zhj98k8.typeform.com/to/a5XDpBkZ?typeform-source=docs) to learn more). +These are API compatible so the only difference from our examples to **Rerun Hub** is that you connect to an existing server instead of launching your own. + +#### Prepare catalog +Before querying or viewing recordings on the catalog we have to register them. +We group recordings as [datasets](../concepts/query-and-transform/catalog-object-model.md#datasets). +Since Rerun indexes existing data in place, registration needs paths to RRDs to index: in object store for **Rerun Hub** or on disk for local catalog server. + +
+ + +
+ +#### Use catalog +At this point a viewer can connect to the prepared catalog or we show the basic steps to perform a query. +We specify what dataset we want to query, get access to a lazy loaded [dataframe](../concepts/query-and-transform/dataframe-queries.md), specify our query, and retrieve the results. +Queries can be specified with SQL or dataframe APIs allowing the flexibility to investigate anything about your data. + +
+ + +
+ +### Train +Use the catalog as a data source for [training](../getting-started/train.md): a dataloader runs a query against the catalog and yields training batches. + +
+ + +
## Get started diff --git a/docs/content/reference.md b/docs/content/reference.md index 863a58acb4e2..d9a9c7aae2f1 100644 --- a/docs/content/reference.md +++ b/docs/content/reference.md @@ -1,6 +1,6 @@ --- title: Reference -order: 2 +order: 5 --- The reference docs detail how to use the logging APIs and the viewer. diff --git a/docs/content/reference/cli.md b/docs/content/reference/cli.md index a4feeb8bb745..ee23e5f69456 100644 --- a/docs/content/reference/cli.md +++ b/docs/content/reference/cli.md @@ -3,6 +3,8 @@ title: ⌨️ CLI manual order: 1150 --- + + ## rerun @@ -21,6 +23,7 @@ The Rerun command-line interface: * `download`: Download recordings and save them as .rrd files. * `man`: Generates the Rerun CLI manual (markdown). * `mcap`: Manipulate the contents of .mcap files. +* `viewer-mcp`: Run an MCP server that controls a running Rerun Viewer. * `reset`: Reset the memory of the Rerun Viewer. * `rrd`: Manipulate the contents of .rrd and .rbl files. * `server`: In-memory Rerun data server. @@ -33,7 +36,7 @@ The Rerun command-line interface: > - A path to a Rerun .rrd recording > - A path to a Rerun .rbl blueprint > - An HTTP(S) URL to an .rrd or .rbl file to load -> - A path to an image or mesh, or any other file that Rerun can load (see https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview?speculative-link) +> - A path to an image or mesh, or any other file that Rerun can load (see https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview) > > If no arguments are given, a server will be hosted which a Rerun SDK can connect to. @@ -138,13 +141,6 @@ The Rerun command-line interface: > > [Default: `false`] -* `--follow ` -> Tail .rrd files, waiting for new data to be appended after reaching EOF. -> -> Without this flag, .rrd files are read once and the viewer stops loading when EOF is reached. With this flag, the viewer will keep watching for new data, which is useful for live streaming from a writer process. -> -> [Default: `false`] - * `-j, --threads ` > The number of compute threads to use. > @@ -183,6 +179,13 @@ The Rerun command-line interface: > > [Default: `false`] +* `--headless ` +> Run the viewer in headless mode (no OS window). +> +> The viewer is driven by an offscreen `egui_kittest` harness, while the gRPC server keeps running so SDK clients can still log data and request screenshots via `save_screenshot`. +> +> [Default: `false`] + * `--window-size ` > Set the screen resolution (in logical points), e.g. "1920x1080". Useful together with `--screenshot-to`. @@ -271,7 +274,7 @@ Authentication with the redap. Log into Rerun. -This command opens a page in your default browser, allowing you to log in to the Rerun Data Platform. +This command opens a page in your default browser, allowing you to log in to Rerun Hub. Once you've logged in, your credentials are stored on your machine. @@ -310,7 +313,7 @@ This command clears the credentials stored on your machine and ends your session Generate a fresh access token. -You can use this token to authorize requests to the Rerun Data Platform. +You can use this token to authorize requests to Rerun Hub. It's closer to an API key than an access token, as it can be revoked before it expires. @@ -335,7 +338,7 @@ It's closer to an API key than an access token, as it can be revoked before it e Download recordings and save them as .rrd files. -Supports downloading from Rerun Cloud as well as any other supported URI. +Supports downloading from Rerun Hub as well as any other supported URI. **Usage**: `rerun download [OPTIONS] …` @@ -360,6 +363,7 @@ Manipulate the contents of .mcap files. **Commands** * `convert`: Convert an .mcap file to an .rrd. +* `info`: Print timeline / sortedness diagnostics for an .mcap file. ## rerun mcap convert @@ -419,6 +423,47 @@ Convert an .mcap file to an .rrd. > > Applied after includes: a topic is kept only if it matches an include (or no includes are set) AND matches no exclude. +* `--start-time
+### Time-windowed trails (e.g. Trajectories) + +snippet: archetypes/line_strips3d_time_window + + + + + + + + + diff --git a/docs/content/reference/types/archetypes/mcap_statistics.md b/docs/content/reference/types/archetypes/mcap_statistics.md index 555882152b74..4e15efb4bf60 100644 --- a/docs/content/reference/types/archetypes/mcap_statistics.md +++ b/docs/content/reference/types/archetypes/mcap_statistics.md @@ -4,7 +4,7 @@ title: "McapStatistics" ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -Recording-level statistics about an MCAP file, logged as a part of [`archetypes.RecordingInfo`](https://rerun.io/docs/reference/types/archetypes/recording_info). +Recording-level statistics about an MCAP file. This archetype contains summary information about an entire MCAP recording, including counts of messages, schemas, channels, and other records, as well as timing information diff --git a/docs/content/reference/types/archetypes/points3d.md b/docs/content/reference/types/archetypes/points3d.md index dcc493eee9cc..d0d0bed92c23 100644 --- a/docs/content/reference/types/archetypes/points3d.md +++ b/docs/content/reference/types/archetypes/points3d.md @@ -18,6 +18,7 @@ If there are multiple instance poses, the entire point cloud will be repeated fo ### Optional * `labels`: [`Text`](../components/text.md) * `show_labels`: [`ShowLabels`](../components/show_labels.md) +* `point_shading`: [`PointShading`](../components/point_shading.md) * `class_ids`: [`ClassId`](../components/class_id.md) * `keypoint_ids`: [`KeypointId`](../components/keypoint_id.md) diff --git a/docs/content/reference/types/archetypes/state_change.md b/docs/content/reference/types/archetypes/state_change.md new file mode 100644 index 000000000000..4f7fc5884dd9 --- /dev/null +++ b/docs/content/reference/types/archetypes/state_change.md @@ -0,0 +1,41 @@ +--- +title: "StateChange" +--- + + +A state change, representing a transition of an entity into a new state. + +Useful for representing discrete state machines, mode transitions, or +state changes over time. Each logged [`archetypes.StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change) marks a new state +at the given time. A `null` state resets the state, showing a gap in the state timeline view. + +The state timeline view displays these as horizontal colored lanes over time. + +## Fields +### Required +* `state`: [`Text`](../components/text.md) + + +## Can be shown in +* [StateTimelineView](../views/state_timeline_view.md) +* [DataframeView](../views/dataframe_view.md) + +## API reference links + * 🌊 [C++ API docs for `StateChange`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1archetypes_1_1StateChange.html) + * 🐍 [Python API docs for `StateChange`](https://ref.rerun.io/docs/python/stable/common/archetypes#rerun.archetypes.StateChange) + * 🦀 [Rust API docs for `StateChange`](https://docs.rs/rerun/latest/rerun/archetypes/struct.StateChange.html) + +## Example + +### State changes over time + +snippet: archetypes/state_change + + + + + + + + + diff --git a/docs/content/reference/types/archetypes/state_configuration.md b/docs/content/reference/types/archetypes/state_configuration.md new file mode 100644 index 000000000000..082985c11f30 --- /dev/null +++ b/docs/content/reference/types/archetypes/state_configuration.md @@ -0,0 +1,43 @@ +--- +title: "StateConfiguration" +--- + + +Define the style and mapping for state values in a state timeline view. + +This archetype provides configuration for how state values are displayed. +It maps raw state values to display labels, colors, and visibility. + +`values`, `labels`, `colors`, and `visible` are parallel arrays: the entry +at index `i` of each describes the same state value, and only the +per-index pairing is meaningful. The four arrays should have matching +length; any secondary array (`labels`, `colors`, `visible`) that is shorter +than `values` falls back to defaults for the missing entries. + +It's generally recommended to log this type as static. + +The underlying data needs to be logged to the same entity path using [`archetypes.StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change). + +## Fields +### Optional +* `values`: [`Text`](../components/text.md) +* `labels`: [`Text`](../components/text.md) +* `colors`: [`Color`](../components/color.md) +* `visible`: [`Visible`](../components/visible.md) + + +## Can be shown in +* [StateTimelineView](../views/state_timeline_view.md) +* [DataframeView](../views/dataframe_view.md) + +## API reference links + * 🌊 [C++ API docs for `StateConfiguration`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1archetypes_1_1StateConfiguration.html) + * 🐍 [Python API docs for `StateConfiguration`](https://ref.rerun.io/docs/python/stable/common/archetypes#rerun.archetypes.StateConfiguration) + * 🦀 [Rust API docs for `StateConfiguration`](https://docs.rs/rerun/latest/rerun/archetypes/struct.StateConfiguration.html) + +## Example + +### State changes with a custom style + +snippet: archetypes/state_configuration + diff --git a/docs/content/reference/types/archetypes/status.md b/docs/content/reference/types/archetypes/status.md deleted file mode 100644 index 2231cd908c6d..000000000000 --- a/docs/content/reference/types/archetypes/status.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: "Status" ---- - - -⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -A status update, representing a change in the status of an entity. - -Useful for representing discrete state machines, mode transitions, or -status changes over time. Each logged [`archetypes.Status`](https://rerun.io/docs/reference/types/archetypes/status?speculative-link) marks a new status -at the given time. A `null` status is ignored by the Status view. - -The Status view displays these as horizontal colored lanes over time. - -## Fields -### Required -* `status`: [`Text`](../components/text.md) - - -## Can be shown in -* [StatusView](../views/status_view.md) -* [DataframeView](../views/dataframe_view.md) - -## API reference links - * 🌊 [C++ API docs for `Status`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1archetypes_1_1Status.html?speculative-link) - * 🐍 [Python API docs for `Status`](https://ref.rerun.io/docs/python/stable/common/archetypes?speculative-link#rerun.archetypes.Status) - * 🦀 [Rust API docs for `Status`](https://docs.rs/rerun/latest/rerun/archetypes/struct.Status.html?speculative-link) - -## Example - -### Status changes over time - -snippet: archetypes/status - - - - - - - - - diff --git a/docs/content/reference/types/archetypes/video_stream.md b/docs/content/reference/types/archetypes/video_stream.md index 1b20a0ea8db5..53b9b6cf8978 100644 --- a/docs/content/reference/types/archetypes/video_stream.md +++ b/docs/content/reference/types/archetypes/video_stream.md @@ -22,6 +22,7 @@ TODO(#10422): [`archetypes.VideoFrameReference`](https://rerun.io/docs/reference * `sample`: [`VideoSample`](../components/video_sample.md) ### Optional +* `is_keyframe`: [`IsKeyframe`](../components/is_keyframe.md) * `opacity`: [`Opacity`](../components/opacity.md) * `draw_order`: [`DrawOrder`](../components/draw_order.md) diff --git a/docs/content/reference/types/archetypes/voxel_grid_map.md b/docs/content/reference/types/archetypes/voxel_grid_map.md new file mode 100644 index 000000000000..60cca704367c --- /dev/null +++ b/docs/content/reference/types/archetypes/voxel_grid_map.md @@ -0,0 +1,47 @@ +--- +title: "VoxelGridMap" +--- + + +⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +A sparse 3D voxel grid map with grid indices and voxel dimensions. + +This archetype is intended for 3D occupancy maps and other volumetric data +represented as a sparse grid of voxels with scene-unit dimensions along the local X/Y/Z axes. + +The minimum corner of the voxel with `[0, 0, 0]` index is located at the origin of the entity's coordinate frame +and can have an additional offset from there through the optional translation and rotation fields. + +A voxel center is at `(index + 0.5) * voxel_size` in local grid coordinates (i.e. relative to the minimum corner). + +## Fields +### Required +* `voxel_indices`: [`VoxelIndex`](../components/voxel_index.md) +* `voxel_size`: [`VoxelSize`](../components/voxel_size.md) + +### Optional +* `values`: [`VoxelValue`](../components/voxel_value.md) +* `colors`: [`Color`](../components/color.md) +* `translation`: [`Translation3D`](../components/translation3d.md) +* `rotation_axis_angle`: [`RotationAxisAngle`](../components/rotation_axis_angle.md) +* `quaternion`: [`RotationQuat`](../components/rotation_quat.md) +* `opacity`: [`Opacity`](../components/opacity.md) +* `value_range`: [`ValueRange`](../components/value_range.md) +* `colormap`: [`Colormap`](../components/colormap.md) + + +## Can be shown in +* [Spatial3DView](../views/spatial3d_view.md) +* [DataframeView](../views/dataframe_view.md) + +## API reference links + * 🌊 [C++ API docs for `VoxelGridMap`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1archetypes_1_1VoxelGridMap.html) + * 🐍 [Python API docs for `VoxelGridMap`](https://ref.rerun.io/docs/python/stable/common/archetypes#rerun.archetypes.VoxelGridMap) + * 🦀 [Rust API docs for `VoxelGridMap`](https://docs.rs/rerun/latest/rerun/archetypes/struct.VoxelGridMap.html) + +## Example + +### Simple sparse voxel grid map + +snippet: archetypes/voxel_grid_map_simple + diff --git a/docs/content/reference/types/components.md b/docs/content/reference/types/components.md index e9152ca209d4..792dd37faf97 100644 --- a/docs/content/reference/types/components.md +++ b/docs/content/reference/types/components.md @@ -1,6 +1,7 @@ --- title: "Components" order: 2 +sort_children: alphabetical --- @@ -43,6 +44,7 @@ on [Entities and Components](../../concepts/logging-and-ingestion/entity-compone * [`ImagePlaneDistance`](components/image_plane_distance.md): The distance from the camera origin to the image plane when the projection is shown in a 3D viewer. * [`Interactive`](components/interactive.md): Whether the entity can be interacted with. * [`InterpolationMode`](components/interpolation_mode.md): Specifies how values between data points are interpolated in time series. +* [`IsKeyframe`](components/is_keyframe.md): Whether a [`components.VideoSample`](https://rerun.io/docs/reference/types/components/video_sample) contains a keyframe (also known as a sync sample or IDR). * [`KeyValuePairs`](components/key_value_pairs.md): A map of string keys to string values. * [`KeypointId`](components/keypoint_id.md): A 16-bit ID representing a type of semantic keypoint within a class. * [`LatLon`](components/lat_lon.md): A geospatial position expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees). @@ -59,6 +61,7 @@ on [Entities and Components](../../concepts/logging-and-ingestion/entity-compone * [`Opacity`](components/opacity.md): Degree of transparency ranging from 0.0 (fully transparent) to 1.0 (fully opaque). * [`PinholeProjection`](components/pinhole_projection.md): Camera projection, from image coordinates to view coordinates. * [`Plane3D`](components/plane3d.md): An infinite 3D plane represented by a unit normal vector and a distance. +* [`PointShading`](components/point_shading.md): Defines how points are shaded. * [`Position2D`](components/position2d.md): A position in 2D space. * [`Position3D`](components/position3d.md): A position in 3D space. * [`Radius`](components/radius.md): The radius of something, e.g. a point. @@ -92,4 +95,7 @@ on [Entities and Components](../../concepts/logging-and-ingestion/entity-compone * [`VideoTimestamp`](components/video_timestamp.md): Timestamp inside a [`archetypes.AssetVideo`](https://rerun.io/docs/reference/types/archetypes/asset_video). * [`ViewCoordinates`](components/view_coordinates.md): How we interpret the coordinate system of an entity/space. * [`Visible`](components/visible.md): Whether the container, view, entity or instance is currently visible. +* [`VoxelIndex`](components/voxel_index.md): Integer index of a voxel in a sparse 3D voxel grid. +* [`VoxelSize`](components/voxel_size.md): The scene-unit dimensions of one voxel in a sparse 3D voxel grid. +* [`VoxelValue`](components/voxel_value.md): Optional scalar occupancy or value associated with a voxel. diff --git a/docs/content/reference/types/components/.gitattributes b/docs/content/reference/types/components/.gitattributes index b808546c9fdf..74000fdebadc 100644 --- a/docs/content/reference/types/components/.gitattributes +++ b/docs/content/reference/types/components/.gitattributes @@ -31,6 +31,7 @@ image_format.md linguist-generated=true image_plane_distance.md linguist-generated=true interactive.md linguist-generated=true interpolation_mode.md linguist-generated=true +is_keyframe.md linguist-generated=true key_value_pairs.md linguist-generated=true keypoint_id.md linguist-generated=true lat_lon.md linguist-generated=true @@ -47,6 +48,7 @@ name.md linguist-generated=true opacity.md linguist-generated=true pinhole_projection.md linguist-generated=true plane3d.md linguist-generated=true +point_shading.md linguist-generated=true position2d.md linguist-generated=true position3d.md linguist-generated=true radius.md linguist-generated=true @@ -80,3 +82,6 @@ video_sample.md linguist-generated=true video_timestamp.md linguist-generated=true view_coordinates.md linguist-generated=true visible.md linguist-generated=true +voxel_index.md linguist-generated=true +voxel_size.md linguist-generated=true +voxel_value.md linguist-generated=true diff --git a/docs/content/reference/types/components/cell_size.md b/docs/content/reference/types/components/cell_size.md index e29458d66fc3..948d29e21a54 100644 --- a/docs/content/reference/types/components/cell_size.md +++ b/docs/content/reference/types/components/cell_size.md @@ -17,11 +17,11 @@ Float32 ``` ## API reference links - * 🌊 [C++ API docs for `CellSize`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1CellSize.html?speculative-link) - * 🐍 [Python API docs for `CellSize`](https://ref.rerun.io/docs/python/stable/common/components?speculative-link#rerun.components.CellSize) - * 🦀 [Rust API docs for `CellSize`](https://docs.rs/rerun/latest/rerun/components/struct.CellSize.html?speculative-link) + * 🌊 [C++ API docs for `CellSize`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1CellSize.html) + * 🐍 [Python API docs for `CellSize`](https://ref.rerun.io/docs/python/stable/common/components#rerun.components.CellSize) + * 🦀 [Rust API docs for `CellSize`](https://docs.rs/rerun/latest/rerun/components/struct.CellSize.html) ## Used by -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) diff --git a/docs/content/reference/types/components/class_id.md b/docs/content/reference/types/components/class_id.md index 1020d5d32b05..78b3971fe488 100644 --- a/docs/content/reference/types/components/class_id.md +++ b/docs/content/reference/types/components/class_id.md @@ -28,6 +28,7 @@ UInt16 * [`Boxes3D`](../archetypes/boxes3d.md) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Cylinders3D`](../archetypes/cylinders3d.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) * [`GeoPoints`](../archetypes/geo_points.md) * [`LineStrips2D`](../archetypes/line_strips2d.md) diff --git a/docs/content/reference/types/components/color.md b/docs/content/reference/types/components/color.md index cae88c5af81c..c3a473164fc7 100644 --- a/docs/content/reference/types/components/color.md +++ b/docs/content/reference/types/components/color.md @@ -32,6 +32,7 @@ UInt32 * [`Boxes3D`](../archetypes/boxes3d.md) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Cylinders3D`](../archetypes/cylinders3d.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) * [`GeoLineStrings`](../archetypes/geo_line_strings.md) * [`GeoPoints`](../archetypes/geo_points.md) @@ -44,4 +45,6 @@ UInt32 * [`Points3D`](../archetypes/points3d.md) * [`SeriesLines`](../archetypes/series_lines.md) * [`SeriesPoints`](../archetypes/series_points.md) +* [`StateConfiguration`](../archetypes/state_configuration.md) * [`TextLog`](../archetypes/text_log.md) +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/colormap.md b/docs/content/reference/types/components/colormap.md index 3ad233067ac8..b5bc3ce0cba6 100644 --- a/docs/content/reference/types/components/colormap.md +++ b/docs/content/reference/types/components/colormap.md @@ -97,4 +97,5 @@ UInt8 * [`DepthImage`](../archetypes/depth_image.md) * [`EncodedDepthImage`](../archetypes/encoded_depth_image.md) -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/draw_order.md b/docs/content/reference/types/components/draw_order.md index 2345407a7743..8c486c4cec73 100644 --- a/docs/content/reference/types/components/draw_order.md +++ b/docs/content/reference/types/components/draw_order.md @@ -30,9 +30,10 @@ Float32 * [`Arrows2D`](../archetypes/arrows2d.md) * [`Boxes2D`](../archetypes/boxes2d.md) * [`DepthImage`](../archetypes/depth_image.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) * [`EncodedDepthImage`](../archetypes/encoded_depth_image.md) * [`EncodedImage`](../archetypes/encoded_image.md) -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) * [`Image`](../archetypes/image.md) * [`LineStrips2D`](../archetypes/line_strips2d.md) * [`Points2D`](../archetypes/points2d.md) diff --git a/docs/content/reference/types/components/half_size2d.md b/docs/content/reference/types/components/half_size2d.md index 9535bfbad93e..ee340e88566f 100644 --- a/docs/content/reference/types/components/half_size2d.md +++ b/docs/content/reference/types/components/half_size2d.md @@ -28,3 +28,4 @@ FixedSizeList(2 x non-null Float32) ## Used by * [`Boxes2D`](../archetypes/boxes2d.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) diff --git a/docs/content/reference/types/components/image_buffer.md b/docs/content/reference/types/components/image_buffer.md index 7222ad2de8cb..3d70e2e2afa8 100644 --- a/docs/content/reference/types/components/image_buffer.md +++ b/docs/content/reference/types/components/image_buffer.md @@ -25,7 +25,7 @@ List(non-null UInt8) ## Used by * [`DepthImage`](../archetypes/depth_image.md) -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) * [`Image`](../archetypes/image.md) * [`Mesh3D`](../archetypes/mesh3d.md) * [`SegmentationImage`](../archetypes/segmentation_image.md) diff --git a/docs/content/reference/types/components/image_format.md b/docs/content/reference/types/components/image_format.md index ec6eceb9be00..11300eede186 100644 --- a/docs/content/reference/types/components/image_format.md +++ b/docs/content/reference/types/components/image_format.md @@ -29,7 +29,7 @@ Struct( ## Used by * [`DepthImage`](../archetypes/depth_image.md) -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) * [`Image`](../archetypes/image.md) * [`Mesh3D`](../archetypes/mesh3d.md) * [`SegmentationImage`](../archetypes/segmentation_image.md) diff --git a/docs/content/reference/types/components/is_keyframe.md b/docs/content/reference/types/components/is_keyframe.md new file mode 100644 index 000000000000..f9682ca57934 --- /dev/null +++ b/docs/content/reference/types/components/is_keyframe.md @@ -0,0 +1,31 @@ +--- +title: "IsKeyframe" +--- + + +Whether a [`components.VideoSample`](https://rerun.io/docs/reference/types/components/video_sample) contains a keyframe (also known as a sync sample or IDR). + +A keyframe in this sense must be _decoder re-entrant_: a decoder must be able to start +decoding the stream from this sample alone, with no prior decoder state. +Not every intra-coded frame qualifies. Some codecs have intra-only frames that may +still reference existing decoder state and are therefore not valid sync points. +See [`components.VideoCodec`](https://rerun.io/docs/reference/types/components/video_codec) for the codec-specific definition of a keyframe. + +## Rerun datatype +[`Bool`](../datatypes/bool.md) + + +## Arrow datatype +``` +Boolean +``` + +## API reference links + * 🌊 [C++ API docs for `IsKeyframe`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1IsKeyframe.html) + * 🐍 [Python API docs for `IsKeyframe`](https://ref.rerun.io/docs/python/stable/common/components#rerun.components.IsKeyframe) + * 🦀 [Rust API docs for `IsKeyframe`](https://docs.rs/rerun/latest/rerun/components/struct.IsKeyframe.html) + + +## Used by + +* [`VideoStream`](../archetypes/video_stream.md) diff --git a/docs/content/reference/types/components/opacity.md b/docs/content/reference/types/components/opacity.md index 5e6d5c210e9b..4ef57be559db 100644 --- a/docs/content/reference/types/components/opacity.md +++ b/docs/content/reference/types/components/opacity.md @@ -26,8 +26,9 @@ Float32 ## Used by * [`EncodedImage`](../archetypes/encoded_image.md) -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) * [`Image`](../archetypes/image.md) * [`SegmentationImage`](../archetypes/segmentation_image.md) * [`VideoFrameReference`](../archetypes/video_frame_reference.md) * [`VideoStream`](../archetypes/video_stream.md) +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/point_shading.md b/docs/content/reference/types/components/point_shading.md new file mode 100644 index 000000000000..dc7810997f7e --- /dev/null +++ b/docs/content/reference/types/components/point_shading.md @@ -0,0 +1,29 @@ +--- +title: "PointShading" +--- + + +Defines how points are shaded. + +## Variants +#### `Gradient` = 1 +Radial gradient for a spherical shadow effect. + +#### `Flat` = 2 +Flat shading. + + +## Arrow datatype +``` +UInt8 +``` + +## API reference links + * 🌊 [C++ API docs for `PointShading`](https://ref.rerun.io/docs/cpp/stable/namespacererun_1_1components.html) + * 🐍 [Python API docs for `PointShading`](https://ref.rerun.io/docs/python/stable/common/components#rerun.components.PointShading) + * 🦀 [Rust API docs for `PointShading`](https://docs.rs/rerun/latest/rerun/components/enum.PointShading.html) + + +## Used by + +* [`Points3D`](../archetypes/points3d.md) diff --git a/docs/content/reference/types/components/position2d.md b/docs/content/reference/types/components/position2d.md index 03a77482a6b1..13c0dd840ef3 100644 --- a/docs/content/reference/types/components/position2d.md +++ b/docs/content/reference/types/components/position2d.md @@ -24,5 +24,6 @@ FixedSizeList(2 x non-null Float32) * [`Arrows2D`](../archetypes/arrows2d.md) * [`Boxes2D`](../archetypes/boxes2d.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) * [`GraphNodes`](../archetypes/graph_nodes.md) * [`Points2D`](../archetypes/points2d.md) diff --git a/docs/content/reference/types/components/radius.md b/docs/content/reference/types/components/radius.md index 4647f72c4116..5fc4dd1727eb 100644 --- a/docs/content/reference/types/components/radius.md +++ b/docs/content/reference/types/components/radius.md @@ -35,6 +35,7 @@ Float32 * [`Boxes3D`](../archetypes/boxes3d.md) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Cylinders3D`](../archetypes/cylinders3d.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) * [`GeoLineStrings`](../archetypes/geo_line_strings.md) * [`GeoPoints`](../archetypes/geo_points.md) diff --git a/docs/content/reference/types/components/rotation_axis_angle.md b/docs/content/reference/types/components/rotation_axis_angle.md index 65e7930fd62e..f5a78c8d7de4 100644 --- a/docs/content/reference/types/components/rotation_axis_angle.md +++ b/docs/content/reference/types/components/rotation_axis_angle.md @@ -32,6 +32,7 @@ Struct( * [`Capsules3D`](../archetypes/capsules3d.md) * [`Cylinders3D`](../archetypes/cylinders3d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) * [`InstancePoses3D`](../archetypes/instance_poses3d.md) * [`Transform3D`](../archetypes/transform3d.md) +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/rotation_quat.md b/docs/content/reference/types/components/rotation_quat.md index 4eaa6463d888..36cdbf085853 100644 --- a/docs/content/reference/types/components/rotation_quat.md +++ b/docs/content/reference/types/components/rotation_quat.md @@ -30,6 +30,7 @@ FixedSizeList(4 x non-null Float32) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Cylinders3D`](../archetypes/cylinders3d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) * [`InstancePoses3D`](../archetypes/instance_poses3d.md) * [`Transform3D`](../archetypes/transform3d.md) +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/show_labels.md b/docs/content/reference/types/components/show_labels.md index 36239c351a14..7d6ae529502f 100644 --- a/docs/content/reference/types/components/show_labels.md +++ b/docs/content/reference/types/components/show_labels.md @@ -32,6 +32,7 @@ Boolean * [`Boxes3D`](../archetypes/boxes3d.md) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Cylinders3D`](../archetypes/cylinders3d.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) * [`GraphNodes`](../archetypes/graph_nodes.md) * [`LineStrips2D`](../archetypes/line_strips2d.md) diff --git a/docs/content/reference/types/components/text.md b/docs/content/reference/types/components/text.md index 4c87fe95a10c..ca08cf78d59d 100644 --- a/docs/content/reference/types/components/text.md +++ b/docs/content/reference/types/components/text.md @@ -28,6 +28,7 @@ Utf8 * [`Boxes3D`](../archetypes/boxes3d.md) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Cylinders3D`](../archetypes/cylinders3d.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) * [`GraphNodes`](../archetypes/graph_nodes.md) * [`LineStrips2D`](../archetypes/line_strips2d.md) @@ -36,6 +37,7 @@ Utf8 * [`McapSchema`](../archetypes/mcap_schema.md) * [`Points2D`](../archetypes/points2d.md) * [`Points3D`](../archetypes/points3d.md) -* [`Status`](../archetypes/status.md?speculative-link) +* [`StateChange`](../archetypes/state_change.md) +* [`StateConfiguration`](../archetypes/state_configuration.md) * [`TextDocument`](../archetypes/text_document.md) * [`TextLog`](../archetypes/text_log.md) diff --git a/docs/content/reference/types/components/translation3d.md b/docs/content/reference/types/components/translation3d.md index 7927f62782a5..5254b04da5a1 100644 --- a/docs/content/reference/types/components/translation3d.md +++ b/docs/content/reference/types/components/translation3d.md @@ -26,6 +26,7 @@ FixedSizeList(3 x non-null Float32) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Cylinders3D`](../archetypes/cylinders3d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) -* [`GridMap`](../archetypes/grid_map.md?speculative-link) +* [`GridMap`](../archetypes/grid_map.md) * [`InstancePoses3D`](../archetypes/instance_poses3d.md) * [`Transform3D`](../archetypes/transform3d.md) +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/value_range.md b/docs/content/reference/types/components/value_range.md index 41cc7429a731..0b23311e1c58 100644 --- a/docs/content/reference/types/components/value_range.md +++ b/docs/content/reference/types/components/value_range.md @@ -26,3 +26,4 @@ FixedSizeList(2 x non-null Float64) * [`DepthImage`](../archetypes/depth_image.md) * [`EncodedDepthImage`](../archetypes/encoded_depth_image.md) * [`Tensor`](../archetypes/tensor.md) +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/video_codec.md b/docs/content/reference/types/components/video_codec.md index 491546b0784e..50c60df5be48 100644 --- a/docs/content/reference/types/components/video_codec.md +++ b/docs/content/reference/types/components/video_codec.md @@ -48,6 +48,20 @@ Key frames (IRAP) require inclusion of a SPS (Sequence Parameter Set) Enum value is the fourcc for 'hev1' (the WebCodec string assigned to this codec) in big endian. +#### `VP8` = 0x76703038 +VP8 + +See + +Enum value is the fourcc for 'vp08' (the WebCodec string assigned to this codec) in big endian. + +#### `VP9` = 0x76703039 +VP9 + +See + +Enum value is the fourcc for 'vp09' (the WebCodec string assigned to this codec) in big endian. + ## Arrow datatype ``` diff --git a/docs/content/reference/types/components/visible.md b/docs/content/reference/types/components/visible.md index 8628b3714ab4..f94e3fefd727 100644 --- a/docs/content/reference/types/components/visible.md +++ b/docs/content/reference/types/components/visible.md @@ -24,3 +24,4 @@ Boolean * [`SeriesLines`](../archetypes/series_lines.md) * [`SeriesPoints`](../archetypes/series_points.md) +* [`StateConfiguration`](../archetypes/state_configuration.md) diff --git a/docs/content/reference/types/components/voxel_index.md b/docs/content/reference/types/components/voxel_index.md new file mode 100644 index 000000000000..f12584aedb78 --- /dev/null +++ b/docs/content/reference/types/components/voxel_index.md @@ -0,0 +1,27 @@ +--- +title: "VoxelIndex" +--- + + +Integer index of a voxel in a sparse 3D voxel grid. + +The voxel center in local grid coordinates is `(index + 0.5) * voxel_size`. + +## Rerun datatype +[`IVec3D`](../datatypes/ivec3d.md) + + +## Arrow datatype +``` +FixedSizeList(3 x non-null Int32) +``` + +## API reference links + * 🌊 [C++ API docs for `VoxelIndex`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1VoxelIndex.html) + * 🐍 [Python API docs for `VoxelIndex`](https://ref.rerun.io/docs/python/stable/common/components#rerun.components.VoxelIndex) + * 🦀 [Rust API docs for `VoxelIndex`](https://docs.rs/rerun/latest/rerun/components/struct.VoxelIndex.html) + + +## Used by + +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/voxel_size.md b/docs/content/reference/types/components/voxel_size.md new file mode 100644 index 000000000000..5a53c4cf477f --- /dev/null +++ b/docs/content/reference/types/components/voxel_size.md @@ -0,0 +1,28 @@ +--- +title: "VoxelSize" +--- + + +The scene-unit dimensions of one voxel in a sparse 3D voxel grid. + +Each component is the size of a voxel along the corresponding local grid axis. +All components must be finite and positive. + +## Rerun datatype +[`Vec3D`](../datatypes/vec3d.md) + + +## Arrow datatype +``` +FixedSizeList(3 x non-null Float32) +``` + +## API reference links + * 🌊 [C++ API docs for `VoxelSize`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1VoxelSize.html) + * 🐍 [Python API docs for `VoxelSize`](https://ref.rerun.io/docs/python/stable/common/components#rerun.components.VoxelSize) + * 🦀 [Rust API docs for `VoxelSize`](https://docs.rs/rerun/latest/rerun/components/struct.VoxelSize.html) + + +## Used by + +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/components/voxel_value.md b/docs/content/reference/types/components/voxel_value.md new file mode 100644 index 000000000000..8d03475cbd34 --- /dev/null +++ b/docs/content/reference/types/components/voxel_value.md @@ -0,0 +1,25 @@ +--- +title: "VoxelValue" +--- + + +Optional scalar occupancy or value associated with a voxel. + +## Rerun datatype +[`Float32`](../datatypes/float32.md) + + +## Arrow datatype +``` +Float32 +``` + +## API reference links + * 🌊 [C++ API docs for `VoxelValue`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1VoxelValue.html) + * 🐍 [Python API docs for `VoxelValue`](https://ref.rerun.io/docs/python/stable/common/components#rerun.components.VoxelValue) + * 🦀 [Rust API docs for `VoxelValue`](https://docs.rs/rerun/latest/rerun/components/struct.VoxelValue.html) + + +## Used by + +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) diff --git a/docs/content/reference/types/datatypes.md b/docs/content/reference/types/datatypes.md index eca19f5eaad5..d2689e01f646 100644 --- a/docs/content/reference/types/datatypes.md +++ b/docs/content/reference/types/datatypes.md @@ -1,6 +1,7 @@ --- title: "Datatypes" order: 3 +sort_children: alphabetical --- @@ -22,6 +23,7 @@ Data types are the lowest layer of the data model hierarchy. They are re-usable * [`EntityPath`](datatypes/entity_path.md): A path to an entity in the `ChunkStore`. * [`Float32`](datatypes/float32.md): A single-precision 32-bit IEEE 754 floating point number. * [`Float64`](datatypes/float64.md): A double-precision 64-bit IEEE 754 floating point number. +* [`IVec3D`](datatypes/ivec3d.md): An int32 vector in 3D space. * [`ImageFormat`](datatypes/image_format.md): The metadata describing the contents of a [`components.ImageBuffer`](https://rerun.io/docs/reference/types/components/image_buffer). * [`KeypointId`](datatypes/keypoint_id.md): A 16-bit ID representing a type of semantic keypoint within a class. * [`KeypointPair`](datatypes/keypoint_pair.md): A connection between two [`datatypes.KeypointId`](https://rerun.io/docs/reference/types/datatypes/keypoint_id)s. diff --git a/docs/content/reference/types/datatypes/.gitattributes b/docs/content/reference/types/datatypes/.gitattributes index 1f71b1d6bfa6..3065210d613d 100644 --- a/docs/content/reference/types/datatypes/.gitattributes +++ b/docs/content/reference/types/datatypes/.gitattributes @@ -17,6 +17,7 @@ entity_path.md linguist-generated=true float32.md linguist-generated=true float64.md linguist-generated=true image_format.md linguist-generated=true +ivec3d.md linguist-generated=true keypoint_id.md linguist-generated=true keypoint_pair.md linguist-generated=true mat3x3.md linguist-generated=true diff --git a/docs/content/reference/types/datatypes/bool.md b/docs/content/reference/types/datatypes/bool.md index 0a296d3ece61..cd114d1e218d 100644 --- a/docs/content/reference/types/datatypes/bool.md +++ b/docs/content/reference/types/datatypes/bool.md @@ -21,5 +21,6 @@ Boolean * [`ClearIsRecursive`](../components/clear_is_recursive.md) * [`Interactive`](../components/interactive.md) +* [`IsKeyframe`](../components/is_keyframe.md) * [`ShowLabels`](../components/show_labels.md) * [`Visible`](../components/visible.md) diff --git a/docs/content/reference/types/datatypes/float32.md b/docs/content/reference/types/datatypes/float32.md index a13b4592b1f7..f23a7f644411 100644 --- a/docs/content/reference/types/datatypes/float32.md +++ b/docs/content/reference/types/datatypes/float32.md @@ -20,7 +20,7 @@ Float32 ## Used by * [`AxisLength`](../components/axis_length.md) -* [`CellSize`](../components/cell_size.md?speculative-link) +* [`CellSize`](../components/cell_size.md) * [`DepthMeter`](../components/depth_meter.md) * [`DrawOrder`](../components/draw_order.md) * [`FillRatio`](../components/fill_ratio.md) @@ -31,3 +31,4 @@ Float32 * [`Opacity`](../components/opacity.md) * [`Radius`](../components/radius.md) * [`StrokeWidth`](../components/stroke_width.md) +* [`VoxelValue`](../components/voxel_value.md) diff --git a/docs/content/reference/types/datatypes/ivec3d.md b/docs/content/reference/types/datatypes/ivec3d.md new file mode 100644 index 000000000000..86d6cadc74e5 --- /dev/null +++ b/docs/content/reference/types/datatypes/ivec3d.md @@ -0,0 +1,22 @@ +--- +title: "IVec3D" +--- + + +An int32 vector in 3D space. + + +## Arrow datatype +``` +FixedSizeList(3 x non-null Int32) +``` + +## API reference links + * 🌊 [C++ API docs for `IVec3D`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1datatypes_1_1IVec3D.html) + * 🐍 [Python API docs for `IVec3D`](https://ref.rerun.io/docs/python/stable/common/datatypes#rerun.datatypes.IVec3D) + * 🦀 [Rust API docs for `IVec3D`](https://docs.rs/rerun/latest/rerun/datatypes/struct.IVec3D.html) + + +## Used by + +* [`VoxelIndex`](../components/voxel_index.md) diff --git a/docs/content/reference/types/datatypes/vec3d.md b/docs/content/reference/types/datatypes/vec3d.md index 677b2fd3479e..de7bd612d427 100644 --- a/docs/content/reference/types/datatypes/vec3d.md +++ b/docs/content/reference/types/datatypes/vec3d.md @@ -26,3 +26,4 @@ FixedSizeList(3 x non-null Float32) * [`Scale3D`](../components/scale3d.md) * [`Translation3D`](../components/translation3d.md) * [`Vector3D`](../components/vector3d.md) +* [`VoxelSize`](../components/voxel_size.md) diff --git a/docs/content/reference/types/datatypes/visible_time_range.md b/docs/content/reference/types/datatypes/visible_time_range.md index 4122673c5f76..7efafb94e44c 100644 --- a/docs/content/reference/types/datatypes/visible_time_range.md +++ b/docs/content/reference/types/datatypes/visible_time_range.md @@ -43,4 +43,18 @@ Struct( * 🐍 [Python API docs for `VisibleTimeRange`](https://ref.rerun.io/docs/python/stable/common/datatypes#rerun.datatypes.VisibleTimeRange) * 🦀 [Rust API docs for `VisibleTimeRange`](https://docs.rs/rerun/latest/rerun/datatypes/struct.VisibleTimeRange.html) +## Example + +### Time-windowed trails (e.g. Trajectories) + +snippet: archetypes/line_strips3d_time_window + + + + + + + + + diff --git a/docs/content/reference/types/views.md b/docs/content/reference/types/views.md index 46f210af41d2..3821de5b6210 100644 --- a/docs/content/reference/types/views.md +++ b/docs/content/reference/types/views.md @@ -1,6 +1,7 @@ --- title: "Views" order: 4 +sort_children: alphabetical --- @@ -13,7 +14,7 @@ Views are the panels shown in the viewer's viewport and the primary means of ins * [`MapView`](views/map_view.md): A 2D map view to display geospatial primitives. * [`Spatial2DView`](views/spatial2d_view.md): For viewing spatial 2D data. * [`Spatial3DView`](views/spatial3d_view.md): For viewing spatial 3D data. -* [`StatusView`](views/status_view.md): A view for displaying status transitions over time, for use with [`archetypes.Status`](https://rerun.io/docs/reference/types/archetypes/status?speculative-link). +* [`StateTimelineView`](views/state_timeline_view.md): A view for displaying state transitions over time, for use with [`archetypes.StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change). * [`TensorView`](views/tensor_view.md): A view on a tensor of any dimensionality. * [`TextDocumentView`](views/text_document_view.md): A view of a single text document, for use with [`archetypes.TextDocument`](https://rerun.io/docs/reference/types/archetypes/text_document). * [`TextLogView`](views/text_log_view.md): A view of a text log, for use with [`archetypes.TextLog`](https://rerun.io/docs/reference/types/archetypes/text_log). diff --git a/docs/content/reference/types/views/.gitattributes b/docs/content/reference/types/views/.gitattributes index b08d56045cc4..d0b49711f928 100644 --- a/docs/content/reference/types/views/.gitattributes +++ b/docs/content/reference/types/views/.gitattributes @@ -7,7 +7,7 @@ graph_view.md linguist-generated=true map_view.md linguist-generated=true spatial2d_view.md linguist-generated=true spatial3d_view.md linguist-generated=true -status_view.md linguist-generated=true +state_timeline_view.md linguist-generated=true tensor_view.md linguist-generated=true text_document_view.md linguist-generated=true text_log_view.md linguist-generated=true diff --git a/docs/content/reference/types/views/graph_view.md b/docs/content/reference/types/views/graph_view.md index c2df68dac72b..cc8145d1eebb 100644 --- a/docs/content/reference/types/views/graph_view.md +++ b/docs/content/reference/types/views/graph_view.md @@ -13,7 +13,7 @@ Configures the background of the graph. ### `visual_bounds` Everything within these bounds is guaranteed to be visible. -Somethings outside of these bounds may also be visible due to letterboxing. +Some things outside of these bounds may also be visible due to letterboxing. ### `force_link` Allows to control the interaction between two nodes connected by an edge. diff --git a/docs/content/reference/types/views/spatial2d_view.md b/docs/content/reference/types/views/spatial2d_view.md index d044b1892b81..bc4aad62b183 100644 --- a/docs/content/reference/types/views/spatial2d_view.md +++ b/docs/content/reference/types/views/spatial2d_view.md @@ -18,6 +18,12 @@ The visible parts of the scene, in the coordinate space of the scene. Everything within these bounds are guaranteed to be visible. Somethings outside of these bounds may also be visible due to letterboxing. +### `spatial_information` +Configuration of spatial information shown in the view. + +* `target_frame`: The target reference frame for all transformations. +* `show_axes`: Whether axes should be shown at the origin. +* `show_bounding_box`: Whether the bounding box should be shown. ### `time_ranges` Configures which range on each timeline is shown by this view (unless specified differently per entity). @@ -51,6 +57,7 @@ snippet: views/spatial2d * [`Clear`](../archetypes/clear.md) * [`CoordinateFrame`](../archetypes/coordinate_frame.md) * [`DepthImage`](../archetypes/depth_image.md) +* [`Ellipses2D`](../archetypes/ellipses2d.md) * [`EncodedDepthImage`](../archetypes/encoded_depth_image.md) * [`EncodedImage`](../archetypes/encoded_image.md) * [`GridMap`](../archetypes/grid_map.md) diff --git a/docs/content/reference/types/views/spatial3d_view.md b/docs/content/reference/types/views/spatial3d_view.md index fc0b648b2955..6b8446dbe0ca 100644 --- a/docs/content/reference/types/views/spatial3d_view.md +++ b/docs/content/reference/types/views/spatial3d_view.md @@ -81,10 +81,12 @@ snippet: views/spatial3d * [`Transform3D`](../archetypes/transform3d.md) * [`TransformAxes3D`](../archetypes/transform_axes3d.md) * [`ViewCoordinates`](../archetypes/view_coordinates.md) +* [`VoxelGridMap`](../archetypes/voxel_grid_map.md) * [`Arrows2D`](../archetypes/arrows2d.md) (if logged under a projection) * [`AssetVideo`](../archetypes/asset_video.md) (if logged under a projection) * [`Boxes2D`](../archetypes/boxes2d.md) (if logged under a projection) * [`DepthImage`](../archetypes/depth_image.md) (if logged under a projection) +* [`Ellipses2D`](../archetypes/ellipses2d.md) (if logged under a projection) * [`EncodedDepthImage`](../archetypes/encoded_depth_image.md) (if logged under a projection) * [`EncodedImage`](../archetypes/encoded_image.md) (if logged under a projection) * [`Image`](../archetypes/image.md) (if logged under a projection) diff --git a/docs/content/reference/types/views/status_view.md b/docs/content/reference/types/views/state_timeline_view.md similarity index 61% rename from docs/content/reference/types/views/status_view.md rename to docs/content/reference/types/views/state_timeline_view.md index 0b5f017066f7..313cacb9f720 100644 --- a/docs/content/reference/types/views/status_view.md +++ b/docs/content/reference/types/views/state_timeline_view.md @@ -1,22 +1,22 @@ --- -title: "StatusView" +title: "StateTimelineView" --- ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** -A view for displaying status transitions over time, for use with [`archetypes.Status`](https://rerun.io/docs/reference/types/archetypes/status?speculative-link). +A view for displaying state transitions over time, for use with [`archetypes.StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change). ## API reference links - * 🐍 [Python API docs for `StatusView`](https://ref.rerun.io/docs/python/stable/common/blueprint_views?speculative-link#rerun.blueprint.views.StatusView) + * 🐍 [Python API docs for `StateTimelineView`](https://ref.rerun.io/docs/python/stable/common/blueprint_views#rerun.blueprint.views.StateTimelineView) ## Example -### Use a blueprint to show a StatusView. +### Use a blueprint to show a StateTimelineView. -snippet: views/status +snippet: views/state_timeline - + @@ -27,5 +27,6 @@ snippet: views/status ## Visualized archetypes -* [`Status`](../archetypes/status.md) +* [`StateChange`](../archetypes/state_change.md) +* [`StateConfiguration`](../archetypes/state_configuration.md) diff --git a/docs/content/reference/types/views/text_document_view.md b/docs/content/reference/types/views/text_document_view.md index fcd84e9d1bda..3bb3d8792332 100644 --- a/docs/content/reference/types/views/text_document_view.md +++ b/docs/content/reference/types/views/text_document_view.md @@ -6,6 +6,13 @@ title: "TextDocumentView" ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** A view of a single text document, for use with [`archetypes.TextDocument`](https://rerun.io/docs/reference/types/archetypes/text_document). +## Properties + +### `format_options` +Formatting options for the text document view. + +* `monospace`: Whether to use a monospace font for the document body. +* `word_wrap`: Whether to wrap long lines in the document body. ## API reference links * 🐍 [Python API docs for `TextDocumentView`](https://ref.rerun.io/docs/python/stable/common/blueprint_views#rerun.blueprint.views.TextDocumentView) diff --git a/docs/content/reference/viewer.md b/docs/content/reference/viewer.md new file mode 100644 index 000000000000..109b3b251ffe --- /dev/null +++ b/docs/content/reference/viewer.md @@ -0,0 +1,5 @@ +--- +title: Viewer +order: 970 +redirect: reference/viewer/overview +--- diff --git a/docs/content/reference/viewer/blueprints.md b/docs/content/reference/viewer/blueprints.md index 373830d50dbd..518238b09175 100644 --- a/docs/content/reference/viewer/blueprints.md +++ b/docs/content/reference/viewer/blueprints.md @@ -44,7 +44,7 @@ Right-click any item for a context menu with additional operations. See [Configu ### Data blueprints -Entities shown in the blueprint panel refer to their *data blueprints*—the entity plus its associated blueprint settings. Changes made here apply only to the specific view where the entity appears. +Entities shown in the blueprint panel refer to their *data blueprints* — the entity plus its associated blueprint settings. Changes made here apply only to the specific view where the entity appears. ### Groups diff --git a/docs/content/reference/viewer/mcp.md b/docs/content/reference/viewer/mcp.md new file mode 100644 index 000000000000..8dbc02dfb68e --- /dev/null +++ b/docs/content/reference/viewer/mcp.md @@ -0,0 +1,57 @@ +--- +title: MCP server +order: 5 +--- + +The Rerun CLI includes an [MCP](https://modelcontextprotocol.io/) server that lets agents such as Codex or Claude interact with a running Viewer. +It allows the agent to interact with the viewer like a real user, allowing it to interact with the ui, adjust settings, type text, or take screenshots. +It works similar to e.g. Claude for Chrome or Codex Computer Use, but tailored to Rerun. + +Some things it is useful for: + +- **Debugging a logging script**: "The left camera doesn't show up in the viewer, investigate and fix via the mcp." +- **Adding a custom blueprint**: "Create a blueprint with two tabs: The first is a grid of the cameras, the second shows the map and 3D view. Verify with rerun viewer-mcp." +- **Explore recordings**: "Look at each recording in this dataset and find where it rains. Write a report including screenshots." + +## Setup + +The server is the `viewer-mcp` subcommand of the `rerun` binary, speaking MCP over stdio. +It connects to a separate, already-running Viewer over gRPC, so an MCP client only needs to know how to launch `rerun viewer-mcp`. + +Add it to **Claude Code**: + +```sh +claude mcp add rerun -- rerun viewer-mcp +``` + +Add it to **Codex**: + +```sh +codex mcp add rerun -- rerun viewer-mcp +``` + +Or configure any MCP client manually. Most accept a `mcp.json` config like this: + +```json +{ + "mcpServers": { + "rerun": { + "command": "rerun", + "args": ["viewer-mcp"], + "env": { + "RUST_LOG": "re_viewer_mcp=info,warn" + } + } + } +} +``` + +These assume `rerun` is installed on your `PATH` (see [install rerun](../../getting-started/install-rerun.md)). +If it is not, replace `rerun` with the absolute path to the binary. + +## Headless usage + +The MCP server works against a headless Viewer too, which is convenient for agents running in the background, in CI or +on some server without a display. +Ask the agent to launch the viewer headless or in the background, and it will use the `rerun --headless` command to +launch it in the background. diff --git a/docs/snippets/CMakeLists.txt b/docs/snippets/CMakeLists.txt index ca97efc14f26..d9a89532e285 100644 --- a/docs/snippets/CMakeLists.txt +++ b/docs/snippets/CMakeLists.txt @@ -7,6 +7,7 @@ file(GLOB_RECURSE sources_list CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/all # Not complete examples (code fragments without main): list(FILTER sources_list EXCLUDE REGEX .*/concepts/static/*) +list(FILTER sources_list EXCLUDE REGEX .*/migration/log_tick_enabled.*) list(FILTER sources_list EXCLUDE REGEX .*/migration/transactional_transforms/*) list(FILTER sources_list EXCLUDE REGEX .*/tutorials/custom-application-id.*) list(FILTER sources_list EXCLUDE REGEX .*/tutorials/custom-recording-id.*) diff --git a/docs/snippets/INDEX.md b/docs/snippets/INDEX.md index f44e94c9c551..4561e1cfd05a 100644 --- a/docs/snippets/INDEX.md +++ b/docs/snippets/INDEX.md @@ -20,16 +20,16 @@ Use it to quickly find copy-pastable snippets of code for any Rerun feature you' | Feature | Example | Description | Python | Rust | C+⁠+ | | ------- | ------- | ----------- | :----: | :--: | :-------: | -| **Query Data Platform** | `dataframe_operations` | Demonstrate common dataframe operations with Rerun Data Platform | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dataframe_operations.py) | | | -| **Query Data Platform** | `dataframe_performance` | Sample snippets highlighting common performance-related improvements | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dataframe_performance.py) | | | -| **Query Data Platform** | `dataset_resampling` | Sample snippets highlighting common performance-related improvements | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dataset_resampling.py) | | | -| **Query Data Platform** | `lerobot_export` | Demonstrate converting Rerun recording to LeRobot dataset | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/lerobot_export.py) | | | -| **Query Data Platform** | `query_images` | Query various image representations | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/query_images.py) | | | -| **Query Data Platform** | `query_videos` | Query video streams | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/query_videos.py) | | | -| **Query Data Platform** | `query_video_keyframes` | Query video streams efficiently using keyframe information | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/query_video_keyframes.py) | | | -| **Query Data Platform** | `sub_dataset` | Create a new dataset from a subset of segments of an existing dataset | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/sub_dataset.py) | | | -| **Query Data Platform** | `time_alignment` | Efficiently time align multirate columns | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/time_alignment.py) | | | -| **Query Data Platform** | `view_operations` | Leverage filters to more efficiently perform downstream queries | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/view_operations.py) | | | +| **Catalog server** | `dataframe_operations` | Demonstrate common dataframe operations with a catalog server | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dataframe_operations.py) | | | +| **Catalog server** | `dataframe_performance` | Sample snippets highlighting common performance-related improvements | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dataframe_performance.py) | | | +| **Catalog server** | `dataset_resampling` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dataset_resampling.py) | | | +| **Catalog server** | `lerobot_export` | Demonstrate converting Rerun recording to LeRobot dataset | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/lerobot_export.py) | | | +| **Catalog server** | `query_images` | Query various image representations | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/query_images.py) | | | +| **Catalog server** | `query_videos` | Query video streams | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/query_videos.py) | | | +| **Catalog server** | `query_video_keyframes` | Query video streams efficiently using keyframe information | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/query_video_keyframes.py) | | | +| **Catalog server** | `sub_dataset` | Create a new dataset from a subset of segments of an existing dataset | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/sub_dataset.py) | | | +| **Catalog server** | `time_alignment` | Efficiently time align multirate columns | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/time_alignment.py) | | | +| **Catalog server** | `view_operations` | Leverage filters to more efficiently perform downstream queries | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/view_operations.py) | | | | **Setting recording properties** | `recording_properties` | Sets the recording properties | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/recording_properties.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/recording_properties.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/recording_properties.cpp) | | **Setting recording properties** | `segment_properties` | Query and display the first 10 rows of a recording | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/query-and-transform/segment_properties.py) | | | | **Setting recording properties** | `layers` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/layers.py) | | | @@ -44,19 +44,18 @@ Use it to quickly find copy-pastable snippets of code for any Rerun feature you' | **Micro batching** | `micro_batching` | Shows how to configure micro-batching directly from code | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/micro_batching.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/micro_batching.rs) | | | **Partial updates** | `points3d_partial_updates` | Update specific properties of a point cloud over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_partial_updates.cpp) | | **Partial updates** | `transform3d_partial_updates` | Update specific properties of a transform over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.cpp) | -| **Partial updates** | `mesh3d_partial_updates` | Log a simple colored triangle, then update its vertices' positions each frame | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.cpp) | +| **Partial updates** | `mesh3d_partial_updates` | Log a colored triangle, then update its vertices' positions each frame | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.cpp) | | **Send custom data** | `any_values` | Log arbitrary data | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/any_values.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/any_values.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/any_values.cpp) | | **Send custom data** | `dynamic_archetype` | Log arbitrary archetype data | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dynamic_archetype.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dynamic_archetype.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dynamic_archetype.cpp) | | **Send custom data** | `extra_values` | Log extra values with a `Points2D` | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/extra_values.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/extra_values.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/extra_values.cpp) | | **Send custom data** | `custom_data` | Shows how to implement custom archetypes and components | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/custom_data.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/custom_data.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/custom_data.cpp) | | **Send columns of custom data** | `any_values_column_updates` | Update custom user-defined values over time, in a single operation | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_values_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_values_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_values_column_updates.cpp) | -| **Send columns of custom data** | `any_batch_value_column_updates` | Use `AnyBatchValue` and `send_column` to send an entire column of custom data to Rerun | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_batch_value_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_batch_value_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_batch_value_column_updates.cpp) | +| **Send columns of custom data** | `any_batch_value_column_updates` | Use `AnyBatchValue` and `send_column` to send a column of custom data | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_batch_value_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_batch_value_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/any_batch_value_column_updates.cpp) | | **Query dataframes** | `dataframe_query_example` | Query and display the first 10 rows of a recording | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/query-and-transform/dataframe_query_example.py) | | | | **Query dataframes** | `dataframe_view_query` | Query and display the first 10 rows of a recording in a dataframe view | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/reference/dataframe_view_query.py) | | | -| **Host web viewer and connect it to a gRPC server** | `serve_web_viewer` | Demonstrates how to log data to a gRPC server and connect the web viewer to it | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/serve_web_viewer.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/serve_web_viewer.rs) | | +| **Host web viewer and connect it to a gRPC server** | `serve_web_viewer` | Log data to a gRPC server and connect the web viewer to it | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/serve_web_viewer.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/serve_web_viewer.rs) | | | **Experimental Viewer client** | `screenshot` | Take screenshots of the viewer or specific views from code | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/screenshot.py) | | | | **Experimental Viewer client** | `send_table` | Sets the recording properties | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/send_table.py) | | | -| **Convert custom MCAP Protobuf** | `convert_mcap_protobuf` | Convert custom MCAP Protobuf messages to Rerun format | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf.py) | | | ## Types @@ -95,27 +94,28 @@ _All snippets, organized by the [`Archetype`](https://rerun.io/docs/reference/ty | **[`Boxes2D`](https://rerun.io/docs/reference/types/archetypes/boxes2d)** | `tutorials⁠/⁠data_out` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/data_out.py) | | | | **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠boxes3d_simple` | Log a single 3D Box | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/boxes3d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/boxes3d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/boxes3d_simple.cpp) | | **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠boxes3d_batch` | Log a batch of oriented bounding boxes | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/boxes3d_batch.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/boxes3d_batch.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/boxes3d_batch.cpp) | -| **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠coordinate_frame_builtin_frames` | Demonstrates using explicit `CoordinateFrame` with implicit transform frames only | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp) | +| **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠coordinate_frame_builtin_frames` | Demonstrates using explicit `CoordinateFrame` with implicit transforms | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp) | | **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠instance_poses3d_combined` | Log a simple 3D box with a regular & instance pose transform | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.cpp) | -| **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠mesh3d_instancing` | Log a simple 3D mesh with several instance pose transforms which instantiate the mesh several times and will not affect its children (known as mesh instancing) | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.cpp) | +| **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠mesh3d_instancing` | Log a simple 3D mesh with several instance pose transforms | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.cpp) | | **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠pinhole_projections` | Demonstrates pinhole camera projections with Rerun blueprints | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_projections.py) | | | | **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠transform3d_column_updates` | Update a transform over time, in a single operation | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.cpp) | | **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠transform3d_partial_updates` | Update specific properties of a transform over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.cpp) | | **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `archetypes⁠/⁠transform3d_row_updates` | Update a transform over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_row_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_row_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_row_updates.cpp) | -| **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `howto⁠/⁠serve_web_viewer` | Demonstrates how to log data to a gRPC server and connect the web viewer to it | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/serve_web_viewer.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/serve_web_viewer.rs) | | +| **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `howto⁠/⁠serve_web_viewer` | Log data to a gRPC server and connect the web viewer to it | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/serve_web_viewer.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/serve_web_viewer.rs) | | | **[`Boxes3D`](https://rerun.io/docs/reference/types/archetypes/boxes3d)** | `views⁠/⁠spatial3d` | Use a blueprint to customize a Spatial3DView | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/spatial3d.py) | | | | **[`Capsules3D`](https://rerun.io/docs/reference/types/archetypes/capsules3d)** | `archetypes⁠/⁠capsules3d_batch` | Log a batch of capsules | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/capsules3d_batch.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/capsules3d_batch.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/capsules3d_batch.cpp) | | **[`Clear`](https://rerun.io/docs/reference/types/archetypes/clear)** | `archetypes⁠/⁠clear_simple` | Log and then clear data | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/clear_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/clear_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/clear_simple.cpp) | | **[`Clear`](https://rerun.io/docs/reference/types/archetypes/clear)** | `archetypes⁠/⁠clear_recursive` | Log and then clear data recursively | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/clear_recursive.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/clear_recursive.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/clear_recursive.cpp) | | **[`Clear`](https://rerun.io/docs/reference/types/archetypes/clear)** | `archetypes⁠/⁠transform3d_partial_updates` | Update specific properties of a transform over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_partial_updates.cpp) | -| **[`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame)** | `archetypes⁠/⁠coordinate_frame_builtin_frames` | Demonstrates using explicit `CoordinateFrame` with implicit transform frames only | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp) | +| **[`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame)** | `archetypes⁠/⁠coordinate_frame_builtin_frames` | Demonstrates using explicit `CoordinateFrame` with implicit transforms | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp) | +| **[`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame)** | `archetypes⁠/⁠grid_map_pose` | Shows how to log a GridMap at a specific pose | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_pose.py) | | | | **[`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame)** | `archetypes⁠/⁠transform3d_hierarchy_frames` | Logs a transform hierarchy using named transform frame relationships | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.cpp) | | **[`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame)** | `concepts⁠/⁠transform3d_hierarchy_named_frames` | Logs a simple transform hierarchy with named frames | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.cpp) | -| **[`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame)** | `howto⁠/⁠convert_mcap_protobuf` | Convert custom MCAP Protobuf messages to Rerun format | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf.py) | | | -| **[`CoordinateFrame`](https://rerun.io/docs/reference/types/archetypes/coordinate_frame)** | `howto⁠/⁠convert_mcap_protobuf_send_column` | Convert custom MCAP Protobuf messages to Rerun format using send_columns | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py) | | | | **[`Cylinders3D`](https://rerun.io/docs/reference/types/archetypes/cylinders3d)** | `archetypes⁠/⁠cylinders3d_batch` | Log a batch of cylinders | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/cylinders3d_batch.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/cylinders3d_batch.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/cylinders3d_batch.cpp) | | **[`DepthImage`](https://rerun.io/docs/reference/types/archetypes/depth_image)** | `archetypes⁠/⁠depth_image_simple` | Create and log a depth image | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_simple.cpp) | | **[`DepthImage`](https://rerun.io/docs/reference/types/archetypes/depth_image)** | `archetypes⁠/⁠depth_image_3d` | Create and log a depth image and pinhole camera | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_3d.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_3d.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_3d.cpp) | +| **[`Ellipses2D`](https://rerun.io/docs/reference/types/archetypes/ellipses2d)** | `archetypes⁠/⁠ellipses2d_simple` | Log a simple 2D ellipse | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipses2d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipses2d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipses2d_simple.cpp) | +| **[`Ellipses2D`](https://rerun.io/docs/reference/types/archetypes/ellipses2d)** | `archetypes⁠/⁠ellipses2d_batch` | Log a batch of 2D ellipses | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipses2d_batch.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipses2d_batch.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipses2d_batch.cpp) | | **[`Ellipsoids3D`](https://rerun.io/docs/reference/types/archetypes/ellipsoids3d)** | `archetypes⁠/⁠ellipsoids3d_simple` | Log random points and the corresponding covariance ellipsoid | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_simple.cpp) | | **[`Ellipsoids3D`](https://rerun.io/docs/reference/types/archetypes/ellipsoids3d)** | `archetypes⁠/⁠ellipsoids3d_batch` | Log a batch of ellipsoids | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_batch.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_batch.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_batch.cpp) | | **[`Ellipsoids3D`](https://rerun.io/docs/reference/types/archetypes/ellipsoids3d)** | `archetypes⁠/⁠transform3d_hierarchy` | Logs a transform hierarchy | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.cpp) | @@ -135,44 +135,44 @@ _All snippets, organized by the [`Archetype`](https://rerun.io/docs/reference/ty | **[`GraphNodes`](https://rerun.io/docs/reference/types/archetypes/graph_nodes)** | `archetypes⁠/⁠graph_undirected` | Log a simple undirected graph | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/graph_undirected.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/graph_undirected.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/graph_undirected.cpp) | | **[`GraphNodes`](https://rerun.io/docs/reference/types/archetypes/graph_nodes)** | `views⁠/⁠graph` | Use a blueprint to customize a graph view | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/graph.py) | | | | **[`GridMap`](https://rerun.io/docs/reference/types/archetypes/grid_map)** | `archetypes⁠/⁠grid_map_simple` | Log a simple occupancy grid map | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_simple.cpp) | +| **[`GridMap`](https://rerun.io/docs/reference/types/archetypes/grid_map)** | `archetypes⁠/⁠grid_map_pose` | Shows how to log a GridMap at a specific pose | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_pose.py) | | | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `archetypes⁠/⁠image_simple` | Create and log an image | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_simple.cpp) | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `archetypes⁠/⁠image_row_updates` | Update an image over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_row_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_row_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_row_updates.cpp) | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `archetypes⁠/⁠image_formats` | Create and log an image with various formats | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_formats.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_formats.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_formats.cpp) | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `archetypes⁠/⁠image_advanced` | Log an image | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/image_advanced.py) | | | +| **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `archetypes⁠/⁠grid_map_pose` | Shows how to log a GridMap at a specific pose | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_pose.py) | | | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `archetypes⁠/⁠pinhole_projections` | Demonstrates pinhole camera projections with Rerun blueprints | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_projections.py) | | | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `archetypes⁠/⁠pinhole_simple` | Log a pinhole and a random image | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_simple.cpp) | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `archetypes⁠/⁠text_document` | Log a `TextDocument` | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/text_document.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/text_document.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/text_document.cpp) | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `howto⁠/⁠query_images` | Query various image representations | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/query_images.py) | | | | **[`Image`](https://rerun.io/docs/reference/types/archetypes/image)** | `views⁠/⁠text_document` | Use a blueprint to show a text document | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/text_document.py) | | | | **[`InstancePoses3D`](https://rerun.io/docs/reference/types/archetypes/instance_poses3d)** | `archetypes⁠/⁠instance_poses3d_combined` | Log a simple 3D box with a regular & instance pose transform | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.cpp) | -| **[`InstancePoses3D`](https://rerun.io/docs/reference/types/archetypes/instance_poses3d)** | `archetypes⁠/⁠mesh3d_instancing` | Log a simple 3D mesh with several instance pose transforms which instantiate the mesh several times and will not affect its children (known as mesh instancing) | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.cpp) | +| **[`InstancePoses3D`](https://rerun.io/docs/reference/types/archetypes/instance_poses3d)** | `archetypes⁠/⁠mesh3d_instancing` | Log a simple 3D mesh with several instance pose transforms | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.cpp) | | **[`LineStrips2D`](https://rerun.io/docs/reference/types/archetypes/line_strips2d)** | `archetypes⁠/⁠line_strips2d_ui_radius` | Log lines with ui points & scene unit radii | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_ui_radius.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_ui_radius.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_ui_radius.cpp) | | **[`LineStrips2D`](https://rerun.io/docs/reference/types/archetypes/line_strips2d)** | `archetypes⁠/⁠line_strips2d_simple` | Log a simple line strip | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_simple.cpp) | | **[`LineStrips2D`](https://rerun.io/docs/reference/types/archetypes/line_strips2d)** | `archetypes⁠/⁠line_strips2d_segments_simple` | Log a couple 2D line segments using 2D line strips | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_segments_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_segments_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_segments_simple.cpp) | | **[`LineStrips2D`](https://rerun.io/docs/reference/types/archetypes/line_strips2d)** | `archetypes⁠/⁠line_strips2d_batch` | Log a batch of 2D line strips | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_batch.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_batch.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips2d_batch.cpp) | | **[`LineStrips2D`](https://rerun.io/docs/reference/types/archetypes/line_strips2d)** | `archetypes⁠/⁠entity_behavior` | Configure interactivity & visibility of entities | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/entity_behavior.py) | | | | **[`LineStrips3D`](https://rerun.io/docs/reference/types/archetypes/line_strips3d)** | `archetypes⁠/⁠line_strips3d_ui_radius` | Log lines with ui points & scene unit radii | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_ui_radius.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_ui_radius.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_ui_radius.cpp) | +| **[`LineStrips3D`](https://rerun.io/docs/reference/types/archetypes/line_strips3d)** | `archetypes⁠/⁠line_strips3d_time_window` | Log line strips over time and view a sliding window (e.g. trajectories) | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_time_window.py) | | | | **[`LineStrips3D`](https://rerun.io/docs/reference/types/archetypes/line_strips3d)** | `archetypes⁠/⁠line_strips3d_simple` | Log a simple line strip | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_simple.cpp) | | **[`LineStrips3D`](https://rerun.io/docs/reference/types/archetypes/line_strips3d)** | `archetypes⁠/⁠line_strips3d_segments_simple` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_segments_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_segments_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_segments_simple.cpp) | | **[`LineStrips3D`](https://rerun.io/docs/reference/types/archetypes/line_strips3d)** | `archetypes⁠/⁠line_strips3d_batch` | Log a batch of 3D line strips | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_batch.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_batch.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_batch.cpp) | | **[`LineStrips3D`](https://rerun.io/docs/reference/types/archetypes/line_strips3d)** | `archetypes⁠/⁠transform3d_hierarchy` | Logs a transform hierarchy | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.cpp) | | **[`LineStrips3D`](https://rerun.io/docs/reference/types/archetypes/line_strips3d)** | `archetypes⁠/⁠transform3d_hierarchy_frames` | Logs a transform hierarchy using named transform frame relationships | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.cpp) | +| **[`LineStrips3D`](https://rerun.io/docs/reference/types/archetypes/line_strips3d)** | `tutorials⁠/⁠dna` | The DNA-abacus example from the Log and Ingest tutorial | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.cpp) | | **[`McapChannel`](https://rerun.io/docs/reference/types/archetypes/mcap_channel)** | `archetypes⁠/⁠mcap_channel_simple` | Log a simple MCAP channel definition | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_channel_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_channel_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_channel_simple.cpp) | | **[`McapMessage`](https://rerun.io/docs/reference/types/archetypes/mcap_message)** | `archetypes⁠/⁠mcap_message_simple` | Log a simple MCAP message with binary data | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_message_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_message_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_message_simple.cpp) | -| **[`McapMessage`](https://rerun.io/docs/reference/types/archetypes/mcap_message)** | `howto⁠/⁠convert_mcap_protobuf` | Convert custom MCAP Protobuf messages to Rerun format | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf.py) | | | -| **[`McapMessage`](https://rerun.io/docs/reference/types/archetypes/mcap_message)** | `howto⁠/⁠convert_mcap_protobuf_send_column` | Convert custom MCAP Protobuf messages to Rerun format using send_columns | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py) | | | | **[`McapSchema`](https://rerun.io/docs/reference/types/archetypes/mcap_schema)** | `archetypes⁠/⁠mcap_schema_simple` | Log a simple MCAP schema definition | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_schema_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_schema_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_schema_simple.cpp) | | **[`McapStatistics`](https://rerun.io/docs/reference/types/archetypes/mcap_statistics)** | `archetypes⁠/⁠mcap_statistics_simple` | Log simple MCAP recording statistics | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_statistics_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_statistics_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mcap_statistics_simple.cpp) | | **[`Mesh3D`](https://rerun.io/docs/reference/types/archetypes/mesh3d)** | `archetypes⁠/⁠mesh3d_simple` | Log a simple colored triangle | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_simple.cpp) | -| **[`Mesh3D`](https://rerun.io/docs/reference/types/archetypes/mesh3d)** | `archetypes⁠/⁠mesh3d_partial_updates` | Log a simple colored triangle, then update its vertices' positions each frame | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.cpp) | -| **[`Mesh3D`](https://rerun.io/docs/reference/types/archetypes/mesh3d)** | `archetypes⁠/⁠mesh3d_instancing` | Log a simple 3D mesh with several instance pose transforms which instantiate the mesh several times and will not affect its children (known as mesh instancing) | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.cpp) | +| **[`Mesh3D`](https://rerun.io/docs/reference/types/archetypes/mesh3d)** | `archetypes⁠/⁠mesh3d_partial_updates` | Log a colored triangle, then update its vertices' positions each frame | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_partial_updates.cpp) | +| **[`Mesh3D`](https://rerun.io/docs/reference/types/archetypes/mesh3d)** | `archetypes⁠/⁠mesh3d_instancing` | Log a simple 3D mesh with several instance pose transforms | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_instancing.cpp) | | **[`Mesh3D`](https://rerun.io/docs/reference/types/archetypes/mesh3d)** | `archetypes⁠/⁠mesh3d_indexed` | Log a simple colored triangle | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_indexed.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_indexed.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/mesh3d_indexed.cpp) | | **[`Pinhole`](https://rerun.io/docs/reference/types/archetypes/pinhole)** | `archetypes⁠/⁠pinhole_simple` | Log a pinhole and a random image | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_simple.cpp) | | **[`Pinhole`](https://rerun.io/docs/reference/types/archetypes/pinhole)** | `archetypes⁠/⁠pinhole_projections` | Demonstrates pinhole camera projections with Rerun blueprints | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_projections.py) | | | | **[`Pinhole`](https://rerun.io/docs/reference/types/archetypes/pinhole)** | `archetypes⁠/⁠pinhole_perspective` | Logs a point cloud and a perspective camera looking at it | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_perspective.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_perspective.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_perspective.cpp) | | **[`Pinhole`](https://rerun.io/docs/reference/types/archetypes/pinhole)** | `archetypes⁠/⁠depth_image_3d` | Create and log a depth image and pinhole camera | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_3d.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_3d.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/depth_image_3d.cpp) | -| **[`Pinhole`](https://rerun.io/docs/reference/types/archetypes/pinhole)** | `howto⁠/⁠convert_mcap_protobuf` | Convert custom MCAP Protobuf messages to Rerun format | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf.py) | | | -| **[`Pinhole`](https://rerun.io/docs/reference/types/archetypes/pinhole)** | `howto⁠/⁠convert_mcap_protobuf_send_column` | Convert custom MCAP Protobuf messages to Rerun format using send_columns | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py) | | | | **[`Points2D`](https://rerun.io/docs/reference/types/archetypes/points2d)** | `archetypes⁠/⁠points2d_ui_radius` | Log some points with ui points & scene unit radii | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_ui_radius.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_ui_radius.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_ui_radius.cpp) | | **[`Points2D`](https://rerun.io/docs/reference/types/archetypes/points2d)** | `archetypes⁠/⁠points2d_simple` | Log some very simple points | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_simple.cpp) | | **[`Points2D`](https://rerun.io/docs/reference/types/archetypes/points2d)** | `archetypes⁠/⁠points2d_random` | Log some random points with color and radii | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_random.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_random.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points2d_random.cpp) | @@ -189,19 +189,22 @@ _All snippets, organized by the [`Archetype`](https://rerun.io/docs/reference/ty | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠points3d_partial_updates` | Update specific properties of a point cloud over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_partial_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_partial_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_partial_updates.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠points3d_column_updates` | Update a point cloud over time, in a single operation | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/points3d_column_updates.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠annotation_context_connections` | Log annotation context with connections between keypoints | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/annotation_context_connections.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/annotation_context_connections.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/annotation_context_connections.cpp) | -| **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠coordinate_frame_builtin_frames` | Demonstrates using explicit `CoordinateFrame` with implicit transform frames only | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp) | +| **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠coordinate_frame_builtin_frames` | Demonstrates using explicit `CoordinateFrame` with implicit transforms | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠ellipsoids3d_simple` | Log random points and the corresponding covariance ellipsoid | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/ellipsoids3d_simple.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠instance_poses3d_combined` | Log a simple 3D box with a regular & instance pose transform | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠pinhole_perspective` | Logs a point cloud and a perspective camera looking at it | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_perspective.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_perspective.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_perspective.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `archetypes⁠/⁠pinhole_projections` | Demonstrates pinhole camera projections with Rerun blueprints | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_projections.py) | | | +| **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `concepts⁠/⁠build_chunk` | Build a `Chunk` with `Chunk.from_columns` and send it via `send_chunks` | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/build_chunk.py) | | | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `concepts⁠/⁠explicit_recording` | Just makes sure that explicit recordings actually work | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/explicit_recording.py) | | | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `concepts⁠/⁠how_helix_was_logged` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/how_helix_was_logged.py) | | | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `concepts⁠/⁠recording_properties` | Sets the recording properties | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/recording_properties.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/recording_properties.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/recording_properties.cpp) | +| **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `concepts⁠/⁠rrd_format` | Save a small recording to RRD and inspect a chunk from it | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/rrd_format.py) | | | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `descriptors⁠/⁠descr_builtin_archetype` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/descriptors/descr_builtin_archetype.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/descriptors/descr_builtin_archetype.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/descriptors/descr_builtin_archetype.cpp) | -| **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `howto⁠/⁠dual_color_point_cloud` | Demonstrates how to visualize the same point cloud with two different color schemes | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dual_color_point_cloud.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dual_color_point_cloud.rs) | | +| **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `howto⁠/⁠dual_color_point_cloud` | Visualize the same point cloud with two different color schemes | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dual_color_point_cloud.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dual_color_point_cloud.rs) | | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `howto⁠/⁠set_sinks` | Log some data to a file and a Viewer at the same time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/set_sinks.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/set_sinks.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/set_sinks.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `quick_start⁠/⁠quick_start_connect` | Connect to the viewer and log some data | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/quick_start/quick_start_connect.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/quick_start/quick_start_connect.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/quick_start/quick_start_connect.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `quick_start⁠/⁠quick_start_spawn` | Spawn a viewer and log some data | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/quick_start/quick_start_spawn.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/quick_start/quick_start_spawn.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/quick_start/quick_start_spawn.cpp) | +| **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `tutorials⁠/⁠dna` | The DNA-abacus example from the Log and Ingest tutorial | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `tutorials⁠/⁠timelines_example` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/timelines_example.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/timelines_example.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/timelines_example.cpp) | | **[`Points3D`](https://rerun.io/docs/reference/types/archetypes/points3d)** | `views⁠/⁠spatial3d` | Use a blueprint to customize a Spatial3DView | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/spatial3d.py) | | | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `archetypes⁠/⁠scalars_simple` | Log a scalar over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_simple.cpp) | @@ -210,10 +213,14 @@ _All snippets, organized by the [`Archetype`](https://rerun.io/docs/reference/ty | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `archetypes⁠/⁠scalars_column_updates` | Update a scalar over time, in a single operation | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_column_updates.cpp) | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `archetypes⁠/⁠series_lines_style` | Log a scalar over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_lines_style.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_lines_style.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_lines_style.cpp) | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `archetypes⁠/⁠series_points_style` | Log a scalar over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_points_style.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_points_style.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_points_style.cpp) | -| **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `concepts⁠/⁠lenses` | Use lenses to extract struct fields and reroute data to a different entity | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/lenses.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/lenses.rs) | | +| **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `concepts⁠/⁠chunk_processing` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/chunk_processing.py) | | | +| **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `concepts⁠/⁠chunk_processing_query` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/chunk_processing_query.py) | | | +| **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `concepts⁠/⁠lenses` | Use lenses to extract struct fields and reroute data to another entity | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/lenses.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/lenses.rs) | | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `concepts⁠/⁠query-and-transform⁠/⁠dataframe_query_example` | Query and display the first 10 rows of a recording | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/query-and-transform/dataframe_query_example.py) | | | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `howto⁠/⁠component_mapping` | Demonstrates how to configure visualizer component mappings from blueprint | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/component_mapping.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/component_mapping.rs) | | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `tutorials⁠/⁠fixed_window_plot` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/fixed_window_plot.py) | | | +| **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `tutorials⁠/⁠getting_started` | Getting Started workflow: Catalog SDK regions (Python only) | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/getting_started.py) | | | +| **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `tutorials⁠/⁠getting_started_log` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/getting_started_log.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/getting_started_log.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/getting_started_log.cpp) | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `tutorials⁠/⁠visualizer-overrides` | Log a scalar over time and override the visualizer | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualizer-overrides.py) | | | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `views⁠/⁠dataframe` | Use a blueprint to customize a DataframeView | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/dataframe.py) | | | | **[`Scalars`](https://rerun.io/docs/reference/types/archetypes/scalars)** | `views⁠/⁠timeseries` | Use a blueprint to customize a TimeSeriesView | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/timeseries.py) | | | @@ -227,7 +234,13 @@ _All snippets, organized by the [`Archetype`](https://rerun.io/docs/reference/ty | **[`SeriesPoints`](https://rerun.io/docs/reference/types/archetypes/series_points)** | `archetypes⁠/⁠series_points_style` | Log a scalar over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_points_style.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_points_style.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/series_points_style.cpp) | | **[`SeriesPoints`](https://rerun.io/docs/reference/types/archetypes/series_points)** | `archetypes⁠/⁠scalars_multiple_plots` | Log a scalar over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_multiple_plots.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_multiple_plots.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/scalars_multiple_plots.cpp) | | **[`SeriesPoints`](https://rerun.io/docs/reference/types/archetypes/series_points)** | `tutorials⁠/⁠visualizer-overrides` | Log a scalar over time and override the visualizer | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualizer-overrides.py) | | | -| **[`Status`](https://rerun.io/docs/reference/types/archetypes/status)** | `archetypes⁠/⁠status` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/status.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/status.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/status.cpp) | +| **[`StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change)** | `archetypes⁠/⁠state_change` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_change.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_change.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_change.cpp) | +| **[`StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change)** | `archetypes⁠/⁠state_configuration` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_configuration.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_configuration.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_configuration.cpp) | +| **[`StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change)** | `howto⁠/⁠state_remapping` | Visualize an arbitrary component as state by remapping `StateChange.state` | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_remapping.py) | | | +| **[`StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change)** | `howto⁠/⁠state_timeline` | Demonstrates the experimental state timeline view | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.cpp) | +| **[`StateChange`](https://rerun.io/docs/reference/types/archetypes/state_change)** | `views⁠/⁠state_timeline` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/state_timeline.py) | | | +| **[`StateConfiguration`](https://rerun.io/docs/reference/types/archetypes/state_configuration)** | `archetypes⁠/⁠state_configuration` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_configuration.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_configuration.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/state_configuration.cpp) | +| **[`StateConfiguration`](https://rerun.io/docs/reference/types/archetypes/state_configuration)** | `howto⁠/⁠state_timeline` | Demonstrates the experimental state timeline view | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.cpp) | | **[`Tensor`](https://rerun.io/docs/reference/types/archetypes/tensor)** | `views⁠/⁠tensor` | Use a blueprint to show a tensor view | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/tensor.py) | | | | **[`Tensor`](https://rerun.io/docs/reference/types/archetypes/tensor)** | `archetypes⁠/⁠tensor_simple` | Create and log a tensor | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/tensor_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/tensor_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/tensor_simple.cpp) | | **[`TextDocument`](https://rerun.io/docs/reference/types/archetypes/text_document)** | `views⁠/⁠text_document` | Use a blueprint to show a text document | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/text_document.py) | | | @@ -249,20 +262,19 @@ _All snippets, organized by the [`Archetype`](https://rerun.io/docs/reference/ty | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠transform3d_hierarchy` | Logs a transform hierarchy | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.cpp) | | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠transform3d_column_updates` | Update a transform over time, in a single operation | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.cpp) | | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠transform3d_axes` | Log different transforms with visualized coordinates axes | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.cpp) | -| **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠coordinate_frame_builtin_frames` | Demonstrates using explicit `CoordinateFrame` with implicit transform frames only | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp) | +| **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠coordinate_frame_builtin_frames` | Demonstrates using explicit `CoordinateFrame` with implicit transforms | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp) | +| **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠grid_map_pose` | Shows how to log a GridMap at a specific pose | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_pose.py) | | | | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠instance_poses3d_combined` | Log a simple 3D box with a regular & instance pose transform | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.cpp) | | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠pinhole_projections` | Demonstrates pinhole camera projections with Rerun blueprints | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_projections.py) | | | -| **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `howto⁠/⁠convert_mcap_protobuf` | Convert custom MCAP Protobuf messages to Rerun format | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf.py) | | | -| **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `howto⁠/⁠convert_mcap_protobuf_send_column` | Convert custom MCAP Protobuf messages to Rerun format using send_columns | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py) | | | | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `howto⁠/⁠load_urdf` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/load_urdf.py) | | | +| **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `tutorials⁠/⁠dna` | The DNA-abacus example from the Log and Ingest tutorial | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/dna.cpp) | +| **[`TransformAxes3D`](https://rerun.io/docs/reference/types/archetypes/transform_axes3d)** | `archetypes⁠/⁠grid_map_pose` | Shows how to log a GridMap at a specific pose | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_pose.py) | | | | **[`TransformAxes3D`](https://rerun.io/docs/reference/types/archetypes/transform_axes3d)** | `archetypes⁠/⁠transform3d_axes` | Log different transforms with visualized coordinates axes | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.cpp) | | **[`TransformAxes3D`](https://rerun.io/docs/reference/types/archetypes/transform_axes3d)** | `archetypes⁠/⁠transform3d_column_updates` | Update a transform over time, in a single operation | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.cpp) | | **[`TransformAxes3D`](https://rerun.io/docs/reference/types/archetypes/transform_axes3d)** | `archetypes⁠/⁠transform3d_row_updates` | Update a transform over time | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_row_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_row_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_row_updates.cpp) | | **[`VideoFrameReference`](https://rerun.io/docs/reference/types/archetypes/video_frame_reference)** | `archetypes⁠/⁠video_auto_frames` | Log a video asset using automatically determined frame references | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_auto_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_auto_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_auto_frames.cpp) | | **[`VideoFrameReference`](https://rerun.io/docs/reference/types/archetypes/video_frame_reference)** | `archetypes⁠/⁠video_manual_frames` | Manual use of individual video frame references | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_manual_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_manual_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_manual_frames.cpp) | | **[`VideoStream`](https://rerun.io/docs/reference/types/archetypes/video_stream)** | `archetypes⁠/⁠video_stream_synthetic` | Video encode images using av and stream them to Rerun | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_stream_synthetic.py) | | | -| **[`VideoStream`](https://rerun.io/docs/reference/types/archetypes/video_stream)** | `howto⁠/⁠convert_mcap_protobuf` | Convert custom MCAP Protobuf messages to Rerun format | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf.py) | | | -| **[`VideoStream`](https://rerun.io/docs/reference/types/archetypes/video_stream)** | `howto⁠/⁠convert_mcap_protobuf_send_column` | Convert custom MCAP Protobuf messages to Rerun format using send_columns | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py) | | | | **[`VideoStream`](https://rerun.io/docs/reference/types/archetypes/video_stream)** | `howto⁠/⁠lerobot_export` | Demonstrate converting Rerun recording to LeRobot dataset | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/lerobot_export.py) | | | | **[`ViewCoordinates`](https://rerun.io/docs/reference/types/archetypes/view_coordinates)** | `archetypes⁠/⁠view_coordinates_simple` | Change the view coordinates for the scene | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/view_coordinates_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/view_coordinates_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/view_coordinates_simple.cpp) | | **[`ViewCoordinates`](https://rerun.io/docs/reference/types/archetypes/view_coordinates)** | `archetypes⁠/⁠asset3d_simple` | Log a simple 3D asset | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/asset3d_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/asset3d_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/asset3d_simple.cpp) | @@ -270,6 +282,7 @@ _All snippets, organized by the [`Archetype`](https://rerun.io/docs/reference/ty | **[`ViewCoordinates`](https://rerun.io/docs/reference/types/archetypes/view_coordinates)** | `archetypes⁠/⁠pinhole_projections` | Demonstrates pinhole camera projections with Rerun blueprints | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_projections.py) | | | | **[`ViewCoordinates`](https://rerun.io/docs/reference/types/archetypes/view_coordinates)** | `archetypes⁠/⁠transform3d_hierarchy` | Logs a transform hierarchy | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.cpp) | | **[`ViewCoordinates`](https://rerun.io/docs/reference/types/archetypes/view_coordinates)** | `archetypes⁠/⁠transform3d_hierarchy_frames` | Logs a transform hierarchy using named transform frame relationships | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy_frames.cpp) | +| **[`VoxelGridMap`](https://rerun.io/docs/reference/types/archetypes/voxel_grid_map)** | `archetypes⁠/⁠voxel_grid_map_simple` | Log a simple sparse voxel grid map | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/voxel_grid_map_simple.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/voxel_grid_map_simple.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/voxel_grid_map_simple.cpp) | ### Views (blueprint) @@ -278,7 +291,7 @@ _All snippets, organized by the [`View`](https://rerun.io/docs/reference/types/v | Component | Snippet | Description | Python | Rust | C+⁠+ | | --------- | ------- | ----------- | :----: | :--: | :-------: | -| **[`BarChartView`](https://rerun.io/docs/reference/types/views/bar_chart_view)** | `tutorials⁠/⁠visualization⁠/⁠save_blueprint` | Craft an example blueprint with the python API and save it to a file for future use | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualization/save_blueprint.py) | | | +| **[`BarChartView`](https://rerun.io/docs/reference/types/views/bar_chart_view)** | `tutorials⁠/⁠visualization⁠/⁠save_blueprint` | Craft an example blueprint with the python API and save it to a file | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualization/save_blueprint.py) | | | | **[`BarChartView`](https://rerun.io/docs/reference/types/views/bar_chart_view)** | `views⁠/⁠bar_chart` | Use a blueprint to show a bar chart | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/bar_chart.py) | | | | **[`DataframeView`](https://rerun.io/docs/reference/types/views/dataframe_view)** | `reference⁠/⁠dataframe_view_query` | Query and display the first 10 rows of a recording in a dataframe view | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/reference/dataframe_view_query.py) | | | | **[`DataframeView`](https://rerun.io/docs/reference/types/views/dataframe_view)** | `views⁠/⁠dataframe` | Use a blueprint to customize a DataframeView | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/dataframe.py) | | | @@ -300,20 +313,24 @@ _All snippets, organized by the [`View`](https://rerun.io/docs/reference/types/v | **[`Spatial2DView`](https://rerun.io/docs/reference/types/views/spatial2d_view)** | `concepts⁠/⁠viscomp-component-override` | Override a component | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/concepts/viscomp-component-override.py) | | | | **[`Spatial2DView`](https://rerun.io/docs/reference/types/views/spatial2d_view)** | `tutorials⁠/⁠extra_values` | Log extra values with a `Points2D` | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/extra_values.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/extra_values.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/extra_values.cpp) | | **[`Spatial2DView`](https://rerun.io/docs/reference/types/views/spatial2d_view)** | `views⁠/⁠spatial2d` | Use a blueprint to customize a Spatial2DView | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/spatial2d.py) | | | +| **[`Spatial3DView`](https://rerun.io/docs/reference/types/views/spatial3d_view)** | `archetypes⁠/⁠grid_map_pose` | Shows how to log a GridMap at a specific pose | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/grid_map_pose.py) | | | +| **[`Spatial3DView`](https://rerun.io/docs/reference/types/views/spatial3d_view)** | `archetypes⁠/⁠line_strips3d_time_window` | Log line strips over time and view a sliding window (e.g. trajectories) | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/line_strips3d_time_window.py) | | | | **[`Spatial3DView`](https://rerun.io/docs/reference/types/views/spatial3d_view)** | `archetypes⁠/⁠pinhole_projections` | Demonstrates pinhole camera projections with Rerun blueprints | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/pinhole_projections.py) | | | | **[`Spatial3DView`](https://rerun.io/docs/reference/types/views/spatial3d_view)** | `archetypes⁠/⁠transform3d_hierarchy` | Logs a transform hierarchy | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_hierarchy.cpp) | -| **[`Spatial3DView`](https://rerun.io/docs/reference/types/views/spatial3d_view)** | `howto⁠/⁠dual_color_point_cloud` | Demonstrates how to visualize the same point cloud with two different color schemes | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dual_color_point_cloud.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dual_color_point_cloud.rs) | | +| **[`Spatial3DView`](https://rerun.io/docs/reference/types/views/spatial3d_view)** | `howto⁠/⁠dual_color_point_cloud` | Visualize the same point cloud with two different color schemes | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dual_color_point_cloud.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/dual_color_point_cloud.rs) | | | **[`Spatial3DView`](https://rerun.io/docs/reference/types/views/spatial3d_view)** | `howto⁠/⁠screenshot` | Take screenshots of the viewer or specific views from code | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/screenshot.py) | | | | **[`Spatial3DView`](https://rerun.io/docs/reference/types/views/spatial3d_view)** | `views⁠/⁠spatial3d` | Use a blueprint to customize a Spatial3DView | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/spatial3d.py) | | | -| **[`StatusView`](https://rerun.io/docs/reference/types/views/status_view)** | `views⁠/⁠status` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/status.py) | | | +| **[`StateTimelineView`](https://rerun.io/docs/reference/types/views/state_timeline_view)** | `howto⁠/⁠state_remapping` | Visualize an arbitrary component as state by remapping `StateChange.state` | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_remapping.py) | | | +| **[`StateTimelineView`](https://rerun.io/docs/reference/types/views/state_timeline_view)** | `howto⁠/⁠state_timeline` | Demonstrates the experimental state timeline view | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/state_timeline.cpp) | +| **[`StateTimelineView`](https://rerun.io/docs/reference/types/views/state_timeline_view)** | `views⁠/⁠state_timeline` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/state_timeline.py) | | | | **[`TensorView`](https://rerun.io/docs/reference/types/views/tensor_view)** | `views⁠/⁠tensor` | Use a blueprint to show a tensor view | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/tensor.py) | | | -| **[`TextDocumentView`](https://rerun.io/docs/reference/types/views/text_document_view)** | `tutorials⁠/⁠visualization⁠/⁠save_blueprint` | Craft an example blueprint with the python API and save it to a file for future use | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualization/save_blueprint.py) | | | +| **[`TextDocumentView`](https://rerun.io/docs/reference/types/views/text_document_view)** | `tutorials⁠/⁠visualization⁠/⁠save_blueprint` | Craft an example blueprint with the python API and save it to a file | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualization/save_blueprint.py) | | | | **[`TextDocumentView`](https://rerun.io/docs/reference/types/views/text_document_view)** | `views⁠/⁠text_document` | Use a blueprint to show a text document | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/text_document.py) | | | | **[`TextLogView`](https://rerun.io/docs/reference/types/views/text_log_view)** | `views⁠/⁠text_log` | Use a blueprint to show a text log | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/text_log.py) | | | | **[`TimeSeriesView`](https://rerun.io/docs/reference/types/views/time_series_view)** | `howto⁠/⁠component_mapping` | Demonstrates how to configure visualizer component mappings from blueprint | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/component_mapping.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/component_mapping.rs) | | -| **[`TimeSeriesView`](https://rerun.io/docs/reference/types/views/time_series_view)** | `howto⁠/⁠visualization⁠/⁠save_blueprint` | Craft a blueprint with the python API and save it to a file for future use | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/visualization/save_blueprint.py) | | | +| **[`TimeSeriesView`](https://rerun.io/docs/reference/types/views/time_series_view)** | `howto⁠/⁠visualization⁠/⁠save_blueprint` | Craft a blueprint with the python API and save it to file | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/howto/visualization/save_blueprint.py) | | | | **[`TimeSeriesView`](https://rerun.io/docs/reference/types/views/time_series_view)** | `tutorials⁠/⁠fixed_window_plot` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/fixed_window_plot.py) | | | -| **[`TimeSeriesView`](https://rerun.io/docs/reference/types/views/time_series_view)** | `tutorials⁠/⁠visualization⁠/⁠save_blueprint` | Craft an example blueprint with the python API and save it to a file for future use | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualization/save_blueprint.py) | | | +| **[`TimeSeriesView`](https://rerun.io/docs/reference/types/views/time_series_view)** | `tutorials⁠/⁠visualization⁠/⁠save_blueprint` | Craft an example blueprint with the python API and save it to a file | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualization/save_blueprint.py) | | | | **[`TimeSeriesView`](https://rerun.io/docs/reference/types/views/time_series_view)** | `tutorials⁠/⁠visualizer-overrides` | Log a scalar over time and override the visualizer | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/tutorials/visualizer-overrides.py) | | | | **[`TimeSeriesView`](https://rerun.io/docs/reference/types/views/time_series_view)** | `views⁠/⁠timeseries` | Use a blueprint to customize a TimeSeriesView | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/views/timeseries.py) | | | diff --git a/docs/snippets/all/.clang-format b/docs/snippets/all/.clang-format new file mode 100644 index 000000000000..1790d39140b5 --- /dev/null +++ b/docs/snippets/all/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: InheritParentConfig + +# Keep snippets tight so they look nice on our web page: +ColumnLimit: 80 diff --git a/docs/snippets/all/archetypes/annotation_context_connections.cpp b/docs/snippets/all/archetypes/annotation_context_connections.cpp index 61392f257fdb..64fdbc3c7b91 100644 --- a/docs/snippets/all/archetypes/annotation_context_connections.cpp +++ b/docs/snippets/all/archetypes/annotation_context_connections.cpp @@ -3,7 +3,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_annotation_context_connections"); + const auto rec = + rerun::RecordingStream("rerun_example_annotation_context_connections"); rec.spawn().exit_on_failure(); // Log an annotation context to assign a label and color to each class diff --git a/docs/snippets/all/archetypes/annotation_context_connections.rs b/docs/snippets/all/archetypes/annotation_context_connections.rs index 7cc8f4889fee..52ffc18f0ebe 100644 --- a/docs/snippets/all/archetypes/annotation_context_connections.rs +++ b/docs/snippets/all/archetypes/annotation_context_connections.rs @@ -1,8 +1,10 @@ //! Log annotation context with connections between keypoints. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_annotation_context_connections") - .spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_annotation_context_connections", + ) + .spawn()?; // Log an annotation context to assign a label and color to each class // Create a class description with labels and color for each keypoint ID as well as some @@ -17,7 +19,11 @@ fn main() -> Result<(), Box> { (2, "two", rerun::Rgba32::from_rgb(0, 0, 255)).into(), (3, "three", rerun::Rgba32::from_rgb(255, 255, 0)).into(), ], - keypoint_connections: rerun::KeypointPair::vec_from([(0, 2), (1, 2), (2, 3)]), + keypoint_connections: rerun::KeypointPair::vec_from([ + (0, 2), + (1, 2), + (2, 3), + ]), }]), )?; diff --git a/docs/snippets/all/archetypes/annotation_context_rects.cpp b/docs/snippets/all/archetypes/annotation_context_rects.cpp index ae56cb09c9db..e3c22a03e91e 100644 --- a/docs/snippets/all/archetypes/annotation_context_rects.cpp +++ b/docs/snippets/all/archetypes/annotation_context_rects.cpp @@ -3,7 +3,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_annotation_context_rects"); + const auto rec = + rerun::RecordingStream("rerun_example_annotation_context_rects"); rec.spawn().exit_on_failure(); // Log an annotation context to assign a label and color to each class diff --git a/docs/snippets/all/archetypes/annotation_context_rects.py b/docs/snippets/all/archetypes/annotation_context_rects.py index b69c17f0311a..0947035fbd35 100644 --- a/docs/snippets/all/archetypes/annotation_context_rects.py +++ b/docs/snippets/all/archetypes/annotation_context_rects.py @@ -3,7 +3,16 @@ rr.init("rerun_example_annotation_context_rects", spawn=True) # Log an annotation context to assign a label and color to each class -rr.log("/", rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), static=True) +rr.log( + "/", + rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), + static=True, +) # Log a batch of 2 rectangles with different `class_ids` -rr.log("detections", rr.Boxes2D(mins=[[-2, -2], [0, 0]], sizes=[[3, 3], [2, 2]], class_ids=[1, 2])) +rr.log( + "detections", + rr.Boxes2D( + mins=[[-2, -2], [0, 0]], sizes=[[3, 3], [2, 2]], class_ids=[1, 2] + ), +) diff --git a/docs/snippets/all/archetypes/annotation_context_rects.rs b/docs/snippets/all/archetypes/annotation_context_rects.rs index 866e037a26ae..4ea93eead6c6 100644 --- a/docs/snippets/all/archetypes/annotation_context_rects.rs +++ b/docs/snippets/all/archetypes/annotation_context_rects.rs @@ -1,8 +1,10 @@ //! Log rectangles with different colors and labels using annotation context fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_annotation_context_rects").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_annotation_context_rects", + ) + .spawn()?; // Log an annotation context to assign a label and color to each class rec.log_static( @@ -16,8 +18,11 @@ fn main() -> Result<(), Box> { // Log a batch of 2 rectangles with different class IDs rec.log( "detections", - &rerun::Boxes2D::from_mins_and_sizes([(-2., -2.), (0., 0.)], [(3., 3.), (2., 2.)]) - .with_class_ids([1, 2]), + &rerun::Boxes2D::from_mins_and_sizes( + [(-2., -2.), (0., 0.)], + [(3., 3.), (2., 2.)], + ) + .with_class_ids([1, 2]), )?; Ok(()) diff --git a/docs/snippets/all/archetypes/annotation_context_segmentation.cpp b/docs/snippets/all/archetypes/annotation_context_segmentation.cpp index 8990c70eaf26..c002cdf6b226 100644 --- a/docs/snippets/all/archetypes/annotation_context_segmentation.cpp +++ b/docs/snippets/all/archetypes/annotation_context_segmentation.cpp @@ -6,7 +6,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_annotation_context_segmentation"); + const auto rec = + rerun::RecordingStream("rerun_example_annotation_context_segmentation"); rec.spawn().exit_on_failure(); // create an annotation context to describe the classes @@ -26,8 +27,15 @@ int main(int argc, char* argv[]) { std::fill_n(data.begin() + y * WIDTH + 50, 70, static_cast(1)); } for (auto y = 100; y < 180; ++y) { - std::fill_n(data.begin() + y * WIDTH + 130, 150, static_cast(2)); + std::fill_n( + data.begin() + y * WIDTH + 130, + 150, + static_cast(2) + ); } - rec.log("segmentation/image", rerun::SegmentationImage(data.data(), {WIDTH, HEIGHT})); + rec.log( + "segmentation/image", + rerun::SegmentationImage(data.data(), {WIDTH, HEIGHT}) + ); } diff --git a/docs/snippets/all/archetypes/annotation_context_segmentation.py b/docs/snippets/all/archetypes/annotation_context_segmentation.py index 64bb016c83ec..3e1d998d6c5b 100644 --- a/docs/snippets/all/archetypes/annotation_context_segmentation.py +++ b/docs/snippets/all/archetypes/annotation_context_segmentation.py @@ -12,6 +12,10 @@ image[100:180, 130:280] = 2 # Log an annotation context to assign a label and color to each class -rr.log("segmentation", rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), static=True) +rr.log( + "segmentation", + rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), + static=True, +) rr.log("segmentation/image", rr.SegmentationImage(image)) diff --git a/docs/snippets/all/archetypes/annotation_context_segmentation.rs b/docs/snippets/all/archetypes/annotation_context_segmentation.rs index 468fe7a46aa6..61f3f07b1ea5 100644 --- a/docs/snippets/all/archetypes/annotation_context_segmentation.rs +++ b/docs/snippets/all/archetypes/annotation_context_segmentation.rs @@ -3,8 +3,10 @@ use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_annotation_context_segmentation") - .spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_annotation_context_segmentation", + ) + .spawn()?; // create an annotation context to describe the classes rec.log_static( diff --git a/docs/snippets/all/archetypes/arrows2d_simple.cpp b/docs/snippets/all/archetypes/arrows2d_simple.cpp index a91bae3d7206..2fafee3117e8 100644 --- a/docs/snippets/all/archetypes/arrows2d_simple.cpp +++ b/docs/snippets/all/archetypes/arrows2d_simple.cpp @@ -8,7 +8,9 @@ int main(int argc, char* argv[]) { rec.log( "arrows", - rerun::Arrows2D::from_vectors({{1.0f, 0.0f}, {0.0f, -1.0f}, {-0.7f, 0.7f}}) + rerun::Arrows2D::from_vectors( + {{1.0f, 0.0f}, {0.0f, -1.0f}, {-0.7f, 0.7f}} + ) .with_radii(0.025f) .with_origins({{0.25f, 0.0f}, {0.25f, 0.0f}, {-0.1f, -0.1f}}) .with_colors({{255, 0, 0}, {0, 255, 0}, {127, 0, 255}}) diff --git a/docs/snippets/all/archetypes/arrows2d_simple.rs b/docs/snippets/all/archetypes/arrows2d_simple.rs index 735abc98d796..e494277c760a 100644 --- a/docs/snippets/all/archetypes/arrows2d_simple.rs +++ b/docs/snippets/all/archetypes/arrows2d_simple.rs @@ -1,7 +1,8 @@ //! Log a batch of 2D arrows. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_arrow2d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_arrow2d").spawn()?; rec.log( "arrows", diff --git a/docs/snippets/all/archetypes/arrows3d_column_updates.cpp b/docs/snippets/all/archetypes/arrows3d_column_updates.cpp index fb54ad230df0..e299475cb206 100644 --- a/docs/snippets/all/archetypes/arrows3d_column_updates.cpp +++ b/docs/snippets/all/archetypes/arrows3d_column_updates.cpp @@ -10,7 +10,8 @@ using namespace std::chrono_literals; int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_arrows3d_column_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_arrows3d_column_updates"); rec.spawn().exit_on_failure(); // Prepare a fixed sequence of arrows over 5 timesteps. @@ -35,14 +36,18 @@ int main(int argc, char* argv[]) { } // At each timestep, all arrows share the same but changing color. - std::vector colors = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; + std::vector colors = + {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; // Log at seconds 10-14 auto times = rerun::Collection{10s, 11s, 12s, 13s, 14s}; - auto time_column = rerun::TimeColumn::from_durations("time", std::move(times)); + auto time_column = + rerun::TimeColumn::from_durations("time", std::move(times)); auto arrows = - rerun::Arrows3D().with_origins(origins).with_vectors(vectors).columns({5, 5, 5, 5, 5}); + rerun::Arrows3D().with_origins(origins).with_vectors(vectors).columns( + {5, 5, 5, 5, 5} + ); rec.send_columns( "arrows", diff --git a/docs/snippets/all/archetypes/arrows3d_column_updates.py b/docs/snippets/all/archetypes/arrows3d_column_updates.py index 2b72bd10c91a..aed8e96c471d 100644 --- a/docs/snippets/all/archetypes/arrows3d_column_updates.py +++ b/docs/snippets/all/archetypes/arrows3d_column_updates.py @@ -1,7 +1,8 @@ """ Update a set of vectors over time, in a single operation. -This is semantically equivalent to the `arrows3d_row_updates` example, albeit much faster. +This is semantically equivalent to the `arrows3d_row_updates` example, +albeit much faster. """ import numpy as np @@ -11,7 +12,8 @@ rr.init("rerun_example_arrows3d_column_updates", spawn=True) # Prepare a fixed sequence of arrows over 5 timesteps. -# Origins stay constant, vectors change magnitude and direction, and each timestep has a unique color. +# Origins stay constant, vectors change magnitude and direction, and each +# timestep has a unique color. times = np.arange(10, 15, 1.0) # At each time step, all arrows maintain their origin. @@ -25,5 +27,7 @@ rr.send_columns( "arrows", indexes=[rr.TimeColumn("time", duration=times)], - columns=[*rr.Arrows3D.columns(origins=origins, vectors=vectors, colors=colors)], + columns=[ + *rr.Arrows3D.columns(origins=origins, vectors=vectors, colors=colors) + ], ) diff --git a/docs/snippets/all/archetypes/arrows3d_column_updates.rs b/docs/snippets/all/archetypes/arrows3d_column_updates.rs index 5b785caec614..369fc184fb2e 100644 --- a/docs/snippets/all/archetypes/arrows3d_column_updates.rs +++ b/docs/snippets/all/archetypes/arrows3d_column_updates.rs @@ -5,8 +5,10 @@ use rerun::demo_util::linspace; fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_arrows3d_column_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_arrows3d_column_updates", + ) + .spawn()?; let times = rerun::TimeColumn::new_duration_secs("time", 10..15); // Prepare a fixed sequence of arrows over 5 timesteps. @@ -16,8 +18,7 @@ fn main() -> Result<(), Box> { let i = i as f32; ( linspace(-1., 1., 5).map(move |x| (x, x, 0.)), - linspace(-1., 1., 5) - .zip(linspace(0., i, 5)) + std::iter::zip(linspace(-1., 1., 5), linspace(0., i, 5)) .map(|(x, z)| (x, x, z)), ) }) @@ -34,7 +35,7 @@ fn main() -> Result<(), Box> { .with_colors(colors) .columns_of_unit_batches()?; - rec.send_columns("arrows", [times], arrows.chain(color))?; + rec.send_columns("arrows", [times], std::iter::chain(arrows, color))?; Ok(()) } diff --git a/docs/snippets/all/archetypes/arrows3d_row_updates.cpp b/docs/snippets/all/archetypes/arrows3d_row_updates.cpp index 7b31bfd3a22c..90effb4b5eee 100644 --- a/docs/snippets/all/archetypes/arrows3d_row_updates.cpp +++ b/docs/snippets/all/archetypes/arrows3d_row_updates.cpp @@ -9,7 +9,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_arrows3d_row_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_arrows3d_row_updates"); rec.spawn().exit_on_failure(); // Prepare a fixed sequence of arrows over 5 timesteps. @@ -36,7 +37,8 @@ int main(int argc, char* argv[]) { } // At each timestep, all arrows share the same but changing color. - std::vector colors = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; + std::vector colors = + {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; for (size_t i = 0; i < 5; i++) { rec.set_time_duration_secs("time", 10.0 + static_cast(i)); diff --git a/docs/snippets/all/archetypes/arrows3d_row_updates.py b/docs/snippets/all/archetypes/arrows3d_row_updates.py index 04f4784398fc..df66c113bee8 100644 --- a/docs/snippets/all/archetypes/arrows3d_row_updates.py +++ b/docs/snippets/all/archetypes/arrows3d_row_updates.py @@ -1,7 +1,8 @@ """ Update a set of vectors over time. -See also the `arrows3d_column_updates` example, which achieves the same thing in a single operation. +See also the `arrows3d_column_updates` example, which achieves the same +thing in a single operation. """ import numpy as np @@ -11,7 +12,8 @@ rr.init("rerun_example_arrows3d_row_updates", spawn=True) # Prepare a fixed sequence of arrows over 5 timesteps. -# Origins stay constant, vectors change magnitude and direction, and each timestep has a unique color. +# Origins stay constant, vectors change magnitude and direction, and each +# timestep has a unique color. times = np.arange(10, 15, 1.0) # At each time step, all arrows maintain their origin. @@ -24,4 +26,7 @@ for i in range(5): rr.set_time("time", duration=10 + i) - rr.log("arrows", rr.Arrows3D(vectors=vectors[i], origins=origins, colors=colors[i])) + rr.log( + "arrows", + rr.Arrows3D(vectors=vectors[i], origins=origins, colors=colors[i]), + ) diff --git a/docs/snippets/all/archetypes/arrows3d_row_updates.rs b/docs/snippets/all/archetypes/arrows3d_row_updates.rs index c4e1f7763899..163431b10214 100644 --- a/docs/snippets/all/archetypes/arrows3d_row_updates.rs +++ b/docs/snippets/all/archetypes/arrows3d_row_updates.rs @@ -5,7 +5,10 @@ use rerun::demo_util::linspace; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_arrows3d_row_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_arrows3d_row_updates", + ) + .spawn()?; // Prepare a fixed sequence of arrows over 5 timesteps. // Origins stay constant, vectors change magnitude and direction, and each timestep has a unique color. @@ -14,8 +17,7 @@ fn main() -> Result<(), Box> { let i = i as f32; ( linspace(-1., 1., 5).map(move |x| (x, x, 0.)), - linspace(-1., 1., 5) - .zip(linspace(0., i, 5)) + std::iter::zip(linspace(-1., 1., 5), linspace(0., i, 5)) .map(|(x, z)| (x, x, z)), ) }) @@ -24,7 +26,9 @@ fn main() -> Result<(), Box> { // At each timestep, all arrows share the same but changing color. let colors = [0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF]; - for (time, origins, vectors, color) in itertools::izip!(10..15, origins, vectors, colors) { + for (time, origins, vectors, color) in + itertools::izip!(10..15, origins, vectors, colors) + { rec.set_duration_secs("time", time); let arrows = rerun::Arrows3D::from_vectors(vectors) diff --git a/docs/snippets/all/archetypes/arrows3d_simple.cpp b/docs/snippets/all/archetypes/arrows3d_simple.cpp index d1c340e1b15f..c6d3ec341470 100644 --- a/docs/snippets/all/archetypes/arrows3d_simple.cpp +++ b/docs/snippets/all/archetypes/arrows3d_simple.cpp @@ -28,6 +28,8 @@ int main(int argc, char* argv[]) { rec.log( "arrows", - rerun::Arrows3D::from_vectors(vectors).with_origins(origins).with_colors(colors) + rerun::Arrows3D::from_vectors(vectors) + .with_origins(origins) + .with_colors(colors) ); } diff --git a/docs/snippets/all/archetypes/arrows3d_simple.py b/docs/snippets/all/archetypes/arrows3d_simple.py index a40133447286..85e95e55d492 100644 --- a/docs/snippets/all/archetypes/arrows3d_simple.py +++ b/docs/snippets/all/archetypes/arrows3d_simple.py @@ -11,7 +11,11 @@ lengths = np.log2(np.arange(0, 100) + 1) angles = np.arange(start=0, stop=tau, step=tau * 0.01) origins = np.zeros((100, 3)) -vectors = np.column_stack([np.sin(angles) * lengths, np.zeros(100), np.cos(angles) * lengths]) +vectors = np.column_stack([ + np.sin(angles) * lengths, + np.zeros(100), + np.cos(angles) * lengths, +]) colors = [[1.0 - c, c, 0.5, 0.5] for c in angles / tau] rr.log("arrows", rr.Arrows3D(origins=origins, vectors=vectors, colors=colors)) diff --git a/docs/snippets/all/archetypes/arrows3d_simple.rs b/docs/snippets/all/archetypes/arrows3d_simple.rs index c5501e6a43b1..8fba08839758 100644 --- a/docs/snippets/all/archetypes/arrows3d_simple.rs +++ b/docs/snippets/all/archetypes/arrows3d_simple.rs @@ -3,7 +3,8 @@ use std::f32::consts::TAU; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_arrow3d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_arrow3d").spawn()?; let origins = vec![rerun::Position3D::ZERO; 100]; let (vectors, colors): (Vec<_>, Vec<_>) = (0..100) @@ -12,7 +13,11 @@ fn main() -> Result<(), Box> { let length = ((i + 1) as f32).log2(); let c = (angle / TAU * 255.0).round() as u8; ( - rerun::Vector3D::from([(length * angle.sin()), 0.0, (length * angle.cos())]), + rerun::Vector3D::from([ + (length * angle.sin()), + 0.0, + (length * angle.cos()), + ]), rerun::Color::from_unmultiplied_rgba(255 - c, c, 128, 128), ) }) diff --git a/docs/snippets/all/archetypes/asset3d_simple.cpp b/docs/snippets/all/archetypes/asset3d_simple.cpp index cb4ba6587683..785b38815ede 100644 --- a/docs/snippets/all/archetypes/asset3d_simple.cpp +++ b/docs/snippets/all/archetypes/asset3d_simple.cpp @@ -6,7 +6,8 @@ int main(int argc, char* argv[]) { if (argc < 2) { - std::cerr << "Usage: " << argv[0] << " " << std::endl; + std::cerr << "Usage: " << argv[0] + << " " << std::endl; return 1; } @@ -15,6 +16,10 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_asset3d"); rec.spawn().exit_on_failure(); - rec.log_static("world", rerun::ViewCoordinates::RIGHT_HAND_Z_UP); // Set an up-axis - rec.log("world/asset", rerun::Asset3D::from_file_path(path).value_or_throw()); + // Set an up-axis: + rec.log_static("world", rerun::ViewCoordinates::RIGHT_HAND_Z_UP); + rec.log( + "world/asset", + rerun::Asset3D::from_file_path(path).value_or_throw() + ); } diff --git a/docs/snippets/all/archetypes/asset3d_simple.py b/docs/snippets/all/archetypes/asset3d_simple.py index b203e343f8c6..1408981fac34 100644 --- a/docs/snippets/all/archetypes/asset3d_simple.py +++ b/docs/snippets/all/archetypes/asset3d_simple.py @@ -10,5 +10,7 @@ rr.init("rerun_example_asset3d", spawn=True) -rr.log("world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True) # Set an up-axis +rr.log( + "world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True +) # Set an up-axis rr.log("world/asset", rr.Asset3D(path=sys.argv[1])) diff --git a/docs/snippets/all/archetypes/asset3d_simple.rs b/docs/snippets/all/archetypes/asset3d_simple.rs index 132b85c78a23..839ab1f15df6 100644 --- a/docs/snippets/all/archetypes/asset3d_simple.rs +++ b/docs/snippets/all/archetypes/asset3d_simple.rs @@ -8,7 +8,8 @@ fn main() -> anyhow::Result<()> { anyhow::bail!("Usage: {} ", args[0]); }; - let rec = rerun::RecordingStreamBuilder::new("rerun_example_asset3d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_asset3d").spawn()?; rec.log_static("world", &rerun::ViewCoordinates::RIGHT_HAND_Z_UP())?; // Set an up-axis rec.log("world/asset", &rerun::Asset3D::from_file_path(path)?)?; diff --git a/docs/snippets/all/archetypes/bar_chart.cpp b/docs/snippets/all/archetypes/bar_chart.cpp index 488dcab98a24..16a775852998 100644 --- a/docs/snippets/all/archetypes/bar_chart.cpp +++ b/docs/snippets/all/archetypes/bar_chart.cpp @@ -10,7 +10,8 @@ int main(int argc, char* argv[]) { rec.log("bar_chart", rerun::BarChart::i64({8, 4, 0, 9, 1, 4, 1, 6, 9, 0})); auto abscissa = std::vector{0, 1, 3, 4, 7, 11}; - auto abscissa_data = rerun::TensorData(rerun::Collection{abscissa.size()}, abscissa); + auto abscissa_data = + rerun::TensorData(rerun::Collection{abscissa.size()}, abscissa); rec.log( "bar_chart_custom_abscissa", rerun::BarChart::i64({8, 4, 0, 9, 1, 4}).with_abscissa(abscissa_data) @@ -19,6 +20,8 @@ int main(int argc, char* argv[]) { auto widths = std::vector{1, 2, 1, 3, 4, 1}; rec.log( "bar_chart_custom_abscissa_and_widths", - rerun::BarChart::i64({8, 4, 0, 9, 1, 4}).with_abscissa(abscissa_data).with_widths(widths) + rerun::BarChart::i64({8, 4, 0, 9, 1, 4}) + .with_abscissa(abscissa_data) + .with_widths(widths) ); } diff --git a/docs/snippets/all/archetypes/bar_chart.py b/docs/snippets/all/archetypes/bar_chart.py index 74dd0c28afaf..655b14fc17d7 100644 --- a/docs/snippets/all/archetypes/bar_chart.py +++ b/docs/snippets/all/archetypes/bar_chart.py @@ -4,8 +4,15 @@ rr.init("rerun_example_bar_chart", spawn=True) rr.log("bar_chart", rr.BarChart([8, 4, 0, 9, 1, 4, 1, 6, 9, 0])) -rr.log("bar_chart_custom_abscissa", rr.BarChart([8, 4, 0, 9, 1, 4], abscissa=[0, 1, 3, 4, 7, 11])) +rr.log( + "bar_chart_custom_abscissa", + rr.BarChart([8, 4, 0, 9, 1, 4], abscissa=[0, 1, 3, 4, 7, 11]), +) rr.log( "bar_chart_custom_abscissa_and_widths", - rr.BarChart([8, 4, 0, 9, 1, 4], abscissa=[0, 1, 3, 4, 7, 11], widths=[1, 2, 1, 3, 4, 1]), + rr.BarChart( + [8, 4, 0, 9, 1, 4], + abscissa=[0, 1, 3, 4, 7, 11], + widths=[1, 2, 1, 3, 4, 1], + ), ) diff --git a/docs/snippets/all/archetypes/bar_chart.rs b/docs/snippets/all/archetypes/bar_chart.rs index 314d0e564bfc..5076001c5f69 100644 --- a/docs/snippets/all/archetypes/bar_chart.rs +++ b/docs/snippets/all/archetypes/bar_chart.rs @@ -1,7 +1,8 @@ //! Create and log a bar chart fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_bar_chart").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_bar_chart") + .spawn()?; rec.log( "bar_chart", diff --git a/docs/snippets/all/archetypes/boxes2d_simple.cpp b/docs/snippets/all/archetypes/boxes2d_simple.cpp index 131b5f188d4b..3482b3e72e05 100644 --- a/docs/snippets/all/archetypes/boxes2d_simple.cpp +++ b/docs/snippets/all/archetypes/boxes2d_simple.cpp @@ -6,5 +6,8 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_box2d"); rec.spawn().exit_on_failure(); - rec.log("simple", rerun::Boxes2D::from_mins_and_sizes({{-1.f, -1.f}}, {{2.f, 2.f}})); + rec.log( + "simple", + rerun::Boxes2D::from_mins_and_sizes({{-1.f, -1.f}}, {{2.f, 2.f}}) + ); } diff --git a/docs/snippets/all/archetypes/boxes2d_simple.rs b/docs/snippets/all/archetypes/boxes2d_simple.rs index e0d785679e36..0367e5a1b568 100644 --- a/docs/snippets/all/archetypes/boxes2d_simple.rs +++ b/docs/snippets/all/archetypes/boxes2d_simple.rs @@ -1,7 +1,8 @@ //! Log some very simple 2D boxes. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_box2d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_box2d").spawn()?; rec.log( "simple", diff --git a/docs/snippets/all/archetypes/boxes3d_batch.py b/docs/snippets/all/archetypes/boxes3d_batch.py index 1b4e8d85ba7c..b30bf4a1ec2b 100644 --- a/docs/snippets/all/archetypes/boxes3d_batch.py +++ b/docs/snippets/all/archetypes/boxes3d_batch.py @@ -11,7 +11,9 @@ half_sizes=[[2.0, 2.0, 1.0], [1.0, 1.0, 0.5], [2.0, 0.5, 1.0]], quaternions=[ rr.Quaternion.identity(), - rr.Quaternion(xyzw=[0.0, 0.0, 0.382683, 0.923880]), # 45 degrees around Z + rr.Quaternion( + xyzw=[0.0, 0.0, 0.382683, 0.923880] + ), # 45 degrees around Z ], radii=0.025, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], diff --git a/docs/snippets/all/archetypes/boxes3d_batch.rs b/docs/snippets/all/archetypes/boxes3d_batch.rs index 554e23a3cc1e..0099e5e18896 100644 --- a/docs/snippets/all/archetypes/boxes3d_batch.rs +++ b/docs/snippets/all/archetypes/boxes3d_batch.rs @@ -1,7 +1,8 @@ //! Log a batch of oriented bounding boxes. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_box3d_batch").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_box3d_batch") + .spawn()?; rec.log( "batch", diff --git a/docs/snippets/all/archetypes/boxes3d_simple.rs b/docs/snippets/all/archetypes/boxes3d_simple.rs index f6a6188808f4..1e751882f950 100644 --- a/docs/snippets/all/archetypes/boxes3d_simple.rs +++ b/docs/snippets/all/archetypes/boxes3d_simple.rs @@ -1,7 +1,8 @@ //! Log a single 3D box. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_box3d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_box3d").spawn()?; rec.log( "simple", diff --git a/docs/snippets/all/archetypes/capsules3d_batch.cpp b/docs/snippets/all/archetypes/capsules3d_batch.cpp index ee49bdba0b6c..471f60b2461b 100644 --- a/docs/snippets/all/archetypes/capsules3d_batch.cpp +++ b/docs/snippets/all/archetypes/capsules3d_batch.cpp @@ -27,11 +27,26 @@ int main(int argc, char* argv[]) { {8.0f, 0.0f, 0.0f}, }) .with_rotation_axis_angles({ - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(0.0)), - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-22.5)), - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-45.0)), - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-67.5)), - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-90.0)), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(0.0) + ), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(-22.5) + ), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(-45.0) + ), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(-67.5) + ), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(-90.0) + ), }) ); } diff --git a/docs/snippets/all/archetypes/capsules3d_batch.rs b/docs/snippets/all/archetypes/capsules3d_batch.rs index 2b61cd058df1..b09194548373 100644 --- a/docs/snippets/all/archetypes/capsules3d_batch.rs +++ b/docs/snippets/all/archetypes/capsules3d_batch.rs @@ -3,7 +3,9 @@ use rerun::external::glam::vec3; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_capsule3d_batch").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_capsule3d_batch") + .spawn()?; rec.log( "capsules", diff --git a/docs/snippets/all/archetypes/clear_recursive.py b/docs/snippets/all/archetypes/clear_recursive.py index ae1f5da363d1..6a118996898f 100644 --- a/docs/snippets/all/archetypes/clear_recursive.py +++ b/docs/snippets/all/archetypes/clear_recursive.py @@ -5,12 +5,21 @@ rr.init("rerun_example_clear_recursive", spawn=True) vectors = [(1.0, 0.0, 0.0), (0.0, -1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] -origins = [(-0.5, 0.5, 0.0), (0.5, 0.5, 0.0), (0.5, -0.5, 0.0), (-0.5, -0.5, 0.0)] +origins = [ + (-0.5, 0.5, 0.0), + (0.5, 0.5, 0.0), + (0.5, -0.5, 0.0), + (-0.5, -0.5, 0.0), +] colors = [(200, 0, 0), (0, 200, 0), (0, 0, 200), (200, 0, 200)] # Log a handful of arrows. -for i, (vector, origin, color) in enumerate(zip(vectors, origins, colors, strict=False)): - rr.log(f"arrows/{i}", rr.Arrows3D(vectors=vector, origins=origin, colors=color)) +for i, (vector, origin, color) in enumerate( + zip(vectors, origins, colors, strict=False) +): + rr.log( + f"arrows/{i}", rr.Arrows3D(vectors=vector, origins=origin, colors=color) + ) # Now clear all of them at once. rr.log("arrows", rr.Clear(recursive=True)) # or `rr.Clear.recursive()` diff --git a/docs/snippets/all/archetypes/clear_recursive.rs b/docs/snippets/all/archetypes/clear_recursive.rs index 221bc6e10b17..1f176fccbe51 100644 --- a/docs/snippets/all/archetypes/clear_recursive.rs +++ b/docs/snippets/all/archetypes/clear_recursive.rs @@ -3,7 +3,9 @@ use rerun::external::glam; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_clear_recursive").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_clear_recursive") + .spawn()?; #[rustfmt::skip] let (vectors, origins, colors) = ( @@ -13,12 +15,16 @@ fn main() -> Result<(), Box> { ); // Log a handful of arrows. - for (i, ((vector, origin), color)) in vectors.into_iter().zip(origins).zip(colors).enumerate() { + for (i, (vector, origin, color)) in + itertools::izip!(vectors, origins, colors).enumerate() + { rec.log( format!("arrows/{i}"), &rerun::Arrows3D::from_vectors([vector]) .with_origins([origin]) - .with_colors([rerun::Color::from_rgb(color.0, color.1, color.2)]), + .with_colors([rerun::Color::from_rgb( + color.0, color.1, color.2, + )]), )?; } diff --git a/docs/snippets/all/archetypes/clear_simple.py b/docs/snippets/all/archetypes/clear_simple.py index c58cf155cb7e..b09cd5e8b53d 100644 --- a/docs/snippets/all/archetypes/clear_simple.py +++ b/docs/snippets/all/archetypes/clear_simple.py @@ -5,12 +5,21 @@ rr.init("rerun_example_clear", spawn=True) vectors = [(1.0, 0.0, 0.0), (0.0, -1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] -origins = [(-0.5, 0.5, 0.0), (0.5, 0.5, 0.0), (0.5, -0.5, 0.0), (-0.5, -0.5, 0.0)] +origins = [ + (-0.5, 0.5, 0.0), + (0.5, 0.5, 0.0), + (0.5, -0.5, 0.0), + (-0.5, -0.5, 0.0), +] colors = [(200, 0, 0), (0, 200, 0), (0, 0, 200), (200, 0, 200)] # Log a handful of arrows. -for i, (vector, origin, color) in enumerate(zip(vectors, origins, colors, strict=False)): - rr.log(f"arrows/{i}", rr.Arrows3D(vectors=vector, origins=origin, colors=color)) +for i, (vector, origin, color) in enumerate( + zip(vectors, origins, colors, strict=False) +): + rr.log( + f"arrows/{i}", rr.Arrows3D(vectors=vector, origins=origin, colors=color) + ) # Now clear them, one by one on each tick. for i in range(len(vectors)): diff --git a/docs/snippets/all/archetypes/clear_simple.rs b/docs/snippets/all/archetypes/clear_simple.rs index 464f322a2d5b..32c7ff5a9dfc 100644 --- a/docs/snippets/all/archetypes/clear_simple.rs +++ b/docs/snippets/all/archetypes/clear_simple.rs @@ -3,7 +3,8 @@ use rerun::external::glam; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_clear").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_clear").spawn()?; #[rustfmt::skip] let (vectors, origins, colors) = ( @@ -13,12 +14,16 @@ fn main() -> Result<(), Box> { ); // Log a handful of arrows. - for (i, ((vector, origin), color)) in vectors.into_iter().zip(origins).zip(colors).enumerate() { + for (i, (vector, origin, color)) in + itertools::izip!(vectors, origins, colors).enumerate() + { rec.log( format!("arrows/{i}"), &rerun::Arrows3D::from_vectors([vector]) .with_origins([origin]) - .with_colors([rerun::Color::from_rgb(color.0, color.1, color.2)]), + .with_colors([rerun::Color::from_rgb( + color.0, color.1, color.2, + )]), )?; } diff --git a/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp b/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp index bc964a39e723..afd503c77b6f 100644 --- a/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp +++ b/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.cpp @@ -3,7 +3,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_transform3d_hierarchy"); + const auto rec = + rerun::RecordingStream("rerun_example_transform3d_hierarchy"); rec.spawn().exit_on_failure(); rec.set_time_sequence("time", 0); diff --git a/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py b/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py index c196c72e0119..30b294615e47 100644 --- a/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py +++ b/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.py @@ -1,4 +1,4 @@ -"""Demonstrates using explicit `CoordinateFrame` with implicit transform frames only.""" +"""Demonstrates using explicit `CoordinateFrame` with implicit transforms.""" import rerun as rr @@ -8,13 +8,15 @@ rr.log( "red_box", rr.Boxes3D(half_sizes=[0.5, 0.5, 0.5], colors=[255, 0, 0]), - # Use Transform3D to place the box, so we actually change the underlying coordinate frame and not just the box's pose. + # Use Transform3D to place the box, so we actually change the underlying + # coordinate frame and not just the box's pose. rr.Transform3D(translation=[2.0, 0.0, 0.0]), ) rr.log( "blue_box", rr.Boxes3D(half_sizes=[0.5, 0.5, 0.5], colors=[0, 0, 255]), - # Use Transform3D to place the box, so we actually change the underlying coordinate frame and not just the box's pose. + # Use Transform3D to place the box, so we actually change the underlying + # coordinate frame and not just the box's pose. rr.Transform3D(translation=[-2.0, 0.0, 0.0]), ) rr.log("point", rr.Points3D([0.0, 0.0, 0.0], radii=0.5)) diff --git a/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs b/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs index 5d1f1fcea9d8..9e80764fd238 100644 --- a/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs +++ b/docs/snippets/all/archetypes/coordinate_frame_builtin_frames.rs @@ -3,13 +3,17 @@ #![expect(clippy::cast_possible_wrap)] fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_transform3d_hierarchy").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_transform3d_hierarchy", + ) + .spawn()?; rec.set_time_sequence("time", 0); rec.log( "red_box", &[ - &rerun::Boxes3D::from_half_sizes([(0.5, 0.5, 0.5)]).with_colors([(255, 0, 0)]) + &rerun::Boxes3D::from_half_sizes([(0.5, 0.5, 0.5)]) + .with_colors([(255, 0, 0)]) as &dyn rerun::AsComponents, // Use Transform3D to place the box, so we actually change the underlying coordinate frame and not just the box's pose. &rerun::Transform3D::from_translation([2.0, 0.0, 0.0]), @@ -18,7 +22,8 @@ fn main() -> Result<(), Box> { rec.log( "blue_box", &[ - &rerun::Boxes3D::from_half_sizes([(0.5, 0.5, 0.5)]).with_colors([(0, 0, 255)]) + &rerun::Boxes3D::from_half_sizes([(0.5, 0.5, 0.5)]) + .with_colors([(0, 0, 255)]) as &dyn rerun::AsComponents, // Use Transform3D to place the box, so we actually change the underlying coordinate frame and not just the box's pose. &rerun::Transform3D::from_translation([-2.0, 0.0, 0.0]), @@ -30,7 +35,8 @@ fn main() -> Result<(), Box> { )?; // Change where the point is located by cycling through its coordinate frame. - for (t, frame_id) in ["tf#/red_box", "tf#/blue_box"].into_iter().enumerate() { + for (t, frame_id) in ["tf#/red_box", "tf#/blue_box"].into_iter().enumerate() + { rec.set_time_sequence("time", t as i64 + 1); // leave it untouched at t==0. rec.log("point", &rerun::CoordinateFrame::new(frame_id))?; } diff --git a/docs/snippets/all/archetypes/cylinders3d_batch.cpp b/docs/snippets/all/archetypes/cylinders3d_batch.cpp index 8dc0e3eae2a1..b2f03e99f493 100644 --- a/docs/snippets/all/archetypes/cylinders3d_batch.cpp +++ b/docs/snippets/all/archetypes/cylinders3d_batch.cpp @@ -27,11 +27,26 @@ int main(int argc, char* argv[]) { {8.0f, 0.0f, 0.0f}, }) .with_rotation_axis_angles({ - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(0.0)), - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-22.5)), - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-45.0)), - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-67.5)), - rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-90.0)), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(0.0) + ), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(-22.5) + ), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(-45.0) + ), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(-67.5) + ), + rerun::RotationAxisAngle( + {1.0f, 0.0f, 0.0f}, + rerun::Angle::degrees(-90.0) + ), }) ); } diff --git a/docs/snippets/all/archetypes/cylinders3d_batch.rs b/docs/snippets/all/archetypes/cylinders3d_batch.rs index b5ff59d35c52..61865e788d43 100644 --- a/docs/snippets/all/archetypes/cylinders3d_batch.rs +++ b/docs/snippets/all/archetypes/cylinders3d_batch.rs @@ -3,7 +3,9 @@ use rerun::external::glam::vec3; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_cylinders3d_batch").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_cylinders3d_batch") + .spawn()?; rec.log( "cylinders", diff --git a/docs/snippets/all/archetypes/depth_image_3d.cpp b/docs/snippets/all/archetypes/depth_image_3d.cpp index 0cff92907cb2..64e9947faba9 100644 --- a/docs/snippets/all/archetypes/depth_image_3d.cpp +++ b/docs/snippets/all/archetypes/depth_image_3d.cpp @@ -14,10 +14,18 @@ int main(int argc, char* argv[]) { const int WIDTH = 300; std::vector data(WIDTH * HEIGHT, 65535); for (auto y = 50; y < 150; ++y) { - std::fill_n(data.begin() + y * WIDTH + 50, 100, static_cast(20000)); + std::fill_n( + data.begin() + y * WIDTH + 50, + 100, + static_cast(20000) + ); } for (auto y = 130; y < 180; ++y) { - std::fill_n(data.begin() + y * WIDTH + 100, 180, static_cast(45000)); + std::fill_n( + data.begin() + y * WIDTH + 100, + 180, + static_cast(45000) + ); } // If we log a pinhole camera model, the depth gets automatically back-projected to 3D @@ -33,6 +41,6 @@ int main(int argc, char* argv[]) { "world/camera/depth", rerun::DepthImage(data.data(), {WIDTH, HEIGHT}) .with_meter(10000.0) - .with_colormap(rerun::components::Colormap::Viridis) + .with_colormap(rerun::Colormap::Viridis) ); } diff --git a/docs/snippets/all/archetypes/depth_image_3d.py b/docs/snippets/all/archetypes/depth_image_3d.py index 113cd810752e..833bed3c11d1 100644 --- a/docs/snippets/all/archetypes/depth_image_3d.py +++ b/docs/snippets/all/archetypes/depth_image_3d.py @@ -10,7 +10,8 @@ rr.init("rerun_example_depth_image_3d", spawn=True) -# If we log a pinhole camera model, the depth gets automatically back-projected to 3D +# If we log a pinhole camera model, the depth gets automatically +# back-projected to 3D rr.log( "world/camera", rr.Pinhole( @@ -21,4 +22,7 @@ ) # Log the tensor. -rr.log("world/camera/depth", rr.DepthImage(depth_image, meter=10_000.0, colormap="viridis")) +rr.log( + "world/camera/depth", + rr.DepthImage(depth_image, meter=10_000.0, colormap="viridis"), +) diff --git a/docs/snippets/all/archetypes/depth_image_3d.rs b/docs/snippets/all/archetypes/depth_image_3d.rs index e9555cc6b188..c120edacb8af 100644 --- a/docs/snippets/all/archetypes/depth_image_3d.rs +++ b/docs/snippets/all/archetypes/depth_image_3d.rs @@ -2,7 +2,9 @@ use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_depth_image_3d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_depth_image_3d") + .spawn()?; let width = 300; let height = 200; diff --git a/docs/snippets/all/archetypes/depth_image_simple.cpp b/docs/snippets/all/archetypes/depth_image_simple.cpp index 1e67fc707f48..93f6cc768dc8 100644 --- a/docs/snippets/all/archetypes/depth_image_simple.cpp +++ b/docs/snippets/all/archetypes/depth_image_simple.cpp @@ -14,11 +14,22 @@ int main(int argc, char* argv[]) { const uint32_t WIDTH = 300; std::vector pixels(WIDTH * HEIGHT, 65535); for (uint32_t y = 50; y < 150; ++y) { - std::fill_n(pixels.begin() + y * WIDTH + 50, 100, static_cast(20000)); + std::fill_n( + pixels.begin() + y * WIDTH + 50, + 100, + static_cast(20000) + ); } for (uint32_t y = 130; y < 180; ++y) { - std::fill_n(pixels.begin() + y * WIDTH + 100, 180, static_cast(45000)); + std::fill_n( + pixels.begin() + y * WIDTH + 100, + 180, + static_cast(45000) + ); } - rec.log("depth", rerun::DepthImage(pixels.data(), {WIDTH, HEIGHT}).with_meter(10000.0)); + rec.log( + "depth", + rerun::DepthImage(pixels.data(), {WIDTH, HEIGHT}).with_meter(10000.0) + ); } diff --git a/docs/snippets/all/archetypes/depth_image_simple.rs b/docs/snippets/all/archetypes/depth_image_simple.rs index 1a5db06c151d..2472ba1df4e5 100644 --- a/docs/snippets/all/archetypes/depth_image_simple.rs +++ b/docs/snippets/all/archetypes/depth_image_simple.rs @@ -3,7 +3,9 @@ use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_depth_image_simple").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_depth_image_simple") + .spawn()?; let mut image = Array::::from_elem((200, 300).f(), 65535); image.slice_mut(s![50..150, 50..150]).fill(20000); diff --git a/docs/snippets/all/archetypes/ellipses2d_batch.cpp b/docs/snippets/all/archetypes/ellipses2d_batch.cpp new file mode 100644 index 000000000000..7a39d419cc58 --- /dev/null +++ b/docs/snippets/all/archetypes/ellipses2d_batch.cpp @@ -0,0 +1,23 @@ +// Log a batch of 2D ellipses. + +#include + +int main(int argc, char* argv[]) { + const auto rec = rerun::RecordingStream("rerun_example_ellipses2d_batch"); + rec.spawn().exit_on_failure(); + + rec.log( + "batch", + rerun::Ellipses2D::from_centers_and_half_sizes( + {{-2.0f, 0.0f}, {0.0f, 0.0f}, {2.5f, 0.0f}}, + {{1.5f, 0.75f}, {0.5f, 0.5f}, {0.75f, 1.5f}} + ) + .with_line_radii({0.025f, 0.05f, 0.025f}) + .with_colors({ + rerun::Rgba32(255, 0, 0), + rerun::Rgba32(0, 255, 0), + rerun::Rgba32(0, 0, 255), + }) + .with_labels({"wide", "circle", "tall"}) + ); +} diff --git a/docs/snippets/all/archetypes/ellipses2d_batch.py b/docs/snippets/all/archetypes/ellipses2d_batch.py new file mode 100644 index 000000000000..a0acc6a7b182 --- /dev/null +++ b/docs/snippets/all/archetypes/ellipses2d_batch.py @@ -0,0 +1,16 @@ +"""Log a batch of 2D ellipses.""" + +import rerun as rr + +rr.init("rerun_example_ellipses2d_batch", spawn=True) + +rr.log( + "batch", + rr.Ellipses2D( + centers=[(-2.0, 0.0), (0.0, 0.0), (2.5, 0.0)], + half_sizes=[(1.5, 0.75), (0.5, 0.5), (0.75, 1.5)], + line_radii=[0.025, 0.05, 0.025], + colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], + labels=["wide", "circle", "tall"], + ), +) diff --git a/docs/snippets/all/archetypes/ellipses2d_batch.rs b/docs/snippets/all/archetypes/ellipses2d_batch.rs new file mode 100644 index 000000000000..e2fe5be483f5 --- /dev/null +++ b/docs/snippets/all/archetypes/ellipses2d_batch.rs @@ -0,0 +1,24 @@ +//! Log a batch of 2D ellipses. + +fn main() -> Result<(), Box> { + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_ellipses2d_batch") + .spawn()?; + + rec.log( + "batch", + &rerun::Ellipses2D::from_centers_and_half_sizes( + [(-2.0, 0.0), (0.0, 0.0), (2.5, 0.0)], + [(1.5, 0.75), (0.5, 0.5), (0.75, 1.5)], + ) + .with_line_radii([0.025, 0.05, 0.025]) + .with_colors([ + rerun::Color::from_rgb(255, 0, 0), + rerun::Color::from_rgb(0, 255, 0), + rerun::Color::from_rgb(0, 0, 255), + ]) + .with_labels(["wide", "circle", "tall"]), + )?; + + Ok(()) +} diff --git a/docs/snippets/all/archetypes/ellipses2d_simple.cpp b/docs/snippets/all/archetypes/ellipses2d_simple.cpp new file mode 100644 index 000000000000..de154878c609 --- /dev/null +++ b/docs/snippets/all/archetypes/ellipses2d_simple.cpp @@ -0,0 +1,16 @@ +// Log some simple 2D ellipses. + +#include + +int main(int argc, char* argv[]) { + const auto rec = rerun::RecordingStream("rerun_example_ellipses2d"); + rec.spawn().exit_on_failure(); + + rec.log( + "simple", + rerun::Ellipses2D::from_centers_and_half_sizes( + {{0.0f, 0.0f}}, + {{2.0f, 1.0f}} + ) + ); +} diff --git a/docs/snippets/all/archetypes/ellipses2d_simple.py b/docs/snippets/all/archetypes/ellipses2d_simple.py new file mode 100644 index 000000000000..afaa4b1ee8b9 --- /dev/null +++ b/docs/snippets/all/archetypes/ellipses2d_simple.py @@ -0,0 +1,7 @@ +"""Log a simple 2D ellipse.""" + +import rerun as rr + +rr.init("rerun_example_ellipses2d", spawn=True) + +rr.log("simple", rr.Ellipses2D(half_sizes=[(2.0, 1.0)], centers=[(0.0, 0.0)])) diff --git a/docs/snippets/all/archetypes/ellipses2d_simple.rs b/docs/snippets/all/archetypes/ellipses2d_simple.rs new file mode 100644 index 000000000000..c8ab253e9686 --- /dev/null +++ b/docs/snippets/all/archetypes/ellipses2d_simple.rs @@ -0,0 +1,16 @@ +//! Log some very simple 2D ellipses. + +fn main() -> Result<(), Box> { + let rec = rerun::RecordingStreamBuilder::new("rerun_example_ellipses2d") + .spawn()?; + + rec.log( + "simple", + &rerun::Ellipses2D::from_centers_and_half_sizes( + [(0.0, 0.0)], + [(2.0, 1.0)], + ), + )?; + + Ok(()) +} diff --git a/docs/snippets/all/archetypes/ellipsoids3d_batch.rs b/docs/snippets/all/archetypes/ellipsoids3d_batch.rs index bf9eb33076ff..7eeeaa09999a 100644 --- a/docs/snippets/all/archetypes/ellipsoids3d_batch.rs +++ b/docs/snippets/all/archetypes/ellipsoids3d_batch.rs @@ -1,7 +1,9 @@ //! Log a batch of `Ellipsoids3D`. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_ellipsoid_batch").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_ellipsoid_batch") + .spawn()?; // Let's build a snowman! let belly_z = 2.5; diff --git a/docs/snippets/all/archetypes/ellipsoids3d_simple.cpp b/docs/snippets/all/archetypes/ellipsoids3d_simple.cpp index 848ecc7f75c1..7307d70e9e95 100644 --- a/docs/snippets/all/archetypes/ellipsoids3d_simple.cpp +++ b/docs/snippets/all/archetypes/ellipsoids3d_simple.cpp @@ -26,7 +26,9 @@ int main(int argc, char* argv[]) { rec.log( "points", - rerun::Points3D(points3d).with_radii(0.02f).with_colors(rerun::Rgba32(188, 77, 185)) + rerun::Points3D(points3d).with_radii(0.02f).with_colors( + rerun::Rgba32(188, 77, 185) + ) ); rec.log( diff --git a/docs/snippets/all/archetypes/ellipsoids3d_simple.rs b/docs/snippets/all/archetypes/ellipsoids3d_simple.rs index 289acf496f43..4831e210cc7d 100644 --- a/docs/snippets/all/archetypes/ellipsoids3d_simple.rs +++ b/docs/snippets/all/archetypes/ellipsoids3d_simple.rs @@ -3,7 +3,9 @@ use rand::prelude::*; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_ellipsoid_simple").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_ellipsoid_simple") + .spawn()?; let sigmas: [f32; 3] = [5., 3., 1.]; diff --git a/docs/snippets/all/archetypes/encoded_depth_image.cpp b/docs/snippets/all/archetypes/encoded_depth_image.cpp index 4e8d43aa6181..dd69a56196d8 100644 --- a/docs/snippets/all/archetypes/encoded_depth_image.cpp +++ b/docs/snippets/all/archetypes/encoded_depth_image.cpp @@ -11,17 +11,20 @@ namespace fs = std::filesystem; int main(int argc, char* argv[]) { if (argc < 2) { - std::cerr << "Usage: " << argv[0] << " " << std::endl; + std::cerr << "Usage: " << argv[0] << " " + << std::endl; return 1; } - const auto rec = rerun::RecordingStream("rerun_example_encoded_depth_image"); + const auto rec = + rerun::RecordingStream("rerun_example_encoded_depth_image"); rec.spawn().exit_on_failure(); const auto depth_path = fs::path(argv[1]); std::ifstream file(depth_path, std::ios::binary); if (!file) { - std::cerr << "Failed to open encoded depth image: " << depth_path << std::endl; + std::cerr << "Failed to open encoded depth image: " << depth_path + << std::endl; return 1; } @@ -29,11 +32,11 @@ int main(int argc, char* argv[]) { std::istreambuf_iterator(file), std::istreambuf_iterator()}; // Determine media type based on file extension - rerun::components::MediaType media_type; + rerun::MediaType media_type; if (depth_path.extension() == ".png") { - media_type = rerun::components::MediaType::png(); + media_type = rerun::MediaType::png(); } else { - media_type = rerun::components::MediaType::rvl(); + media_type = rerun::MediaType::rvl(); } rec.log( diff --git a/docs/snippets/all/archetypes/encoded_depth_image.py b/docs/snippets/all/archetypes/encoded_depth_image.py index c83c835a6a96..89c9a2ab992f 100644 --- a/docs/snippets/all/archetypes/encoded_depth_image.py +++ b/docs/snippets/all/archetypes/encoded_depth_image.py @@ -6,7 +6,9 @@ import rerun as rr if len(sys.argv) < 2: - print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + print( + f"Usage: {sys.argv[0]} ", file=sys.stderr + ) sys.exit(1) depth_path = Path(sys.argv[1]) diff --git a/docs/snippets/all/archetypes/encoded_depth_image.rs b/docs/snippets/all/archetypes/encoded_depth_image.rs index e2c3d5bda76c..6f2213ef0571 100644 --- a/docs/snippets/all/archetypes/encoded_depth_image.rs +++ b/docs/snippets/all/archetypes/encoded_depth_image.rs @@ -8,7 +8,9 @@ fn main() -> anyhow::Result<()> { anyhow::bail!("Usage: {} ", args[0]); }; - let rec = rerun::RecordingStreamBuilder::new("rerun_example_encoded_depth_image").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_encoded_depth_image") + .spawn()?; let depth_blob = std::fs::read(path)?; let encoded_depth = rerun::EncodedDepthImage::new(depth_blob) diff --git a/docs/snippets/all/archetypes/encoded_image.cpp b/docs/snippets/all/archetypes/encoded_image.cpp index 3607c44ac300..c2012eaae759 100644 --- a/docs/snippets/all/archetypes/encoded_image.cpp +++ b/docs/snippets/all/archetypes/encoded_image.cpp @@ -15,5 +15,8 @@ int main(int argc, char* argv[]) { fs::path image_filepath = fs::path(__FILE__).parent_path() / "ferris.png"; - rec.log("image", rerun::EncodedImage::from_file(image_filepath).value_or_throw()); + rec.log( + "image", + rerun::EncodedImage::from_file(image_filepath).value_or_throw() + ); } diff --git a/docs/snippets/all/archetypes/encoded_image.rs b/docs/snippets/all/archetypes/encoded_image.rs index 3932f37a4e84..82168e128b17 100644 --- a/docs/snippets/all/archetypes/encoded_image.rs +++ b/docs/snippets/all/archetypes/encoded_image.rs @@ -1,7 +1,8 @@ //! Log a PNG image fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_encoded_image").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_encoded_image") + .spawn()?; let image = include_bytes!("ferris.png"); diff --git a/docs/snippets/all/archetypes/entity_behavior.py b/docs/snippets/all/archetypes/entity_behavior.py index a53b1b093bf0..a71a05015b91 100644 --- a/docs/snippets/all/archetypes/entity_behavior.py +++ b/docs/snippets/all/archetypes/entity_behavior.py @@ -5,7 +5,8 @@ rr.init("rerun_example_entity_behavior", spawn=True) -# Use `EntityBehavior` to override visibility & interactivity of entities in the blueprint. +# Use `EntityBehavior` to override visibility & interactivity of entities +# in the blueprint. rr.send_blueprint( rrb.Spatial2DView( overrides={ @@ -20,4 +21,7 @@ rr.log("hidden_subtree/also_hidden", rr.LineStrips2D(strips=[(-1, 1), (1, -1)])) rr.log("hidden_subtree/not_hidden", rr.LineStrips2D(strips=[(1, 1), (-1, -1)])) rr.log("non_interactive_subtree", rr.Boxes2D(centers=(0, 0), half_sizes=(1, 1))) -rr.log("non_interactive_subtree/also_non_interactive", rr.Boxes2D(centers=(0, 0), half_sizes=(0.5, 0.5))) +rr.log( + "non_interactive_subtree/also_non_interactive", + rr.Boxes2D(centers=(0, 0), half_sizes=(0.5, 0.5)), +) diff --git a/docs/snippets/all/archetypes/entity_path.cpp b/docs/snippets/all/archetypes/entity_path.cpp index 522b3df82089..6d7e9bc50815 100644 --- a/docs/snippets/all/archetypes/entity_path.cpp +++ b/docs/snippets/all/archetypes/entity_path.cpp @@ -11,10 +11,17 @@ int main(int argc, char* argv[]) { rerun::TextDocument("This entity path was escaped manually") ); rec.log( - rerun::new_entity_path({"world", std::to_string(42), "unescaped string!"}), - rerun::TextDocument("This entity path was provided as a list of unescaped strings") + rerun::new_entity_path( + {"world", std::to_string(42), "unescaped string!"} + ), + rerun::TextDocument( + "This entity path was provided as a list of unescaped strings" + ) ); assert(rerun::escape_entity_path_part("my string!") == R"(my\ string\!)"); - assert(rerun::new_entity_path({"world", "42", "my string!"}) == R"(/world/42/my\ string\!)"); + assert( + rerun::new_entity_path({"world", "42", "my string!"}) == + R"(/world/42/my\ string\!)" + ); } diff --git a/docs/snippets/all/archetypes/entity_path.py b/docs/snippets/all/archetypes/entity_path.py index 9597de628319..0b995553e3b7 100644 --- a/docs/snippets/all/archetypes/entity_path.py +++ b/docs/snippets/all/archetypes/entity_path.py @@ -2,11 +2,18 @@ rr.init("rerun_example_entity_path", spawn=True) -rr.log(r"world/42/escaped\ string\!", rr.TextDocument("This entity path was escaped manually")) +rr.log( + r"world/42/escaped\ string\!", + rr.TextDocument("This entity path was escaped manually"), +) rr.log( ["world", 42, "unescaped string!"], - rr.TextDocument("This entity path was provided as a list of unescaped strings"), + rr.TextDocument( + "This entity path was provided as a list of unescaped strings" + ), ) assert rr.escape_entity_path_part("my string!") == r"my\ string\!" -assert rr.new_entity_path(["world", 42, "my string!"]) == r"/world/42/my\ string\!" +assert ( + rr.new_entity_path(["world", 42, "my string!"]) == r"/world/42/my\ string\!" +) diff --git a/docs/snippets/all/archetypes/entity_path.rs b/docs/snippets/all/archetypes/entity_path.rs index 465d662cddb8..8340f3bb43f6 100644 --- a/docs/snippets/all/archetypes/entity_path.rs +++ b/docs/snippets/all/archetypes/entity_path.rs @@ -1,7 +1,8 @@ //! Example of different ways of constructing an entity path. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_entity_path").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_entity_path") + .spawn()?; rec.log( r"world/42/escaped\ string\!", @@ -9,7 +10,9 @@ fn main() -> Result<(), Box> { )?; rec.log( rerun::entity_path!["world", 42, "unescaped string!"], - &rerun::TextDocument::new("This entity path was provided as a list of unescaped strings"), + &rerun::TextDocument::new( + "This entity path was provided as a list of unescaped strings", + ), )?; Ok(()) diff --git a/docs/snippets/all/archetypes/geo_line_strings_simple.cpp b/docs/snippets/all/archetypes/geo_line_strings_simple.cpp index 1ac316530c3f..7eb6f83d9b7d 100644 --- a/docs/snippets/all/archetypes/geo_line_strings_simple.cpp +++ b/docs/snippets/all/archetypes/geo_line_strings_simple.cpp @@ -6,7 +6,7 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_geo_line_strings"); rec.spawn().exit_on_failure(); - auto line_string = rerun::components::GeoLineString::from_lat_lon( + auto line_string = rerun::GeoLineString::from_lat_lon( {{41.0000, -109.0452}, {41.0000, -102.0415}, {36.9931, -102.0415}, diff --git a/docs/snippets/all/archetypes/geo_line_strings_simple.rs b/docs/snippets/all/archetypes/geo_line_strings_simple.rs index d29969aa4266..de4e57683c9a 100644 --- a/docs/snippets/all/archetypes/geo_line_strings_simple.rs +++ b/docs/snippets/all/archetypes/geo_line_strings_simple.rs @@ -1,7 +1,9 @@ //! Log a simple geospatial line string. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_geo_line_strings").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_geo_line_strings") + .spawn()?; rec.log( "colorado", diff --git a/docs/snippets/all/archetypes/geo_points_simple.rs b/docs/snippets/all/archetypes/geo_points_simple.rs index 2659c0c48644..1059d82652a5 100644 --- a/docs/snippets/all/archetypes/geo_points_simple.rs +++ b/docs/snippets/all/archetypes/geo_points_simple.rs @@ -1,7 +1,8 @@ //! Log some very simple geospatial point. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_geo_points").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_geo_points") + .spawn()?; rec.log( "rerun_hq", diff --git a/docs/snippets/all/archetypes/graph_directed.cpp b/docs/snippets/all/archetypes/graph_directed.cpp index c20305322cbe..d8201e88b2d4 100644 --- a/docs/snippets/all/archetypes/graph_directed.cpp +++ b/docs/snippets/all/archetypes/graph_directed.cpp @@ -13,6 +13,6 @@ int main(int argc, char* argv[]) { .with_labels({"A", "B", "C"}), rerun::GraphEdges({{"a", "b"}, {"b", "c"}, {"c", "a"}}) // Graphs are undirected by default. - .with_graph_type(rerun::components::GraphType::Directed) + .with_graph_type(rerun::GraphType::Directed) ); } diff --git a/docs/snippets/all/archetypes/graph_directed.py b/docs/snippets/all/archetypes/graph_directed.py index 737d6259d769..236757671af3 100644 --- a/docs/snippets/all/archetypes/graph_directed.py +++ b/docs/snippets/all/archetypes/graph_directed.py @@ -11,5 +11,7 @@ positions=[(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)], labels=["A", "B", "C"], ), - rr.GraphEdges(edges=[("a", "b"), ("b", "c"), ("c", "a")], graph_type="directed"), + rr.GraphEdges( + edges=[("a", "b"), ("b", "c"), ("c", "a")], graph_type="directed" + ), ) diff --git a/docs/snippets/all/archetypes/graph_directed.rs b/docs/snippets/all/archetypes/graph_directed.rs index 8a411238fb45..72b2c97d7e14 100644 --- a/docs/snippets/all/archetypes/graph_directed.rs +++ b/docs/snippets/all/archetypes/graph_directed.rs @@ -1,15 +1,19 @@ //! Log a simple directed graph. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_graph_directed").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_graph_directed") + .spawn()?; rec.log( "simple", &[ &rerun::GraphNodes::new(["a", "b", "c"]) .with_positions([(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)]) - .with_labels(["A", "B", "C"]) as &dyn rerun::AsComponents, - &rerun::GraphEdges::new([("a", "b"), ("b", "c"), ("c", "a")]).with_directed_edges(), + .with_labels(["A", "B", "C"]) + as &dyn rerun::AsComponents, + &rerun::GraphEdges::new([("a", "b"), ("b", "c"), ("c", "a")]) + .with_directed_edges(), ], )?; diff --git a/docs/snippets/all/archetypes/graph_undirected.cpp b/docs/snippets/all/archetypes/graph_undirected.cpp index de4ed9cba2e1..7e6fdffd238f 100644 --- a/docs/snippets/all/archetypes/graph_undirected.cpp +++ b/docs/snippets/all/archetypes/graph_undirected.cpp @@ -13,6 +13,6 @@ int main(int argc, char* argv[]) { .with_labels({"A", "B", "C"}), rerun::GraphEdges({{"a", "b"}, {"b", "c"}, {"c", "a"}}) // Optional: graphs are undirected by default. - .with_graph_type(rerun::components::GraphType::Undirected) + .with_graph_type(rerun::GraphType::Undirected) ); } diff --git a/docs/snippets/all/archetypes/graph_undirected.rs b/docs/snippets/all/archetypes/graph_undirected.rs index c87219a931f2..01f3f7328624 100644 --- a/docs/snippets/all/archetypes/graph_undirected.rs +++ b/docs/snippets/all/archetypes/graph_undirected.rs @@ -1,14 +1,17 @@ //! Log a simple undirected graph. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_graph_undirected").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_graph_undirected") + .spawn()?; rec.log( "simple", &[ &rerun::GraphNodes::new(["a", "b", "c"]) .with_positions([(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)]) - .with_labels(["A", "B", "C"]) as &dyn rerun::AsComponents, + .with_labels(["A", "B", "C"]) + as &dyn rerun::AsComponents, &rerun::GraphEdges::new([("a", "b"), ("b", "c"), ("c", "a")]) // Optional: graphs are undirected by default. .with_undirected_edges(), diff --git a/docs/snippets/all/archetypes/grid_map_pose.py b/docs/snippets/all/archetypes/grid_map_pose.py new file mode 100644 index 000000000000..68e20be0443c --- /dev/null +++ b/docs/snippets/all/archetypes/grid_map_pose.py @@ -0,0 +1,63 @@ +"""Shows how to log a GridMap at a specific pose.""" + +import math +from pathlib import Path + +from PIL import Image as PILImage + +import rerun as rr +import rerun.blueprint as rrb + +rr.init("rerun_example_grid_map_pose", spawn=True) + +# Log the transform for the map origin. +# Here we use ROS TF-style parent & child frame names. +rr.log( + "/tf", + rr.Transform3D( + translation=[1.0, 2.0, 0.0], + rotation_axis_angle=rr.components.RotationAxisAngle( + [0, 0, 1], -math.pi / 3 + ), + parent_frame="world", + child_frame="map", + ), + static=True, +) + +# We use a dummy image for the map in this example. +image = PILImage.open(Path(__file__).parent / "ferris.png").convert("RGBA") + +# Log the grid map at the map origin. +rr.log( + "demo_map", + rr.CoordinateFrame("map"), + rr.GridMap( + data=image.tobytes(), + format=rr.components.ImageFormat( + width=image.size[0], + height=image.size[1], + color_model="RGBA", + channel_datatype="U8", + ), + opacity=0.5, + # The size of a pixel in scene units. + cell_size=0.01, + # Specify the pose of the lower-left image corner relative to the + # map frame, in scene units. + translation=[1.1, -1.6, 0.0], + rotation_axis_angle=rr.components.RotationAxisAngle( + [0, 0, 1], math.pi / 4.0 + ), + ), +) + +# Show transform axes with frame names. +rr.send_blueprint( + rrb.Spatial3DView( + origin="/", + overrides={ + "/tf": [rr.TransformAxes3D(axis_length=0.5, show_frame=True)], + }, + ) +) diff --git a/docs/snippets/all/archetypes/grid_map_simple.cpp b/docs/snippets/all/archetypes/grid_map_simple.cpp index f58d61b5133d..3cf7170531ff 100644 --- a/docs/snippets/all/archetypes/grid_map_simple.cpp +++ b/docs/snippets/all/archetypes/grid_map_simple.cpp @@ -29,7 +29,7 @@ int main(int argc, char* argv[]) { rec.log( "world/map", rerun::archetypes::GridMap() - .with_data(rerun::components::ImageBuffer(grid)) + .with_data(rerun::ImageBuffer(grid)) .with_format(rerun::components::ImageFormat( {width, height}, rerun::ColorModel::L, @@ -41,6 +41,6 @@ int main(int argc, char* argv[]) { -(static_cast(height) * cell_size) / 2.0f, 0.0f} ) - .with_colormap(rerun::components::Colormap::RvizMap) + .with_colormap(rerun::Colormap::RvizMap) ); } diff --git a/docs/snippets/all/archetypes/grid_map_simple.py b/docs/snippets/all/archetypes/grid_map_simple.py index f65abfcc8a21..a04acfa1a4c1 100644 --- a/docs/snippets/all/archetypes/grid_map_simple.py +++ b/docs/snippets/all/archetypes/grid_map_simple.py @@ -7,8 +7,8 @@ width, height = 64, 64 cell_size = 0.1 -# Create a synthetic image with ROS `nav_msgs/OccupancyGrid` cell value conventions: -# -1 (255) unknown, 0 free, 100 occupied. +# Create a synthetic image with ROS `nav_msgs/OccupancyGrid` cell value +# conventions: -1 (255) unknown, 0 free, 100 occupied. grid = np.full((height, width), -1, dtype=np.int8) grid[8:56, 8:56] = 0 grid[20:44, 20:44] = 100 @@ -26,7 +26,11 @@ channel_datatype="U8", ), cell_size=cell_size, - translation=[-(width * cell_size) / 2.0, -(height * cell_size) / 2.0, 0.0], + translation=[ + -(width * cell_size) / 2.0, + -(height * cell_size) / 2.0, + 0.0, + ], colormap=rr.components.Colormap.RvizMap, ), ) diff --git a/docs/snippets/all/archetypes/grid_map_simple.rs b/docs/snippets/all/archetypes/grid_map_simple.rs index 0e55ebc25204..5d9967405866 100644 --- a/docs/snippets/all/archetypes/grid_map_simple.rs +++ b/docs/snippets/all/archetypes/grid_map_simple.rs @@ -1,7 +1,8 @@ //! Log a simple occupancy grid map. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_grid_map").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_grid_map").spawn()?; let width: usize = 64; let height: usize = 64; diff --git a/docs/snippets/all/archetypes/image_column_updates.cpp b/docs/snippets/all/archetypes/image_column_updates.cpp index 93a9046b215c..3afe566674bb 100644 --- a/docs/snippets/all/archetypes/image_column_updates.cpp +++ b/docs/snippets/all/archetypes/image_column_updates.cpp @@ -38,9 +38,12 @@ int main(int argc, char* argv[]) { // Split up the image data into several components referencing the underlying data. const size_t image_size_in_bytes = width * height * 3; - std::vector image_data(times.size()); + std::vector image_data(times.size()); for (size_t i = 0; i < times.size(); ++i) { - image_data[i] = rerun::borrow(images.data() + i * image_size_in_bytes, image_size_in_bytes); + image_data[i] = rerun::borrow( + images.data() + i * image_size_in_bytes, + image_size_in_bytes + ); } // Send all images at once. diff --git a/docs/snippets/all/archetypes/image_column_updates.py b/docs/snippets/all/archetypes/image_column_updates.py index 957c3354dd73..aa5b85d7c2bb 100644 --- a/docs/snippets/all/archetypes/image_column_updates.py +++ b/docs/snippets/all/archetypes/image_column_updates.py @@ -1,7 +1,8 @@ """ Update an image over time, in a single operation. -This is semantically equivalent to the `image_row_updates` example, albeit much faster. +This is semantically equivalent to the `image_row_updates` example, +albeit much faster. """ import numpy as np @@ -21,7 +22,9 @@ images[t, 50:150, (t * 10) : (t * 10 + 100), 1] = 255 # Log the ImageFormat and indicator once, as static. -format = rr.components.ImageFormat(width=width, height=height, color_model="RGB", channel_datatype="U8") +format = rr.components.ImageFormat( + width=width, height=height, color_model="RGB", channel_datatype="U8" +) rr.log("images", rr.Image.from_fields(format=format), static=True) # Send all images at once. @@ -30,7 +33,10 @@ indexes=[rr.TimeColumn("step", sequence=times)], # Reshape the images so `Image` can tell that this is several blobs. # - # Note that the `Image` consumes arrays of bytes, so we should ensure that we take a - # uint8 view of it. This way, this also works when working with datatypes other than `U8`. - columns=rr.Image.columns(buffer=images.view(np.uint8).reshape(len(times), -1)), + # Note that the `Image` consumes arrays of bytes, so we should ensure + # that we take a uint8 view of it. This way, this also works when + # working with datatypes other than `U8`. + columns=rr.Image.columns( + buffer=images.view(np.uint8).reshape(len(times), -1) + ), ) diff --git a/docs/snippets/all/archetypes/image_column_updates.rs b/docs/snippets/all/archetypes/image_column_updates.rs index 1a48deefbefc..84b9106abd6b 100644 --- a/docs/snippets/all/archetypes/image_column_updates.rs +++ b/docs/snippets/all/archetypes/image_column_updates.rs @@ -5,7 +5,10 @@ use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_image_column_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_image_column_updates", + ) + .spawn()?; // Timeline on which the images are distributed. let times = (0..20).collect::>(); @@ -25,12 +28,17 @@ fn main() -> Result<(), Box> { } // Log the ImageFormat and indicator once, as static. - let format = rerun::components::ImageFormat::rgb8([width as _, height as _]); - rec.log_static("images", &rerun::Image::update_fields().with_format(format))?; + let format = + rerun::components::ImageFormat::rgb8([width as _, height as _]); + rec.log_static( + "images", + &rerun::Image::update_fields().with_format(format), + )?; // Split up the image data into several components referencing the underlying data. let image_size_in_bytes = width * height * 3; - let timeline_values = rerun::TimeColumn::new_sequence("step", times.clone()); + let timeline_values = + rerun::TimeColumn::new_sequence("step", times.clone()); let buffer = images.into_raw_vec_and_offset().0; rec.send_columns( "images", diff --git a/docs/snippets/all/archetypes/image_formats.cpp b/docs/snippets/all/archetypes/image_formats.cpp index 48cadf3e08b8..f29601fc73f2 100644 --- a/docs/snippets/all/archetypes/image_formats.cpp +++ b/docs/snippets/all/archetypes/image_formats.cpp @@ -13,7 +13,8 @@ int main(int argc, char* argv[]) { for (size_t y = 0; y < 256; ++y) { for (size_t x = 0; x < 256; ++x) { image[(y * 256 + x) * 3 + 0] = static_cast(x); - image[(y * 256 + x) * 3 + 1] = static_cast(std::min(255, x + y)); + image[(y * 256 + x) * 3 + 1] = + static_cast(std::min(255, x + y)); image[(y * 256 + x) * 3 + 2] = static_cast(y); } } @@ -28,7 +29,11 @@ int main(int argc, char* argv[]) { } rec.log( "image_green_only", - rerun::Image(rerun::borrow(green_channel), {256, 256}, rerun::ColorModel::L) + rerun::Image( + rerun::borrow(green_channel), + {256, 256}, + rerun::ColorModel::L + ) ); // BGR image @@ -40,24 +45,38 @@ int main(int argc, char* argv[]) { } rec.log( "image_bgr", - rerun::Image(rerun::borrow(bgr_image), {256, 256}, rerun::ColorModel::BGR) + rerun::Image( + rerun::borrow(bgr_image), + {256, 256}, + rerun::ColorModel::BGR + ) ); // New image with Separate Y/U/V planes with 4:2:2 chroma downsampling std::vector yuv_bytes(256 * 256 + 128 * 256 * 2); - std::fill_n(yuv_bytes.begin(), 256 * 256, static_cast(128)); // Fixed value for Y + std::fill_n( + yuv_bytes.begin(), + 256 * 256, + static_cast(128) // Fixed value for Y + ); size_t u_plane_offset = 256 * 256; size_t v_plane_offset = u_plane_offset + 128 * 256; for (size_t y = 0; y < 256; ++y) { for (size_t x = 0; x < 128; ++x) { auto coord = y * 128 + x; - yuv_bytes[u_plane_offset + coord] = static_cast(x * 2); // Gradient for U - yuv_bytes[v_plane_offset + coord] = static_cast(y); // Gradient for V + yuv_bytes[u_plane_offset + coord] = + static_cast(x * 2); // Gradient for U + yuv_bytes[v_plane_offset + coord] = + static_cast(y); // Gradient for V } } rec.log( "image_yuv422", - rerun::Image(rerun::borrow(yuv_bytes), {256, 256}, rerun::PixelFormat::Y_U_V16_FullRange) + rerun::Image( + rerun::borrow(yuv_bytes), + {256, 256}, + rerun::PixelFormat::Y_U_V16_FullRange + ) ); return 0; diff --git a/docs/snippets/all/archetypes/image_formats.py b/docs/snippets/all/archetypes/image_formats.py index c8371828de54..9a1835fd3dbc 100644 --- a/docs/snippets/all/archetypes/image_formats.py +++ b/docs/snippets/all/archetypes/image_formats.py @@ -7,13 +7,28 @@ rr.init("rerun_example_image_formats", spawn=True) # Simple gradient image, logged in different formats. -image = np.array([[[x, min(255, x + y), y] for x in range(256)] for y in range(256)], dtype=np.uint8) +image = np.array( + [[[x, min(255, x + y), y] for x in range(256)] for y in range(256)], + dtype=np.uint8, +) rr.log("image_rgb", rr.Image(image)) -rr.log("image_green_only", rr.Image(image[:, :, 1], color_model="l")) # Luminance only +rr.log( + "image_green_only", rr.Image(image[:, :, 1], color_model="l") +) # Luminance only rr.log("image_bgr", rr.Image(image[:, :, ::-1], color_model="bgr")) # BGR # New image with Separate Y/U/V planes with 4:2:2 chroma downsampling y = bytes([128 for y in range(256) for x in range(256)]) -u = bytes([x * 2 for y in range(256) for x in range(128)]) # Half horizontal resolution for chroma. +u = bytes([ + x * 2 for y in range(256) for x in range(128) +]) # Half horizontal resolution for chroma. v = bytes([y for y in range(256) for x in range(128)]) -rr.log("image_yuv422", rr.Image(bytes=y + u + v, width=256, height=256, pixel_format=rr.PixelFormat.Y_U_V16_FullRange)) +rr.log( + "image_yuv422", + rr.Image( + bytes=y + u + v, + width=256, + height=256, + pixel_format=rr.PixelFormat.Y_U_V16_FullRange, + ), +) diff --git a/docs/snippets/all/archetypes/image_formats.rs b/docs/snippets/all/archetypes/image_formats.rs index d82df9f76d59..9d2f61a03c9f 100644 --- a/docs/snippets/all/archetypes/image_formats.rs +++ b/docs/snippets/all/archetypes/image_formats.rs @@ -1,20 +1,25 @@ use rerun::external::ndarray; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_image_formats").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_image_formats") + .spawn()?; // Simple gradient image - let image = ndarray::Array3::from_shape_fn((256, 256, 3), |(y, x, c)| match c { - 0 => x as u8, - 1 => (x + y).min(255) as u8, - 2 => y as u8, - _ => unreachable!(), - }); + let image = + ndarray::Array3::from_shape_fn((256, 256, 3), |(y, x, c)| match c { + 0 => x as u8, + 1 => (x + y).min(255) as u8, + 2 => y as u8, + _ => unreachable!(), + }); // RGB image rec.log( "image_rgb", - &rerun::Image::from_color_model_and_tensor(rerun::ColorModel::RGB, image.clone())?, + &rerun::Image::from_color_model_and_tensor( + rerun::ColorModel::RGB, + image.clone(), + )?, )?; // Green channel only (Luminance) diff --git a/docs/snippets/all/archetypes/image_row_updates.py b/docs/snippets/all/archetypes/image_row_updates.py index c113b5ec5674..1446d848c4a2 100644 --- a/docs/snippets/all/archetypes/image_row_updates.py +++ b/docs/snippets/all/archetypes/image_row_updates.py @@ -1,7 +1,8 @@ """ Update an image over time. -See also the `image_column_updates` example, which achieves the same thing in a single operation. +See also the `image_column_updates` example, which achieves the same +thing in a single operation. """ import numpy as np diff --git a/docs/snippets/all/archetypes/image_row_updates.rs b/docs/snippets/all/archetypes/image_row_updates.rs index 81ce816685d7..81129740e92d 100644 --- a/docs/snippets/all/archetypes/image_row_updates.rs +++ b/docs/snippets/all/archetypes/image_row_updates.rs @@ -5,7 +5,9 @@ use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_image_row_updates").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_image_row_updates") + .spawn()?; for t in 0..20 { rec.set_time_sequence("time", t); @@ -18,7 +20,10 @@ fn main() -> Result<(), Box> { rec.log( "image", - &rerun::Image::from_color_model_and_tensor(rerun::ColorModel::RGB, image)?, + &rerun::Image::from_color_model_and_tensor( + rerun::ColorModel::RGB, + image, + )?, )?; } diff --git a/docs/snippets/all/archetypes/image_simple.rs b/docs/snippets/all/archetypes/image_simple.rs index 6ae666f4d6b3..a66b081ab2a0 100644 --- a/docs/snippets/all/archetypes/image_simple.rs +++ b/docs/snippets/all/archetypes/image_simple.rs @@ -3,7 +3,8 @@ use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_image").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_image").spawn()?; let mut image = Array::::zeros((200, 300, 3).f()); image.slice_mut(s![.., .., 0]).fill(255); @@ -12,7 +13,10 @@ fn main() -> Result<(), Box> { rec.log( "image", - &rerun::Image::from_color_model_and_tensor(rerun::ColorModel::RGB, image)?, + &rerun::Image::from_color_model_and_tensor( + rerun::ColorModel::RGB, + image, + )?, )?; Ok(()) diff --git a/docs/snippets/all/archetypes/instance_poses3d_combined.cpp b/docs/snippets/all/archetypes/instance_poses3d_combined.cpp index 8c8957c2d676..4c65520b26e7 100644 --- a/docs/snippets/all/archetypes/instance_poses3d_combined.cpp +++ b/docs/snippets/all/archetypes/instance_poses3d_combined.cpp @@ -4,14 +4,17 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_instance_pose3d_combined"); + const auto rec = + rerun::RecordingStream("rerun_example_instance_pose3d_combined"); rec.set_time_sequence("frame", 0); // Log a box and points further down in the hierarchy. rec.log("world/box", rerun::Boxes3D::from_half_sizes({{1.0, 1.0, 1.0}})); rec.log( "world/box/points", - rerun::Points3D(rerun::demo::grid3d(-10.0f, 10.0f, 10)) + rerun::Points3D( + rerun::demo::grid3d(-10.0f, 10.0f, 10) + ) ); for (int i = 0; i < 180; ++i) { @@ -29,7 +32,9 @@ int main(int argc, char* argv[]) { rec.log( "world/box", rerun::InstancePoses3D().with_translations( - {{0.0f, 0.0f, std::abs(static_cast(i) * 0.1f - 5.0f) - 5.0f}} + {{0.0f, + 0.0f, + std::abs(static_cast(i) * 0.1f - 5.0f) - 5.0f}} ) ); } diff --git a/docs/snippets/all/archetypes/instance_poses3d_combined.py b/docs/snippets/all/archetypes/instance_poses3d_combined.py index 523fd274318e..918404d58c32 100644 --- a/docs/snippets/all/archetypes/instance_poses3d_combined.py +++ b/docs/snippets/all/archetypes/instance_poses3d_combined.py @@ -19,7 +19,17 @@ rr.set_time("frame", sequence=i) # Log a regular transform which affects both the box and the points. - rr.log("world/box", rr.Transform3D(rotation_axis_angle=rr.RotationAxisAngle([0, 0, 1], angle=rr.Angle(deg=i * 2)))) + rr.log( + "world/box", + rr.Transform3D( + rotation_axis_angle=rr.RotationAxisAngle( + [0, 0, 1], angle=rr.Angle(deg=i * 2) + ) + ), + ) # Log an instance pose which affects only the box. - rr.log("world/box", rr.InstancePoses3D(translations=[0, 0, abs(i * 0.1 - 5.0) - 5.0])) + rr.log( + "world/box", + rr.InstancePoses3D(translations=[0, 0, abs(i * 0.1 - 5.0) - 5.0]), + ) diff --git a/docs/snippets/all/archetypes/instance_poses3d_combined.rs b/docs/snippets/all/archetypes/instance_poses3d_combined.rs index 519092e7f58a..04fdba02c9e6 100644 --- a/docs/snippets/all/archetypes/instance_poses3d_combined.rs +++ b/docs/snippets/all/archetypes/instance_poses3d_combined.rs @@ -6,8 +6,10 @@ use rerun::{ }; fn main() -> anyhow::Result<()> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_instance_pose3d_combined").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_instance_pose3d_combined", + ) + .spawn()?; rec.set_time_sequence("frame", 0); @@ -18,7 +20,11 @@ fn main() -> anyhow::Result<()> { )?; rec.log( "world/box/points", - &rerun::Points3D::new(grid(glam::Vec3::splat(-10.0), glam::Vec3::splat(10.0), 10)), + &rerun::Points3D::new(grid( + glam::Vec3::splat(-10.0), + glam::Vec3::splat(10.0), + 10, + )), )?; for i in 0..180 { diff --git a/docs/snippets/all/archetypes/line_strips2d_batch.cpp b/docs/snippets/all/archetypes/line_strips2d_batch.cpp index ce9ebba8ca5f..d54a71f2f5b9 100644 --- a/docs/snippets/all/archetypes/line_strips2d_batch.cpp +++ b/docs/snippets/all/archetypes/line_strips2d_batch.cpp @@ -8,9 +8,19 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_line_strip2d_batch"); rec.spawn().exit_on_failure(); - rerun::Collection strip1 = {{0.f, 0.f}, {2.f, 1.f}, {4.f, -1.f}, {6.f, 0.f}}; - rerun::Collection strip2 = - {{0.f, 3.f}, {1.f, 4.f}, {2.f, 2.f}, {3.f, 4.f}, {4.f, 2.f}, {5.f, 4.f}, {6.f, 3.f}}; + rerun::Collection strip1 = { + {0.f, 0.f}, + {2.f, 1.f}, + {4.f, -1.f}, + {6.f, 0.f}}; + rerun::Collection strip2 = { + {0.f, 3.f}, + {1.f, 4.f}, + {2.f, 2.f}, + {3.f, 4.f}, + {4.f, 2.f}, + {5.f, 4.f}, + {6.f, 3.f}}; rec.log( "strips", rerun::LineStrips2D({strip1, strip2}) diff --git a/docs/snippets/all/archetypes/line_strips2d_batch.py b/docs/snippets/all/archetypes/line_strips2d_batch.py index c13d51010326..ae887536560e 100644 --- a/docs/snippets/all/archetypes/line_strips2d_batch.py +++ b/docs/snippets/all/archetypes/line_strips2d_batch.py @@ -19,4 +19,8 @@ ) # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 7], y_range=[-3, 6]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 7], y_range=[-3, 6]) + ) +) diff --git a/docs/snippets/all/archetypes/line_strips2d_batch.rs b/docs/snippets/all/archetypes/line_strips2d_batch.rs index 2425ecea2a45..fda767a0f429 100644 --- a/docs/snippets/all/archetypes/line_strips2d_batch.rs +++ b/docs/snippets/all/archetypes/line_strips2d_batch.rs @@ -1,7 +1,9 @@ //! Log a batch of 2D line strips. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_strip2d_batch").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_line_strip2d_batch") + .spawn()?; let strip1 = [[0., 0.], [2., 1.], [4., -1.], [6., 0.]]; #[rustfmt::skip] diff --git a/docs/snippets/all/archetypes/line_strips2d_segments_simple.py b/docs/snippets/all/archetypes/line_strips2d_segments_simple.py index 12c60638ff69..ab424c33df0b 100644 --- a/docs/snippets/all/archetypes/line_strips2d_segments_simple.py +++ b/docs/snippets/all/archetypes/line_strips2d_segments_simple.py @@ -13,4 +13,8 @@ ) # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 7], y_range=[-3, 3]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 7], y_range=[-3, 3]) + ) +) diff --git a/docs/snippets/all/archetypes/line_strips2d_segments_simple.rs b/docs/snippets/all/archetypes/line_strips2d_segments_simple.rs index c926a0a30144..0cb38b73dd07 100644 --- a/docs/snippets/all/archetypes/line_strips2d_segments_simple.rs +++ b/docs/snippets/all/archetypes/line_strips2d_segments_simple.rs @@ -1,7 +1,9 @@ //! Log a couple 2D line segments using 2D line strips. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_segments2d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_line_segments2d") + .spawn()?; let points = [[0., 0.], [2., 1.], [4., -1.], [6., 0.]]; rec.log("segments", &rerun::LineStrips2D::new(points.chunks(2)))?; diff --git a/docs/snippets/all/archetypes/line_strips2d_simple.cpp b/docs/snippets/all/archetypes/line_strips2d_simple.cpp index 734833e31751..f1ef73dd4721 100644 --- a/docs/snippets/all/archetypes/line_strips2d_simple.cpp +++ b/docs/snippets/all/archetypes/line_strips2d_simple.cpp @@ -6,7 +6,8 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_line_strip2d"); rec.spawn().exit_on_failure(); - const auto strip = rerun::LineStrip2D({{0.f, 0.f}, {2.f, 1.f}, {4.f, -1.f}, {6.f, 0.f}}); + const auto strip = + rerun::LineStrip2D({{0.f, 0.f}, {2.f, 1.f}, {4.f, -1.f}, {6.f, 0.f}}); rec.log("strip", rerun::LineStrips2D(strip)); // TODO(#5520): log VisualBounds2D diff --git a/docs/snippets/all/archetypes/line_strips2d_simple.py b/docs/snippets/all/archetypes/line_strips2d_simple.py index 9c536d7bd9c4..08f2f72cd32c 100644 --- a/docs/snippets/all/archetypes/line_strips2d_simple.py +++ b/docs/snippets/all/archetypes/line_strips2d_simple.py @@ -11,4 +11,8 @@ ) # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 7], y_range=[-3, 3]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 7], y_range=[-3, 3]) + ) +) diff --git a/docs/snippets/all/archetypes/line_strips2d_simple.rs b/docs/snippets/all/archetypes/line_strips2d_simple.rs index 589ce8ae00fd..7f4c9b3533e7 100644 --- a/docs/snippets/all/archetypes/line_strips2d_simple.rs +++ b/docs/snippets/all/archetypes/line_strips2d_simple.rs @@ -1,7 +1,8 @@ //! Log a simple line strip. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_strip2d").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_strip2d") + .spawn()?; let points = [[0., 0.], [2., 1.], [4., -1.], [6., 0.]]; rec.log("strip", &rerun::LineStrips2D::new([points]))?; diff --git a/docs/snippets/all/archetypes/line_strips2d_ui_radius.cpp b/docs/snippets/all/archetypes/line_strips2d_ui_radius.cpp index 55eab1adf4cc..34ed0d795ed5 100644 --- a/docs/snippets/all/archetypes/line_strips2d_ui_radius.cpp +++ b/docs/snippets/all/archetypes/line_strips2d_ui_radius.cpp @@ -3,11 +3,14 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_line_strip2d_ui_radius"); + const auto rec = + rerun::RecordingStream("rerun_example_line_strip2d_ui_radius"); rec.spawn().exit_on_failure(); // A blue line with a scene unit radii of 0.01. - rerun::LineStrip2D linestrip_blue({{0.f, 0.f}, {0.f, 1.f}, {1.f, 0.f}, {1.f, 1.f}}); + rerun::LineStrip2D linestrip_blue( + {{0.f, 0.f}, {0.f, 1.f}, {1.f, 0.f}, {1.f, 1.f}} + ); rec.log( "scene_unit_line", rerun::LineStrips2D(linestrip_blue) @@ -19,7 +22,9 @@ int main(int argc, char* argv[]) { // A red line with a ui point radii of 5. // UI points are independent of zooming in Views, but are sensitive to the application UI scaling. // For 100 % ui scaling, UI points are equal to pixels. - rerun::LineStrip2D linestrip_red({{3.f, 0.f}, {3.f, 1.f}, {4.f, 0.f}, {4.f, 1.f}}); + rerun::LineStrip2D linestrip_red( + {{3.f, 0.f}, {3.f, 1.f}, {4.f, 0.f}, {4.f, 1.f}} + ); rec.log( "ui_points_line", rerun::LineStrips2D(linestrip_red) diff --git a/docs/snippets/all/archetypes/line_strips2d_ui_radius.py b/docs/snippets/all/archetypes/line_strips2d_ui_radius.py index 5c920213436b..c86e37687a9a 100644 --- a/docs/snippets/all/archetypes/line_strips2d_ui_radius.py +++ b/docs/snippets/all/archetypes/line_strips2d_ui_radius.py @@ -18,18 +18,24 @@ ) # A red line with a ui point radii of 5. -# UI points are independent of zooming in Views, but are sensitive to the application UI scaling. +# UI points are independent of zooming in Views, but are sensitive to the +# application UI scaling. # For 100% ui scaling, UI points are equal to pixels. points = [[3, 0], [3, 1], [4, 0], [4, 1]] rr.log( "ui_points_line", rr.LineStrips2D( [points], - # rr.Radius.ui_points produces radii that the viewer interprets as given in ui points. + # rr.Radius.ui_points produces radii that the viewer interprets + # as given in ui points. radii=rr.Radius.ui_points(5.0), colors=[255, 0, 0], ), ) # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 5], y_range=[-1, 2]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 5], y_range=[-1, 2]) + ) +) diff --git a/docs/snippets/all/archetypes/line_strips2d_ui_radius.rs b/docs/snippets/all/archetypes/line_strips2d_ui_radius.rs index 5db9f5548728..309418c302aa 100644 --- a/docs/snippets/all/archetypes/line_strips2d_ui_radius.rs +++ b/docs/snippets/all/archetypes/line_strips2d_ui_radius.rs @@ -1,7 +1,10 @@ //! Log lines with ui points & scene unit radii. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_strip2d_ui_radius").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_line_strip2d_ui_radius", + ) + .spawn()?; // A blue line with a scene unit radii of 0.01. let points = [[0., 0.], [0., 1.], [1., 0.], [1., 1.]]; diff --git a/docs/snippets/all/archetypes/line_strips3d_batch.rs b/docs/snippets/all/archetypes/line_strips3d_batch.rs index 42ea0620ef69..7581a75e0bab 100644 --- a/docs/snippets/all/archetypes/line_strips3d_batch.rs +++ b/docs/snippets/all/archetypes/line_strips3d_batch.rs @@ -1,7 +1,9 @@ //! Log a batch of 2D line strips. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_strip3d_batch").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_line_strip3d_batch") + .spawn()?; let strip1 = [[0., 0., 2.], [1., 0., 2.], [1., 1., 2.], [0., 1., 2.]]; let strip2 = [ diff --git a/docs/snippets/all/archetypes/line_strips3d_segments_simple.rs b/docs/snippets/all/archetypes/line_strips3d_segments_simple.rs index f0ac34096a4e..4f9a5d67963a 100644 --- a/docs/snippets/all/archetypes/line_strips3d_segments_simple.rs +++ b/docs/snippets/all/archetypes/line_strips3d_segments_simple.rs @@ -1,7 +1,9 @@ //! Log a simple set of line segments. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_segments3d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_line_segments3d") + .spawn()?; let points = [ [0., 0., 0.], diff --git a/docs/snippets/all/archetypes/line_strips3d_simple.rs b/docs/snippets/all/archetypes/line_strips3d_simple.rs index 9c9e839f5cc6..c3f05ee1bc17 100644 --- a/docs/snippets/all/archetypes/line_strips3d_simple.rs +++ b/docs/snippets/all/archetypes/line_strips3d_simple.rs @@ -1,7 +1,8 @@ //! Log a simple line strip. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_strip3d").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_strip3d") + .spawn()?; let points = [ [0., 0., 0.], diff --git a/docs/snippets/all/archetypes/line_strips3d_time_window.py b/docs/snippets/all/archetypes/line_strips3d_time_window.py new file mode 100644 index 000000000000..50002564fa5b --- /dev/null +++ b/docs/snippets/all/archetypes/line_strips3d_time_window.py @@ -0,0 +1,46 @@ +"""Log line strips over time and view a sliding window (e.g. trajectories).""" + +import math + +import rerun as rr +import rerun.blueprint as rrb + + +def point(t: float, phase: float) -> list[float]: + # Sample a point on a helix. + angle = 0.5 * t + phase + return [math.cos(angle), math.sin(angle), 0.1 * t] + + +rr.init("rerun_example_line_strips3d_time_window", spawn=True) + +# Configure the visible time range in the blueprint. +# You can also override this per entity. +rr.send_blueprint( + rrb.Spatial3DView( + origin="/", + time_ranges=rrb.VisibleTimeRange( + "time", + start=rrb.TimeRangeBoundary.cursor_relative(seconds=-5.0), + end=rrb.TimeRangeBoundary.cursor_relative(), + ), + ) +) + +# Log the line strip increments with timestamps. +for i in range(600): + t0 = i / 30.0 + t1 = (i + 1) / 30.0 + + rr.set_time("time", duration=t1) + rr.log( + "trails", + rr.LineStrips3D( + [ + [point(t0, 0.0), point(t1, 0.0)], + [point(t0, math.pi), point(t1, math.pi)], + ], + colors=[[255, 120, 0], [0, 180, 255]], + radii=0.02, + ), + ) diff --git a/docs/snippets/all/archetypes/line_strips3d_ui_radius.cpp b/docs/snippets/all/archetypes/line_strips3d_ui_radius.cpp index 30ca03c266ec..f6eba0ea6544 100644 --- a/docs/snippets/all/archetypes/line_strips3d_ui_radius.cpp +++ b/docs/snippets/all/archetypes/line_strips3d_ui_radius.cpp @@ -3,7 +3,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_line_strip3d_ui_radius"); + const auto rec = + rerun::RecordingStream("rerun_example_line_strip3d_ui_radius"); rec.spawn().exit_on_failure(); // A blue line with a scene unit radii of 0.01. diff --git a/docs/snippets/all/archetypes/line_strips3d_ui_radius.py b/docs/snippets/all/archetypes/line_strips3d_ui_radius.py index 1b69cdcfe95d..e2b25e5540c1 100644 --- a/docs/snippets/all/archetypes/line_strips3d_ui_radius.py +++ b/docs/snippets/all/archetypes/line_strips3d_ui_radius.py @@ -17,14 +17,16 @@ ) # A red line with a ui point radii of 5. -# UI points are independent of zooming in Views, but are sensitive to the application UI scaling. +# UI points are independent of zooming in Views, but are sensitive to the +# application UI scaling. # For 100% ui scaling, UI points are equal to pixels. points = [[3, 0, 0], [3, 0, 1], [4, 0, 0], [4, 0, 1]] rr.log( "ui_points_line", rr.LineStrips3D( [points], - # rr.Radius.ui_points produces radii that the viewer interprets as given in ui points. + # rr.Radius.ui_points produces radii that the viewer interprets + # as given in ui points. radii=rr.Radius.ui_points(5.0), colors=[255, 0, 0], ), diff --git a/docs/snippets/all/archetypes/line_strips3d_ui_radius.rs b/docs/snippets/all/archetypes/line_strips3d_ui_radius.rs index 6e3d5594457e..826d464c22d0 100644 --- a/docs/snippets/all/archetypes/line_strips3d_ui_radius.rs +++ b/docs/snippets/all/archetypes/line_strips3d_ui_radius.rs @@ -1,7 +1,10 @@ //! Log lines with ui points & scene unit radii. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_line_strip3d_ui_radius").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_line_strip3d_ui_radius", + ) + .spawn()?; // A blue line with a scene unit radii of 0.01. let points = [[0., 0., 0.], [0., 0., 1.], [1., 0., 0.], [1., 0., 1.]]; diff --git a/docs/snippets/all/archetypes/mcap_channel_simple.cpp b/docs/snippets/all/archetypes/mcap_channel_simple.cpp index dd36866e80f9..a5f5e1a64678 100644 --- a/docs/snippets/all/archetypes/mcap_channel_simple.cpp +++ b/docs/snippets/all/archetypes/mcap_channel_simple.cpp @@ -14,6 +14,6 @@ int main(int argc, char* argv[]) { rec.log( "mcap/channels/camera", rerun::archetypes::McapChannel(1, "/camera/image", "cdr") - .with_metadata(rerun::components::KeyValuePairs(metadata)) + .with_metadata(rerun::KeyValuePairs(metadata)) ); } diff --git a/docs/snippets/all/archetypes/mcap_channel_simple.rs b/docs/snippets/all/archetypes/mcap_channel_simple.rs index 3b6230c8c0ec..67d3149bd8f5 100644 --- a/docs/snippets/all/archetypes/mcap_channel_simple.rs +++ b/docs/snippets/all/archetypes/mcap_channel_simple.rs @@ -1,7 +1,8 @@ //! Log a simple MCAP channel definition. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_mcap_channel").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_mcap_channel") + .spawn()?; rec.log( "mcap/channels/camera", diff --git a/docs/snippets/all/archetypes/mcap_message_simple.py b/docs/snippets/all/archetypes/mcap_message_simple.py index e2c0a2f43b7b..c7f49253422d 100644 --- a/docs/snippets/all/archetypes/mcap_message_simple.py +++ b/docs/snippets/all/archetypes/mcap_message_simple.py @@ -6,7 +6,9 @@ # Example binary message data (could be from a ROS message, protobuf, etc.) # This represents a simple sensor reading encoded as bytes -sensor_data = b"sensor_reading: temperature=23.5, humidity=65.2, timestamp=1743465600" +sensor_data = ( + b"sensor_reading: temperature=23.5, humidity=65.2, timestamp=1743465600" +) rr.log( "mcap/messages/sensor_reading", diff --git a/docs/snippets/all/archetypes/mcap_message_simple.rs b/docs/snippets/all/archetypes/mcap_message_simple.rs index 539faf53b81e..ad261baeb84c 100644 --- a/docs/snippets/all/archetypes/mcap_message_simple.rs +++ b/docs/snippets/all/archetypes/mcap_message_simple.rs @@ -1,11 +1,13 @@ //! Log a simple MCAP message with binary data. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_mcap_message").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_mcap_message") + .spawn()?; // Example binary message data (could be from a ROS message, protobuf, etc.) // This represents a simple sensor reading encoded as bytes - let sensor_data = "sensor_reading: temperature=23.5, humidity=65.2, timestamp=1743465600"; + let sensor_data = + "sensor_reading: temperature=23.5, humidity=65.2, timestamp=1743465600"; rec.log( "mcap/messages/sensor_reading", diff --git a/docs/snippets/all/archetypes/mcap_schema_simple.rs b/docs/snippets/all/archetypes/mcap_schema_simple.rs index ec8b74fc9b0d..76dfc1a2216e 100644 --- a/docs/snippets/all/archetypes/mcap_schema_simple.rs +++ b/docs/snippets/all/archetypes/mcap_schema_simple.rs @@ -1,7 +1,8 @@ //! Log a simple MCAP schema definition. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_mcap_schema").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_mcap_schema") + .spawn()?; // Example ROS2 message definition for a simple Point message let point_schema = "float64 x\nfloat64 y\nfloat64 z"; diff --git a/docs/snippets/all/archetypes/mcap_statistics_simple.cpp b/docs/snippets/all/archetypes/mcap_statistics_simple.cpp index 362926fd67ed..ca03ddfd2dbc 100644 --- a/docs/snippets/all/archetypes/mcap_statistics_simple.cpp +++ b/docs/snippets/all/archetypes/mcap_statistics_simple.cpp @@ -15,7 +15,8 @@ int main(int argc, char* argv[]) { .with_attachment_count(2) .with_metadata_count(8) .with_chunk_count(25) - .with_message_start_time(1743465600000000000) // 2024-04-01 00:00:00 UTC in nanoseconds + .with_message_start_time(1743465600000000000 + ) // 2024-04-01 00:00:00 UTC in nanoseconds .with_message_end_time( 1743466200000000000 // 2024-04-01 00:10:00 UTC in nanoseconds (10 minute recording) ) diff --git a/docs/snippets/all/archetypes/mcap_statistics_simple.py b/docs/snippets/all/archetypes/mcap_statistics_simple.py index 1cb319f85551..fb18b92184bc 100644 --- a/docs/snippets/all/archetypes/mcap_statistics_simple.py +++ b/docs/snippets/all/archetypes/mcap_statistics_simple.py @@ -13,7 +13,9 @@ attachment_count=2, metadata_count=8, chunk_count=25, - message_start_time=1743465600000000000, # 2024-04-01 00:00:00 UTC in nanoseconds - message_end_time=1743466200000000000, # 2024-04-01 00:10:00 UTC in nanoseconds (10 minute recording) + # 2024-04-01 00:00:00 UTC in nanoseconds + message_start_time=1743465600000000000, + # 2024-04-01 00:10:00 UTC in nanoseconds (10 minute recording) + message_end_time=1743466200000000000, ), ) diff --git a/docs/snippets/all/archetypes/mcap_statistics_simple.rs b/docs/snippets/all/archetypes/mcap_statistics_simple.rs index 5207702d80b9..954e8c80f007 100644 --- a/docs/snippets/all/archetypes/mcap_statistics_simple.rs +++ b/docs/snippets/all/archetypes/mcap_statistics_simple.rs @@ -1,7 +1,9 @@ //! Log simple MCAP recording statistics. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_mcap_statistics").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_mcap_statistics") + .spawn()?; rec.log( "mcap/statistics/recording_overview", diff --git a/docs/snippets/all/archetypes/mesh3d_indexed.rs b/docs/snippets/all/archetypes/mesh3d_indexed.rs index 62c85f8e1d57..d6ed6ae1f264 100644 --- a/docs/snippets/all/archetypes/mesh3d_indexed.rs +++ b/docs/snippets/all/archetypes/mesh3d_indexed.rs @@ -1,14 +1,20 @@ //! Log a simple colored triangle with indexed drawing. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_mesh3d_indexed").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_mesh3d_indexed") + .spawn()?; rec.log( "triangle", - &rerun::Mesh3D::new([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) - .with_vertex_normals([[0.0, 0.0, 1.0]]) - .with_vertex_colors([0x0000FFFF, 0x00FF00FF, 0xFF0000FF]) - .with_triangle_indices([[2, 1, 0]]), + &rerun::Mesh3D::new([ + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ]) + .with_vertex_normals([[0.0, 0.0, 1.0]]) + .with_vertex_colors([0x0000FFFF, 0x00FF00FF, 0xFF0000FF]) + .with_triangle_indices([[2, 1, 0]]), )?; Ok(()) diff --git a/docs/snippets/all/archetypes/mesh3d_instancing.cpp b/docs/snippets/all/archetypes/mesh3d_instancing.cpp index 20815c6aaea2..36eeb7a0f630 100644 --- a/docs/snippets/all/archetypes/mesh3d_instancing.cpp +++ b/docs/snippets/all/archetypes/mesh3d_instancing.cpp @@ -9,11 +9,14 @@ int main(int argc, char* argv[]) { rec.set_time_sequence("frame", 0); rec.log( "shape", - rerun::Mesh3D( - {{1.0f, 1.0f, 1.0f}, {-1.0f, -1.0f, 1.0f}, {-1.0f, 1.0f, -1.0f}, {1.0f, -1.0f, -1.0f}} - ) + rerun::Mesh3D({{1.0f, 1.0f, 1.0f}, + {-1.0f, -1.0f, 1.0f}, + {-1.0f, 1.0f, -1.0f}, + {1.0f, -1.0f, -1.0f}}) .with_triangle_indices({{0, 2, 1}, {0, 3, 1}, {0, 3, 2}, {1, 3, 2}}) - .with_vertex_colors({0xFF0000FF, 0x00FF00FF, 0x00000FFFF, 0xFFFF00FF}) + .with_vertex_colors( + {0xFF0000FF, 0x00FF00FF, 0x00000FFFF, 0xFFFF00FF} + ) ); // This box will not be affected by its parent's instance poses! rec.log("shape/box", rerun::Boxes3D::from_half_sizes({{5.0f, 5.0f, 5.0f}})); diff --git a/docs/snippets/all/archetypes/mesh3d_instancing.py b/docs/snippets/all/archetypes/mesh3d_instancing.py index 1ae12c56584e..1e0dbb5be771 100644 --- a/docs/snippets/all/archetypes/mesh3d_instancing.py +++ b/docs/snippets/all/archetypes/mesh3d_instancing.py @@ -1,4 +1,9 @@ -"""Log a simple 3D mesh with several instance pose transforms which instantiate the mesh several times and will not affect its children (known as mesh instancing).""" +""" +Log a simple 3D mesh with several instance pose transforms. + +This instantiate the mesh several times and will not +affect its children. This is known as mesh instancing. +""" import rerun as rr @@ -25,6 +30,8 @@ "shape", rr.InstancePoses3D( translations=[[2, 0, 0], [0, 2, 0], [0, -2, 0], [-2, 0, 0]], - rotation_axis_angles=rr.RotationAxisAngle([0, 0, 1], rr.Angle(deg=i * 2)), + rotation_axis_angles=rr.RotationAxisAngle( + [0, 0, 1], rr.Angle(deg=i * 2) + ), ), ) diff --git a/docs/snippets/all/archetypes/mesh3d_instancing.rs b/docs/snippets/all/archetypes/mesh3d_instancing.rs index a54b4c6d5686..2263579c5592 100644 --- a/docs/snippets/all/archetypes/mesh3d_instancing.rs +++ b/docs/snippets/all/archetypes/mesh3d_instancing.rs @@ -1,7 +1,9 @@ //! Log a simple 3D mesh with several instance pose transforms which instantiate the mesh several times and will not affect its children (known as mesh instancing). fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_mesh3d_instancing").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_mesh3d_instancing") + .spawn()?; rec.set_time_sequence("frame", 0); rec.log( @@ -13,7 +15,12 @@ fn main() -> Result<(), Box> { [1.0, -1.0, -1.0], ]) .with_triangle_indices([[0, 2, 1], [0, 3, 1], [0, 3, 2], [1, 3, 2]]) - .with_vertex_colors([0xFF0000FF, 0x00FF00FF, 0x00000FFFF, 0xFFFF00FF]), + .with_vertex_colors([ + 0xFF0000FF, + 0x00FF00FF, + 0x00000FFFF, + 0xFFFF00FF, + ]), )?; // This box will not be affected by its parent's instance poses! rec.log( diff --git a/docs/snippets/all/archetypes/mesh3d_partial_updates.cpp b/docs/snippets/all/archetypes/mesh3d_partial_updates.cpp index f96ead099435..cc0121fdbb14 100644 --- a/docs/snippets/all/archetypes/mesh3d_partial_updates.cpp +++ b/docs/snippets/all/archetypes/mesh3d_partial_updates.cpp @@ -10,7 +10,8 @@ rerun::Position3D mul_pos(float factor, rerun::Position3D vec) { } int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_mesh3d_partial_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_mesh3d_partial_updates"); rec.spawn().exit_on_failure(); rerun::Position3D vertex_positions[3] = { @@ -45,7 +46,9 @@ int main(int argc, char* argv[]) { }; rec.log( "triangle", - rerun::Mesh3D::update_fields().with_vertex_positions(modified_vertex_positions) + rerun::Mesh3D::update_fields().with_vertex_positions( + modified_vertex_positions + ) ); } } diff --git a/docs/snippets/all/archetypes/mesh3d_partial_updates.py b/docs/snippets/all/archetypes/mesh3d_partial_updates.py index dcbd9f4eeae0..753e17101738 100644 --- a/docs/snippets/all/archetypes/mesh3d_partial_updates.py +++ b/docs/snippets/all/archetypes/mesh3d_partial_updates.py @@ -1,4 +1,4 @@ -"""Log a simple colored triangle, then update its vertices' positions each frame.""" +"""Log a colored triangle, then update its vertices' positions each frame.""" import numpy as np @@ -6,7 +6,9 @@ rr.init("rerun_example_mesh3d_partial_updates", spawn=True) -vertex_positions = np.array([[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=np.float32) +vertex_positions = np.array( + [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=np.float32 +) # Log the initial state of our triangle rr.set_time("frame", sequence=0) @@ -23,4 +25,7 @@ for i in range(1, 300): factor = np.abs(np.sin(i * 0.04)) rr.set_time("frame", sequence=i) - rr.log("triangle", rr.Mesh3D.from_fields(vertex_positions=vertex_positions * factor)) + rr.log( + "triangle", + rr.Mesh3D.from_fields(vertex_positions=vertex_positions * factor), + ) diff --git a/docs/snippets/all/archetypes/mesh3d_partial_updates.rs b/docs/snippets/all/archetypes/mesh3d_partial_updates.rs index b7d9eb14a7be..d8c5cd532fdd 100644 --- a/docs/snippets/all/archetypes/mesh3d_partial_updates.rs +++ b/docs/snippets/all/archetypes/mesh3d_partial_updates.rs @@ -3,7 +3,10 @@ use rerun::external::glam; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_mesh3d_partial_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_mesh3d_partial_updates", + ) + .spawn()?; let vertex_positions = [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]; @@ -28,7 +31,8 @@ fn main() -> Result<(), Box> { ]; rec.log( "triangle", - &rerun::Mesh3D::update_fields().with_vertex_positions(vertex_positions), + &rerun::Mesh3D::update_fields() + .with_vertex_positions(vertex_positions), )?; } diff --git a/docs/snippets/all/archetypes/mesh3d_simple.rs b/docs/snippets/all/archetypes/mesh3d_simple.rs index cdc9a93f8207..e7893d3c58ff 100644 --- a/docs/snippets/all/archetypes/mesh3d_simple.rs +++ b/docs/snippets/all/archetypes/mesh3d_simple.rs @@ -1,13 +1,18 @@ //! Log a simple colored triangle. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_mesh3d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_mesh3d").spawn()?; rec.log( "triangle", - &rerun::Mesh3D::new([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) - .with_vertex_normals([[0.0, 0.0, 1.0]]) - .with_vertex_colors([0xFF0000FF, 0x00FF00FF, 0x0000FFFF]), + &rerun::Mesh3D::new([ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + ]) + .with_vertex_normals([[0.0, 0.0, 1.0]]) + .with_vertex_colors([0xFF0000FF, 0x00FF00FF, 0x0000FFFF]), )?; Ok(()) diff --git a/docs/snippets/all/archetypes/pinhole_perspective.cpp b/docs/snippets/all/archetypes/pinhole_perspective.cpp index 7871f7aba138..139bd9afa322 100644 --- a/docs/snippets/all/archetypes/pinhole_perspective.cpp +++ b/docs/snippets/all/archetypes/pinhole_perspective.cpp @@ -3,7 +3,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_pinhole_perspective"); + const auto rec = + rerun::RecordingStream("rerun_example_pinhole_perspective"); rec.spawn().exit_on_failure(); const float fov_y = 0.7853982f; @@ -19,7 +20,8 @@ int main(int argc, char* argv[]) { rec.log( "world/points", - rerun::Points3D({{0.0f, 0.0f, -0.5f}, {0.1f, 0.1f, -0.5f}, {-0.1f, -0.1f, -0.5f}} + rerun::Points3D( + {{0.0f, 0.0f, -0.5f}, {0.1f, 0.1f, -0.5f}, {-0.1f, -0.1f, -0.5f}} ).with_radii({0.025f}) ); } diff --git a/docs/snippets/all/archetypes/pinhole_perspective.py b/docs/snippets/all/archetypes/pinhole_perspective.py index 37396408e151..5c9a84cf6e9c 100644 --- a/docs/snippets/all/archetypes/pinhole_perspective.py +++ b/docs/snippets/all/archetypes/pinhole_perspective.py @@ -16,4 +16,9 @@ ), ) -rr.log("world/points", rr.Points3D([(0.0, 0.0, -0.5), (0.1, 0.1, -0.5), (-0.1, -0.1, -0.5)], radii=0.025)) +rr.log( + "world/points", + rr.Points3D( + [(0.0, 0.0, -0.5), (0.1, 0.1, -0.5), (-0.1, -0.1, -0.5)], radii=0.025 + ), +) diff --git a/docs/snippets/all/archetypes/pinhole_perspective.rs b/docs/snippets/all/archetypes/pinhole_perspective.rs index d762ca04e5c5..f30686c57d43 100644 --- a/docs/snippets/all/archetypes/pinhole_perspective.rs +++ b/docs/snippets/all/archetypes/pinhole_perspective.rs @@ -1,7 +1,9 @@ //! Logs a point cloud and a perspective camera looking at it. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_pinhole_perspective").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_pinhole_perspective") + .spawn()?; let fov_y = std::f32::consts::FRAC_PI_4; let aspect_ratio = 1.7777778; @@ -16,8 +18,12 @@ fn main() -> Result<(), Box> { rec.log( "world/points", - &rerun::Points3D::new([(0.0, 0.0, -0.5), (0.1, 0.1, -0.5), (-0.1, -0.1, -0.5)]) - .with_radii([0.025]), + &rerun::Points3D::new([ + (0.0, 0.0, -0.5), + (0.1, 0.1, -0.5), + (-0.1, -0.1, -0.5), + ]) + .with_radii([0.025]), )?; Ok(()) diff --git a/docs/snippets/all/archetypes/pinhole_projections.py b/docs/snippets/all/archetypes/pinhole_projections.py index a978f54173c0..d175b696d9a4 100644 --- a/docs/snippets/all/archetypes/pinhole_projections.py +++ b/docs/snippets/all/archetypes/pinhole_projections.py @@ -10,23 +10,39 @@ img_height, img_width = 12, 16 # Create a 3D scene with a camera and an image. -rr.log("world/box", rr.Boxes3D(centers=[0, 0, 0], half_sizes=[1, 1, 1], colors=[255, 0, 0])) +rr.log( + "world/box", + rr.Boxes3D(centers=[0, 0, 0], half_sizes=[1, 1, 1], colors=[255, 0, 0]), +) rr.log( "world/points", rr.Points3D( positions=[(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1)], - colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (255, 0, 255)], + colors=[ + (255, 0, 0), + (0, 255, 0), + (0, 0, 255), + (255, 255, 0), + (255, 0, 255), + ], radii=0.1, ), ) rr.log( "camera", rr.Transform3D(translation=[0, 3, 0]), - rr.Pinhole(width=img_width, height=img_height, focal_length=10, camera_xyz=rr.ViewCoordinates.LEFT_HAND_Z_UP), + rr.Pinhole( + width=img_width, + height=img_height, + focal_length=10, + camera_xyz=rr.ViewCoordinates.LEFT_HAND_Z_UP, + ), ) # Create a simple test image. checkerboard = np.zeros((img_height, img_width, 1), dtype=np.uint8) -checkerboard[(np.arange(img_height)[:, None] + np.arange(img_width)) % 2 == 0] = 255 +checkerboard[ + (np.arange(img_height)[:, None] + np.arange(img_width)) % 2 == 0 +] = 255 rr.log("camera/image", rr.Image(checkerboard)) # Use a blueprint to show both 3D and 2D views side by side. @@ -38,13 +54,15 @@ name="3D Scene", contents=["/**"], overrides={ - # Adjust visual size of camera frustum in 3D view for better visibility. + # Adjust visual size of camera frustum in 3D view for + # better visibility. "camera": rr.Pinhole.from_fields(image_plane_distance=1.0) }, ), # 2D projection from angled camera rrb.Spatial2DView( - origin="camera", # Make sure that the origin is at the camera's path. + # Make sure that the origin is at the camera's path. + origin="camera", name="Camera", contents=["/**"], # Add everything, so 3D objects get projected. ), diff --git a/docs/snippets/all/archetypes/pinhole_simple.cpp b/docs/snippets/all/archetypes/pinhole_simple.cpp index 30e6244be37d..7349cc43eb3c 100644 --- a/docs/snippets/all/archetypes/pinhole_simple.cpp +++ b/docs/snippets/all/archetypes/pinhole_simple.cpp @@ -10,7 +10,10 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_pinhole"); rec.spawn().exit_on_failure(); - rec.log("world/image", rerun::Pinhole::from_focal_length_and_resolution(3.0f, {3.0f, 3.0f})); + rec.log( + "world/image", + rerun::Pinhole::from_focal_length_and_resolution(3.0f, {3.0f, 3.0f}) + ); std::vector random_data(3 * 3 * 3); std::generate(random_data.begin(), random_data.end(), [] { diff --git a/docs/snippets/all/archetypes/pinhole_simple.rs b/docs/snippets/all/archetypes/pinhole_simple.rs index c230725a77e9..d487b3c0a1b1 100644 --- a/docs/snippets/all/archetypes/pinhole_simple.rs +++ b/docs/snippets/all/archetypes/pinhole_simple.rs @@ -4,7 +4,8 @@ use ndarray::{Array, ShapeBuilder as _}; use rand::prelude::*; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_pinhole").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_pinhole").spawn()?; let mut image = Array::::default((3, 3, 3).f()); let mut rng = rand::rngs::SmallRng::seed_from_u64(42); @@ -16,7 +17,10 @@ fn main() -> Result<(), Box> { )?; rec.log( "world/image", - &rerun::Image::from_color_model_and_tensor(rerun::ColorModel::RGB, image)?, + &rerun::Image::from_color_model_and_tensor( + rerun::ColorModel::RGB, + image, + )?, )?; Ok(()) diff --git a/docs/snippets/all/archetypes/points2d_random.cpp b/docs/snippets/all/archetypes/points2d_random.cpp index 63070122e643..abe52c957fd6 100644 --- a/docs/snippets/all/archetypes/points2d_random.cpp +++ b/docs/snippets/all/archetypes/points2d_random.cpp @@ -31,7 +31,10 @@ int main(int argc, char* argv[]) { std::vector radii(10); std::generate(radii.begin(), radii.end(), [&] { return dist_radius(gen); }); - rec.log("random", rerun::Points2D(points2d).with_colors(colors).with_radii(radii)); + rec.log( + "random", + rerun::Points2D(points2d).with_colors(colors).with_radii(radii) + ); // TODO(#5520): log VisualBounds2D } diff --git a/docs/snippets/all/archetypes/points2d_random.py b/docs/snippets/all/archetypes/points2d_random.py index 011c4acfdebd..2dfd0d7fa263 100644 --- a/docs/snippets/all/archetypes/points2d_random.py +++ b/docs/snippets/all/archetypes/points2d_random.py @@ -15,4 +15,8 @@ rr.log("random", rr.Points2D(positions, colors=colors, radii=radii)) # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-4, 4], y_range=[-4, 4]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-4, 4], y_range=[-4, 4]) + ) +) diff --git a/docs/snippets/all/archetypes/points2d_random.rs b/docs/snippets/all/archetypes/points2d_random.rs index ccd356dc3ec5..bde68aac38ec 100644 --- a/docs/snippets/all/archetypes/points2d_random.rs +++ b/docs/snippets/all/archetypes/points2d_random.rs @@ -3,18 +3,22 @@ use rand::prelude::*; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_points2d_random").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_points2d_random") + .spawn()?; let mut rng = rand::rngs::SmallRng::seed_from_u64(42); let dist = rand::distr::Uniform::new(-3., 3.)?; rec.log( "random", - &rerun::Points2D::new((0..10).map(|_| (rng.sample(dist), rng.sample(dist)))) - .with_colors( - (0..10).map(|_| rerun::Color::from_rgb(rng.random(), rng.random(), rng.random())), - ) - .with_radii((0..10).map(|_| rng.random::())), + &rerun::Points2D::new( + (0..10).map(|_| (rng.sample(dist), rng.sample(dist))), + ) + .with_colors((0..10).map(|_| { + rerun::Color::from_rgb(rng.random(), rng.random(), rng.random()) + })) + .with_radii((0..10).map(|_| rng.random::())), )?; // TODO(#5521): log VisualBounds2D diff --git a/docs/snippets/all/archetypes/points2d_simple.py b/docs/snippets/all/archetypes/points2d_simple.py index c26ee8d9355d..9ea8afb647bb 100644 --- a/docs/snippets/all/archetypes/points2d_simple.py +++ b/docs/snippets/all/archetypes/points2d_simple.py @@ -8,4 +8,8 @@ rr.log("points", rr.Points2D([[0, 0], [1, 1]])) # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]) + ) +) diff --git a/docs/snippets/all/archetypes/points2d_simple.rs b/docs/snippets/all/archetypes/points2d_simple.rs index 1df6f6ffdf17..800b2192a173 100644 --- a/docs/snippets/all/archetypes/points2d_simple.rs +++ b/docs/snippets/all/archetypes/points2d_simple.rs @@ -1,7 +1,8 @@ //! Log some very simple points. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_points2d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_points2d").spawn()?; rec.log("points", &rerun::Points2D::new([(0.0, 0.0), (1.0, 1.0)]))?; diff --git a/docs/snippets/all/archetypes/points2d_ui_radius.py b/docs/snippets/all/archetypes/points2d_ui_radius.py index 5017aec2c183..03910986acad 100644 --- a/docs/snippets/all/archetypes/points2d_ui_radius.py +++ b/docs/snippets/all/archetypes/points2d_ui_radius.py @@ -17,17 +17,23 @@ ) # Two red points with ui point radii of 40 and 60. -# UI points are independent of zooming in Views, but are sensitive to the application UI scaling. +# UI points are independent of zooming in Views, but are sensitive to the +# application UI scaling. # For 100% ui scaling, UI points are equal to pixels. rr.log( "ui_points", rr.Points2D( [[1, 0], [1, 1]], - # rr.Radius.ui_points produces radii that the viewer interprets as given in ui points. + # rr.Radius.ui_points produces radii that the viewer interprets + # as given in ui points. radii=rr.Radius.ui_points([40.0, 60.0]), colors=[255, 0, 0], ), ) # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]) + ) +) diff --git a/docs/snippets/all/archetypes/points2d_ui_radius.rs b/docs/snippets/all/archetypes/points2d_ui_radius.rs index 846d67fd1355..a7bc05ceea6e 100644 --- a/docs/snippets/all/archetypes/points2d_ui_radius.rs +++ b/docs/snippets/all/archetypes/points2d_ui_radius.rs @@ -1,7 +1,9 @@ //! Log some points with ui points & scene unit radii. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_points2d_ui_radius").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_points2d_ui_radius") + .spawn()?; // Two blue points with scene unit radii of 0.1 and 0.3. rec.log( diff --git a/docs/snippets/all/archetypes/points3d_column_updates.cpp b/docs/snippets/all/archetypes/points3d_column_updates.cpp index 8ed72ebba48e..8f451d0701d7 100644 --- a/docs/snippets/all/archetypes/points3d_column_updates.cpp +++ b/docs/snippets/all/archetypes/points3d_column_updates.cpp @@ -9,7 +9,8 @@ using namespace std::chrono_literals; int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_points3d_column_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_points3d_column_updates"); rec.spawn().exit_on_failure(); // Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. @@ -24,16 +25,20 @@ int main(int argc, char* argv[]) { }; // At each timestep, all points in the cloud share the same but changing color and radius. - std::vector colors = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; + std::vector colors = + {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; std::vector radii = {0.05f, 0.01f, 0.2f, 0.1f, 0.3f}; // Log at seconds 10-14 auto times = rerun::Collection{10s, 11s, 12s, 13s, 14s}; - auto time_column = rerun::TimeColumn::from_durations("time", std::move(times)); + auto time_column = + rerun::TimeColumn::from_durations("time", std::move(times)); // Partition our data as expected across the 5 timesteps. - auto position = rerun::Points3D().with_positions(positions).columns({2, 4, 4, 3, 4}); - auto color_and_radius = rerun::Points3D().with_colors(colors).with_radii(radii).columns(); + auto position = + rerun::Points3D().with_positions(positions).columns({2, 4, 4, 3, 4}); + auto color_and_radius = + rerun::Points3D().with_colors(colors).with_radii(radii).columns(); rec.send_columns("points", time_column, position, color_and_radius); } diff --git a/docs/snippets/all/archetypes/points3d_column_updates.py b/docs/snippets/all/archetypes/points3d_column_updates.py index 8065f9e2d991..634a54906adc 100644 --- a/docs/snippets/all/archetypes/points3d_column_updates.py +++ b/docs/snippets/all/archetypes/points3d_column_updates.py @@ -1,7 +1,8 @@ """ Update a point cloud over time, in a single operation. -This is semantically equivalent to the `points3d_row_updates` example, albeit much faster. +This is semantically equivalent to the `points3d_row_updates` example, +albeit much faster. """ from __future__ import annotations @@ -12,7 +13,8 @@ rr.init("rerun_example_points3d_column_updates", spawn=True) -# Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. +# Prepare a point cloud that evolves over 5 timesteps, changing the +# number of points in the process. times = np.arange(10, 15, 1.0) # fmt: off positions = [ @@ -24,7 +26,8 @@ ] # fmt: on -# At each timestep, all points in the cloud share the same but changing color and radius. +# At each timestep, all points in the cloud share the same but changing +# color and radius. colors = [0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF] radii = [0.05, 0.01, 0.2, 0.1, 0.3] @@ -32,7 +35,9 @@ "points", indexes=[rr.TimeColumn("time", duration=times)], columns=[ - *rr.Points3D.columns(positions=positions).partition(lengths=[2, 4, 4, 3, 4]), + *rr.Points3D.columns(positions=positions).partition( + lengths=[2, 4, 4, 3, 4] + ), *rr.Points3D.columns(colors=colors, radii=radii), ], ) diff --git a/docs/snippets/all/archetypes/points3d_column_updates.rs b/docs/snippets/all/archetypes/points3d_column_updates.rs index 2b0676fa4270..d26ba9371153 100644 --- a/docs/snippets/all/archetypes/points3d_column_updates.rs +++ b/docs/snippets/all/archetypes/points3d_column_updates.rs @@ -3,8 +3,10 @@ //! This is semantically equivalent to the `points3d_row_updates` example, albeit much faster. fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_points3d_column_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_points3d_column_updates", + ) + .spawn()?; let times = rerun::TimeColumn::new_duration_secs("time", 10..15); @@ -31,7 +33,11 @@ fn main() -> Result<(), Box> { .with_radii(radii) .columns_of_unit_batches()?; - rec.send_columns("points", [times], position.chain(color_and_radius))?; + rec.send_columns( + "points", + [times], + std::iter::chain(position, color_and_radius), + )?; Ok(()) } diff --git a/docs/snippets/all/archetypes/points3d_partial_updates.cpp b/docs/snippets/all/archetypes/points3d_partial_updates.cpp index 585ebfeb217f..a6ad0e0f1f0b 100644 --- a/docs/snippets/all/archetypes/points3d_partial_updates.cpp +++ b/docs/snippets/all/archetypes/points3d_partial_updates.cpp @@ -6,7 +6,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_points3d_partial_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_points3d_partial_updates"); rec.spawn().exit_on_failure(); std::vector positions; @@ -38,7 +39,12 @@ int main(int argc, char* argv[]) { // Update only the colors and radii, leaving everything else as-is. rec.set_time_sequence("frame", i); - rec.log("points", rerun::Points3D::update_fields().with_radii(radii).with_colors(colors)); + rec.log( + "points", + rerun::Points3D::update_fields().with_radii(radii).with_colors( + colors + ) + ); } std::vector radii; @@ -46,5 +52,10 @@ int main(int argc, char* argv[]) { // Update the positions and radii, and clear everything else in the process. rec.set_time_sequence("frame", 20); - rec.log("points", rerun::Points3D::clear_fields().with_positions(positions).with_radii(radii)); + rec.log( + "points", + rerun::Points3D::clear_fields().with_positions(positions).with_radii( + radii + ) + ); } diff --git a/docs/snippets/all/archetypes/points3d_partial_updates.py b/docs/snippets/all/archetypes/points3d_partial_updates.py index a1b6e8e99216..f3ed81bc80a5 100644 --- a/docs/snippets/all/archetypes/points3d_partial_updates.py +++ b/docs/snippets/all/archetypes/points3d_partial_updates.py @@ -19,4 +19,7 @@ # Update the positions and radii, and clear everything else in the process. rr.set_time("frame", sequence=20) -rr.log("points", rr.Points3D.from_fields(clear_unset=True, positions=positions, radii=0.3)) +rr.log( + "points", + rr.Points3D.from_fields(clear_unset=True, positions=positions, radii=0.3), +) diff --git a/docs/snippets/all/archetypes/points3d_partial_updates.rs b/docs/snippets/all/archetypes/points3d_partial_updates.rs index 18155ededc6d..b8c494d030a5 100644 --- a/docs/snippets/all/archetypes/points3d_partial_updates.rs +++ b/docs/snippets/all/archetypes/points3d_partial_updates.rs @@ -1,8 +1,10 @@ //! Update specific properties of a point cloud over time. fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_points3d_partial_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_points3d_partial_updates", + ) + .spawn()?; let positions = || (0..10).map(|i| (i as f32, 0.0, 0.0)); diff --git a/docs/snippets/all/archetypes/points3d_random.cpp b/docs/snippets/all/archetypes/points3d_random.cpp index 9dd4f444a823..b340f64093cf 100644 --- a/docs/snippets/all/archetypes/points3d_random.cpp +++ b/docs/snippets/all/archetypes/points3d_random.cpp @@ -31,5 +31,8 @@ int main(int argc, char* argv[]) { std::vector radii(10); std::generate(radii.begin(), radii.end(), [&] { return dist_radius(gen); }); - rec.log("random", rerun::Points3D(points3d).with_colors(colors).with_radii(radii)); + rec.log( + "random", + rerun::Points3D(points3d).with_colors(colors).with_radii(radii) + ); } diff --git a/docs/snippets/all/archetypes/points3d_random.rs b/docs/snippets/all/archetypes/points3d_random.rs index e68a1c53db56..f9d20979a0a7 100644 --- a/docs/snippets/all/archetypes/points3d_random.rs +++ b/docs/snippets/all/archetypes/points3d_random.rs @@ -3,7 +3,9 @@ use rand::prelude::*; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_points3d_random").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_points3d_random") + .spawn()?; let mut rng = rand::rngs::SmallRng::seed_from_u64(42); let dist = rand::distr::Uniform::new(-5., 5.)?; @@ -11,11 +13,13 @@ fn main() -> Result<(), Box> { rec.log( "random", &rerun::Points3D::new( - (0..10).map(|_| (rng.sample(dist), rng.sample(dist), rng.sample(dist))), - ) - .with_colors( - (0..10).map(|_| rerun::Color::from_rgb(rng.random(), rng.random(), rng.random())), + (0..10).map(|_| { + (rng.sample(dist), rng.sample(dist), rng.sample(dist)) + }), ) + .with_colors((0..10).map(|_| { + rerun::Color::from_rgb(rng.random(), rng.random(), rng.random()) + })) .with_radii((0..10).map(|_| rng.random::())), )?; diff --git a/docs/snippets/all/archetypes/points3d_row_updates.cpp b/docs/snippets/all/archetypes/points3d_row_updates.cpp index dc42df13614e..9d41013c18e9 100644 --- a/docs/snippets/all/archetypes/points3d_row_updates.cpp +++ b/docs/snippets/all/archetypes/points3d_row_updates.cpp @@ -8,7 +8,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_points3d_row_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_points3d_row_updates"); rec.spawn().exit_on_failure(); // Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. @@ -23,14 +24,17 @@ int main(int argc, char* argv[]) { }; // At each timestep, all points in the cloud share the same but changing color and radius. - std::vector colors = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; + std::vector colors = + {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; std::vector radii = {0.05f, 0.01f, 0.2f, 0.1f, 0.3f}; for (size_t i = 0; i < 5; i++) { rec.set_time_duration_secs("time", 10.0 + static_cast(i)); rec.log( "points", - rerun::Points3D(positions[i]).with_colors(colors[i]).with_radii(radii[i]) + rerun::Points3D(positions[i]) + .with_colors(colors[i]) + .with_radii(radii[i]) ); } } diff --git a/docs/snippets/all/archetypes/points3d_row_updates.py b/docs/snippets/all/archetypes/points3d_row_updates.py index 1f4937a25abe..0813c51b17eb 100644 --- a/docs/snippets/all/archetypes/points3d_row_updates.py +++ b/docs/snippets/all/archetypes/points3d_row_updates.py @@ -1,7 +1,8 @@ """ Update a point cloud over time. -See also the `points3d_column_updates` example, which achieves the same thing in a single operation. +See also the `points3d_column_updates` example, which achieves the same +thing in a single operation. """ import numpy as np @@ -10,7 +11,8 @@ rr.init("rerun_example_points3d_row_updates", spawn=True) -# Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. +# Prepare a point cloud that evolves over 5 timesteps, changing the +# number of points in the process. times = np.arange(10, 15, 1.0) # fmt: off positions = [ @@ -22,10 +24,13 @@ ] # fmt: on -# At each timestep, all points in the cloud share the same but changing color and radius. +# At each timestep, all points in the cloud share the same but changing +# color and radius. colors = [0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF] radii = [0.05, 0.01, 0.2, 0.1, 0.3] for i in range(5): rr.set_time("time", duration=10 + i) - rr.log("points", rr.Points3D(positions[i], colors=colors[i], radii=radii[i])) + rr.log( + "points", rr.Points3D(positions[i], colors=colors[i], radii=radii[i]) + ) diff --git a/docs/snippets/all/archetypes/points3d_row_updates.rs b/docs/snippets/all/archetypes/points3d_row_updates.rs index 7d2221860210..e02845f6bdc5 100644 --- a/docs/snippets/all/archetypes/points3d_row_updates.rs +++ b/docs/snippets/all/archetypes/points3d_row_updates.rs @@ -3,7 +3,10 @@ //! See also the `points3d_column_updates` example, which achieves the same thing in a single operation. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_points3d_row_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_points3d_row_updates", + ) + .spawn()?; // Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. #[rustfmt::skip] @@ -19,7 +22,9 @@ fn main() -> Result<(), Box> { let colors = [0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF]; let radii = [0.05, 0.01, 0.2, 0.1, 0.3]; - for (time, positions, color, radius) in itertools::izip!(10..15, positions, colors, radii) { + for (time, positions, color, radius) in + itertools::izip!(10..15, positions, colors, radii) + { rec.set_duration_secs("time", time); let point_cloud = rerun::Points3D::new(positions) diff --git a/docs/snippets/all/archetypes/points3d_simple.cpp b/docs/snippets/all/archetypes/points3d_simple.cpp index 60838c230e63..189903c8a94d 100644 --- a/docs/snippets/all/archetypes/points3d_simple.cpp +++ b/docs/snippets/all/archetypes/points3d_simple.cpp @@ -6,5 +6,8 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_points3d"); rec.spawn().exit_on_failure(); - rec.log("points", rerun::Points3D({{0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f}})); + rec.log( + "points", + rerun::Points3D({{0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f}}) + ); } diff --git a/docs/snippets/all/archetypes/points3d_simple.rs b/docs/snippets/all/archetypes/points3d_simple.rs index 3fd93598775c..94f729c50624 100644 --- a/docs/snippets/all/archetypes/points3d_simple.rs +++ b/docs/snippets/all/archetypes/points3d_simple.rs @@ -1,7 +1,8 @@ //! Log some very simple points. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_points3d").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_points3d").spawn()?; rec.log( "points", diff --git a/docs/snippets/all/archetypes/points3d_ui_radius.py b/docs/snippets/all/archetypes/points3d_ui_radius.py index 57832c24bf8a..0027d5d9faac 100644 --- a/docs/snippets/all/archetypes/points3d_ui_radius.py +++ b/docs/snippets/all/archetypes/points3d_ui_radius.py @@ -16,13 +16,15 @@ ) # Two red points with ui point radii of 40 and 60. -# UI points are independent of zooming in Views, but are sensitive to the application UI scaling. +# UI points are independent of zooming in Views, but are sensitive to the +# application UI scaling. # For 100% ui scaling, UI points are equal to pixels. rr.log( "ui_points", rr.Points3D( [[0, 0, 0], [1, 0, 1]], - # rr.Radius.ui_points produces radii that the viewer interprets as given in ui points. + # rr.Radius.ui_points produces radii that the viewer interprets + # as given in ui points. radii=rr.Radius.ui_points([40.0, 60.0]), colors=[255, 0, 0], ), diff --git a/docs/snippets/all/archetypes/points3d_ui_radius.rs b/docs/snippets/all/archetypes/points3d_ui_radius.rs index 65ae06771eef..180a254a5bf0 100644 --- a/docs/snippets/all/archetypes/points3d_ui_radius.rs +++ b/docs/snippets/all/archetypes/points3d_ui_radius.rs @@ -1,7 +1,9 @@ //! Log some points with ui points & scene unit radii. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_points3d_ui_radius").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_points3d_ui_radius") + .spawn()?; // Two blue points with scene unit radii of 0.1 and 0.3. rec.log( diff --git a/docs/snippets/all/archetypes/scalars_column_updates.cpp b/docs/snippets/all/archetypes/scalars_column_updates.cpp index 8328624309de..92113b3110f8 100644 --- a/docs/snippets/all/archetypes/scalars_column_updates.cpp +++ b/docs/snippets/all/archetypes/scalars_column_updates.cpp @@ -9,7 +9,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_scalar_column_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_scalar_column_updates"); rec.spawn().exit_on_failure(); // Native scalars & times. diff --git a/docs/snippets/all/archetypes/scalars_column_updates.py b/docs/snippets/all/archetypes/scalars_column_updates.py index eb2d5e6337ab..39c7d181b9b0 100644 --- a/docs/snippets/all/archetypes/scalars_column_updates.py +++ b/docs/snippets/all/archetypes/scalars_column_updates.py @@ -1,7 +1,8 @@ """ Update a scalar over time, in a single operation. -This is semantically equivalent to the `scalar_row_updates` example, albeit much faster. +This is semantically equivalent to the `scalar_row_updates` example, +albeit much faster. """ from __future__ import annotations diff --git a/docs/snippets/all/archetypes/scalars_column_updates.rs b/docs/snippets/all/archetypes/scalars_column_updates.rs index 63826596e3db..0a49b70ff28a 100644 --- a/docs/snippets/all/archetypes/scalars_column_updates.rs +++ b/docs/snippets/all/archetypes/scalars_column_updates.rs @@ -5,7 +5,10 @@ use rerun::TimeColumn; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_scalar_column_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_scalar_column_updates", + ) + .spawn()?; let times = TimeColumn::new_sequence("step", 0..64); let scalars = (0..64).map(|step| (step as f64 / 10.0).sin()); diff --git a/docs/snippets/all/archetypes/scalars_multiple_plots.cpp b/docs/snippets/all/archetypes/scalars_multiple_plots.cpp index a506ea664c12..681965126dbf 100644 --- a/docs/snippets/all/archetypes/scalars_multiple_plots.cpp +++ b/docs/snippets/all/archetypes/scalars_multiple_plots.cpp @@ -7,7 +7,8 @@ constexpr float TAU = 6.28318530717958647692528676655900577f; int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_scalar_multiple_plots"); + const auto rec = + rerun::RecordingStream("rerun_example_scalar_multiple_plots"); rec.spawn().exit_on_failure(); int64_t lcg_state = 0; @@ -17,11 +18,15 @@ int main(int argc, char* argv[]) { // Log two lines series under a shared root so that they show in the same plot by default. rec.log_static( "trig/sin", - rerun::SeriesLines().with_colors(rerun::Rgba32{255, 0, 0}).with_names("sin(0.01t)") + rerun::SeriesLines() + .with_colors(rerun::Rgba32{255, 0, 0}) + .with_names("sin(0.01t)") ); rec.log_static( "trig/cos", - rerun::SeriesLines().with_colors(rerun::Rgba32{0, 255, 0}).with_names("cos(0.01t)") + rerun::SeriesLines() + .with_colors(rerun::Rgba32{0, 255, 0}) + .with_names("cos(0.01t)") ); // NOTE: `SeriesLines` and `SeriesPoints` can both be logged without any associated data @@ -32,18 +37,24 @@ int main(int argc, char* argv[]) { // Log scattered points under a different root so that they show in a different plot by default. rec.log_static( "scatter/lcg", - rerun::SeriesPoints().with_markers(rerun::components::MarkerShape::Circle) + rerun::SeriesPoints().with_markers(rerun::MarkerShape::Circle) ); // Log the data on a timeline called "step". for (int t = 0; t < static_cast(TAU * 2.0 * 100.0); ++t) { rec.set_time_sequence("step", t); - rec.log("trig/sin", rerun::Scalars(sin(static_cast(t) / 100.0))); - rec.log("trig/cos", rerun::Scalars(cos(static_cast(t) / 100.0))); - - lcg_state = - (1140671485 * lcg_state + 128201163) % 16777216; // simple linear congruency generator + rec.log( + "trig/sin", + rerun::Scalars(sin(static_cast(t) / 100.0)) + ); + rec.log( + "trig/cos", + rerun::Scalars(cos(static_cast(t) / 100.0)) + ); + + lcg_state = (1140671485 * lcg_state + 128201163) % + 16777216; // simple linear congruency generator rec.log("scatter/lcg", rerun::Scalars(static_cast(lcg_state))); } } diff --git a/docs/snippets/all/archetypes/scalars_multiple_plots.py b/docs/snippets/all/archetypes/scalars_multiple_plots.py index 2a1dd1a8002a..acda67982517 100644 --- a/docs/snippets/all/archetypes/scalars_multiple_plots.py +++ b/docs/snippets/all/archetypes/scalars_multiple_plots.py @@ -10,19 +10,31 @@ lcg_state = np.int64(0) # Set up plot styling: -# They are logged as static as they don't change over time and apply to all timelines. -# Log two lines series under a shared root so that they show in the same plot by default. -rr.log("trig/sin", rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)"), static=True) -rr.log("trig/cos", rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)"), static=True) - - -# NOTE: `SeriesLines` and `SeriesPoints` can both be logged without any associated data -# (all fields are optional). In `v0.24` we removed indicators, which now results in -# no data logged at all, when no fields are specified. Therefore, we log a circle shape -# as a marker if no arguments are supplied. +# They are logged as static as they don't change over time and apply to +# all timelines. +# Log two lines series under a shared root so that they show in the same +# plot by default. +rr.log( + "trig/sin", + rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)"), + static=True, +) +rr.log( + "trig/cos", + rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)"), + static=True, +) + + +# NOTE: `SeriesLines` and `SeriesPoints` can both be logged without any +# associated data (all fields are optional). In `v0.24` we removed +# indicators, which now results in no data logged at all, when no +# fields are specified. Therefore, we log a circle shape as a +# marker if no arguments are supplied. # More information: https://github.com/rerun-io/rerun/issues/10512 -# Log scattered points under a different root so that they show in a different plot by default. +# Log scattered points under a different root so that they show in a +# different plot by default. rr.log("scatter/lcg", rr.SeriesPoints(), static=True) # Log the data on a timeline called "step". @@ -32,5 +44,6 @@ rr.log("trig/sin", rr.Scalars(sin(float(t) / 100.0))) rr.log("trig/cos", rr.Scalars(cos(float(t) / 100.0))) - lcg_state = (1140671485 * lcg_state + 128201163) % 16777216 # simple linear congruency generator + # simple linear congruency generator + lcg_state = (1140671485 * lcg_state + 128201163) % 16777216 rr.log("scatter/lcg", rr.Scalars(lcg_state.astype(np.float64))) diff --git a/docs/snippets/all/archetypes/scalars_multiple_plots.rs b/docs/snippets/all/archetypes/scalars_multiple_plots.rs index 7a55f9ffdc57..bdc29fece373 100644 --- a/docs/snippets/all/archetypes/scalars_multiple_plots.rs +++ b/docs/snippets/all/archetypes/scalars_multiple_plots.rs @@ -1,7 +1,10 @@ //! Log a scalar over time. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_scalar_multiple_plots").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_scalar_multiple_plots", + ) + .spawn()?; let mut lcg_state = 0_i64; // Set up plot styling: @@ -28,7 +31,8 @@ fn main() -> Result<(), Box> { // Log scattered points under a different root so that they show in a different plot by default. rec.log_static( "scatter/lcg", - &rerun::SeriesPoints::new().with_markers([rerun::components::MarkerShape::Circle]), + &rerun::SeriesPoints::new() + .with_markers([rerun::components::MarkerShape::Circle]), )?; for t in 0..((std::f32::consts::TAU * 2.0 * 100.0) as i64) { diff --git a/docs/snippets/all/archetypes/scalars_row_updates.cpp b/docs/snippets/all/archetypes/scalars_row_updates.cpp index b44ea089b3f1..efd555a2eb3e 100644 --- a/docs/snippets/all/archetypes/scalars_row_updates.cpp +++ b/docs/snippets/all/archetypes/scalars_row_updates.cpp @@ -12,6 +12,9 @@ int main(int argc, char* argv[]) { for (int step = 0; step < 64; ++step) { rec.set_time_sequence("step", step); - rec.log("scalars", rerun::Scalars(sin(static_cast(step) / 10.0))); + rec.log( + "scalars", + rerun::Scalars(sin(static_cast(step) / 10.0)) + ); } } diff --git a/docs/snippets/all/archetypes/scalars_row_updates.py b/docs/snippets/all/archetypes/scalars_row_updates.py index e36b651a2102..1562eefab24a 100644 --- a/docs/snippets/all/archetypes/scalars_row_updates.py +++ b/docs/snippets/all/archetypes/scalars_row_updates.py @@ -1,7 +1,8 @@ """ Update a scalar over time. -See also the `scalar_column_updates` example, which achieves the same thing in a single operation. +See also the `scalar_column_updates` example, which achieves the same +thing in a single operation. """ from __future__ import annotations diff --git a/docs/snippets/all/archetypes/scalars_row_updates.rs b/docs/snippets/all/archetypes/scalars_row_updates.rs index 83adfe807f83..db159dd51170 100644 --- a/docs/snippets/all/archetypes/scalars_row_updates.rs +++ b/docs/snippets/all/archetypes/scalars_row_updates.rs @@ -3,7 +3,9 @@ //! See also the `scalar_column_updates` example, which achieves the same thing in a single operation. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_scalar_row_updates").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_scalar_row_updates") + .spawn()?; for step in 0..64 { rec.set_time_sequence("step", step); diff --git a/docs/snippets/all/archetypes/scalars_simple.cpp b/docs/snippets/all/archetypes/scalars_simple.cpp index 63a00c396560..90ebc7ecb05a 100644 --- a/docs/snippets/all/archetypes/scalars_simple.cpp +++ b/docs/snippets/all/archetypes/scalars_simple.cpp @@ -11,6 +11,9 @@ int main(int argc, char* argv[]) { // Log the data on a timeline called "step". for (int step = 0; step < 64; ++step) { rec.set_time_sequence("step", step); - rec.log("scalar", rerun::Scalars(std::sin(static_cast(step) / 10.0))); + rec.log( + "scalar", + rerun::Scalars(std::sin(static_cast(step) / 10.0)) + ); } } diff --git a/docs/snippets/all/archetypes/scalars_simple.rs b/docs/snippets/all/archetypes/scalars_simple.rs index c4405cde7542..cae6f7576287 100644 --- a/docs/snippets/all/archetypes/scalars_simple.rs +++ b/docs/snippets/all/archetypes/scalars_simple.rs @@ -1,7 +1,8 @@ //! Log a scalar over time. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_scalar").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_scalar").spawn()?; // Log the data on a timeline called "step". for step in 0..64 { diff --git a/docs/snippets/all/archetypes/segmentation_image_simple.cpp b/docs/snippets/all/archetypes/segmentation_image_simple.cpp index 09402837ebbb..0bd5d4ed7ea5 100644 --- a/docs/snippets/all/archetypes/segmentation_image_simple.cpp +++ b/docs/snippets/all/archetypes/segmentation_image_simple.cpp @@ -13,11 +13,13 @@ int main(int argc, char* argv[]) { const int HEIGHT = 8; const int WIDTH = 12; std::vector data(WIDTH * HEIGHT, 0); - for (auto y = 0; y < 4; ++y) { // top half - std::fill_n(data.begin() + y * WIDTH, 6, static_cast(1)); // left half + for (auto y = 0; y < 4; ++y) { // top half + // left half: + std::fill_n(data.begin() + y * WIDTH, 6, static_cast(1)); } - for (auto y = 4; y < 8; ++y) { // bottom half - std::fill_n(data.begin() + y * WIDTH + 6, 6, static_cast(2)); // right half + for (auto y = 4; y < 8; ++y) { // bottom half + // right half: + std::fill_n(data.begin() + y * WIDTH + 6, 6, static_cast(2)); } // create an annotation context to describe the classes diff --git a/docs/snippets/all/archetypes/segmentation_image_simple.py b/docs/snippets/all/archetypes/segmentation_image_simple.py index df35746d3d88..12acb4e2ce2b 100644 --- a/docs/snippets/all/archetypes/segmentation_image_simple.py +++ b/docs/snippets/all/archetypes/segmentation_image_simple.py @@ -12,6 +12,10 @@ rr.init("rerun_example_segmentation_image", spawn=True) # Assign a label and color to each class -rr.log("/", rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), static=True) +rr.log( + "/", + rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), + static=True, +) rr.log("image", rr.SegmentationImage(image)) diff --git a/docs/snippets/all/archetypes/segmentation_image_simple.rs b/docs/snippets/all/archetypes/segmentation_image_simple.rs index debf2a3e4138..c339c98a90e0 100644 --- a/docs/snippets/all/archetypes/segmentation_image_simple.rs +++ b/docs/snippets/all/archetypes/segmentation_image_simple.rs @@ -3,7 +3,9 @@ use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_segmentation_image").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_segmentation_image") + .spawn()?; // create a segmentation image let mut image = Array::::zeros((8, 12).f()); diff --git a/docs/snippets/all/archetypes/series_lines_style.cpp b/docs/snippets/all/archetypes/series_lines_style.cpp index e5817f951ec6..90a86bb7d018 100644 --- a/docs/snippets/all/archetypes/series_lines_style.cpp +++ b/docs/snippets/all/archetypes/series_lines_style.cpp @@ -32,7 +32,13 @@ int main(int argc, char* argv[]) { for (int t = 0; t < static_cast(TAU * 2.0 * 100.0); ++t) { rec.set_time_sequence("step", t); - rec.log("trig/sin", rerun::Scalars(sin(static_cast(t) / 100.0))); - rec.log("trig/cos", rerun::Scalars(cos(static_cast(t) / 100.0))); + rec.log( + "trig/sin", + rerun::Scalars(sin(static_cast(t) / 100.0)) + ); + rec.log( + "trig/cos", + rerun::Scalars(cos(static_cast(t) / 100.0)) + ); } } diff --git a/docs/snippets/all/archetypes/series_lines_style.py b/docs/snippets/all/archetypes/series_lines_style.py index e6a7b9c8b43f..162c9cf5a5e4 100644 --- a/docs/snippets/all/archetypes/series_lines_style.py +++ b/docs/snippets/all/archetypes/series_lines_style.py @@ -7,10 +7,20 @@ rr.init("rerun_example_series_line_style", spawn=True) # Set up plot styling: -# They are logged as static as they don't change over time and apply to all timelines. -# Log two lines series under a shared root so that they show in the same plot by default. -rr.log("trig/sin", rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)", widths=2), static=True) -rr.log("trig/cos", rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)", widths=4), static=True) +# They are logged as static as they don't change over time and apply to +# all timelines. +# Log two lines series under a shared root so that they show in the same +# plot by default. +rr.log( + "trig/sin", + rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)", widths=2), + static=True, +) +rr.log( + "trig/cos", + rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)", widths=4), + static=True, +) # Log the data on a timeline called "step". for t in range(int(tau * 2 * 100.0)): diff --git a/docs/snippets/all/archetypes/series_lines_style.rs b/docs/snippets/all/archetypes/series_lines_style.rs index 9834df355563..9408b4ebbe8a 100644 --- a/docs/snippets/all/archetypes/series_lines_style.rs +++ b/docs/snippets/all/archetypes/series_lines_style.rs @@ -1,7 +1,9 @@ //! Log a scalar over time. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_series_line_style").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_series_line_style") + .spawn()?; // Set up plot styling: // They are logged static as they don't change over time and apply to all timelines. diff --git a/docs/snippets/all/archetypes/series_points_style.cpp b/docs/snippets/all/archetypes/series_points_style.cpp index 8671fdbe6642..bc82adb0d45a 100644 --- a/docs/snippets/all/archetypes/series_points_style.cpp +++ b/docs/snippets/all/archetypes/series_points_style.cpp @@ -18,7 +18,7 @@ int main(int argc, char* argv[]) { rerun::SeriesPoints() .with_colors(rerun::Rgba32{255, 0, 0}) .with_names("sin(0.01t)") - .with_markers(rerun::components::MarkerShape::Circle) + .with_markers(rerun::MarkerShape::Circle) .with_marker_sizes(4.0f) ); rec.log_static( @@ -26,7 +26,7 @@ int main(int argc, char* argv[]) { rerun::SeriesPoints() .with_colors(rerun::Rgba32{0, 255, 0}) .with_names("cos(0.01t)") - .with_markers(rerun::components::MarkerShape::Cross) + .with_markers(rerun::MarkerShape::Cross) .with_marker_sizes(2.0f) ); diff --git a/docs/snippets/all/archetypes/series_points_style.py b/docs/snippets/all/archetypes/series_points_style.py index a2abd512aa05..3b0da4806ba4 100644 --- a/docs/snippets/all/archetypes/series_points_style.py +++ b/docs/snippets/all/archetypes/series_points_style.py @@ -7,8 +7,9 @@ rr.init("rerun_example_series_point_style", spawn=True) # Set up plot styling: -# They are logged as static as they don't change over time and apply to all timelines. -# Log two point series under a shared root so that they show in the same plot by default. +# They are logged as static as they don't change over time and apply to all +# timelines. Log two point series under a shared root so that they show in the +# same plot by default. rr.log( "trig/sin", rr.SeriesPoints( diff --git a/docs/snippets/all/archetypes/series_points_style.rs b/docs/snippets/all/archetypes/series_points_style.rs index f15acdb52c8d..0e0bd8551c89 100644 --- a/docs/snippets/all/archetypes/series_points_style.rs +++ b/docs/snippets/all/archetypes/series_points_style.rs @@ -1,7 +1,9 @@ //! Log a scalar over time. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_series_point_style").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_series_point_style") + .spawn()?; // Set up plot styling: // They are logged static as they don't change over time and apply to all timelines. diff --git a/docs/snippets/all/archetypes/state_change.cpp b/docs/snippets/all/archetypes/state_change.cpp new file mode 100644 index 000000000000..1704d56c7c91 --- /dev/null +++ b/docs/snippets/all/archetypes/state_change.cpp @@ -0,0 +1,17 @@ +// Log a `StateChange` + +#include + +int main(int argc, char* argv[]) { + const auto rec = rerun::RecordingStream("rerun_example_state_change"); + rec.spawn().exit_on_failure(); + + rec.set_time_sequence("step", 0); + rec.log("door", rerun::StateChange().with_state({"open"})); + + rec.set_time_sequence("step", 1); + rec.log("door", rerun::StateChange().with_state({"closed"})); + + rec.set_time_sequence("step", 2); + rec.log("door", rerun::StateChange().with_state({"open"})); +} diff --git a/docs/snippets/all/archetypes/state_change.py b/docs/snippets/all/archetypes/state_change.py new file mode 100644 index 000000000000..b333ad1eb0eb --- /dev/null +++ b/docs/snippets/all/archetypes/state_change.py @@ -0,0 +1,14 @@ +# Log a `StateChange`. + +import rerun as rr + +rr.init("rerun_example_state_change", spawn=True) + +rr.set_time("step", sequence=0) +rr.log("door", rr.StateChange(state="open")) + +rr.set_time("step", sequence=1) +rr.log("door", rr.StateChange(state="closed")) + +rr.set_time("step", sequence=2) +rr.log("door", rr.StateChange(state="open")) diff --git a/docs/snippets/all/archetypes/status.rs b/docs/snippets/all/archetypes/state_change.rs similarity index 51% rename from docs/snippets/all/archetypes/status.rs rename to docs/snippets/all/archetypes/state_change.rs index 2c5ab053050a..d67cbf15c86c 100644 --- a/docs/snippets/all/archetypes/status.rs +++ b/docs/snippets/all/archetypes/state_change.rs @@ -1,16 +1,17 @@ -//! Log a `Status` +//! Log a `StateChange` fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_status").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_state_change") + .spawn()?; rec.set_time_sequence("step", 0); - rec.log("door", &rerun::Status::new().with_status("open"))?; + rec.log("door", &rerun::StateChange::single("open"))?; rec.set_time_sequence("step", 1); - rec.log("door", &rerun::Status::new().with_status("closed"))?; + rec.log("door", &rerun::StateChange::single("closed"))?; rec.set_time_sequence("step", 2); - rec.log("door", &rerun::Status::new().with_status("open"))?; + rec.log("door", &rerun::StateChange::single("open"))?; Ok(()) } diff --git a/docs/snippets/all/archetypes/state_configuration.cpp b/docs/snippets/all/archetypes/state_configuration.cpp new file mode 100644 index 000000000000..329d1672a05a --- /dev/null +++ b/docs/snippets/all/archetypes/state_configuration.cpp @@ -0,0 +1,27 @@ +// Log a `StateChange` together with a `StateConfiguration` that customizes its display. + +#include + +int main(int argc, char* argv[]) { + const auto rec = + rerun::RecordingStream("rerun_example_state_configuration"); + rec.spawn().exit_on_failure(); + + // Configure how each raw state value is displayed (label, color, visibility). + rec.log_static( + "door", + rerun::StateConfiguration() + .with_values({"open", "closed"}) + .with_labels({"Open", "Closed"}) + .with_colors({0x4CAF50FF, 0xEF5350FF}) + ); + + rec.set_time_sequence("step", 0); + rec.log("door", rerun::StateChange().with_state({"open"})); + + rec.set_time_sequence("step", 1); + rec.log("door", rerun::StateChange().with_state({"closed"})); + + rec.set_time_sequence("step", 2); + rec.log("door", rerun::StateChange().with_state({"open"})); +} diff --git a/docs/snippets/all/archetypes/state_configuration.py b/docs/snippets/all/archetypes/state_configuration.py new file mode 100644 index 000000000000..461dc20c139b --- /dev/null +++ b/docs/snippets/all/archetypes/state_configuration.py @@ -0,0 +1,26 @@ +# Log a `StateChange` together with a `StateConfiguration` that customizes +# its display. + +import rerun as rr + +rr.init("rerun_example_state_configuration", spawn=True) + +# Configure how each raw state value is displayed (label, color, visibility). +rr.log( + "door", + rr.StateConfiguration( + values=["open", "closed"], + labels=["Open", "Closed"], + colors=[0x4CAF50FF, 0xEF5350FF], + ), + static=True, +) + +rr.set_time("step", sequence=0) +rr.log("door", rr.StateChange(state="open")) + +rr.set_time("step", sequence=1) +rr.log("door", rr.StateChange(state="closed")) + +rr.set_time("step", sequence=2) +rr.log("door", rr.StateChange(state="open")) diff --git a/docs/snippets/all/archetypes/state_configuration.rs b/docs/snippets/all/archetypes/state_configuration.rs new file mode 100644 index 000000000000..92e9b087b0aa --- /dev/null +++ b/docs/snippets/all/archetypes/state_configuration.rs @@ -0,0 +1,27 @@ +//! Log a `StateChange` together with a `StateConfiguration` that customizes its display. + +fn main() -> Result<(), Box> { + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_state_configuration") + .spawn()?; + + // Configure how each raw state value is displayed (label, color, visibility). + rec.log_static( + "door", + &rerun::StateConfiguration::new() + .with_values(["open", "closed"]) + .with_labels(["Open", "Closed"]) + .with_colors([0x4CAF50FFu32, 0xEF5350FFu32]), + )?; + + rec.set_time_sequence("step", 0); + rec.log("door", &rerun::StateChange::single("open"))?; + + rec.set_time_sequence("step", 1); + rec.log("door", &rerun::StateChange::single("closed"))?; + + rec.set_time_sequence("step", 2); + rec.log("door", &rerun::StateChange::single("open"))?; + + Ok(()) +} diff --git a/docs/snippets/all/archetypes/status.cpp b/docs/snippets/all/archetypes/status.cpp deleted file mode 100644 index bfccd0ff527b..000000000000 --- a/docs/snippets/all/archetypes/status.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// Log a `Status` - -#include - -int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_status"); - rec.spawn().exit_on_failure(); - - rec.set_time_sequence("step", 0); - rec.log("door", rerun::Status().with_status("open")); - - rec.set_time_sequence("step", 1); - rec.log("door", rerun::Status().with_status("closed")); - - rec.set_time_sequence("step", 2); - rec.log("door", rerun::Status().with_status("open")); -} diff --git a/docs/snippets/all/archetypes/status.py b/docs/snippets/all/archetypes/status.py deleted file mode 100644 index 5539df06f2bf..000000000000 --- a/docs/snippets/all/archetypes/status.py +++ /dev/null @@ -1,14 +0,0 @@ -# Log a `Status`. - -import rerun as rr - -rr.init("rerun_example_status", spawn=True) - -rr.set_time("step", sequence=0) -rr.log("door", rr.Status(status="open")) - -rr.set_time("step", sequence=1) -rr.log("door", rr.Status(status="closed")) - -rr.set_time("step", sequence=2) -rr.log("door", rr.Status(status="open")) diff --git a/docs/snippets/all/archetypes/tensor_simple.cpp b/docs/snippets/all/archetypes/tensor_simple.cpp index b764d7b95b73..82e49f471bd3 100644 --- a/docs/snippets/all/archetypes/tensor_simple.cpp +++ b/docs/snippets/all/archetypes/tensor_simple.cpp @@ -15,10 +15,13 @@ int main(int argc, char* argv[]) { std::uniform_int_distribution dist(0, 255); std::vector data(8 * 6 * 3 * 5); - std::generate(data.begin(), data.end(), [&] { return static_cast(dist(gen)); }); + std::generate(data.begin(), data.end(), [&] { + return static_cast(dist(gen)); + }); rec.log( "tensor", - rerun::Tensor({8, 6, 3, 5}, data).with_dim_names({"width", "height", "channel", "batch"}) + rerun::Tensor({8, 6, 3, 5}, data) + .with_dim_names({"width", "height", "channel", "batch"}) ); } diff --git a/docs/snippets/all/archetypes/tensor_simple.py b/docs/snippets/all/archetypes/tensor_simple.py index f5e83d80224b..5b25c0db72bd 100644 --- a/docs/snippets/all/archetypes/tensor_simple.py +++ b/docs/snippets/all/archetypes/tensor_simple.py @@ -4,9 +4,14 @@ import rerun as rr -tensor = np.random.randint(0, 256, (8, 6, 3, 5), dtype=np.uint8) # 4-dimensional tensor +tensor = np.random.randint( + 0, 256, (8, 6, 3, 5), dtype=np.uint8 +) # 4-dimensional tensor rr.init("rerun_example_tensor", spawn=True) # Log the tensor, assigning names to each dimension -rr.log("tensor", rr.Tensor(tensor, dim_names=("width", "height", "channel", "batch"))) +rr.log( + "tensor", + rr.Tensor(tensor, dim_names=("width", "height", "channel", "batch")), +) diff --git a/docs/snippets/all/archetypes/tensor_simple.rs b/docs/snippets/all/archetypes/tensor_simple.rs index ac8a11c93ca0..07c2284890f0 100644 --- a/docs/snippets/all/archetypes/tensor_simple.rs +++ b/docs/snippets/all/archetypes/tensor_simple.rs @@ -4,14 +4,15 @@ use ndarray::{Array, ShapeBuilder as _}; use rand::prelude::*; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_tensor").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_tensor").spawn()?; let mut data = Array::::default((8, 6, 3, 5).f()); let mut rng = rand::rngs::SmallRng::seed_from_u64(42); data.map_inplace(|x| *x = rng.random()); - let tensor = - rerun::Tensor::try_from(data)?.with_dim_names(["width", "height", "channel", "batch"]); + let tensor = rerun::Tensor::try_from(data)? + .with_dim_names(["width", "height", "channel", "batch"]); rec.log("tensor", &tensor)?; Ok(()) diff --git a/docs/snippets/all/archetypes/text_document.rs b/docs/snippets/all/archetypes/text_document.rs index 85df17b3eedf..819106d09880 100644 --- a/docs/snippets/all/archetypes/text_document.rs +++ b/docs/snippets/all/archetypes/text_document.rs @@ -1,7 +1,8 @@ //! Log a `TextDocument` fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_text_document").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_text_document") + .spawn()?; rec.log( "text_document", diff --git a/docs/snippets/all/archetypes/text_log.cpp b/docs/snippets/all/archetypes/text_log.cpp index 4b592661476a..2b13a32820d5 100644 --- a/docs/snippets/all/archetypes/text_log.cpp +++ b/docs/snippets/all/archetypes/text_log.cpp @@ -6,5 +6,9 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_text_log"); rec.spawn().exit_on_failure(); - rec.log("log", rerun::TextLog("Application started.").with_level(rerun::TextLogLevel::Info)); + rec.log( + "log", + rerun::TextLog("Application started.") + .with_level(rerun::TextLogLevel::Info) + ); } diff --git a/docs/snippets/all/archetypes/text_log.rs b/docs/snippets/all/archetypes/text_log.rs index 8127f9239be5..50be96c619b9 100644 --- a/docs/snippets/all/archetypes/text_log.rs +++ b/docs/snippets/all/archetypes/text_log.rs @@ -1,7 +1,8 @@ //! Log a `TextLog` fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_text_log").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_text_log").spawn()?; rec.log( "log", diff --git a/docs/snippets/all/archetypes/text_log_integration.cpp b/docs/snippets/all/archetypes/text_log_integration.cpp index 7b733f8e5a53..14caf5b9e541 100644 --- a/docs/snippets/all/archetypes/text_log_integration.cpp +++ b/docs/snippets/all/archetypes/text_log_integration.cpp @@ -6,7 +6,8 @@ void loguru_to_rerun(void* user_data, const loguru::Message& message) { // NOTE: `rerun::RecordingStream` is thread-safe. - const rerun::RecordingStream* rec = reinterpret_cast(user_data); + const rerun::RecordingStream* rec = + reinterpret_cast(user_data); rerun::TextLogLevel level; if (message.verbosity == loguru::Verbosity_FATAL) { @@ -32,13 +33,15 @@ void loguru_to_rerun(void* user_data, const loguru::Message& message) { } int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_text_log_integration"); + const auto rec = + rerun::RecordingStream("rerun_example_text_log_integration"); rec.spawn().exit_on_failure(); // Log a text entry directly: rec.log( "logs", - rerun::TextLog("this entry has loglevel TRACE").with_level(rerun::TextLogLevel::Trace) + rerun::TextLog("this entry has loglevel TRACE") + .with_level(rerun::TextLogLevel::Trace) ); loguru::add_callback( @@ -48,7 +51,11 @@ int main(int argc, char* argv[]) { loguru::Verbosity_INFO ); - LOG_F(INFO, "This INFO log got added through the standard logging interface"); + LOG_F( + INFO, + "This INFO log got added through the standard logging interface" + ); - loguru::remove_callback("rerun"); // we need to do this before `rec` goes out of scope + // we need to do this before `rec` goes out of scope: + loguru::remove_callback("rerun"); } diff --git a/docs/snippets/all/archetypes/text_log_integration.py b/docs/snippets/all/archetypes/text_log_integration.py index 93025b1d6502..21cee387b36f 100644 --- a/docs/snippets/all/archetypes/text_log_integration.py +++ b/docs/snippets/all/archetypes/text_log_integration.py @@ -7,7 +7,10 @@ rr.init("rerun_example_text_log_integration", spawn=True) # Log a text entry directly -rr.log("logs", rr.TextLog("this entry has loglevel TRACE", level=rr.TextLogLevel.TRACE)) +rr.log( + "logs", + rr.TextLog("this entry has loglevel TRACE", level=rr.TextLogLevel.TRACE), +) # Or log via a logging handler logging.getLogger().addHandler(rr.LoggingHandler("logs/handler")) diff --git a/docs/snippets/all/archetypes/text_log_integration.rs b/docs/snippets/all/archetypes/text_log_integration.rs index 9f71c49ffb7d..7b694990ad56 100644 --- a/docs/snippets/all/archetypes/text_log_integration.rs +++ b/docs/snippets/all/archetypes/text_log_integration.rs @@ -3,7 +3,10 @@ use rerun::external::log; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_text_log_integration").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_text_log_integration", + ) + .spawn()?; // Log a text entry directly: rec.log( @@ -18,7 +21,9 @@ fn main() -> Result<(), Box> { // You can also use the standard `RUST_LOG` environment variable! .with_filter(rerun::default_log_filter()) .init()?; - log::info!("This INFO log got added through the standard logging interface"); + log::info!( + "This INFO log got added through the standard logging interface" + ); log::logger().flush(); diff --git a/docs/snippets/all/archetypes/transform3d_axes.cpp b/docs/snippets/all/archetypes/transform3d_axes.cpp index 1ede21b34dfa..12bfa35fa136 100644 --- a/docs/snippets/all/archetypes/transform3d_axes.cpp +++ b/docs/snippets/all/archetypes/transform3d_axes.cpp @@ -15,10 +15,12 @@ int main(int argc, char* argv[]) { rec.log( "base/rotated", - rerun::Transform3D().with_rotation_axis_angle(rerun::RotationAxisAngle( - {1.0f, 1.0f, 1.0f}, - rerun::Angle::degrees(static_cast(deg)) - )), + rerun::Transform3D().with_rotation_axis_angle( + rerun::RotationAxisAngle( + {1.0f, 1.0f, 1.0f}, + rerun::Angle::degrees(static_cast(deg)) + ) + ), rerun::TransformAxes3D(0.5) ); diff --git a/docs/snippets/all/archetypes/transform3d_axes.rs b/docs/snippets/all/archetypes/transform3d_axes.rs index 16d5a1a9a1d7..d8d2b9c3a29a 100644 --- a/docs/snippets/all/archetypes/transform3d_axes.rs +++ b/docs/snippets/all/archetypes/transform3d_axes.rs @@ -3,7 +3,9 @@ use rerun::AsComponents; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_transform3d_axes").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_transform3d_axes") + .spawn()?; rec.set_time_sequence("step", 0); @@ -20,17 +22,20 @@ fn main() -> Result<(), Box> { rec.log( "base/rotated", &[ - &rerun::Transform3D::new().with_rotation(rerun::RotationAxisAngle::new( - [1.0, 1.0, 1.0], - rerun::Angle::from_degrees(deg as f32), - )) as &dyn AsComponents, + &rerun::Transform3D::new().with_rotation( + rerun::RotationAxisAngle::new( + [1.0, 1.0, 1.0], + rerun::Angle::from_degrees(deg as f32), + ), + ) as &dyn AsComponents, &rerun::TransformAxes3D::new(0.5), ], )?; rec.log( "base/rotated/translated", &[ - &rerun::Transform3D::new().with_translation([2.0, 0.0, 0.0]) as &dyn AsComponents, + &rerun::Transform3D::new().with_translation([2.0, 0.0, 0.0]) + as &dyn AsComponents, &rerun::TransformAxes3D::new(0.5), ], )?; diff --git a/docs/snippets/all/archetypes/transform3d_column_updates.cpp b/docs/snippets/all/archetypes/transform3d_column_updates.cpp index e519e8206a62..080847c15fe0 100644 --- a/docs/snippets/all/archetypes/transform3d_column_updates.cpp +++ b/docs/snippets/all/archetypes/transform3d_column_updates.cpp @@ -11,17 +11,20 @@ float truncated_radians(int deg) { auto degf = static_cast(deg); const auto pi = 3.14159265358979323846f; - return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / 1000.0f; + return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / + 1000.0f; } int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_transform3d_column_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_transform3d_column_updates"); rec.spawn().exit_on_failure(); rec.set_time_sequence("tick", 0); rec.log( "box", - rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}}).with_fill_mode(rerun::FillMode::Solid), + rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}} + ).with_fill_mode(rerun::FillMode::Solid), rerun::TransformAxes3D(10.0) ); diff --git a/docs/snippets/all/archetypes/transform3d_column_updates.py b/docs/snippets/all/archetypes/transform3d_column_updates.py index 989b18130a7e..ee122aa1f8ed 100644 --- a/docs/snippets/all/archetypes/transform3d_column_updates.py +++ b/docs/snippets/all/archetypes/transform3d_column_updates.py @@ -1,7 +1,8 @@ """ Update a transform over time, in a single operation. -This is semantically equivalent to the `transform3d_row_updates` example, albeit much faster. +This is semantically equivalent to the `transform3d_row_updates` example, +albeit much faster. """ import math @@ -18,7 +19,9 @@ def truncated_radians(deg: float) -> float: rr.set_time("tick", sequence=0) rr.log( "box", - rr.Boxes3D(half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid), + rr.Boxes3D( + half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid + ), rr.TransformAxes3D(10.0), ) @@ -28,7 +31,10 @@ def truncated_radians(deg: float) -> float: columns=rr.Transform3D.columns( translation=[[0, 0, t / 10.0] for t in range(100)], rotation_axis_angle=[ - rr.RotationAxisAngle(axis=[0.0, 1.0, 0.0], radians=truncated_radians(t * 4)) for t in range(100) + rr.RotationAxisAngle( + axis=[0.0, 1.0, 0.0], radians=truncated_radians(t * 4) + ) + for t in range(100) ], ), ) diff --git a/docs/snippets/all/archetypes/transform3d_column_updates.rs b/docs/snippets/all/archetypes/transform3d_column_updates.rs index c71e00e1e6d7..6201fb67ac6c 100644 --- a/docs/snippets/all/archetypes/transform3d_column_updates.rs +++ b/docs/snippets/all/archetypes/transform3d_column_updates.rs @@ -3,23 +3,32 @@ //! This is semantically equivalent to the `transform3d_row_updates` example, albeit much faster. fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_transform3d_column_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_transform3d_column_updates", + ) + .spawn()?; rec.set_time_sequence("tick", 0); rec.log( "box", &[ &rerun::Boxes3D::from_half_sizes([(4.0, 2.0, 1.0)]) - .with_fill_mode(rerun::FillMode::Solid) as &dyn rerun::AsComponents, + .with_fill_mode(rerun::FillMode::Solid) + as &dyn rerun::AsComponents, &rerun::TransformAxes3D::new(10.0), ], )?; let translations = (0..100).map(|t| [0.0, 0.0, t as f32 / 10.0]); - let rotations = (0..100) - .map(|t| truncated_radians((t * 4) as f32)) - .map(|rad| rerun::RotationAxisAngle::new([0.0, 1.0, 0.0], rerun::Angle::from_radians(rad))); + let rotations = + (0..100) + .map(|t| truncated_radians((t * 4) as f32)) + .map(|rad| { + rerun::RotationAxisAngle::new( + [0.0, 1.0, 0.0], + rerun::Angle::from_radians(rad), + ) + }); let ticks = rerun::TimeColumn::new_sequence("tick", 1..101); rec.send_columns( diff --git a/docs/snippets/all/archetypes/transform3d_hierarchy.cpp b/docs/snippets/all/archetypes/transform3d_hierarchy.cpp index bf63b5bf187d..10fbd5484fd3 100644 --- a/docs/snippets/all/archetypes/transform3d_hierarchy.cpp +++ b/docs/snippets/all/archetypes/transform3d_hierarchy.cpp @@ -5,7 +5,8 @@ constexpr float TAU = 6.28318530717958647692528676655900577f; int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_transform3d_hierarchy"); + const auto rec = + rerun::RecordingStream("rerun_example_transform3d_hierarchy"); rec.spawn().exit_on_failure(); // TODO(#5521): log two views as in the python example @@ -18,16 +19,22 @@ int main(int argc, char* argv[]) { // Setup spheres, all are in the center of their own space: rec.log( "sun", - rerun::Ellipsoids3D::from_centers_and_half_sizes({{0.0f, 0.0f, 0.0f}}, {{1.0f, 1.0f, 1.0f}}) + rerun::Ellipsoids3D::from_centers_and_half_sizes( + {{0.0f, 0.0f, 0.0f}}, + {{1.0f, 1.0f, 1.0f}} + ) .with_colors(rerun::Color(255, 200, 10)) - .with_fill_mode(rerun::components::FillMode::Solid) + .with_fill_mode(rerun::FillMode::Solid) ); rec.log( "sun/planet", - rerun::Ellipsoids3D::from_centers_and_half_sizes({{0.0f, 0.0f, 0.0f}}, {{0.4f, 0.4f, 0.4f}}) + rerun::Ellipsoids3D::from_centers_and_half_sizes( + {{0.0f, 0.0f, 0.0f}}, + {{0.4f, 0.4f, 0.4f}} + ) .with_colors(rerun::Color(40, 80, 200)) - .with_fill_mode(rerun::components::FillMode::Solid) + .with_fill_mode(rerun::FillMode::Solid) ); rec.log( @@ -37,7 +44,7 @@ int main(int argc, char* argv[]) { {{0.15f, 0.15f, 0.15f}} ) .with_colors(rerun::Color(180, 180, 180)) - .with_fill_mode(rerun::components::FillMode::Solid) + .with_fill_mode(rerun::FillMode::Solid) ); // Draw fixed paths where the planet & moon move. @@ -51,8 +58,14 @@ int main(int argc, char* argv[]) { planet_path.push_back({circle_x * d_planet, circle_y * d_planet, 0.0f}); moon_path.push_back({circle_x * d_moon, circle_y * d_moon, 0.0f}); } - rec.log("sun/planet_path", rerun::LineStrips3D(rerun::LineStrip3D(planet_path))); - rec.log("sun/planet/moon_path", rerun::LineStrips3D(rerun::LineStrip3D(moon_path))); + rec.log( + "sun/planet_path", + rerun::LineStrips3D(rerun::LineStrip3D(planet_path)) + ); + rec.log( + "sun/planet/moon_path", + rerun::LineStrips3D(rerun::LineStrip3D(moon_path)) + ); // Movement via transforms. for (int i = 0; i < 6 * 120; i++) { @@ -64,7 +77,9 @@ int main(int argc, char* argv[]) { rec.log( "sun/planet", rerun::Transform3D::from_translation_rotation( - {std::sin(r_planet) * d_planet, std::cos(r_planet) * d_planet, 0.0f}, + {std::sin(r_planet) * d_planet, + std::cos(r_planet) * d_planet, + 0.0f}, rerun::RotationAxisAngle{ {1.0, 0.0f, 0.0f}, rerun::Angle::degrees(20.0f), @@ -76,7 +91,7 @@ int main(int argc, char* argv[]) { rerun::Transform3D::from_translation( {std::cos(r_moon) * d_moon, std::sin(r_moon) * d_moon, 0.0f} ) - .with_relation(rerun::components::TransformRelation::ChildFromParent) + .with_relation(rerun::TransformRelation::ChildFromParent) ); } } diff --git a/docs/snippets/all/archetypes/transform3d_hierarchy.py b/docs/snippets/all/archetypes/transform3d_hierarchy.py index 60662ea75a0b..cd2f5024ff82 100644 --- a/docs/snippets/all/archetypes/transform3d_hierarchy.py +++ b/docs/snippets/all/archetypes/transform3d_hierarchy.py @@ -9,9 +9,13 @@ if False: # One space with the sun in the center, and another one with the planet. - # TODO(#5521): enable this once we have it in Rust too, so that the snippets compare equally + # TODO(#5521): enable this once we have it in Rust too, so that the + # snippets compare equally rr.send_blueprint( - rrb.Horizontal(rrb.Spatial3DView(origin="sun"), rrb.Spatial3DView(origin="sun/planet", contents="sun/**")), + rrb.Horizontal( + rrb.Spatial3DView(origin="sun"), + rrb.Spatial3DView(origin="sun/planet", contents="sun/**"), + ), ) rr.set_time("sim_time", duration=0) @@ -54,7 +58,9 @@ d_planet = 6.0 d_moon = 3.0 angles = np.arange(0.0, 1.01, 0.01) * np.pi * 2 -circle = np.array([np.sin(angles), np.cos(angles), angles * 0.0], dtype=np.float32).transpose() +circle = np.array( + [np.sin(angles), np.cos(angles), angles * 0.0], dtype=np.float32 +).transpose() rr.log("sun/planet_path", rr.LineStrips3D(circle * d_planet)) rr.log("sun/planet/moon_path", rr.LineStrips3D(circle * d_moon)) @@ -68,7 +74,11 @@ rr.log( "sun/planet", rr.Transform3D( - translation=[np.sin(r_planet) * d_planet, np.cos(r_planet) * d_planet, 0.0], + translation=[ + np.sin(r_planet) * d_planet, + np.cos(r_planet) * d_planet, + 0.0, + ], rotation=rr.RotationAxisAngle(axis=(1, 0, 0), degrees=20), ), ) diff --git a/docs/snippets/all/archetypes/transform3d_hierarchy.rs b/docs/snippets/all/archetypes/transform3d_hierarchy.rs index d4b30dce9de6..c6693d5f5b6a 100644 --- a/docs/snippets/all/archetypes/transform3d_hierarchy.rs +++ b/docs/snippets/all/archetypes/transform3d_hierarchy.rs @@ -1,7 +1,10 @@ //! Log different transforms between three arrows. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_transform3d_hierarchy").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_transform3d_hierarchy", + ) + .spawn()?; // TODO(#5521): log two views as in the python example @@ -13,30 +16,40 @@ fn main() -> Result<(), Box> { // Setup spheres, all are in the center of their own space: rec.log( "sun", - &rerun::Ellipsoids3D::from_centers_and_half_sizes([[0.0, 0.0, 0.0]], [[1.0, 1.0, 1.0]]) - .with_colors([rerun::Color::from_rgb(255, 200, 10)]) - .with_fill_mode(rerun::components::FillMode::Solid), + &rerun::Ellipsoids3D::from_centers_and_half_sizes( + [[0.0, 0.0, 0.0]], + [[1.0, 1.0, 1.0]], + ) + .with_colors([rerun::Color::from_rgb(255, 200, 10)]) + .with_fill_mode(rerun::components::FillMode::Solid), )?; rec.log( "sun/planet", - &rerun::Ellipsoids3D::from_centers_and_half_sizes([[0.0, 0.0, 0.0]], [[0.4, 0.4, 0.4]]) - .with_colors([rerun::Color::from_rgb(40, 80, 200)]) - .with_fill_mode(rerun::components::FillMode::Solid), + &rerun::Ellipsoids3D::from_centers_and_half_sizes( + [[0.0, 0.0, 0.0]], + [[0.4, 0.4, 0.4]], + ) + .with_colors([rerun::Color::from_rgb(40, 80, 200)]) + .with_fill_mode(rerun::components::FillMode::Solid), )?; rec.log( "sun/planet/moon", - &rerun::Ellipsoids3D::from_centers_and_half_sizes([[0.0, 0.0, 0.0]], [[0.15, 0.15, 0.15]]) - .with_colors([rerun::Color::from_rgb(180, 180, 180)]) - .with_fill_mode(rerun::components::FillMode::Solid), + &rerun::Ellipsoids3D::from_centers_and_half_sizes( + [[0.0, 0.0, 0.0]], + [[0.15, 0.15, 0.15]], + ) + .with_colors([rerun::Color::from_rgb(180, 180, 180)]) + .with_fill_mode(rerun::components::FillMode::Solid), )?; // Draw fixed paths where the planet & moon move. let d_planet = 6.0; let d_moon = 3.0; let angles = (0..=100).map(|i| i as f32 * 0.01 * std::f32::consts::TAU); - let circle: Vec<_> = angles.map(|angle| [angle.sin(), angle.cos()]).collect(); + let circle: Vec<_> = + angles.map(|angle| [angle.sin(), angle.cos()]).collect(); rec.log( "sun/planet_path", &rerun::LineStrips3D::new([rerun::LineStrip3D::from_iter( diff --git a/docs/snippets/all/archetypes/transform3d_hierarchy_frames.cpp b/docs/snippets/all/archetypes/transform3d_hierarchy_frames.cpp index 1c085616f50d..e78664326226 100644 --- a/docs/snippets/all/archetypes/transform3d_hierarchy_frames.cpp +++ b/docs/snippets/all/archetypes/transform3d_hierarchy_frames.cpp @@ -5,7 +5,8 @@ constexpr float TAU = 6.28318530717958647692528676655900577f; int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_transform3d_hierarchy_frames"); + const auto rec = + rerun::RecordingStream("rerun_example_transform3d_hierarchy_frames"); rec.spawn().exit_on_failure(); rec.set_time_duration_secs("sim_time", 0.0); @@ -16,17 +17,23 @@ int main(int argc, char* argv[]) { // Setup spheres, all are in the center of their own space: rec.log( "sun", - rerun::Ellipsoids3D::from_centers_and_half_sizes({{0.0f, 0.0f, 0.0f}}, {{1.0f, 1.0f, 1.0f}}) + rerun::Ellipsoids3D::from_centers_and_half_sizes( + {{0.0f, 0.0f, 0.0f}}, + {{1.0f, 1.0f, 1.0f}} + ) .with_colors(rerun::Color(255, 200, 10)) - .with_fill_mode(rerun::components::FillMode::Solid), + .with_fill_mode(rerun::FillMode::Solid), rerun::CoordinateFrame("sun_frame") ); rec.log( "planet", - rerun::Ellipsoids3D::from_centers_and_half_sizes({{0.0f, 0.0f, 0.0f}}, {{0.4f, 0.4f, 0.4f}}) + rerun::Ellipsoids3D::from_centers_and_half_sizes( + {{0.0f, 0.0f, 0.0f}}, + {{0.4f, 0.4f, 0.4f}} + ) .with_colors(rerun::Color(40, 80, 200)) - .with_fill_mode(rerun::components::FillMode::Solid), + .with_fill_mode(rerun::FillMode::Solid), rerun::CoordinateFrame("planet_frame") ); @@ -37,7 +44,7 @@ int main(int argc, char* argv[]) { {{0.15f, 0.15f, 0.15f}} ) .with_colors(rerun::Color(180, 180, 180)) - .with_fill_mode(rerun::components::FillMode::Solid), + .with_fill_mode(rerun::FillMode::Solid), rerun::CoordinateFrame("moon_frame") ); @@ -77,7 +84,9 @@ int main(int argc, char* argv[]) { rec.log( "planet_transforms", rerun::Transform3D::from_translation_rotation( - {std::sin(r_planet) * d_planet, std::cos(r_planet) * d_planet, 0.0f}, + {std::sin(r_planet) * d_planet, + std::cos(r_planet) * d_planet, + 0.0f}, rerun::RotationAxisAngle{ {1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(20.0f), @@ -91,7 +100,7 @@ int main(int argc, char* argv[]) { rerun::Transform3D::from_translation( {std::cos(r_moon) * d_moon, std::sin(r_moon) * d_moon, 0.0f} ) - .with_relation(rerun::components::TransformRelation::ChildFromParent) + .with_relation(rerun::TransformRelation::ChildFromParent) .with_child_frame("moon_frame") .with_parent_frame("planet_frame") ); diff --git a/docs/snippets/all/archetypes/transform3d_hierarchy_frames.py b/docs/snippets/all/archetypes/transform3d_hierarchy_frames.py index a08503e0d3ea..084d09a4c601 100644 --- a/docs/snippets/all/archetypes/transform3d_hierarchy_frames.py +++ b/docs/snippets/all/archetypes/transform3d_hierarchy_frames.py @@ -45,17 +45,29 @@ rr.CoordinateFrame("moon_frame"), ) -# The viewer automatically creates a 3D view at `/`. To connect it to our transform hierarchy, we set its coordinate frame -# to `sun_frame` as well. Alternatively, we could also set a blueprint that makes `/sun` the space origin. +# The viewer automatically creates a 3D view at `/`. To connect it to our +# transform hierarchy, we set its coordinate frame to `sun_frame` as well. +# Alternatively, we could also set a blueprint that makes `/sun` the space +# origin. rr.log("/", rr.CoordinateFrame("sun_frame")) # Draw fixed paths where the planet & moon move. d_planet = 6.0 d_moon = 3.0 angles = np.arange(0.0, 1.01, 0.01) * np.pi * 2 -circle = np.array([np.sin(angles), np.cos(angles), angles * 0.0], dtype=np.float32).transpose() -rr.log("planet_path", rr.LineStrips3D(circle * d_planet), rr.CoordinateFrame("sun_frame")) -rr.log("moon_path", rr.LineStrips3D(circle * d_moon), rr.CoordinateFrame("planet_frame")) +circle = np.array( + [np.sin(angles), np.cos(angles), angles * 0.0], dtype=np.float32 +).transpose() +rr.log( + "planet_path", + rr.LineStrips3D(circle * d_planet), + rr.CoordinateFrame("sun_frame"), +) +rr.log( + "moon_path", + rr.LineStrips3D(circle * d_moon), + rr.CoordinateFrame("planet_frame"), +) # Movement via transforms. for i in range(6 * 120): @@ -67,7 +79,11 @@ rr.log( "planet_transforms", rr.Transform3D( - translation=[np.sin(r_planet) * d_planet, np.cos(r_planet) * d_planet, 0.0], + translation=[ + np.sin(r_planet) * d_planet, + np.cos(r_planet) * d_planet, + 0.0, + ], rotation=rr.RotationAxisAngle(axis=(1, 0, 0), degrees=20), child_frame="planet_frame", parent_frame="sun_frame", diff --git a/docs/snippets/all/archetypes/transform3d_hierarchy_frames.rs b/docs/snippets/all/archetypes/transform3d_hierarchy_frames.rs index 4ba1d2ccc24f..59dbcb761751 100644 --- a/docs/snippets/all/archetypes/transform3d_hierarchy_frames.rs +++ b/docs/snippets/all/archetypes/transform3d_hierarchy_frames.rs @@ -1,8 +1,10 @@ //! Logs a transform hierarchy using named transform frame relationships. fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_transform3d_hierarchy_frames").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_transform3d_hierarchy_frames", + ) + .spawn()?; rec.set_duration_secs("sim_time", 0.0); @@ -13,9 +15,12 @@ fn main() -> Result<(), Box> { rec.log( "sun", &[ - &rerun::Ellipsoids3D::from_centers_and_half_sizes([[0.0, 0.0, 0.0]], [[1.0, 1.0, 1.0]]) - .with_colors([rerun::Color::from_rgb(255, 200, 10)]) - .with_fill_mode(rerun::components::FillMode::Solid) + &rerun::Ellipsoids3D::from_centers_and_half_sizes( + [[0.0, 0.0, 0.0]], + [[1.0, 1.0, 1.0]], + ) + .with_colors([rerun::Color::from_rgb(255, 200, 10)]) + .with_fill_mode(rerun::components::FillMode::Solid) as &dyn rerun::AsComponents, &rerun::CoordinateFrame::new("sun_frame"), ], @@ -24,9 +29,12 @@ fn main() -> Result<(), Box> { rec.log( "planet", &[ - &rerun::Ellipsoids3D::from_centers_and_half_sizes([[0.0, 0.0, 0.0]], [[0.4, 0.4, 0.4]]) - .with_colors([rerun::Color::from_rgb(40, 80, 200)]) - .with_fill_mode(rerun::components::FillMode::Solid) + &rerun::Ellipsoids3D::from_centers_and_half_sizes( + [[0.0, 0.0, 0.0]], + [[0.4, 0.4, 0.4]], + ) + .with_colors([rerun::Color::from_rgb(40, 80, 200)]) + .with_fill_mode(rerun::components::FillMode::Solid) as &dyn rerun::AsComponents, &rerun::CoordinateFrame::new("planet_frame"), ], @@ -54,7 +62,8 @@ fn main() -> Result<(), Box> { let d_planet = 6.0; let d_moon = 3.0; let angles = (0..=100).map(|i| i as f32 * 0.01 * std::f32::consts::TAU); - let circle: Vec<_> = angles.map(|angle| [angle.sin(), angle.cos()]).collect(); + let circle: Vec<_> = + angles.map(|angle| [angle.sin(), angle.cos()]).collect(); rec.log( "planet_path", &[ diff --git a/docs/snippets/all/archetypes/transform3d_partial_updates.cpp b/docs/snippets/all/archetypes/transform3d_partial_updates.cpp index 58a44422addd..ef23cad08a50 100644 --- a/docs/snippets/all/archetypes/transform3d_partial_updates.cpp +++ b/docs/snippets/all/archetypes/transform3d_partial_updates.cpp @@ -5,17 +5,20 @@ float truncated_radians(int deg) { auto degf = static_cast(deg); const auto pi = 3.14159265358979323846f; - return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / 1000.0f; + return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / + 1000.0f; } int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_transform3d_partial_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_transform3d_partial_updates"); rec.spawn().exit_on_failure(); // Set up a 3D box. rec.log( "box", - rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}}).with_fill_mode(rerun::FillMode::Solid) + rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}} + ).with_fill_mode(rerun::FillMode::Solid) ); // Update only the rotation of the box. @@ -23,9 +26,10 @@ int main(int argc, char* argv[]) { auto rad = truncated_radians(deg * 4); rec.log( "box", - rerun::Transform3D::from_rotation( - rerun::RotationAxisAngle({0.0f, 1.0f, 0.0f}, rerun::Angle::radians(rad)) - ) + rerun::Transform3D::from_rotation(rerun::RotationAxisAngle( + {0.0f, 1.0f, 0.0f}, + rerun::Angle::radians(rad) + )) ); } @@ -33,7 +37,9 @@ int main(int argc, char* argv[]) { for (int t = 0; t <= 50; t++) { rec.log( "box", - rerun::Transform3D::from_translation({0.0f, 0.0f, static_cast(t) / 10.0f}) + rerun::Transform3D::from_translation( + {0.0f, 0.0f, static_cast(t) / 10.0f} + ) ); } @@ -42,9 +48,10 @@ int main(int argc, char* argv[]) { auto rad = truncated_radians((deg + 45) * 4); rec.log( "box", - rerun::Transform3D::from_rotation( - rerun::RotationAxisAngle({0.0f, 1.0f, 0.0f}, rerun::Angle::radians(rad)) - ) + rerun::Transform3D::from_rotation(rerun::RotationAxisAngle( + {0.0f, 1.0f, 0.0f}, + rerun::Angle::radians(rad) + )) ); } diff --git a/docs/snippets/all/archetypes/transform3d_partial_updates.py b/docs/snippets/all/archetypes/transform3d_partial_updates.py index a6f91e33060e..f22ac691f506 100644 --- a/docs/snippets/all/archetypes/transform3d_partial_updates.py +++ b/docs/snippets/all/archetypes/transform3d_partial_updates.py @@ -14,7 +14,9 @@ def truncated_radians(deg: float) -> float: # Set up a 3D box. rr.log( "box", - rr.Boxes3D(half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid), + rr.Boxes3D( + half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid + ), ) # Update only the rotation of the box. @@ -23,7 +25,9 @@ def truncated_radians(deg: float) -> float: rr.log( "box", rr.Transform3D.from_fields( - rotation_axis_angle=rr.RotationAxisAngle(axis=[0.0, 1.0, 0.0], radians=rad), + rotation_axis_angle=rr.RotationAxisAngle( + axis=[0.0, 1.0, 0.0], radians=rad + ), ), ) @@ -40,7 +44,9 @@ def truncated_radians(deg: float) -> float: rr.log( "box", rr.Transform3D.from_fields( - rotation_axis_angle=rr.RotationAxisAngle(axis=[0.0, 1.0, 0.0], radians=rad), + rotation_axis_angle=rr.RotationAxisAngle( + axis=[0.0, 1.0, 0.0], radians=rad + ), ), ) diff --git a/docs/snippets/all/archetypes/transform3d_partial_updates.rs b/docs/snippets/all/archetypes/transform3d_partial_updates.rs index 46bfe6567851..528730fb2f01 100644 --- a/docs/snippets/all/archetypes/transform3d_partial_updates.rs +++ b/docs/snippets/all/archetypes/transform3d_partial_updates.rs @@ -3,16 +3,17 @@ use rerun::AsComponents; fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_transform3d_partial_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_transform3d_partial_updates", + ) + .spawn()?; // Set up a 3D box. rec.log( "box", - &[ - &rerun::Boxes3D::from_half_sizes([(4.0, 2.0, 1.0)]) - .with_fill_mode(rerun::FillMode::Solid) as &dyn AsComponents, - ], + &[&rerun::Boxes3D::from_half_sizes([(4.0, 2.0, 1.0)]) + .with_fill_mode(rerun::FillMode::Solid) + as &dyn AsComponents], )?; // Update only the rotation of the box. @@ -20,10 +21,12 @@ fn main() -> Result<(), Box> { let rad = truncated_radians((deg * 4) as f32); rec.log( "box", - &rerun::Transform3D::new().with_rotation(rerun::RotationAxisAngle::new( - [0.0, 1.0, 0.0], - rerun::Angle::from_radians(rad), - )), + &rerun::Transform3D::new().with_rotation( + rerun::RotationAxisAngle::new( + [0.0, 1.0, 0.0], + rerun::Angle::from_radians(rad), + ), + ), )?; } @@ -31,7 +34,11 @@ fn main() -> Result<(), Box> { for t in 0..=50 { rec.log( "box", - &rerun::Transform3D::new().with_translation([0.0, 0.0, t as f32 / 10.0]), + &rerun::Transform3D::new().with_translation([ + 0.0, + 0.0, + t as f32 / 10.0, + ]), )?; } @@ -40,10 +47,12 @@ fn main() -> Result<(), Box> { let rad = truncated_radians(((deg + 45) * 4) as f32); rec.log( "box", - &rerun::Transform3D::new().with_rotation(rerun::RotationAxisAngle::new( - [0.0, 1.0, 0.0], - rerun::Angle::from_radians(rad), - )), + &rerun::Transform3D::new().with_rotation( + rerun::RotationAxisAngle::new( + [0.0, 1.0, 0.0], + rerun::Angle::from_radians(rad), + ), + ), )?; } diff --git a/docs/snippets/all/archetypes/transform3d_row_updates.cpp b/docs/snippets/all/archetypes/transform3d_row_updates.cpp index 317636e13e5b..97a83d9ffc1e 100644 --- a/docs/snippets/all/archetypes/transform3d_row_updates.cpp +++ b/docs/snippets/all/archetypes/transform3d_row_updates.cpp @@ -7,17 +7,20 @@ float truncated_radians(int deg) { auto degf = static_cast(deg); const auto pi = 3.14159265358979323846f; - return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / 1000.0f; + return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / + 1000.0f; } int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_transform3d_row_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_transform3d_row_updates"); rec.spawn().exit_on_failure(); rec.set_time_sequence("tick", 0); rec.log( "box", - rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}}).with_fill_mode(rerun::FillMode::Solid), + rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}} + ).with_fill_mode(rerun::FillMode::Solid), rerun::TransformAxes3D(10.0) ); diff --git a/docs/snippets/all/archetypes/transform3d_row_updates.py b/docs/snippets/all/archetypes/transform3d_row_updates.py index d30acf62e29d..665ff0f22d1c 100644 --- a/docs/snippets/all/archetypes/transform3d_row_updates.py +++ b/docs/snippets/all/archetypes/transform3d_row_updates.py @@ -1,7 +1,8 @@ """ Update a transform over time. -See also the `transform3d_column_updates` example, which achieves the same thing in a single operation. +See also the `transform3d_column_updates` example, which achieves the same +thing in a single operation. """ import math @@ -18,7 +19,9 @@ def truncated_radians(deg: float) -> float: rr.set_time("tick", sequence=0) rr.log( "box", - rr.Boxes3D(half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid), + rr.Boxes3D( + half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid + ), rr.TransformAxes3D(10.0), ) @@ -28,6 +31,8 @@ def truncated_radians(deg: float) -> float: "box", rr.Transform3D( translation=[0, 0, t / 10.0], - rotation_axis_angle=rr.RotationAxisAngle(axis=[0.0, 1.0, 0.0], radians=truncated_radians(t * 4)), + rotation_axis_angle=rr.RotationAxisAngle( + axis=[0.0, 1.0, 0.0], radians=truncated_radians(t * 4) + ), ), ) diff --git a/docs/snippets/all/archetypes/transform3d_row_updates.rs b/docs/snippets/all/archetypes/transform3d_row_updates.rs index 2b320200b685..1a54d8715fc7 100644 --- a/docs/snippets/all/archetypes/transform3d_row_updates.rs +++ b/docs/snippets/all/archetypes/transform3d_row_updates.rs @@ -3,15 +3,18 @@ //! See also the `transform3d_column_updates` example, which achieves the same thing in a single operation. fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_transform3d_row_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_transform3d_row_updates", + ) + .spawn()?; rec.set_time_sequence("tick", 0); rec.log( "box", &[ &rerun::Boxes3D::from_half_sizes([(4.0, 2.0, 1.0)]) - .with_fill_mode(rerun::FillMode::Solid) as &dyn rerun::AsComponents, + .with_fill_mode(rerun::FillMode::Solid) + as &dyn rerun::AsComponents, &rerun::TransformAxes3D::new(10.0), ], )?; @@ -24,7 +27,9 @@ fn main() -> Result<(), Box> { .with_translation([0.0, 0.0, t as f32 / 10.0]) .with_rotation(rerun::RotationAxisAngle::new( [0.0, 1.0, 0.0], - rerun::Angle::from_radians(truncated_radians((t * 4) as f32)), + rerun::Angle::from_radians(truncated_radians( + (t * 4) as f32, + )), )), )?; } diff --git a/docs/snippets/all/archetypes/transform3d_simple.cpp b/docs/snippets/all/archetypes/transform3d_simple.cpp index 5c4161c64df4..200aadc7110b 100644 --- a/docs/snippets/all/archetypes/transform3d_simple.cpp +++ b/docs/snippets/all/archetypes/transform3d_simple.cpp @@ -8,18 +8,24 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_transform3d"); rec.spawn().exit_on_failure(); - auto arrow = - rerun::Arrows3D::from_vectors({{0.0f, 1.0f, 0.0f}}).with_origins({{0.0f, 0.0f, 0.0f}}); + auto arrow = rerun::Arrows3D::from_vectors({{0.0f, 1.0f, 0.0f}} + ).with_origins({{0.0f, 0.0f, 0.0f}}); rec.log("base", arrow); - rec.log("base/translated", rerun::Transform3D::from_translation({1.0f, 0.0f, 0.0f})); + rec.log( + "base/translated", + rerun::Transform3D::from_translation({1.0f, 0.0f, 0.0f}) + ); rec.log("base/translated", arrow); rec.log( "base/rotated_scaled", rerun::Transform3D::from_rotation_scale( - rerun::RotationAxisAngle({0.0f, 0.0f, 1.0f}, rerun::Angle::radians(TAU / 8.0f)), + rerun::RotationAxisAngle( + {0.0f, 0.0f, 1.0f}, + rerun::Angle::radians(TAU / 8.0f) + ), 2.0f ) ); diff --git a/docs/snippets/all/archetypes/transform3d_simple.rs b/docs/snippets/all/archetypes/transform3d_simple.rs index a96c68613480..78bf90e3b5e1 100644 --- a/docs/snippets/all/archetypes/transform3d_simple.rs +++ b/docs/snippets/all/archetypes/transform3d_simple.rs @@ -3,9 +3,11 @@ use std::f32::consts::TAU; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_transform3d").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_transform3d") + .spawn()?; - let arrow = rerun::Arrows3D::from_vectors([(0.0, 1.0, 0.0)]).with_origins([(0.0, 0.0, 0.0)]); + let arrow = rerun::Arrows3D::from_vectors([(0.0, 1.0, 0.0)]) + .with_origins([(0.0, 0.0, 0.0)]); rec.log("base", &arrow)?; @@ -19,7 +21,10 @@ fn main() -> Result<(), Box> { rec.log( "base/rotated_scaled", &rerun::Transform3D::from_rotation_scale( - rerun::RotationAxisAngle::new([0.0, 0.0, 1.0], rerun::Angle::from_radians(TAU / 8.0)), + rerun::RotationAxisAngle::new( + [0.0, 0.0, 1.0], + rerun::Angle::from_radians(TAU / 8.0), + ), rerun::Scale3D::from(2.0), ), )?; diff --git a/docs/snippets/all/archetypes/video_auto_frames.cpp b/docs/snippets/all/archetypes/video_auto_frames.cpp index a2a1cf499bfd..903e096cbeb3 100644 --- a/docs/snippets/all/archetypes/video_auto_frames.cpp +++ b/docs/snippets/all/archetypes/video_auto_frames.cpp @@ -9,13 +9,15 @@ using namespace std::chrono_literals; int main(int argc, char* argv[]) { if (argc < 2) { // TODO(#7354): Only mp4 is supported for now. - std::cerr << "Usage: " << argv[0] << " " << std::endl; + std::cerr << "Usage: " << argv[0] << " " + << std::endl; return 1; } const auto path = argv[1]; - const auto rec = rerun::RecordingStream("rerun_example_asset_video_auto_frames"); + const auto rec = + rerun::RecordingStream("rerun_example_asset_video_auto_frames"); rec.spawn().exit_on_failure(); // Log video asset which is referred to by frame references. @@ -26,17 +28,24 @@ int main(int argc, char* argv[]) { std::vector frame_timestamps_ns = video_asset.read_frame_timestamps_nanos().value_or_throw(); // Note timeline values don't have to be the same as the video timestamps. - auto time_column = - rerun::TimeColumn::from_durations("video_time", rerun::borrow(frame_timestamps_ns)); + auto time_column = rerun::TimeColumn::from_durations( + "video_time", + rerun::borrow(frame_timestamps_ns) + ); - std::vector video_timestamps(frame_timestamps_ns.size()); + std::vector video_timestamps( + frame_timestamps_ns.size() + ); for (size_t i = 0; i < frame_timestamps_ns.size(); i++) { - video_timestamps[i] = rerun::components::VideoTimestamp(frame_timestamps_ns[i]); + video_timestamps[i] = + rerun::components::VideoTimestamp(frame_timestamps_ns[i]); } rec.send_columns( "video", time_column, - rerun::VideoFrameReference().with_many_timestamp(rerun::borrow(video_timestamps)).columns() + rerun::VideoFrameReference() + .with_many_timestamp(rerun::borrow(video_timestamps)) + .columns() ); } diff --git a/docs/snippets/all/archetypes/video_auto_frames.rs b/docs/snippets/all/archetypes/video_auto_frames.rs index 9eeaaaebd9d3..f1e8b8bf79fd 100644 --- a/docs/snippets/all/archetypes/video_auto_frames.rs +++ b/docs/snippets/all/archetypes/video_auto_frames.rs @@ -9,8 +9,10 @@ fn main() -> anyhow::Result<()> { anyhow::bail!("Usage: {} ", args[0]); }; - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_asset_video_auto_frames").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_asset_video_auto_frames", + ) + .spawn()?; // Log video asset which is referred to by frame references. let video_asset = rerun::AssetVideo::from_file_path(path)?; diff --git a/docs/snippets/all/archetypes/video_manual_frames.cpp b/docs/snippets/all/archetypes/video_manual_frames.cpp index 2e0286ee9ae4..ca98fae58c9a 100644 --- a/docs/snippets/all/archetypes/video_manual_frames.cpp +++ b/docs/snippets/all/archetypes/video_manual_frames.cpp @@ -9,21 +9,32 @@ using namespace std::chrono_literals; int main(int argc, char* argv[]) { if (argc < 2) { // TODO(#7354): Only mp4 is supported for now. - std::cerr << "Usage: " << argv[0] << " " << std::endl; + std::cerr << "Usage: " << argv[0] << " " + << std::endl; return 1; } const auto path = argv[1]; - const auto rec = rerun::RecordingStream("rerun_example_asset_video_manual_frames"); + const auto rec = + rerun::RecordingStream("rerun_example_asset_video_manual_frames"); rec.spawn().exit_on_failure(); // Log video asset which is referred to by frame references. - rec.log_static("video_asset", rerun::AssetVideo::from_file(path).value_or_throw()); + rec.log_static( + "video_asset", + rerun::AssetVideo::from_file(path).value_or_throw() + ); // Create two entities, showing the same video frozen at different times. - rec.log("frame_1s", rerun::VideoFrameReference(1.0s).with_video_reference("video_asset")); - rec.log("frame_2s", rerun::VideoFrameReference(2.0s).with_video_reference("video_asset")); + rec.log( + "frame_1s", + rerun::VideoFrameReference(1.0s).with_video_reference("video_asset") + ); + rec.log( + "frame_2s", + rerun::VideoFrameReference(2.0s).with_video_reference("video_asset") + ); // TODO(#5520): log blueprint once supported } diff --git a/docs/snippets/all/archetypes/video_manual_frames.py b/docs/snippets/all/archetypes/video_manual_frames.py index a263e9e4afe5..88d8d1127fdf 100644 --- a/docs/snippets/all/archetypes/video_manual_frames.py +++ b/docs/snippets/all/archetypes/video_manual_frames.py @@ -26,4 +26,9 @@ ) # Send blueprint that shows two 2D views next to each other. -rr.send_blueprint(rrb.Horizontal(rrb.Spatial2DView(origin="frame_1s"), rrb.Spatial2DView(origin="frame_2s"))) +rr.send_blueprint( + rrb.Horizontal( + rrb.Spatial2DView(origin="frame_1s"), + rrb.Spatial2DView(origin="frame_2s"), + ) +) diff --git a/docs/snippets/all/archetypes/video_manual_frames.rs b/docs/snippets/all/archetypes/video_manual_frames.rs index d8732e020a18..3063d5032af8 100644 --- a/docs/snippets/all/archetypes/video_manual_frames.rs +++ b/docs/snippets/all/archetypes/video_manual_frames.rs @@ -9,8 +9,10 @@ fn main() -> anyhow::Result<()> { anyhow::bail!("Usage: {} ", args[0]); }; - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_asset_video_manual_frames").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_asset_video_manual_frames", + ) + .spawn()?; // Log video asset which is referred to by frame references. rec.log_static("video_asset", &rerun::AssetVideo::from_file_path(path)?)?; @@ -18,13 +20,17 @@ fn main() -> anyhow::Result<()> { // Create two entities, showing the same video frozen at different times. rec.log( "frame_1s", - &rerun::VideoFrameReference::new(rerun::components::VideoTimestamp::from_secs(1.0)) - .with_video_reference("video_asset"), + &rerun::VideoFrameReference::new( + rerun::components::VideoTimestamp::from_secs(1.0), + ) + .with_video_reference("video_asset"), )?; rec.log( "frame_2s", - &rerun::VideoFrameReference::new(rerun::components::VideoTimestamp::from_secs(2.0)) - .with_video_reference("video_asset"), + &rerun::VideoFrameReference::new( + rerun::components::VideoTimestamp::from_secs(2.0), + ) + .with_video_reference("video_asset"), )?; // TODO(#5520): log blueprint once supported diff --git a/docs/snippets/all/archetypes/video_stream_query_and_mux.py b/docs/snippets/all/archetypes/video_stream_query_and_mux.py index 62024f2cbeb4..bf616f788403 100644 --- a/docs/snippets/all/archetypes/video_stream_query_and_mux.py +++ b/docs/snippets/all/archetypes/video_stream_query_and_mux.py @@ -11,7 +11,9 @@ import rerun as rr -def read_h264_samples_from_rrd(rrd_path: str, video_entity: str, timeline: str) -> tuple[ChunkedArray, ChunkedArray]: +def read_h264_samples_from_rrd( + rrd_path: str, video_entity: str, timeline: str +) -> tuple[ChunkedArray, ChunkedArray]: """Load recording data and query video stream.""" server = rr.server.Server(datasets={"video_stream": [rrd_path]}) @@ -20,21 +22,34 @@ def read_h264_samples_from_rrd(rrd_path: str, video_entity: str, timeline: str) df = dataset.filter_contents(video_entity).reader(index=timeline) # Make sure this is H.264 encoded. - # For that we just read out the first codec value batch and check whether it's H.264. - first_codec_batch = df.select(f"/{video_entity}:VideoStream:codec").execute_stream().next() + # For that we just read out the first codec value batch and check whether + # it's H.264. + first_codec_batch = ( + df.select(f"/{video_entity}:VideoStream:codec").execute_stream().next() + ) if first_codec_batch is None: - raise ValueError(f"There's no video stream codec specified at {video_entity} for timeline {timeline}.") + raise ValueError( + f"There's no video stream codec specified at {video_entity} " + f"for timeline {timeline}." + ) codec_value = first_codec_batch.to_pyarrow().column(0)[0][0].as_py() if codec_value != rr.VideoCodec.H264.value: + h264 = hex(rr.VideoCodec.H264.value) raise ValueError( - f"Video stream codec is not H.264 at {video_entity} for timeline {timeline}. " - f"Got {hex(codec_value)}, but the value for H.264 is {hex(rr.VideoCodec.H264.value)}." + f"Video stream codec is not H.264 at {video_entity} for " + f"timeline {timeline}. " + f"Got {hex(codec_value)}, but the value for H.264 is {h264}." ) else: - print(f"Video stream codec is H.264 at {video_entity} for timeline {timeline}.") + print( + f"Video stream codec is H.264 at {video_entity} " + f"for timeline {timeline}." + ) # Get the video stream - timestamps_and_samples = df.select(timeline, f"/{video_entity}:VideoStream:sample").to_arrow_table() + timestamps_and_samples = df.select( + timeline, f"/{video_entity}:VideoStream:sample" + ).to_arrow_table() times = timestamps_and_samples[0] samples = timestamps_and_samples[1] @@ -43,7 +58,9 @@ def read_h264_samples_from_rrd(rrd_path: str, video_entity: str, timeline: str) return times, samples -def mux_h264_to_mp4(times: ChunkedArray, samples: ChunkedArray, output_path: str) -> None: +def mux_h264_to_mp4( + times: ChunkedArray, samples: ChunkedArray, output_path: str +) -> None: """Mux H.264 Annex B samples to an mp4 file using PyAV.""" # See https://pyav.basswood-io.com/docs/stable/cookbook/basics.html#remuxing @@ -52,7 +69,9 @@ def mux_h264_to_mp4(times: ChunkedArray, samples: ChunkedArray, output_path: str sample_bytes = io.BytesIO(sample_bytes.buffers()[1]) # Setup samples as input container. - input_container = av.open(sample_bytes, mode="r", format="h264") # Input is AnnexB H.264 stream. + input_container = av.open( + sample_bytes, mode="r", format="h264" + ) # Input is AnnexB H.264 stream. input_stream = input_container.streams.video[0] # Setup output container. @@ -64,8 +83,12 @@ def mux_h264_to_mp4(times: ChunkedArray, samples: ChunkedArray, output_path: str print(f"Offsetting timestamps with start time: {start_time}") # Demux and mux packets. - for packet, time in zip(input_container.demux(input_stream), times, strict=False): - packet.time_base = Fraction(1, 1_000_000_000) # Assuming duration timestamps in nanoseconds. + for packet, time in zip( + input_container.demux(input_stream), times, strict=False + ): + packet.time_base = Fraction( + 1, 1_000_000_000 + ) # Assuming duration timestamps in nanoseconds. packet.pts = int(time.value - start_time.value) packet.dts = packet.pts # dts == pts since there's no B-frames. packet.stream = output_stream @@ -76,20 +99,41 @@ def mux_h264_to_mp4(times: ChunkedArray, samples: ChunkedArray, output_path: str def main() -> None: - parser = argparse.ArgumentParser(description="Query video stream from a recording and mux it to an mp4 video file.") - parser.add_argument("input_rrd", type=str, help="Path to the input .rrd recording file") + parser = argparse.ArgumentParser( + description=( + "Query video stream from a recording and mux it to an mp4 " + "video file." + ) + ) parser.add_argument( - "-o", "--output", type=str, default="output.mp4", help="Output mp4 file path (default: output.mp4)" + "input_rrd", type=str, help="Path to the input .rrd recording file" ) parser.add_argument( - "--entity", type=str, default="video_stream", help="Video entity path to query (default: video_stream)" + "-o", + "--output", + type=str, + default="output.mp4", + help="Output mp4 file path (default: output.mp4)", + ) + parser.add_argument( + "--entity", + type=str, + default="video_stream", + help="Video entity path to query (default: video_stream)", + ) + parser.add_argument( + "--timeline", + type=str, + default="time", + help="Name of the timeline to query (default: time)", ) - parser.add_argument("--timeline", type=str, default="time", help="Name of the timeline to query (default: time)") args = parser.parse_args() # Load recording data print(f"Loading recording from: {args.input_rrd}") - times, samples = read_h264_samples_from_rrd(args.input_rrd, args.entity, args.timeline) + times, samples = read_h264_samples_from_rrd( + args.input_rrd, args.entity, args.timeline + ) print(f"Creating video file: {args.output}") mux_h264_to_mp4(times, samples, args.output) diff --git a/docs/snippets/all/archetypes/video_stream_synthetic.py b/docs/snippets/all/archetypes/video_stream_synthetic.py index d60255e5ef77..73bfb2d6c87d 100644 --- a/docs/snippets/all/archetypes/video_stream_synthetic.py +++ b/docs/snippets/all/archetypes/video_stream_synthetic.py @@ -20,7 +20,11 @@ def create_example_video_frame(frame_i: int) -> npt.NDArray[np.uint8]: img = np.zeros((height, width, 3), dtype=np.uint8) for h in range(height): - img[h, :] = [0, int(100 * h / height), int(200 * h / height)] # Blue to purple gradient. + img[h, :] = [ + 0, + int(100 * h / height), + int(200 * h / height), + ] # Blue to purple gradient. x_pos = width // 2 # Center horizontally. y_pos = height // 2 + 80 * np.sin(2 * np.pi * frame_i / fps) @@ -35,17 +39,21 @@ def create_example_video_frame(frame_i: int) -> npt.NDArray[np.uint8]: # Setup encoding pipeline. av.logging.set_level(av.logging.VERBOSE) -container = av.open("/dev/null", "w", format=formats[codec]) # Use AnnexB H.265 stream. +container = av.open( + "/dev/null", "w", format=formats[codec] +) # Use AnnexB H.265 stream. stream = container.add_stream(encoders[codec], rate=fps) # Type narrowing assert isinstance(stream, av.video.stream.VideoStream) stream.width = width stream.height = height # TODO(#10090): Rerun Video Streams don't support b-frames yet. -# Note that b-frames are generally not recommended for low-latency streaming and may make logging more complex. +# Note that b-frames are generally not recommended for low-latency streaming +# and may make logging more complex. stream.max_b_frames = 0 -# Log codec only once as static data (it naturally never changes). This isn't strictly necessary, but good practice. +# Log codec only once as static data (it naturally never changes). +# This isn't strictly necessary, but good practice. rr.log("video_stream", rr.VideoStream(codec=codec), static=True) # Generate frames and stream them directly to Rerun. diff --git a/docs/snippets/all/archetypes/view_coordinates_simple.cpp b/docs/snippets/all/archetypes/view_coordinates_simple.cpp index cd0f81df5d8d..6a8b822e4844 100644 --- a/docs/snippets/all/archetypes/view_coordinates_simple.cpp +++ b/docs/snippets/all/archetypes/view_coordinates_simple.cpp @@ -6,10 +6,12 @@ int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_view_coordinates"); rec.spawn().exit_on_failure(); - rec.log_static("world", rerun::ViewCoordinates::RIGHT_HAND_Z_UP); // Set an up-axis + // Set an up-axis: + rec.log_static("world", rerun::ViewCoordinates::RIGHT_HAND_Z_UP); rec.log( "world/xyz", - rerun::Arrows3D::from_vectors({{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}} + rerun::Arrows3D::from_vectors( + {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}} ).with_colors({{255, 0, 0}, {0, 255, 0}, {0, 0, 255}}) ); } diff --git a/docs/snippets/all/archetypes/view_coordinates_simple.py b/docs/snippets/all/archetypes/view_coordinates_simple.py index 208efc8499fb..30b24b653c5b 100644 --- a/docs/snippets/all/archetypes/view_coordinates_simple.py +++ b/docs/snippets/all/archetypes/view_coordinates_simple.py @@ -4,7 +4,9 @@ rr.init("rerun_example_view_coordinates", spawn=True) -rr.log("world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True) # Set an up-axis +rr.log( + "world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True +) # Set an up-axis rr.log( "world/xyz", rr.Arrows3D( diff --git a/docs/snippets/all/archetypes/view_coordinates_simple.rs b/docs/snippets/all/archetypes/view_coordinates_simple.rs index 871891825e14..189adae6e411 100644 --- a/docs/snippets/all/archetypes/view_coordinates_simple.rs +++ b/docs/snippets/all/archetypes/view_coordinates_simple.rs @@ -1,7 +1,9 @@ //! Change the view coordinates for the scene. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_view_coordinates").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_view_coordinates") + .spawn()?; rec.log_static("world", &rerun::ViewCoordinates::RIGHT_HAND_Z_UP())?; // Set an up-axis rec.log( diff --git a/docs/snippets/all/archetypes/voxel_grid_map_simple.cpp b/docs/snippets/all/archetypes/voxel_grid_map_simple.cpp new file mode 100644 index 000000000000..b21e01d5ae0a --- /dev/null +++ b/docs/snippets/all/archetypes/voxel_grid_map_simple.cpp @@ -0,0 +1,43 @@ +// Log a simple sparse voxel grid map. + +#include + +#include +#include + +int main(int argc, char* argv[]) { + const auto rec = + rerun::RecordingStream("rerun_example_voxel_grid_map_simple"); + rec.spawn().exit_on_failure(); + + const std::vector voxel_indices = { + rerun::components::VoxelIndex(-1, 0, 0), + rerun::components::VoxelIndex(1, 0, 0), + rerun::components::VoxelIndex(1, 1, 0), + rerun::components::VoxelIndex(3, 0, 0), + rerun::components::VoxelIndex(3, 0, 1), + rerun::components::VoxelIndex(4, 0, 1), + }; + const std::vector values = { + 0.0f, + 0.2f, + 0.4f, + 0.6f, + 0.8f, + 1.0f, + }; + + rec.log( + "world/voxels", + rerun::archetypes::VoxelGridMap( + voxel_indices, + std::array{0.25f, 0.25f, 0.25f} + ) + .with_values(values) + .with_value_range( + rerun::components::ValueRange(std::array{0.0, 1.0}) + ) + .with_colormap(rerun::components::Colormap::Turbo) + .with_translation({-0.5f, -0.5f, 0.0f}) + ); +} diff --git a/docs/snippets/all/archetypes/voxel_grid_map_simple.py b/docs/snippets/all/archetypes/voxel_grid_map_simple.py new file mode 100644 index 000000000000..c1805158e19d --- /dev/null +++ b/docs/snippets/all/archetypes/voxel_grid_map_simple.py @@ -0,0 +1,32 @@ +"""Log a simple sparse voxel grid map.""" + +import numpy as np + +import rerun as rr + +voxel_indices = np.array( + [ + [-1, 0, 0], + [1, 0, 0], + [1, 1, 0], + [3, 0, 0], + [3, 0, 1], + [4, 0, 1], + ], + dtype=np.int32, +) +values = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0], dtype=np.float32) + +rr.init("rerun_example_voxel_grid_map_simple", spawn=True) + +rr.log( + "world/voxels", + rr.VoxelGridMap( + voxel_indices, + voxel_size=[0.25, 0.25, 0.25], + values=values, + value_range=[0.0, 1.0], + colormap=rr.components.Colormap.Turbo, + translation=[-0.5, -0.5, 0.0], + ), +) diff --git a/docs/snippets/all/archetypes/voxel_grid_map_simple.rs b/docs/snippets/all/archetypes/voxel_grid_map_simple.rs new file mode 100644 index 000000000000..c338863625ef --- /dev/null +++ b/docs/snippets/all/archetypes/voxel_grid_map_simple.rs @@ -0,0 +1,29 @@ +//! Log a simple sparse voxel grid map. + +fn main() -> Result<(), Box> { + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_voxel_grid_map_simple", + ) + .spawn()?; + + let voxel_indices = [ + (-1, 0, 0), + (1, 0, 0), + (1, 1, 0), + (3, 0, 0), + (3, 0, 1), + (4, 0, 1), + ]; + let values = [0.0_f32, 0.2, 0.4, 0.6, 0.8, 1.0]; + + rec.log( + "world/voxels", + &rerun::VoxelGridMap::new(voxel_indices, [0.25, 0.25, 0.25]) + .with_values(values) + .with_value_range([0.0, 1.0]) + .with_colormap(rerun::components::Colormap::Turbo) + .with_translation([-0.5, -0.5, 0.0]), + )?; + + Ok(()) +} diff --git a/docs/snippets/all/concepts/build_chunk.py b/docs/snippets/all/concepts/build_chunk.py new file mode 100644 index 000000000000..f9a18db755b7 --- /dev/null +++ b/docs/snippets/all/concepts/build_chunk.py @@ -0,0 +1,23 @@ +"""Build a `Chunk` with `Chunk.from_columns` and send it via `send_chunks`.""" + +from __future__ import annotations + +import rerun as rr +import rerun.experimental as rrx + +rr.init("rerun_example_build_chunk") + +chunk = rrx.Chunk.from_columns( + "/points", + indexes=[rr.TimeColumn("frame", sequence=[0, 1, 2])], + columns=rr.Points3D.columns( + positions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]], + radii=[0.1, 0.2, 0.3], + ), +) + +# Chunks can be inspected in many ways, including a text representation of +# its content +print(chunk) + +rrx.send_chunks(chunk) diff --git a/docs/snippets/all/concepts/build_chunk_from_record_batch.py b/docs/snippets/all/concepts/build_chunk_from_record_batch.py new file mode 100644 index 000000000000..dc89f05d5864 --- /dev/null +++ b/docs/snippets/all/concepts/build_chunk_from_record_batch.py @@ -0,0 +1,37 @@ +"""Build chunks from an Arrow `RecordBatch` with `Chunk.from_record_batch`.""" + +from __future__ import annotations + +import pyarrow as pa + +import rerun.experimental as rrx + +# region: body +# Create an index column. +frame = pa.array([0, 1, 2], type=pa.int64()) + +# Create two component columns. +positions_datatype = pa.list_( + pa.list_(pa.field("item", pa.float32(), nullable=False), 3) +) +left = pa.array( + [[[1.0, 0.0, 0.0]], [[2.0, 0.0, 0.0]], [[3.0, 0.0, 0.0]]], + type=positions_datatype, +) +right = pa.array( + [[[0.0, 1.0, 0.0]], [[0.0, 2.0, 0.0]], [[0.0, 3.0, 0.0]]], + type=positions_datatype, +) + +# The `/entity:Archetype:component` column-name convention tells +# `from_record_batch` which entity and component each column maps to. +batch = pa.RecordBatch.from_arrays( + [frame, left, right], + names=["frame", "/left:Points3D:positions", "/right:Points3D:positions"], +) + +chunks = rrx.Chunk.from_record_batch(batch, index="frame") + +for chunk in chunks: + print(chunk) +# endregion: body diff --git a/docs/snippets/all/concepts/chunk_processing.py b/docs/snippets/all/concepts/chunk_processing.py new file mode 100644 index 000000000000..bb98f7ccee5f --- /dev/null +++ b/docs/snippets/all/concepts/chunk_processing.py @@ -0,0 +1,104 @@ +# region: setup +from __future__ import annotations + +import math +import uuid +from collections.abc import Callable +from pathlib import Path + +import pyarrow as pa +import pyarrow.compute as pc + +import rerun as rr +from rerun.experimental import ( + Chunk, + DeriveLens, + LazyChunkStream, + McapReader, + Selector, +) + +MCAP = ( + Path(__file__).resolve().parents[4] + / "tests" + / "assets" + / "mcap" + / "trossen_transfer_cube.mcap" +) +OUT = Path("chunk_processing.rrd") +# endregion: setup + + +# region: reading +stream = McapReader(MCAP).stream() +# endregion: reading + + +# region: processing +JOINTS = [ + "waist", + "shoulder", + "elbow", + "forearm_roll", + "wrist_angle", + "wrist_rotate", +] + + +def pick_joint(i: int) -> Callable[[pa.Array], pa.Array]: + """Extract joint `i` from a list column and convert rad → deg.""" + return lambda arr: pc.multiply(pc.list_element(arr, i), 180.0 / math.pi) + + +def fan(side: str) -> list[DeriveLens]: + return [ + DeriveLens( + "schemas.proto.JointState:message", + output_entity=f"/joints_deg/{side}/{name}", + ).to_component( + rr.Scalars.descriptor_scalars(), + Selector(".joint_positions").pipe(pick_joint(i)), + ) + for i, name in enumerate(JOINTS) + ] + + +processed = ( + stream + .drop(content="/video_raw/**") + .lenses( + fan("left"), + content="/robot_left/**", + output_mode="forward_unmatched", + ) + .lenses( + fan("right"), + content="/robot_right/**", + output_mode="forward_unmatched", + ) +) +# endregion: processing + +# TODO(ab): change this to merge properties instead, when we have proper +# interop between logging SDK and py-chunk + +# region: merging +metadata = Chunk.from_columns( + "/metadata", + indexes=[], + columns=rr.AnyValues.columns( + processing_type="ingestion", + processing_version="v1", + ), +) +merged = LazyChunkStream.merge(processed, LazyChunkStream.from_iter([metadata])) +# endregion: merging + + +# region: write +merged.write_rrd( + OUT, + application_id="rerun_example_chunk_processing", + recording_id=str(uuid.uuid4()), +) +# endregion: write diff --git a/docs/snippets/all/concepts/chunk_processing_intro.py b/docs/snippets/all/concepts/chunk_processing_intro.py new file mode 100644 index 000000000000..422d608af0c2 --- /dev/null +++ b/docs/snippets/all/concepts/chunk_processing_intro.py @@ -0,0 +1,32 @@ +"""Walk through a basic chunk-processing pipeline: read, filter, write.""" + +from __future__ import annotations + +from pathlib import Path + +mcap_path = ( + Path(__file__).resolve().parents[4] + / "tests" + / "assets" + / "mcap" + / "trossen_transfer_cube.mcap" +) +output_path = Path("chunk_processing_intro.rrd") + +# region: read +from rerun.experimental import McapReader + +stream = McapReader(mcap_path).stream() +# endregion: read + +# region: filter +stream = stream.filter(content="/robot_left/**") +# endregion: filter + +# region: terminal +stream.write_rrd( + output_path, + application_id="rerun_example_chunk_processing_intro", + recording_id="run1", +) +# endregion: terminal diff --git a/docs/snippets/all/concepts/chunk_processing_query.py b/docs/snippets/all/concepts/chunk_processing_query.py new file mode 100644 index 000000000000..9eadb455a4f2 --- /dev/null +++ b/docs/snippets/all/concepts/chunk_processing_query.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from datafusion import col + +# region: build_store +import rerun as rr +from rerun.experimental import Chunk, ChunkStore + +chunk = Chunk.from_columns( + "/sensor", + indexes=[rr.TimeColumn("frame", sequence=[0, 1, 2, 3])], + columns=rr.Scalars.columns(scalars=[0.0, 0.5, 1.0, 1.5]), +) +store = ChunkStore.from_chunks([chunk]) +# endregion: build_store + +# region: query +df = store.reader(index="frame") +df = df.filter(col("/sensor:Scalars:scalars")[0] >= 1.0) +print(df) # or convert to Pandas, Polars, PyArrow, etc. +# endregion: query diff --git a/docs/snippets/all/concepts/different_data_per_timeline.cpp b/docs/snippets/all/concepts/different_data_per_timeline.cpp index 6336691bcdb2..bf7c1d155185 100644 --- a/docs/snippets/all/concepts/different_data_per_timeline.cpp +++ b/docs/snippets/all/concepts/different_data_per_timeline.cpp @@ -3,7 +3,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_different_data_per_timeline"); + const auto rec = + rerun::RecordingStream("rerun_example_different_data_per_timeline"); rec.spawn().exit_on_failure(); rec.set_time_sequence("blue timeline", 0); @@ -13,12 +14,18 @@ int main(int argc, char* argv[]) { // Log a red color on one timeline. rec.reset_time(); // Clears all set timeline info. rec.set_time_duration_secs("red timeline", 1.0); - rec.log("points", rerun::Points2D::update_fields().with_colors(rerun::Color(0xFF0000FF))); + rec.log( + "points", + rerun::Points2D::update_fields().with_colors(rerun::Color(0xFF0000FF)) + ); // And a blue color on the other. rec.reset_time(); // Clears all set timeline info. rec.set_time_sequence("blue timeline", 1); - rec.log("points", rerun::Points2D::update_fields().with_colors(rerun::Color(0x0000FFFF))); + rec.log( + "points", + rerun::Points2D::update_fields().with_colors(rerun::Color(0x0000FFFF)) + ); // TODO(#5521): log VisualBounds2D } diff --git a/docs/snippets/all/concepts/different_data_per_timeline.py b/docs/snippets/all/concepts/different_data_per_timeline.py index 81261748d6ec..348878dc0f88 100644 --- a/docs/snippets/all/concepts/different_data_per_timeline.py +++ b/docs/snippets/all/concepts/different_data_per_timeline.py @@ -21,4 +21,8 @@ # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]) + ) +) diff --git a/docs/snippets/all/concepts/different_data_per_timeline.rs b/docs/snippets/all/concepts/different_data_per_timeline.rs index e512056d134c..167224fe0b67 100644 --- a/docs/snippets/all/concepts/different_data_per_timeline.rs +++ b/docs/snippets/all/concepts/different_data_per_timeline.rs @@ -1,8 +1,10 @@ //! Log different data on different timelines. fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_different_data_per_timeline").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_different_data_per_timeline", + ) + .spawn()?; rec.set_time_sequence("blue timeline", 0); rec.set_duration_secs("red timeline", 0.0); diff --git a/docs/snippets/all/concepts/explicit_recording.py b/docs/snippets/all/concepts/explicit_recording.py index d02708a43b5e..83b93f1f960f 100644 --- a/docs/snippets/all/concepts/explicit_recording.py +++ b/docs/snippets/all/concepts/explicit_recording.py @@ -9,4 +9,6 @@ rec.log("points", rr.Points3D([[0, 0, 0], [1, 1, 1]])) dir = os.path.dirname(os.path.abspath(__file__)) -rec.log_file_from_path(os.path.join(dir, "../../../../tests/assets/mesh/cube.glb")) +rec.log_file_from_path( + os.path.join(dir, "../../../../tests/assets/mesh/cube.glb") +) diff --git a/docs/snippets/all/concepts/file_sink.rs b/docs/snippets/all/concepts/file_sink.rs index fe77a966e1d9..6d2dcb312d60 100644 --- a/docs/snippets/all/concepts/file_sink.rs +++ b/docs/snippets/all/concepts/file_sink.rs @@ -1,7 +1,8 @@ //! Create and set a file sink. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_file_sink").buffered()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_file_sink") + .buffered()?; rec.set_sink(Box::new(rerun::sink::FileSink::new("recording.rrd")?)); diff --git a/docs/snippets/all/concepts/grpc_sink.cpp b/docs/snippets/all/concepts/grpc_sink.cpp index 633ca05cabd3..f0a67c638cf2 100644 --- a/docs/snippets/all/concepts/grpc_sink.cpp +++ b/docs/snippets/all/concepts/grpc_sink.cpp @@ -7,5 +7,6 @@ int main(int argc, char* argv[]) { // The default URL is `rerun+http://127.0.0.1:9876/proxy` // This can be used to connect to a viewer on a different machine - rec.set_sinks(rerun::GrpcSink{"rerun+http://127.0.0.1:9876/proxy"}).exit_on_failure(); + rec.set_sinks(rerun::GrpcSink{"rerun+http://127.0.0.1:9876/proxy"}) + .exit_on_failure(); } diff --git a/docs/snippets/all/concepts/grpc_sink.rs b/docs/snippets/all/concepts/grpc_sink.rs index 927795586687..bd9f8fa94db7 100644 --- a/docs/snippets/all/concepts/grpc_sink.rs +++ b/docs/snippets/all/concepts/grpc_sink.rs @@ -1,7 +1,8 @@ //! Create and set a GRPC sink. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_grpc_sink").buffered()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_grpc_sink") + .buffered()?; // The default URL is `rerun+http://127.0.0.1:9876/proxy` // This can be used to connect to a viewer on a different machine diff --git a/docs/snippets/all/concepts/how-does-rerun-work/log-to-grpc.rs b/docs/snippets/all/concepts/how-does-rerun-work/log-to-grpc.rs index a1e54b591787..de8cb8ce15dc 100644 --- a/docs/snippets/all/concepts/how-does-rerun-work/log-to-grpc.rs +++ b/docs/snippets/all/concepts/how-does-rerun-work/log-to-grpc.rs @@ -1,7 +1,8 @@ fn main() -> Result<(), Box> { // Connect to the Rerun gRPC server using the default address and // port: localhost:9876 - let rec = rerun::RecordingStreamBuilder::new("rerun_example_log_to_grpc").connect_grpc()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_log_to_grpc") + .connect_grpc()?; // Log data as usual, thereby pushing it into the stream. loop { diff --git a/docs/snippets/all/concepts/indices.cpp b/docs/snippets/all/concepts/indices.cpp index 76b2c79e4de6..07d5e00a3bd9 100644 --- a/docs/snippets/all/concepts/indices.cpp +++ b/docs/snippets/all/concepts/indices.cpp @@ -9,7 +9,10 @@ int main(int argc, char* argv[]) { rec.set_time_sequence("frame_nr", 42); rec.set_time_duration_secs("elapsed", 12.0); rec.set_time_timestamp_secs_since_epoch("time", 1'741'017'564); - rec.set_time_timestamp_nanos_since_epoch("precise_time", 1'741'017'564'987'654'000); + rec.set_time_timestamp_nanos_since_epoch( + "precise_time", + 1'741'017'564'987'654'000 + ); // All following logged data will be timestamped with the above times: rec.log("points", rerun::Points2D({{0.0, 0.0}, {1.0, 1.0}})); diff --git a/docs/snippets/all/concepts/indices.py b/docs/snippets/all/concepts/indices.py index 843d6320bb0c..79d466a8e793 100644 --- a/docs/snippets/all/concepts/indices.py +++ b/docs/snippets/all/concepts/indices.py @@ -12,7 +12,9 @@ rr.set_time("elapsed", duration=12) # elapsed seconds rr.set_time("time", timestamp=1_741_017_564) # Seconds since unix epoch rr.set_time("time", timestamp=datetime.fromisoformat("2025-03-03T15:59:24")) -rr.set_time("precise_time", timestamp=np.datetime64(1_741_017_564_987_654_000, "ns")) # Nanoseconds since unix epoch +rr.set_time( + "precise_time", timestamp=np.datetime64(1_741_017_564_987_654_000, "ns") +) # Nanoseconds since unix epoch # All following logged data will be timestamped with the above times: rr.log("points", rr.Points2D([[0, 0], [1, 1]])) diff --git a/docs/snippets/all/concepts/indices.rs b/docs/snippets/all/concepts/indices.rs index bb4972452695..3e97c664d7f4 100644 --- a/docs/snippets/all/concepts/indices.rs +++ b/docs/snippets/all/concepts/indices.rs @@ -1,7 +1,9 @@ //! Set different types of indices. fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_different_indices").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_different_indices") + .spawn()?; rec.set_time_sequence("frame_nr", 42); rec.set_duration_secs("elapsed", 12.0); @@ -9,7 +11,7 @@ fn main() -> Result<(), Box> { rec.set_time( "precise_time", std::time::SystemTime::UNIX_EPOCH - + std::time::Duration::from_nanos(1_741_017_564_987_654_000), + + std::time::Duration::from_micros(1_741_017_564_987_654), ); // All following logged data will be timestamped with the above times: diff --git a/docs/snippets/all/concepts/lenses.py b/docs/snippets/all/concepts/lenses.py index 46f4fa2e6b81..b6077eca8c2c 100644 --- a/docs/snippets/all/concepts/lenses.py +++ b/docs/snippets/all/concepts/lenses.py @@ -1,9 +1,16 @@ -"""Use lenses to extract struct fields and reroute data to a different entity.""" +"""Use lenses to extract struct fields and reroute data to another entity.""" import pyarrow as pa import rerun as rr -from rerun.experimental import Chunk, LazyChunkStream, Lens, LensOutput, Selector, send_chunk +from rerun.experimental import ( + Chunk, + DeriveLens, + LazyChunkStream, + MutateLens, + Selector, + send_chunks, +) rr.init("rerun_example_lenses", spawn=True) @@ -21,41 +28,46 @@ chunk = Chunk.from_columns( "/sensor/imu", indexes=[rr.TimeColumn("frame", sequence=[0, 1, 2])], - columns=rr.DynamicArchetype.columns(archetype="Imu", components={"accel": imu_data, "status": status_data}), + columns=rr.DynamicArchetype.columns( + archetype="Imu", components={"accel": imu_data, "status": status_data} + ), ) # endregion: log_data # Extract the "x" field as a Scalar on the same entity. -extract_x = Lens( - "Imu:accel", - LensOutput().to_component(rr.Scalars.descriptor_scalars(), ".x"), +extract_x = DeriveLens("Imu:accel").to_component( + rr.Scalars.descriptor_scalars(), ".x" ) -# region: lens_definition -# Extract the "y" field to a different entity and the "elapsed" field as a new timeline. -extract_y = Lens( - "Imu:accel", - to_entity={ - "/new_entity/accel_y": LensOutput() - .to_component(rr.Scalars.descriptor_scalars(), ".y") - .to_timeline("sensor_elapsed", "duration_ns", ".elapsed") - }, +# region: derive_lens +# Extract the "y" field to a different entity and the "elapsed" field as a +# new timeline. +extract_y = ( + DeriveLens("Imu:accel", output_entity="/new_entity/accel_y") + .to_component(rr.Scalars.descriptor_scalars(), ".y") + .to_timeline("sensor_elapsed", "duration_ns", ".elapsed") ) -# endregion: lens_definition +# endregion: derive_lens + +# region: mutate_lens +# Simplify the accel struct to just its "x" field in-place. +simplify_accel = MutateLens("Imu:accel", ".x") +# endregion: mutate_lens # region: pipe_example # Use pipe to apply a custom transformation after extracting a field. -extract_scaled_x = Lens( - "Imu:accel", - LensOutput().to_component( - rr.Scalars.descriptor_scalars(), - Selector(".x").pipe(lambda arr: pa.compute.multiply(arr, 9.81)), - ), +extract_scaled_x = DeriveLens( + "Imu:accel", output_entity="/new_entity/accel_scaled_x" +).to_component( + rr.Scalars.descriptor_scalars(), + Selector(".x").pipe(lambda arr: pa.compute.multiply(arr, 9.81)), ) # endregion: pipe_example # Apply all lenses via the ChunkStream API and send the resulting chunks. stream = LazyChunkStream.from_iter([chunk]) -results = stream.lenses([extract_x, extract_y, extract_scaled_x], output_mode="forward_unmatched") -for result in results: - send_chunk(result) +results = stream.lenses( + [extract_x, extract_y, simplify_accel, extract_scaled_x], + output_mode="forward_unmatched", +) +send_chunks(results) diff --git a/docs/snippets/all/concepts/lenses.rs b/docs/snippets/all/concepts/lenses.rs index 0d7333075e84..61f3cc6b6cd4 100644 --- a/docs/snippets/all/concepts/lenses.rs +++ b/docs/snippets/all/concepts/lenses.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use rerun::external::arrow::array::{ - Array as _, ArrayRef, AsArray as _, Float64Array, Int64Array, ListArray, StringArray, - StructArray, + Array as _, ArrayRef, AsArray as _, Float64Array, Int64Array, ListArray, + StringArray, StructArray, }; use rerun::external::arrow::buffer::OffsetBuffer; use rerun::external::arrow::compute; @@ -14,7 +14,8 @@ use rerun::log::{Chunk, TimeColumn}; use rerun::time::TimeType; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_lenses").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_lenses").spawn()?; // region: log_data // Build a chunk with a struct-typed component. @@ -53,11 +54,13 @@ fn main() -> Result<(), Box> { [TimeColumn::new_sequence("frame", [0, 1, 2])], [ ( - rerun::ComponentDescriptor::partial("Imu:accel").with_archetype("Imu".into()), + rerun::ComponentDescriptor::partial("Imu:accel") + .with_archetype("Imu".into()), imu_list, ), ( - rerun::ComponentDescriptor::partial("Imu:status").with_archetype("Imu".into()), + rerun::ComponentDescriptor::partial("Imu:status") + .with_archetype("Imu".into()), status_list, ), ], @@ -65,47 +68,61 @@ fn main() -> Result<(), Box> { // endregion: log_data // Extract the "x" field as a Scalar on the same entity. - let extract_x = Lens::for_input_column("Imu:accel") - .output_columns(|out| { - out.component(rerun::Scalars::descriptor_scalars(), Selector::parse(".x")?) - })? - .build(); + let extract_x = Lens::derive("Imu:accel") + .to_component( + rerun::Scalars::descriptor_scalars(), + Selector::parse(".x")?, + ) + .build()?; - // region: lens_definition + // region: derive_lens // Extract the "y" field to a different entity and the "elapsed" field as a new timeline. - let extract_y = Lens::for_input_column("Imu:accel") - .output_columns_at("/new_entity/accel_y", |out| { - out.component(rerun::Scalars::descriptor_scalars(), Selector::parse(".y")?)? - .time( - "sensor_elapsed", - TimeType::DurationNs, - Selector::parse(".elapsed")?, - ) - })? - .build(); - // endregion: lens_definition + let extract_y = Lens::derive("Imu:accel") + .output_entity("/new_entity/accel_y") + .to_component( + rerun::Scalars::descriptor_scalars(), + Selector::parse(".y")?, + ) + .to_timeline( + "sensor_elapsed", + TimeType::DurationNs, + Selector::parse(".elapsed")?, + ) + .build()?; + // endregion: derive_lens + + // region: mutate_lens + // Simplify the accel struct to just its "x" field in-place. + let simplify_accel = + Lens::mutate("Imu:accel", Selector::parse(".x")?).build(); + // endregion: mutate_lens // region: pipe_example // Use pipe to apply a custom transformation after extracting a field. - let scale_x = Lens::for_input_column("Imu:accel") - .output_columns(|out| { - out.component( - rerun::Scalars::descriptor_scalars(), - Selector::parse(".x")?.pipe(|arr: &ArrayRef| { - let scaled: Float64Array = - compute::unary(arr.as_primitive::(), |v| v * 9.81); - Ok(Some(Arc::new(scaled) as _)) - }), - ) - })? - .build(); + let scale_x = Lens::derive("Imu:accel") + .output_entity("/new_entity/accel_scaled_x") + .to_component( + rerun::Scalars::descriptor_scalars(), + Selector::parse(".x")?.pipe(|arr: &ArrayRef| { + let scaled: Float64Array = + compute::unary(arr.as_primitive::(), |v| { + v * 9.81 + }); + Ok(Some(Arc::new(scaled) as _)) + }), + ) + .build()?; // endregion: pipe_example // Apply all lenses and send the resulting chunks. let results = chunk - .apply_lenses(&[extract_x, extract_y, scale_x]) + .apply_lenses( + &[extract_x, extract_y, simplify_accel, scale_x], + &rerun::lenses::default_runtime(), + ) .map_err(|partial| { - let errors: Vec<_> = partial.errors().map(|e| e.to_string()).collect(); + let errors: Vec<_> = + partial.errors().map(|e| e.to_string()).collect(); format!("Lens errors: {}", errors.join(", ")) })?; rec.send_chunks(results); diff --git a/docs/snippets/all/concepts/query-and-transform/dataframe_query_example.py b/docs/snippets/all/concepts/query-and-transform/dataframe_query_example.py index 253e2989bc83..fdfe82f3a5ca 100644 --- a/docs/snippets/all/concepts/query-and-transform/dataframe_query_example.py +++ b/docs/snippets/all/concepts/query-and-transform/dataframe_query_example.py @@ -11,7 +11,9 @@ # should be a cross-platform way to generate a rrd path. RRD_PATH = tempfile.mktemp(suffix=".rrd") -atexit.register(lambda: os.unlink(RRD_PATH) if os.path.exists(RRD_PATH) else None) +atexit.register( + lambda: os.unlink(RRD_PATH) if os.path.exists(RRD_PATH) else None +) # region: setup # create some data diff --git a/docs/snippets/all/concepts/query-and-transform/segment_properties.py b/docs/snippets/all/concepts/query-and-transform/segment_properties.py index 9d3d79c78ceb..b81ac5960757 100644 --- a/docs/snippets/all/concepts/query-and-transform/segment_properties.py +++ b/docs/snippets/all/concepts/query-and-transform/segment_properties.py @@ -11,7 +11,9 @@ import rerun as rr RRD_DIR = Path(tempfile.mkdtemp()) -atexit.register(lambda: shutil.rmtree(RRD_DIR) if os.path.exists(RRD_DIR) else None) +atexit.register( + lambda: shutil.rmtree(RRD_DIR) if os.path.exists(RRD_DIR) else None +) # region: setup rrd_paths = [RRD_DIR / f"recording_{i}.rrd" for i in range(5)] @@ -46,7 +48,9 @@ segment_table = dataset.segment_table() # sort and select columns of interest - segment_table = segment_table.sort(col("property:RecordingInfo:name")[0]).select( + segment_table = segment_table.sort( + col("property:RecordingInfo:name")[0] + ).select( "rerun_segment_id", "property:RecordingInfo:name", "property:RecordingInfo:start_time", diff --git a/docs/snippets/all/concepts/recording_properties.cpp b/docs/snippets/all/concepts/recording_properties.cpp index 567ef522a73a..8c19d0cef586 100644 --- a/docs/snippets/all/concepts/recording_properties.cpp +++ b/docs/snippets/all/concepts/recording_properties.cpp @@ -6,7 +6,8 @@ #include arrow::Status run_main() { - const auto rec = rerun::RecordingStream("rerun_example_recording_properties"); + const auto rec = + rerun::RecordingStream("rerun_example_recording_properties"); rec.spawn().exit_on_failure(); // Overwrites the name from above. @@ -24,10 +25,14 @@ arrow::Status run_main() { std::shared_ptr arrow_array; arrow::DoubleBuilder confidences_builder; - ARROW_RETURN_NOT_OK(confidences_builder.AppendValues({0.3, 0.4, 0.5, 0.6})); + ARROW_RETURN_NOT_OK( + confidences_builder.AppendValues({0.3, 0.4, 0.5, 0.6}) + ); ARROW_RETURN_NOT_OK(confidences_builder.Finish(&arrow_array)); - auto confidences = - rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "confidences"); + auto confidences = rerun::ComponentBatch::from_arrow_array( + std::move(arrow_array), + "confidences" + ); arrow::StringBuilder traffic_builder; ARROW_RETURN_NOT_OK(traffic_builder.Append("low")); diff --git a/docs/snippets/all/concepts/recording_properties.rs b/docs/snippets/all/concepts/recording_properties.rs index 8b8cd0f6335e..3dad066aa860 100644 --- a/docs/snippets/all/concepts/recording_properties.rs +++ b/docs/snippets/all/concepts/recording_properties.rs @@ -5,7 +5,10 @@ use std::sync::Arc; use rerun::external::arrow; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_recording_properties").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_recording_properties", + ) + .spawn()?; // Recordings can have an optional name. rec.send_recording_name("My recording")?; @@ -22,7 +25,9 @@ fn main() -> Result<(), Box> { let other = rerun::AnyValues::default() .with_component_from_data( "confidences", - Arc::new(arrow::array::Float64Array::from(vec![0.3, 0.4, 0.5, 0.6])), + Arc::new(arrow::array::Float64Array::from(vec![ + 0.3, 0.4, 0.5, 0.6, + ])), ) .with_component_from_data( "traffic", diff --git a/docs/snippets/all/concepts/rrd_format.py b/docs/snippets/all/concepts/rrd_format.py new file mode 100644 index 000000000000..e8a2dbdd23dc --- /dev/null +++ b/docs/snippets/all/concepts/rrd_format.py @@ -0,0 +1,42 @@ +"""Save a small recording to RRD and inspect a chunk from it.""" + +from __future__ import annotations + +import atexit +import os +import tempfile +from pathlib import Path + +import rerun as rr + +output_path = Path(tempfile.mktemp(suffix=".rrd")) +atexit.register( + lambda: os.unlink(output_path) if output_path.exists() else None +) + +# region: write +with rr.RecordingStream( + "rerun_example_rrd_format", recording_id="example" +) as rec: + rec.save(output_path) + rec.set_time("frame", sequence=0) + rec.log( + "/points", + rr.Points3D( + [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]], + colors=[(255, 0, 0), (0, 255, 0)], + ), + ) + rec.set_time("frame", sequence=1) + rec.log("/points", rr.Points3D([[2.0, 2.0, 2.0]], colors=[(0, 0, 255)])) +# endregion: write + +# region: inspect +from rerun.experimental import RrdReader + +reader = RrdReader(output_path) +for chunk in reader.stream(): + if chunk.entity_path == "/points": + print(chunk.format(trim_metadata_keys=False)) + break +# endregion: inspect diff --git a/docs/snippets/all/concepts/send_chunks.py b/docs/snippets/all/concepts/send_chunks.py new file mode 100644 index 000000000000..5f1d36408554 --- /dev/null +++ b/docs/snippets/all/concepts/send_chunks.py @@ -0,0 +1,20 @@ +"""Send chunks loaded from an RRD into a recording stream.""" + +import sys + +import rerun as rr +import rerun.experimental as rrx + +path_to_rrd = sys.argv[1] + +# NOTE: This is specifically demonstrating how to forward chunks from an RRD +# into the viewer. +# If you just want to view an RRD file, use the simpler `rr.log_file()` +# function instead: +# rr.log_file("path/to/file.rrd", spawn=True) + +reader = rrx.RrdReader(path_to_rrd) +entry = reader.recordings()[0] + +rr.init(entry.application_id, recording_id=entry.recording_id, spawn=True) +rrx.send_chunks(reader.store()) diff --git a/docs/snippets/all/concepts/send_chunks.rs b/docs/snippets/all/concepts/send_chunks.rs new file mode 100644 index 000000000000..1c0945197404 --- /dev/null +++ b/docs/snippets/all/concepts/send_chunks.rs @@ -0,0 +1,28 @@ +//! Send a `.rrd` to a new recording stream. + +use rerun::{ChunkStore, ChunkStoreConfig}; + +fn main() -> Result<(), Box> { + // Get the filename from the command-line args. + let filename = + std::env::args().nth(2).ok_or("Missing filename argument")?; + + // Load the chunk store from the file. + let mut rrd_file = std::fs::File::open(&filename)?; + let (store_id, store) = + ChunkStore::from_rrd_reader(&ChunkStoreConfig::DEFAULT, &mut rrd_file)? + .into_iter() + .next() + .ok_or("Expected exactly one recording in the archive")?; + + // Use the same app and recording IDs as the original. + let new_recording = + rerun::RecordingStreamBuilder::from_store_id(&store_id).spawn()?; + + // Forward all chunks to the new recording stream. + for chunk in store.iter_physical_chunks() { + new_recording.send_chunk((**chunk).clone()); + } + + Ok(()) +} diff --git a/docs/snippets/all/concepts/send_dataframe.py b/docs/snippets/all/concepts/send_dataframe.py new file mode 100644 index 000000000000..93e8a14f5985 --- /dev/null +++ b/docs/snippets/all/concepts/send_dataframe.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import pyarrow as pa + +import rerun as rr +import rerun.experimental as rrx + +rr.init("rerun_example_send_dataframe") + +# region: build_table +# An index column… +index = pa.array([0, 1, 2], type=pa.int64()) + +# …and a component column. Each row is a list (one component batch per row). +positions = pa.array( + [ + [[1.0, 0.0, 0.0]], + [[0.0, 1.0, 0.0]], + [[0.0, 0.0, 1.0]], + ], + type=pa.list_(pa.list_(pa.field("item", pa.float32(), nullable=False), 3)), +) + +# Tag each column with the `rerun:*` metadata keys that `Chunk.from_dataframe` +# recognizes. +schema = pa.schema([ + pa.field( + "frame", + index.type, + metadata={b"rerun:index_name": b"frame", b"rerun:kind": b"index"}, + ), + pa.field( + "/points:Points3D:positions", + positions.type, + metadata={ + b"rerun:entity_path": b"/points", + b"rerun:archetype": b"rerun.archetypes.Points3D", + b"rerun:component": b"Points3D:positions", + b"rerun:component_type": b"rerun.components.Position3D", + b"rerun:kind": b"data", + }, + ), +]) + +table = pa.Table.from_arrays([index, positions], schema=schema) +# endregion: build_table + +# region: from_dataframe +chunks = list(rrx.Chunk.from_dataframe(table)) +for chunk in chunks: + print(chunk) +# endregion: from_dataframe + +# region: send_dataframe +rr.send_dataframe(table) +# endregion: send_dataframe diff --git a/docs/snippets/all/concepts/send_recording.py b/docs/snippets/all/concepts/send_recording.py deleted file mode 100644 index 179c8918e588..000000000000 --- a/docs/snippets/all/concepts/send_recording.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Send a dataframe to a new recording stream.""" - -import sys - -import rerun as rr - -path_to_rrd = sys.argv[1] - -# NOTE: This is specifically demonstrating how to send `rr.recording.Recording` into the viewer. -# If you just want to view an RRD file, use the simpler `rr.log_file()` function instead: -# rr.log_file("path/to/file.rrd", spawn=True) - -recording = rr.recording.load_recording(path_to_rrd) - -rr.init(recording.application_id(), recording_id=recording.recording_id(), spawn=True) -rr.send_recording(recording) diff --git a/docs/snippets/all/concepts/send_recording.rs b/docs/snippets/all/concepts/send_recording.rs deleted file mode 100644 index 5172a243e70d..000000000000 --- a/docs/snippets/all/concepts/send_recording.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Send a `.rrd` to a new recording stream. - -use rerun::external::re_chunk_store::{ChunkStore, ChunkStoreConfig}; - -fn main() -> Result<(), Box> { - // Get the filename from the command-line args. - let filename = std::env::args().nth(2).ok_or("Missing filename argument")?; - - // Load the chunk store from the file. - let (store_id, store) = ChunkStore::from_rrd_filepath(&ChunkStoreConfig::DEFAULT, filename)? - .into_iter() - .next() - .ok_or("Expected exactly one recording in the archive")?; - - // Use the same app and recording IDs as the original. - let new_recording = rerun::RecordingStreamBuilder::from_store_id(&store_id).spawn()?; - - // Forward all chunks to the new recording stream. - for chunk in store.iter_physical_chunks() { - new_recording.send_chunk((**chunk).clone()); - } - - Ok(()) -} diff --git a/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.cpp b/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.cpp index 00c0e974978e..c133e46c918f 100644 --- a/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.cpp +++ b/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.cpp @@ -3,7 +3,9 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_transform3d_hierarchy_named_frames"); + const auto rec = rerun::RecordingStream( + "rerun_example_transform3d_hierarchy_named_frames" + ); rec.spawn().exit_on_failure(); // Define entities with explicit coordinate frames. diff --git a/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.py b/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.py index 8ec1295ee976..6dc03093b0a0 100644 --- a/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.py +++ b/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.py @@ -7,29 +7,45 @@ # Define entities with explicit coordinate frames. rr.log( "sun", - rr.Ellipsoids3D(half_sizes=[1, 1, 1], colors=[255, 200, 10], fill_mode="solid"), + rr.Ellipsoids3D( + half_sizes=[1, 1, 1], colors=[255, 200, 10], fill_mode="solid" + ), rr.CoordinateFrame("sun_frame"), ) rr.log( "planet", - rr.Ellipsoids3D(half_sizes=[0.4, 0.4, 0.4], colors=[40, 80, 200], fill_mode="solid"), + rr.Ellipsoids3D( + half_sizes=[0.4, 0.4, 0.4], colors=[40, 80, 200], fill_mode="solid" + ), rr.CoordinateFrame("planet_frame"), ) rr.log( "moon", - rr.Ellipsoids3D(half_sizes=[0.15, 0.15, 0.15], colors=[180, 180, 180], fill_mode="solid"), + rr.Ellipsoids3D( + half_sizes=[0.15, 0.15, 0.15], colors=[180, 180, 180], fill_mode="solid" + ), rr.CoordinateFrame("moon_frame"), ) # Define explicit frame relationships. rr.log( "planet_transform", - rr.Transform3D(translation=[6.0, 0.0, 0.0], child_frame="planet_frame", parent_frame="sun_frame"), + rr.Transform3D( + translation=[6.0, 0.0, 0.0], + child_frame="planet_frame", + parent_frame="sun_frame", + ), ) rr.log( - "moon_transform", rr.Transform3D(translation=[3.0, 0.0, 0.0], child_frame="moon_frame", parent_frame="planet_frame") + "moon_transform", + rr.Transform3D( + translation=[3.0, 0.0, 0.0], + child_frame="moon_frame", + parent_frame="planet_frame", + ), ) # Connect the viewer to the sun's coordinate frame. -# This is only needed in the absence of blueprints since a default view will typically be created at `/`. +# This is only needed in the absence of blueprints since a default view will +# typically be created at `/`. rr.log("/", rr.CoordinateFrame("sun_frame"), static=True) diff --git a/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.rs b/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.rs index 615d934a9b16..392b79e87336 100644 --- a/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.rs +++ b/docs/snippets/all/concepts/transform3d_hierarchy_named_frames.rs @@ -1,9 +1,10 @@ //! Logs a simple transform hierarchy with named frames. fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_transform3d_hierarchy_named_frames") - .spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_transform3d_hierarchy_named_frames", + ) + .spawn()?; // Define entities with explicit coordinate frames. rec.log( @@ -11,7 +12,8 @@ fn main() -> Result<(), Box> { &[ &rerun::Ellipsoids3D::from_half_sizes([[1.0, 1.0, 1.0]]) .with_colors([rerun::Color::from_rgb(255, 200, 10)]) - .with_fill_mode(rerun::FillMode::Solid) as &dyn rerun::AsComponents, + .with_fill_mode(rerun::FillMode::Solid) + as &dyn rerun::AsComponents, &rerun::CoordinateFrame::new("sun_frame"), ], )?; @@ -21,7 +23,8 @@ fn main() -> Result<(), Box> { &[ &rerun::Ellipsoids3D::from_half_sizes([[0.4, 0.4, 0.4]]) .with_colors([rerun::Color::from_rgb(40, 80, 200)]) - .with_fill_mode(rerun::FillMode::Solid) as &dyn rerun::AsComponents, + .with_fill_mode(rerun::FillMode::Solid) + as &dyn rerun::AsComponents, &rerun::CoordinateFrame::new("planet_frame"), ], )?; @@ -31,7 +34,8 @@ fn main() -> Result<(), Box> { &[ &rerun::Ellipsoids3D::from_half_sizes([[0.15, 0.15, 0.15]]) .with_colors([rerun::Color::from_rgb(180, 180, 180)]) - .with_fill_mode(rerun::FillMode::Solid) as &dyn rerun::AsComponents, + .with_fill_mode(rerun::FillMode::Solid) + as &dyn rerun::AsComponents, &rerun::CoordinateFrame::new("moon_frame"), ], )?; diff --git a/docs/snippets/all/concepts/transform3d_hierarchy_simple.cpp b/docs/snippets/all/concepts/transform3d_hierarchy_simple.cpp index 8eb8462af11f..d81783083f53 100644 --- a/docs/snippets/all/concepts/transform3d_hierarchy_simple.cpp +++ b/docs/snippets/all/concepts/transform3d_hierarchy_simple.cpp @@ -3,7 +3,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_transform3d_hierarchy_simple"); + const auto rec = + rerun::RecordingStream("rerun_example_transform3d_hierarchy_simple"); rec.spawn().exit_on_failure(); // Log entities at their hierarchy positions. diff --git a/docs/snippets/all/concepts/transform3d_hierarchy_simple.py b/docs/snippets/all/concepts/transform3d_hierarchy_simple.py index 83849d4ec735..1bff8cf407c6 100644 --- a/docs/snippets/all/concepts/transform3d_hierarchy_simple.py +++ b/docs/snippets/all/concepts/transform3d_hierarchy_simple.py @@ -5,10 +5,29 @@ rr.init("rerun_example_transform3d_hierarchy_simple", spawn=True) # Log entities at their hierarchy positions. -rr.log("sun", rr.Ellipsoids3D(half_sizes=[1, 1, 1], colors=[255, 200, 10], fill_mode="solid")) -rr.log("sun/planet", rr.Ellipsoids3D(half_sizes=[0.4, 0.4, 0.4], colors=[40, 80, 200], fill_mode="solid")) -rr.log("sun/planet/moon", rr.Ellipsoids3D(half_sizes=[0.15, 0.15, 0.15], colors=[180, 180, 180], fill_mode="solid")) +rr.log( + "sun", + rr.Ellipsoids3D( + half_sizes=[1, 1, 1], colors=[255, 200, 10], fill_mode="solid" + ), +) +rr.log( + "sun/planet", + rr.Ellipsoids3D( + half_sizes=[0.4, 0.4, 0.4], colors=[40, 80, 200], fill_mode="solid" + ), +) +rr.log( + "sun/planet/moon", + rr.Ellipsoids3D( + half_sizes=[0.15, 0.15, 0.15], colors=[180, 180, 180], fill_mode="solid" + ), +) # Define transforms - each describes the relationship to its parent. -rr.log("sun/planet", rr.Transform3D(translation=[6.0, 0.0, 0.0])) # Planet 6 units from sun. -rr.log("sun/planet/moon", rr.Transform3D(translation=[3.0, 0.0, 0.0])) # Moon 3 units from planet. +rr.log( + "sun/planet", rr.Transform3D(translation=[6.0, 0.0, 0.0]) +) # Planet 6 units from sun. +rr.log( + "sun/planet/moon", rr.Transform3D(translation=[3.0, 0.0, 0.0]) +) # Moon 3 units from planet. diff --git a/docs/snippets/all/concepts/transform3d_hierarchy_simple.rs b/docs/snippets/all/concepts/transform3d_hierarchy_simple.rs index e408f9a914e2..343fa4b6d5a9 100644 --- a/docs/snippets/all/concepts/transform3d_hierarchy_simple.rs +++ b/docs/snippets/all/concepts/transform3d_hierarchy_simple.rs @@ -1,8 +1,10 @@ //! Logs a simple transform hierarchy. fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_transform3d_hierarchy_simple").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_transform3d_hierarchy_simple", + ) + .spawn()?; // Log entities at their hierarchy positions. rec.log( diff --git a/docs/snippets/all/descriptors/descr_builtin_archetype.cpp b/docs/snippets/all/descriptors/descr_builtin_archetype.cpp index c98ecd7a18c2..82e477a898d7 100644 --- a/docs/snippets/all/descriptors/descr_builtin_archetype.cpp +++ b/docs/snippets/all/descriptors/descr_builtin_archetype.cpp @@ -1,10 +1,14 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_descriptors_builtin_archetype"); + const auto rec = + rerun::RecordingStream("rerun_example_descriptors_builtin_archetype"); rec.spawn().exit_on_failure(); - rec.log_static("data", rerun::Points3D({{1.0f, 2.0f, 3.0f}}).with_radii({0.3f, 0.2f, 0.1f})); + rec.log_static( + "data", + rerun::Points3D({{1.0f, 2.0f, 3.0f}}).with_radii({0.3f, 0.2f, 0.1f}) + ); // The tags are indirectly checked by the Rust version (have a look over there for more info). } diff --git a/docs/snippets/all/descriptors/descr_builtin_archetype.py b/docs/snippets/all/descriptors/descr_builtin_archetype.py index 750caf41ad18..8621b3415cbf 100755 --- a/docs/snippets/all/descriptors/descr_builtin_archetype.py +++ b/docs/snippets/all/descriptors/descr_builtin_archetype.py @@ -9,4 +9,5 @@ rr.log("data", rr.Points3D([[1, 2, 3]], radii=[0.3, 0.2, 0.1]), static=True) -# The tags are indirectly checked by the Rust version (have a look over there for more info). +# The tags are indirectly checked by the Rust version (have a look over there +# for more info). diff --git a/docs/snippets/all/descriptors/descr_builtin_archetype.rs b/docs/snippets/all/descriptors/descr_builtin_archetype.rs index a11cf3e7bd49..a9a85a552226 100644 --- a/docs/snippets/all/descriptors/descr_builtin_archetype.rs +++ b/docs/snippets/all/descriptors/descr_builtin_archetype.rs @@ -1,6 +1,8 @@ use rerun::{ChunkStore, ChunkStoreConfig, ComponentDescriptor}; -fn example(rec: &rerun::RecordingStream) -> Result<(), Box> { +fn example( + rec: &rerun::RecordingStream, +) -> Result<(), Box> { rec.log_static( "data", &rerun::Points3D::new([(1.0, 2.0, 3.0)]).with_radii([0.3, 0.2, 0.1]), @@ -34,8 +36,12 @@ fn check_tags(rec: &rerun::RecordingStream) { if let Ok(path_to_rrd) = std::env::var("_RERUN_TEST_FORCE_SAVE") { rec.flush_blocking().unwrap(); - let stores = - ChunkStore::from_rrd_filepath(&ChunkStoreConfig::ALL_DISABLED, path_to_rrd).unwrap(); + let mut rrd_file = std::fs::File::open(&path_to_rrd).unwrap(); + let stores = ChunkStore::from_rrd_reader( + &ChunkStoreConfig::ALL_DISABLED, + &mut rrd_file, + ) + .unwrap(); assert_eq!(1, stores.len()); let store = stores.into_values().next().unwrap(); diff --git a/docs/snippets/all/descriptors/descr_builtin_component.cpp b/docs/snippets/all/descriptors/descr_builtin_component.cpp index cfc1d442f140..6cd4cbdabfeb 100644 --- a/docs/snippets/all/descriptors/descr_builtin_component.cpp +++ b/docs/snippets/all/descriptors/descr_builtin_component.cpp @@ -1,7 +1,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_descriptors_builtin_component"); + const auto rec = + rerun::RecordingStream("rerun_example_descriptors_builtin_component"); rec.spawn().exit_on_failure(); rec.log_static( @@ -9,9 +10,10 @@ int main(int argc, char* argv[]) { rerun::ComponentBatch::from_loggable( rerun::Position3D(1.0f, 2.0f, 3.0f), rerun::ComponentDescriptor( - "user.CustomPoints3D", // archetype name - "user.CustomPoints3D:points", // component - rerun::Loggable::ComponentType // component type + "user.CustomPoints3D", // archetype name + "user.CustomPoints3D:points", // component + rerun::Loggable< + rerun::Position3D>::ComponentType // component type ) ) ); diff --git a/docs/snippets/all/descriptors/descr_builtin_component.py b/docs/snippets/all/descriptors/descr_builtin_component.py index 56d887a1f4af..83c2ab7be954 100755 --- a/docs/snippets/all/descriptors/descr_builtin_component.py +++ b/docs/snippets/all/descriptors/descr_builtin_component.py @@ -21,4 +21,5 @@ static=True, ) -# The tags are indirectly checked by the Rust version (have a look over there for more info). +# The tags are indirectly checked by the Rust version (have a look over there +# for more info). diff --git a/docs/snippets/all/descriptors/descr_builtin_component.rs b/docs/snippets/all/descriptors/descr_builtin_component.rs index 364f855e1064..2d67b4a2b119 100644 --- a/docs/snippets/all/descriptors/descr_builtin_component.rs +++ b/docs/snippets/all/descriptors/descr_builtin_component.rs @@ -1,19 +1,20 @@ use rerun::{ChunkStore, ChunkStoreConfig, ComponentDescriptor}; -fn example(rec: &rerun::RecordingStream) -> Result<(), Box> { +fn example( + rec: &rerun::RecordingStream, +) -> Result<(), Box> { use rerun::ComponentBatch as _; rec.log_static( "data", &[ - rerun::components::Position3D::new(1.0, 2.0, 3.0).try_serialized( - ComponentDescriptor { - archetype: Some("user.CustomPoints3D".into()), - component: "user.CustomPoints3D:points".into(), - component_type: Some( - ::name(), - ), - }, - )?, + rerun::components::Position3D::new(1.0, 2.0, 3.0) + .try_serialized(ComponentDescriptor { + archetype: Some("user.CustomPoints3D".into()), + component: "user.CustomPoints3D:points".into(), + component_type: Some( + ::name(), + ), + })?, ], )?; @@ -45,8 +46,12 @@ fn check_tags(rec: &rerun::RecordingStream) { if let Ok(path_to_rrd) = std::env::var("_RERUN_TEST_FORCE_SAVE") { rec.flush_blocking().unwrap(); - let stores = - ChunkStore::from_rrd_filepath(&ChunkStoreConfig::ALL_DISABLED, path_to_rrd).unwrap(); + let mut rrd_file = std::fs::File::open(&path_to_rrd).unwrap(); + let stores = ChunkStore::from_rrd_reader( + &ChunkStoreConfig::ALL_DISABLED, + &mut rrd_file, + ) + .unwrap(); assert_eq!(1, stores.len()); let store = stores.into_values().next().unwrap(); @@ -67,7 +72,9 @@ fn check_tags(rec: &rerun::RecordingStream) { ComponentDescriptor { archetype: Some("user.CustomPoints3D".into()), component: "user.CustomPoints3D:points".into(), - component_type: Some(::name()), + component_type: Some( + ::name(), + ), }, // ]; diff --git a/docs/snippets/all/descriptors/descr_custom_archetype.cpp b/docs/snippets/all/descriptors/descr_custom_archetype.cpp index 772279b5fdc7..8a8c1cd9f281 100644 --- a/docs/snippets/all/descriptors/descr_custom_archetype.cpp +++ b/docs/snippets/all/descriptors/descr_custom_archetype.cpp @@ -2,7 +2,7 @@ #include struct CustomPosition3D { - rerun::components::Position3D position; + rerun::Position3D position; }; template <> @@ -10,15 +10,15 @@ struct rerun::Loggable { static constexpr ComponentDescriptor Descriptor = "user.CustomPosition3D"; static const std::shared_ptr& arrow_datatype() { - return rerun::Loggable::arrow_datatype(); + return rerun::Loggable::arrow_datatype(); } // TODO(#4257) should take a rerun::Collection instead of pointer and size. static rerun::Result> to_arrow( const CustomPosition3D* instances, size_t num_instances ) { - return rerun::Loggable::to_arrow( - reinterpret_cast(instances), + return rerun::Loggable::to_arrow( + reinterpret_cast(instances), num_instances ); } @@ -32,7 +32,9 @@ struct CustomPoints3D { template <> struct rerun::AsComponents { - static Result> as_batches(const CustomPoints3D& archetype) { + static Result> as_batches( + const CustomPoints3D& archetype + ) { std::vector batches; auto positions_descr = rerun::ComponentDescriptor( @@ -41,16 +43,20 @@ struct rerun::AsComponents { "user.CustomPosition3D" ); batches.push_back( - ComponentBatch::from_loggable(archetype.positions, positions_descr).value_or_throw() + ComponentBatch::from_loggable(archetype.positions, positions_descr) + .value_or_throw() ); if (archetype.colors) { auto colors_descr = rerun::ComponentDescriptor("user.CustomPoints3D:colors") .with_archetype("user.CustomPoints3D") - .with_component_type(rerun::Loggable::ComponentType); + .with_component_type( + rerun::Loggable::ComponentType + ); batches.push_back( - ComponentBatch::from_loggable(archetype.colors, colors_descr).value_or_throw() + ComponentBatch::from_loggable(archetype.colors, colors_descr) + .value_or_throw() ); } @@ -59,12 +65,15 @@ struct rerun::AsComponents { }; int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_descriptors_custom_archetype"); + const auto rec = + rerun::RecordingStream("rerun_example_descriptors_custom_archetype"); rec.spawn().exit_on_failure(); rec.log_static( "data", - CustomPoints3D{CustomPosition3D{{1.0f, 2.0f, 3.0f}}, rerun::Color(0xFF00FFFF)} + CustomPoints3D{ + CustomPosition3D{{1.0f, 2.0f, 3.0f}}, + rerun::Color(0xFF00FFFF)} ); // The tags are indirectly checked by the Rust version (have a look over there for more info). diff --git a/docs/snippets/all/descriptors/descr_custom_archetype.py b/docs/snippets/all/descriptors/descr_custom_archetype.py index 0844653f8b5e..9ac17f0c3444 100755 --- a/docs/snippets/all/descriptors/descr_custom_archetype.py +++ b/docs/snippets/all/descriptors/descr_custom_archetype.py @@ -11,7 +11,9 @@ class CustomPoints3D(rr.AsComponents): # type: ignore[misc] - def __init__(self: Any, positions: npt.ArrayLike, colors: npt.ArrayLike) -> None: + def __init__( + self: Any, positions: npt.ArrayLike, colors: npt.ArrayLike + ) -> None: self.positions = rr.components.Position3DBatch(positions).described( rr.ComponentDescriptor( "user.CustomPoints3D:custom_positions", @@ -35,4 +37,5 @@ def as_component_batches(self) -> list[rr.DescribedComponentBatch]: rr.log("data", CustomPoints3D([[1, 2, 3]], [0xFF00FFFF]), static=True) -# The tags are indirectly checked by the Rust version (have a look over there for more info). +# The tags are indirectly checked by the Rust version (have a look over there +# for more info). diff --git a/docs/snippets/all/descriptors/descr_custom_archetype.rs b/docs/snippets/all/descriptors/descr_custom_archetype.rs index 5f2acaf43705..67a9460f1985 100644 --- a/docs/snippets/all/descriptors/descr_custom_archetype.rs +++ b/docs/snippets/all/descriptors/descr_custom_archetype.rs @@ -1,4 +1,6 @@ -use rerun::{ChunkStore, ChunkStoreConfig, ComponentBatch as _, ComponentDescriptor}; +use rerun::{ + ChunkStore, ChunkStoreConfig, ComponentBatch as _, ComponentDescriptor, +}; struct CustomPoints3D { positions: Vec, @@ -17,7 +19,9 @@ impl CustomPoints3D { fn overridden_color_descriptor() -> ComponentDescriptor { ComponentDescriptor::partial("user.CustomPoints3D:colors") .or_with_archetype(|| "user.CustomPoints3D".into()) - .or_with_component_type(::name) + .or_with_component_type( + ::name, + ) } } @@ -26,9 +30,9 @@ impl rerun::AsComponents for CustomPoints3D { [ self.positions .serialized(Self::overridden_position_descriptor()), - self.colors - .as_ref() - .and_then(|colors| colors.serialized(Self::overridden_color_descriptor())), + self.colors.as_ref().and_then(|colors| { + colors.serialized(Self::overridden_color_descriptor()) + }), ] .into_iter() .flatten() @@ -36,7 +40,9 @@ impl rerun::AsComponents for CustomPoints3D { } } -fn example(rec: &rerun::RecordingStream) -> Result<(), Box> { +fn example( + rec: &rerun::RecordingStream, +) -> Result<(), Box> { let positions = rerun::components::Position3D::new(1.0, 2.0, 3.0); let colors = rerun::components::Color::new(0xFF00FFFF); @@ -75,8 +81,12 @@ fn check_tags(rec: &rerun::RecordingStream) { if let Ok(path_to_rrd) = std::env::var("_RERUN_TEST_FORCE_SAVE") { rec.flush_blocking().unwrap(); - let stores = - ChunkStore::from_rrd_filepath(&ChunkStoreConfig::ALL_DISABLED, path_to_rrd).unwrap(); + let mut rrd_file = std::fs::File::open(&path_to_rrd).unwrap(); + let stores = ChunkStore::from_rrd_reader( + &ChunkStoreConfig::ALL_DISABLED, + &mut rrd_file, + ) + .unwrap(); assert_eq!(1, stores.len()); let store = stores.into_values().next().unwrap(); diff --git a/docs/snippets/all/descriptors/descr_custom_component.cpp b/docs/snippets/all/descriptors/descr_custom_component.cpp index aad831a36429..4450ed95939d 100644 --- a/docs/snippets/all/descriptors/descr_custom_component.cpp +++ b/docs/snippets/all/descriptors/descr_custom_component.cpp @@ -1,37 +1,39 @@ #include struct CustomPosition3D { - rerun::components::Position3D position; + rerun::Position3D position; }; template <> struct rerun::Loggable { static constexpr const ComponentDescriptor Descriptor = ComponentDescriptor( - "user.CustomArchetype", "user.CustomArchetype:custom_positions", "user.CustomPosition3D" + "user.CustomArchetype", "user.CustomArchetype:custom_positions", + "user.CustomPosition3D" ); static const std::shared_ptr& arrow_datatype() { - return rerun::Loggable::arrow_datatype(); + return rerun::Loggable::arrow_datatype(); } // TODO(#4257) should take a rerun::Collection instead of pointer and size. static rerun::Result> to_arrow( const CustomPosition3D* instances, size_t num_instances ) { - return rerun::Loggable::to_arrow( - reinterpret_cast(instances), + return rerun::Loggable::to_arrow( + reinterpret_cast(instances), num_instances ); } }; int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_descriptors_custom_component"); + const auto rec = + rerun::RecordingStream("rerun_example_descriptors_custom_component"); rec.spawn().exit_on_failure(); rec.log_static( "data", - rerun::ComponentBatch::from_loggable( + rerun::ComponentBatch::from_loggable( {1.0f, 2.0f, 3.0f}, rerun::Loggable::Descriptor ) diff --git a/docs/snippets/all/descriptors/descr_custom_component.py b/docs/snippets/all/descriptors/descr_custom_component.py index b1bfdc57d413..7bf8d1c58c10 100755 --- a/docs/snippets/all/descriptors/descr_custom_component.py +++ b/docs/snippets/all/descriptors/descr_custom_component.py @@ -16,4 +16,5 @@ ) rr.log("data", [positions], static=True) -# The tags are indirectly checked by the Rust version (have a look over there for more info). +# The tags are indirectly checked by the Rust version (have a look over there +# for more info). diff --git a/docs/snippets/all/descriptors/descr_custom_component.rs b/docs/snippets/all/descriptors/descr_custom_component.rs index 78309e494181..8aa981a508dd 100644 --- a/docs/snippets/all/descriptors/descr_custom_component.rs +++ b/docs/snippets/all/descriptors/descr_custom_component.rs @@ -1,8 +1,12 @@ -use rerun::{ChunkStore, ChunkStoreConfig, ComponentBatch as _, ComponentDescriptor}; +use rerun::{ + ChunkStore, ChunkStoreConfig, ComponentBatch as _, ComponentDescriptor, +}; -fn example(rec: &rerun::RecordingStream) -> Result<(), Box> { - let positions = - rerun::components::Position3D::new(1.0, 2.0, 3.0).try_serialized(ComponentDescriptor { +fn example( + rec: &rerun::RecordingStream, +) -> Result<(), Box> { + let positions = rerun::components::Position3D::new(1.0, 2.0, 3.0) + .try_serialized(ComponentDescriptor { archetype: Some("user.CustomArchetype".into()), component: "user.CustomArchetype:custom_positions".into(), component_type: Some("user.CustomPosition3D".into()), @@ -37,8 +41,12 @@ fn check_tags(rec: &rerun::RecordingStream) { if let Ok(path_to_rrd) = std::env::var("_RERUN_TEST_FORCE_SAVE") { rec.flush_blocking().unwrap(); - let stores = - ChunkStore::from_rrd_filepath(&ChunkStoreConfig::ALL_DISABLED, path_to_rrd).unwrap(); + let mut rrd_file = std::fs::File::open(&path_to_rrd).unwrap(); + let stores = ChunkStore::from_rrd_reader( + &ChunkStoreConfig::ALL_DISABLED, + &mut rrd_file, + ) + .unwrap(); assert_eq!(1, stores.len()); let store = stores.into_values().next().unwrap(); diff --git a/docs/snippets/all/howto/any_batch_value_column_updates.cpp b/docs/snippets/all/howto/any_batch_value_column_updates.cpp index b7b70615ab8e..b5c4371cc078 100644 --- a/docs/snippets/all/howto/any_batch_value_column_updates.cpp +++ b/docs/snippets/all/howto/any_batch_value_column_updates.cpp @@ -8,7 +8,8 @@ #include arrow::Status run_main() { - const auto rec = rerun::RecordingStream("rerun_example_any_batch_value_column_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_any_batch_value_column_updates"); rec.spawn().exit_on_failure(); constexpr int64_t STEPS = 64; @@ -20,27 +21,36 @@ arrow::Status run_main() { arrow::DoubleBuilder one_per_timestamp_builder; for (int64_t i = 0; i < STEPS; i++) { - ARROW_RETURN_NOT_OK(one_per_timestamp_builder.Append(sin(static_cast(i) / 10.0))); + ARROW_RETURN_NOT_OK( + one_per_timestamp_builder.Append(sin(static_cast(i) / 10.0)) + ); } ARROW_RETURN_NOT_OK(one_per_timestamp_builder.Finish(&arrow_array)); - auto one_per_timestamp = - rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "custom_component_single") - .value_or_throw(); + auto one_per_timestamp_result = rerun::ComponentBatch::from_arrow_array( + std::move(arrow_array), + "custom_component_single" + ); + auto one_per_timestamp = one_per_timestamp_result.value_or_throw(); arrow::DoubleBuilder ten_per_timestamp_builder; for (int64_t i = 0; i < STEPS * 10; i++) { - ARROW_RETURN_NOT_OK(ten_per_timestamp_builder.Append(cos(static_cast(i) / 100.0))); + ARROW_RETURN_NOT_OK(ten_per_timestamp_builder.Append( + cos(static_cast(i) / 100.0) + )); } ARROW_RETURN_NOT_OK(ten_per_timestamp_builder.Finish(&arrow_array)); - auto ten_per_timestamp = - rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "custom_component_multi") - .value_or_throw(); + auto ten_per_timestamp_result = rerun::ComponentBatch::from_arrow_array( + std::move(arrow_array), + "custom_component_multi" + ); + auto ten_per_timestamp = ten_per_timestamp_result.value_or_throw(); rec.send_columns( "/", rerun::TimeColumn::from_sequence("step", std::move(times)), one_per_timestamp.partitioned().value_or_throw(), - ten_per_timestamp.partitioned(std::vector(STEPS, 10)).value_or_throw() + ten_per_timestamp.partitioned(std::vector(STEPS, 10)) + .value_or_throw() ); return arrow::Status::OK(); diff --git a/docs/snippets/all/howto/any_batch_value_column_updates.py b/docs/snippets/all/howto/any_batch_value_column_updates.py index a869b7bc8aa9..f797ef8ac4ac 100644 --- a/docs/snippets/all/howto/any_batch_value_column_updates.py +++ b/docs/snippets/all/howto/any_batch_value_column_updates.py @@ -1,4 +1,4 @@ -"""Use `AnyBatchValue` and `send_column` to send an entire column of custom data to Rerun.""" +"""Use `AnyBatchValue` and `send_column` to send a column of custom data.""" from __future__ import annotations @@ -13,17 +13,23 @@ one_per_timestamp = np.sin(timestamps / 10.0) ten_per_timestamp = np.cos(np.arange(0, N * 10) / 100.0) -maybe_single_batch = rr.AnyBatchValue.column("custom_component_single", one_per_timestamp) +maybe_single_batch = rr.AnyBatchValue.column( + "custom_component_single", one_per_timestamp +) if maybe_single_batch is not None: single_batch = maybe_single_batch else: raise ValueError("Failed to create AnyBatchValue for single_per_timestamp") -maybe_multi_batch = rr.AnyBatchValue.column("custom_component_multi", ten_per_timestamp) +maybe_multi_batch = rr.AnyBatchValue.column( + "custom_component_multi", ten_per_timestamp +) if maybe_multi_batch is not None: multi_batch = maybe_multi_batch.partition([10] * N) else: - raise ValueError("Failed to create AnyBatchValue for multiple_per_timestamp") + raise ValueError( + "Failed to create AnyBatchValue for multiple_per_timestamp" + ) rr.send_columns( diff --git a/docs/snippets/all/howto/any_batch_value_column_updates.rs b/docs/snippets/all/howto/any_batch_value_column_updates.rs index 7ec1e677d4fb..d3240c1f8c28 100644 --- a/docs/snippets/all/howto/any_batch_value_column_updates.rs +++ b/docs/snippets/all/howto/any_batch_value_column_updates.rs @@ -7,8 +7,10 @@ use std::sync::Arc; use rerun::{TimeColumn, external::arrow}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_any_batch_value_column_updates") - .spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_any_batch_value_column_updates", + ) + .spawn()?; const STEPS: i64 = 64; @@ -32,8 +34,10 @@ fn main() -> Result<(), Box> { "/", [times], [ - one_per_timestamp.partitioned(std::iter::repeat_n(1, STEPS as _))?, - ten_per_timestamp.partitioned(std::iter::repeat_n(10, STEPS as _))?, + one_per_timestamp + .partitioned(std::iter::repeat_n(1, STEPS as _))?, + ten_per_timestamp + .partitioned(std::iter::repeat_n(10, STEPS as _))?, ], )?; diff --git a/docs/snippets/all/howto/any_values_column_updates.cpp b/docs/snippets/all/howto/any_values_column_updates.cpp index cd15dc80d183..e545639cf99e 100644 --- a/docs/snippets/all/howto/any_values_column_updates.cpp +++ b/docs/snippets/all/howto/any_values_column_updates.cpp @@ -10,7 +10,8 @@ #include arrow::Status run_main() { - const auto rec = rerun::RecordingStream("rerun_example_any_values_column_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_any_values_column_updates"); rec.spawn().exit_on_failure(); constexpr int64_t STEPS = 64; @@ -22,19 +23,25 @@ arrow::Status run_main() { arrow::DoubleBuilder sin_builder; for (int64_t i = 0; i < STEPS; i++) { - ARROW_RETURN_NOT_OK(sin_builder.Append(sin(static_cast(i) / 10.0))); + ARROW_RETURN_NOT_OK( + sin_builder.Append(sin(static_cast(i) / 10.0)) + ); } ARROW_RETURN_NOT_OK(sin_builder.Finish(&arrow_array)); auto sin = - rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "sin").value_or_throw(); + rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "sin") + .value_or_throw(); arrow::DoubleBuilder cos_builder; for (int64_t i = 0; i < STEPS; i++) { - ARROW_RETURN_NOT_OK(cos_builder.Append(cos(static_cast(i) / 10.0))); + ARROW_RETURN_NOT_OK( + cos_builder.Append(cos(static_cast(i) / 10.0)) + ); } ARROW_RETURN_NOT_OK(cos_builder.Finish(&arrow_array)); auto cos = - rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "cos").value_or_throw(); + rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "cos") + .value_or_throw(); rec.send_columns( "/", diff --git a/docs/snippets/all/howto/any_values_column_updates.py b/docs/snippets/all/howto/any_values_column_updates.py index 2f7220b10f53..98dfdf60bd37 100644 --- a/docs/snippets/all/howto/any_values_column_updates.py +++ b/docs/snippets/all/howto/any_values_column_updates.py @@ -1,7 +1,8 @@ """ Update custom user-defined values over time, in a single operation. -This is semantically equivalent to the `any_values_row_updates` example, albeit much faster. +This is semantically equivalent to the `any_values_row_updates` example, +albeit much faster. """ from __future__ import annotations @@ -17,5 +18,7 @@ rr.send_columns( "/", indexes=[rr.TimeColumn("step", sequence=timestamps)], - columns=rr.AnyValues.columns(sin=np.sin(timestamps / 10.0), cos=np.cos(timestamps / 10.0)), + columns=rr.AnyValues.columns( + sin=np.sin(timestamps / 10.0), cos=np.cos(timestamps / 10.0) + ), ) diff --git a/docs/snippets/all/howto/any_values_column_updates.rs b/docs/snippets/all/howto/any_values_column_updates.rs index 315c1e7185c7..7f6aad8fcaa2 100644 --- a/docs/snippets/all/howto/any_values_column_updates.rs +++ b/docs/snippets/all/howto/any_values_column_updates.rs @@ -9,8 +9,10 @@ use std::sync::Arc; use rerun::{TimeColumn, external::arrow}; fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_any_values_column_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_any_values_column_updates", + ) + .spawn()?; const STEPS: i64 = 64; diff --git a/docs/snippets/all/howto/any_values_row_updates.cpp b/docs/snippets/all/howto/any_values_row_updates.cpp index df259b27344c..9cf403deb0ee 100644 --- a/docs/snippets/all/howto/any_values_row_updates.cpp +++ b/docs/snippets/all/howto/any_values_row_updates.cpp @@ -9,7 +9,8 @@ #include arrow::Status run_main() { - const auto rec = rerun::RecordingStream("rerun_example_any_values_row_updates"); + const auto rec = + rerun::RecordingStream("rerun_example_any_values_row_updates"); rec.spawn().exit_on_failure(); for (int64_t i = 0; i < 64; i++) { @@ -18,16 +19,26 @@ arrow::Status run_main() { std::shared_ptr arrow_array; arrow::DoubleBuilder sin_builder; - ARROW_RETURN_NOT_OK(sin_builder.Append(sin(static_cast(i) / 10.0))); + ARROW_RETURN_NOT_OK( + sin_builder.Append(sin(static_cast(i) / 10.0)) + ); ARROW_RETURN_NOT_OK(sin_builder.Finish(&arrow_array)); - auto sin = - rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "sin").value_or_throw(); + auto sin_result = rerun::ComponentBatch::from_arrow_array( + std::move(arrow_array), + "sin" + ); + auto sin = sin_result.value_or_throw(); arrow::DoubleBuilder cos_builder; - ARROW_RETURN_NOT_OK(cos_builder.Append(cos(static_cast(i) / 10.0))); + ARROW_RETURN_NOT_OK( + cos_builder.Append(cos(static_cast(i) / 10.0)) + ); ARROW_RETURN_NOT_OK(cos_builder.Finish(&arrow_array)); - auto cos = - rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "cos").value_or_throw(); + auto cos_result = rerun::ComponentBatch::from_arrow_array( + std::move(arrow_array), + "cos" + ); + auto cos = cos_result.value_or_throw(); rec.log("/", sin, cos); } diff --git a/docs/snippets/all/howto/any_values_row_updates.py b/docs/snippets/all/howto/any_values_row_updates.py index c143068267bd..e0fd21ee6f93 100644 --- a/docs/snippets/all/howto/any_values_row_updates.py +++ b/docs/snippets/all/howto/any_values_row_updates.py @@ -1,7 +1,8 @@ """ Update custom user-defined values over time. -See also the `any_values_column_updates` example, which achieves the same thing in a single operation. +See also the `any_values_column_updates` example, which achieves the same +thing in a single operation. """ from __future__ import annotations @@ -14,4 +15,6 @@ for step in range(64): rr.set_time("step", sequence=step) - rr.log("/", rr.AnyValues(sin=math.sin(step / 10.0), cos=math.cos(step / 10.0))) + rr.log( + "/", rr.AnyValues(sin=math.sin(step / 10.0), cos=math.cos(step / 10.0)) + ) diff --git a/docs/snippets/all/howto/any_values_row_updates.rs b/docs/snippets/all/howto/any_values_row_updates.rs index dae5d8da9f1a..38ea8cfc4dad 100644 --- a/docs/snippets/all/howto/any_values_row_updates.rs +++ b/docs/snippets/all/howto/any_values_row_updates.rs @@ -7,7 +7,10 @@ use std::sync::Arc; use rerun::external::arrow; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_any_values_row_updates").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_any_values_row_updates", + ) + .spawn()?; for step in 0..64 { let sin_cos = rerun::AnyValues::default() diff --git a/docs/snippets/all/howto/check_connection_status.rs b/docs/snippets/all/howto/check_connection_status.rs index 9276117e0011..8f7e7b59fdbc 100644 --- a/docs/snippets/all/howto/check_connection_status.rs +++ b/docs/snippets/all/howto/check_connection_status.rs @@ -5,8 +5,10 @@ #![expect(clippy::disallowed_methods)] // We forbid naked `send` calls in core Rerun, but they are fine in snippets fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_check_connection_status") - .connect_grpc()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_check_connection_status", + ) + .connect_grpc()?; let (tx, rx) = crossbeam::channel::bounded(1); diff --git a/docs/snippets/all/howto/component_mapping.py b/docs/snippets/all/howto/component_mapping.py index 7f95af21e258..01a1116fc81a 100644 --- a/docs/snippets/all/howto/component_mapping.py +++ b/docs/snippets/all/howto/component_mapping.py @@ -1,7 +1,8 @@ """ Demonstrates how to configure visualizer component mappings from blueprint. -⚠️TODO(#12600): The API for component mappings is still evolving, so this example may change in the future. +⚠️TODO(#12600): The API for component mappings is still evolving, so this +example may change in the future. """ from __future__ import annotations @@ -11,19 +12,25 @@ import rerun as rr import rerun.blueprint as rrb -from rerun.blueprint.datatypes import ComponentSourceKind, VisualizerComponentMapping +from rerun.blueprint.datatypes import ( + ComponentSourceKind, + VisualizerComponentMapping, +) # region: nested_struct def make_sigmoid_struct_array(steps: int) -> pa.StructArray: """Creates a StructArray with a `values` field containing sigmoid data. - Note: We intentionally use float32 here to demonstrate that the data will be - automatically cast to the correct type (float64) when resolved by the visualizer. + Note: We intentionally use float32 here to demonstrate that the data will + be automatically cast to the correct type (float64) when resolved by the + visualizer. """ x = np.arange(steps, dtype=np.float32) / 10.0 sigmoid_values = 1.0 / (1.0 + np.exp(-(x - 3.0))) - return pa.StructArray.from_arrays([pa.array(sigmoid_values, type=pa.float32())], names=["values"]) + return pa.StructArray.from_arrays( + [pa.array(sigmoid_values, type=pa.float32())], names=["values"] + ) # endregion: nested_struct @@ -41,10 +48,14 @@ def make_sigmoid_struct_array(steps: int) -> pa.StructArray: *rr.Scalars.columns(scalars=np.sin(times / 10.0)), # region: custom_data # Custom scalar batch with a cos using a custom component name. - *rr.DynamicArchetype.columns(archetype="custom", components={"my_custom_scalar": np.cos(times / 10.0)}), + *rr.DynamicArchetype.columns( + archetype="custom", + components={"my_custom_scalar": np.cos(times / 10.0)}, + ), # Nested custom scalar batch with a sigmoid inside a struct. *rr.DynamicArchetype.columns( - archetype="custom", components={"my_nested_scalar": make_sigmoid_struct_array(64)} + archetype="custom", + components={"my_nested_scalar": make_sigmoid_struct_array(64)}, ), # endregion: custom_data ], @@ -67,7 +78,8 @@ def make_sigmoid_struct_array(steps: int) -> pa.StructArray: # Red sine: # * set the name via an override # * explicitly use the view's default for color - # * everything else uses the automatic component mappings, so it will pick up scalars from the store. + # * everything else uses the automatic component mappings, + # so it will pick up scalars from the store. rr.SeriesLines(names="sine (store)").visualizer( mappings=[ VisualizerComponentMapping( @@ -79,25 +91,31 @@ def make_sigmoid_struct_array(steps: int) -> pa.StructArray: # endregion: custom_value # region: source_mapping # Green cosine: - # * source scalars from the custom component "custom:my_custom_scalar" + # * source scalars from the custom component + # "custom:my_custom_scalar" # * set the name via an override - # * everything else uses the automatic component mappings, so it will pick up colors from the view default. + # * everything else uses the automatic component mappings, + # so it will pick up colors from the view default. rr.SeriesLines(names="cosine (custom)").visualizer( mappings=[ # Map scalars to the custom component. VisualizerComponentMapping( target="Scalars:scalars", source_kind=ComponentSourceKind.SourceComponent, - source_component="custom:my_custom_scalar", # Map from custom component + # Map from custom component + source_component="custom:my_custom_scalar", ), ] ), # endregion: source_mapping # region: selector_mapping # Blue sigmoid: - # * source scalars from a nested struct using a selector to extract the "values" field + # * source scalars from a nested struct using a selector to + # extract the "values" field # * set the name and an explicit blue color via overrides - rr.SeriesLines(names="sigmoid (nested)", colors=[0, 0, 255]).visualizer( + rr.SeriesLines( + names="sigmoid (nested)", colors=[0, 0, 255] + ).visualizer( mappings=[ VisualizerComponentMapping( target="Scalars:scalars", diff --git a/docs/snippets/all/howto/component_mapping.rs b/docs/snippets/all/howto/component_mapping.rs index 3fef8c7b8fc6..d9dddcad6568 100644 --- a/docs/snippets/all/howto/component_mapping.rs +++ b/docs/snippets/all/howto/component_mapping.rs @@ -2,9 +2,12 @@ use std::sync::Arc; +use itertools::Itertools as _; use rerun::AsComponents as _; use rerun::blueprint::VisualizableArchetype as _; -use rerun::external::arrow::array::{Array, Float32Array, Float64Array, StructArray}; +use rerun::external::arrow::array::{ + Array, Float32Array, Float64Array, StructArray, +}; use rerun::external::arrow::datatypes::{DataType, Field}; // region: nested_struct @@ -28,7 +31,9 @@ fn make_sigmoid_struct_array(steps: usize) -> StructArray { // endregion: nested_struct fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_component_mapping").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_component_mapping") + .spawn()?; // Generate columns for regular scalars (sin) let sin = (0..64).map(|step| (step as f64 / 10.0).sin()); @@ -38,28 +43,28 @@ fn main() -> Result<(), Box> { // Generate columns for custom component (cos) let cos = (0..64).map(|step| (step as f64 / 10.0).cos()); let cos_array = Arc::new(cos.collect::()); - let custom_columns = rerun::DynamicArchetype::new("custom") + let custom_columns: Vec<_> = rerun::DynamicArchetype::new("custom") .with_component_from_data("my_custom_scalar", cos_array) .as_serialized_batches() .into_iter() .map(|batch| batch.column_of_unit_batches()) - .collect::, _>>()?; + .try_collect()?; // Generate columns for nested custom component (sigmoid) let sigmoid_array = Arc::new(make_sigmoid_struct_array(64)); - let nested_columns = rerun::DynamicArchetype::new("custom") + let nested_columns: Vec<_> = rerun::DynamicArchetype::new("custom") .with_component_from_data("my_nested_scalar", sigmoid_array) .as_serialized_batches() .into_iter() .map(|batch| batch.column_of_unit_batches()) - .collect::, _>>()?; + .try_collect()?; // endregion: custom_data // Send plot data using send_columns. rec.send_columns( "plot", [rerun::TimeColumn::new_sequence("step", 0..64)], - sin_columns.chain(custom_columns).chain(nested_columns), + itertools::chain!(sin_columns, custom_columns, nested_columns), )?; // Add a line series color to the store data diff --git a/docs/snippets/all/howto/convert_mcap_protobuf.py b/docs/snippets/all/howto/convert_mcap_protobuf.py deleted file mode 100644 index 117faabd80c7..000000000000 --- a/docs/snippets/all/howto/convert_mcap_protobuf.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -Convert custom MCAP Protobuf messages to Rerun format. - -This example shows how to read MCAP files containing custom Protobuf messages -(not Foxglove schemas) and convert them to Rerun archetypes. It demonstrates: -- Converting transform messages -- Converting camera calibration to Pinhole -- Converting compressed video streams -- Using DynamicArchetype as a fallback for arbitrary protobuf messages -""" - -from __future__ import annotations - -import argparse -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -import numpy as np -from google.protobuf.json_format import MessageToDict -from mcap.reader import make_reader -from mcap_protobuf.decoder import DecoderFactory - -import rerun as rr - - -@dataclass -class McapMessage: - """Wrapper for decoded MCAP message data.""" - - topic: str - log_time_ns: int - publish_time_ns: int - proto_msg: Any - - -# Track static data we've already logged (to avoid duplicate static warnings) -_logged_static_transforms: set[tuple[str, str]] = set() -_logged_static_calibrations: set[str] = set() - - -def convert_timestamp(secs: int, nanos: int) -> np.datetime64: - """Convert ROS2 timestamp to nanoseconds since epoch.""" - epoch_nanos = secs * 1_000_000_000 + nanos - return np.datetime64(epoch_nanos, "ns") - - -# region: set_message_times -def set_mcap_message_times(rec: rr.RecordingStream, msg: McapMessage) -> None: - """ - Set both MCAP message timestamps on the recording stream. - - log_time_ns: when the message was logged by the recorder - publish_time_ns: when the message was published - """ - rec.set_time(timeline="message_log_time", timestamp=np.datetime64(msg.log_time_ns, "ns")) - rec.set_time(timeline="message_publish_time", timestamp=np.datetime64(msg.publish_time_ns, "ns")) - - -# endregion: set_message_times - - -def transform_msg(rec: rr.RecordingStream, msg: McapMessage) -> bool: - """Convert FrameTransforms messages to Rerun transforms.""" - if msg.proto_msg.DESCRIPTOR.name != "FrameTransforms": - return False - - # Static transform topics: transforms_static, etc. - static_topics = {"transforms_static"} - is_static = msg.topic in static_topics - - for transform in msg.proto_msg.transforms: - parent = transform.parent_frame_id - child = transform.child_frame_id - - rr_transform = rr.Transform3D( - translation=(transform.translation.x, transform.translation.y, transform.translation.z), - quaternion=rr.Quaternion( - xyzw=[transform.rotation.x, transform.rotation.y, transform.rotation.z, transform.rotation.w] - ), - parent_frame=parent, - child_frame=child, - ) - - entity_path = f"transforms/{child}" - if is_static: - key = (parent, child) - if key in _logged_static_transforms: - continue - _logged_static_transforms.add(key) - rec.log(entity_path, rr_transform, static=True) - else: - set_mcap_message_times(rec, msg) - rec.log(entity_path, rr_transform) - return True - - -def camera_calibration(rec: rr.RecordingStream, msg: McapMessage) -> bool: - """Convert CameraCalibration messages to Rerun Pinhole.""" - if msg.proto_msg.DESCRIPTOR.name != "CameraCalibration": - return False - - if msg.topic in _logged_static_calibrations: - return True - _logged_static_calibrations.add(msg.topic) - - info = msg.proto_msg - # Use from_fields to set parent_frame directly on the Pinhole - # This connects the pinhole to the named transform frame without needing a separate Transform3D - camera_info = rr.Pinhole.from_fields( - image_from_camera=info.K, - resolution=(info.width, info.height), - parent_frame=info.frame_id, - ) - rec.log(msg.topic, camera_info, static=True) - return True - - -# region: compressed_video -def compressed_video(rec: rr.RecordingStream, msg: McapMessage) -> bool: - """Convert CompressedVideo messages to Rerun VideoStream.""" - if msg.proto_msg.DESCRIPTOR.name != "CompressedVideo": - return False - - video_blob = rr.VideoStream( - codec=msg.proto_msg.format, - sample=msg.proto_msg.data, - ) - set_mcap_message_times(rec, msg) - rec.log(msg.topic, video_blob) - return True - - -# endregion: compressed_video - - -def implicit_convert(rec: rr.RecordingStream, msg: McapMessage) -> bool: - """Fallback converter: any protobuf message -> DynamicArchetype.""" - contents = MessageToDict( - msg.proto_msg, - preserving_proto_field_name=True, - always_print_fields_with_no_presence=True, - ) - - try: - dynamic_archetype = rr.DynamicArchetype( - archetype=msg.proto_msg.DESCRIPTOR.full_name, - components=contents, - ) - except Exception as e: - raise ValueError(f"{msg.proto_msg.DESCRIPTOR.full_name} {contents}") from e - - rec.log(msg.topic, dynamic_archetype, strict=True) - return True - - -# --- Main execution --- - -parser = argparse.ArgumentParser(description="Convert MCAP Protobuf messages to Rerun format.") -parser.add_argument("mcap_file", help="Path to the MCAP file to convert") -parser.add_argument( - "--urdf-dir", - type=Path, - help="Directory containing robot URDF files (optional)", -) -args = parser.parse_args() - -path_to_mcap = args.mcap_file - -with rr.RecordingStream("rerun_example_convert_mcap_protobuf") as rec: - rec.save("convert_mcap_protobuf.rrd") - - # Connect the viewer's root to the "world" frame. - rec.log("/", rr.CoordinateFrame("world"), static=True) - - # Load all URDF files from directory if provided - if args.urdf_dir: - for urdf_path in args.urdf_dir.glob("*.urdf"): - rec.log_file_from_path(urdf_path, static=True) - rec.flush() # Ensure URDFs finish loading before processing messages - - # region: conversion_loop - with open(path_to_mcap, "rb") as f: - reader = make_reader(f, decoder_factories=[DecoderFactory()]) - for _schema, channel, message, proto_msg in reader.iter_decoded_messages(): - msg = McapMessage( - topic=channel.topic, - log_time_ns=message.log_time, - publish_time_ns=message.publish_time, - proto_msg=proto_msg, - ) - if camera_calibration(rec, msg): - continue - if compressed_video(rec, msg): - continue - if transform_msg(rec, msg): - continue - if implicit_convert(rec, msg): - continue - print(f"Unhandled message on topic {msg.topic} of type {msg.proto_msg.DESCRIPTOR.name}") -# endregion: conversion_loop diff --git a/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py b/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py deleted file mode 100644 index 90ad874b1eb0..000000000000 --- a/docs/snippets/all/howto/convert_mcap_protobuf_send_column.py +++ /dev/null @@ -1,305 +0,0 @@ -""" -Convert custom MCAP Protobuf messages to Rerun format using send_columns. - -This example shows how to read MCAP files containing custom Protobuf messages - and convert them to Rerun archetypes. It demonstrates: -- Converting transform messages -- Converting camera calibration to Pinhole -- Converting compressed video streams -- Using DynamicArchetype as a fallback for arbitrary protobuf messages -""" - -from __future__ import annotations - -import argparse -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -import numpy as np -from google.protobuf.json_format import MessageToDict -from mcap.reader import make_reader -from mcap_protobuf.decoder import DecoderFactory - -import rerun as rr - -# --- Data types --- - - -@dataclass -class McapMessage: - """Wrapper for decoded MCAP message data.""" - - topic: str - log_time_ns: int - publish_time_ns: int - proto_msg: Any - - -@dataclass -class VideoFrameData: - """Parsed video frame data ready for collection.""" - - log_time_ns: int - publish_time_ns: int - data: bytes - codec: Any - - -@dataclass -class TransformData: - """Parsed transform data ready for collection.""" - - entity_path: str - log_time_ns: int - publish_time_ns: int - translation: tuple[float, float, float] - quaternion: tuple[float, float, float, float] - parent_frame: str - child_frame: str - - -class ColumnCollector: - """Collects time-series data for sending via send_columns API. - - Handles both regular archetypes (VideoStream, Transform3D, etc.) and DynamicArchetype. - - For regular archetypes: ColumnCollector(path, rr.VideoStream) - - For DynamicArchetype: ColumnCollector(path, rr.DynamicArchetype, archetype_name="custom.CompressedImage") - """ - - def __init__(self, entity_path: str, archetype_type: type, archetype_name: str | None = None): - self.entity_path = entity_path - self.archetype_type: type = archetype_type - self.archetype_name = archetype_name # Only used for DynamicArchetype - self.indexes: dict[str, list[int]] = {} - self.components: dict[str, list[Any]] = {} - - def append(self, indexes: dict[str, int], **components: Any) -> None: - """Append a row of data with time indexes and component values.""" - for name, value in indexes.items(): - self.indexes.setdefault(name, []).append(value) - for name, value in components.items(): - self.components.setdefault(name, []).append(value) - - def send(self, rec: rr.RecordingStream, **kwargs: Any) -> None: - """Send collected data via send_columns API.""" - if not self.indexes: - return - - time_columns = [ - rr.TimeColumn(name, timestamp=[np.datetime64(t, "ns") for t in timestamps]) - for name, timestamps in self.indexes.items() - ] - - if self.archetype_type is rr.DynamicArchetype: - columns = rr.DynamicArchetype.columns(archetype=self.archetype_name, components=self.components) # type: ignore[arg-type] - kwargs.setdefault("strict", True) - else: - # .columns() is code-generated per-archetype with type-specific signatures, not on base class. - # So mypy can't verify it exists on 'type'. - columns = self.archetype_type.columns(**self.components) # type: ignore[attr-defined] - - rec.send_columns( - self.entity_path, - indexes=time_columns, - columns=columns, - **kwargs, - ) - - -# --- Message handlers --- - - -def convert_timestamp(secs: int, nanos: int) -> np.datetime64: - """Convert ROS2 timestamp to nanoseconds since epoch.""" - epoch_nanos = secs * 1_000_000_000 + nanos - return np.datetime64(epoch_nanos, "ns") - - -def camera_calibration(rec: rr.RecordingStream, msg: McapMessage, logged: set[str]) -> bool: - """Convert CameraCalibration messages to Rerun Pinhole. Logs statically.""" - if msg.proto_msg.DESCRIPTOR.name != "CameraCalibration": - return False - - if msg.topic in logged: - return True - logged.add(msg.topic) - - info = msg.proto_msg - # Use from_fields to set parent_frame directly on the Pinhole - # This connects the pinhole to the named transform frame without needing a separate Transform3D - camera_info = rr.Pinhole.from_fields( - image_from_camera=info.K, - resolution=(info.width, info.height), - parent_frame=info.frame_id, - ) - rec.log(msg.topic, camera_info, static=True) - return True - - -def compressed_video(msg: McapMessage) -> VideoFrameData | None: - """Extract video frame data from CompressedVideo messages.""" - if msg.proto_msg.DESCRIPTOR.name != "CompressedVideo": - return None - - return VideoFrameData( - log_time_ns=msg.log_time_ns, - publish_time_ns=msg.publish_time_ns, - data=msg.proto_msg.data, - codec=msg.proto_msg.format, - ) - - -def transform_msg(rec: rr.RecordingStream, msg: McapMessage, logged: set[tuple[str, str]]) -> list[TransformData]: - """Extract transform data from FrameTransforms messages. Static transforms are logged immediately.""" - if msg.proto_msg.DESCRIPTOR.name != "FrameTransforms": - return [] - - static_topics = {"transforms_static"} - is_static = msg.topic in static_topics - result: list[TransformData] = [] - - for transform in msg.proto_msg.transforms: - parent = transform.parent_frame_id - child = transform.child_frame_id - translation = (transform.translation.x, transform.translation.y, transform.translation.z) - quaternion = (transform.rotation.x, transform.rotation.y, transform.rotation.z, transform.rotation.w) - entity_path = f"transforms/{child}" - - if is_static: - key = (parent, child) - if key in logged: - continue - logged.add(key) - rec.log( - entity_path, - rr.Transform3D( - translation=translation, - quaternion=rr.Quaternion(xyzw=quaternion), - parent_frame=parent, - child_frame=child, - ), - static=True, - ) - else: - result.append( - TransformData( - entity_path=entity_path, - log_time_ns=msg.log_time_ns, - publish_time_ns=msg.publish_time_ns, - translation=translation, - quaternion=quaternion, - parent_frame=parent, - child_frame=child, - ) - ) - - return result - - -def implicit_collect(msg: McapMessage) -> tuple[str, str, dict[str, Any]]: - """Fallback: extract any protobuf message as DynamicArchetype data.""" - contents = MessageToDict( - msg.proto_msg, - preserving_proto_field_name=True, - always_print_fields_with_no_presence=True, - ) - return msg.proto_msg.DESCRIPTOR.full_name, msg.topic, contents - - -def send_collected_columns(rec: rr.RecordingStream, *collector_maps: dict[str, ColumnCollector]) -> None: - """Send all collected time-series data using columnar API.""" - for collectors in collector_maps: - for collector in collectors.values(): - collector.send(rec) - - -# --- Main execution --- - -parser = argparse.ArgumentParser(description="Convert MCAP Protobuf messages to Rerun format.") -parser.add_argument("mcap_file", help="Path to the MCAP file to convert") -parser.add_argument( - "--urdf-dir", - type=Path, - help="Directory containing robot URDF files (optional)", -) -args = parser.parse_args() - -path_to_mcap = args.mcap_file - -with rr.RecordingStream("rerun_example_convert_mcap_protobuf_send_column") as rec: - rec.save("convert_mcap_protobuf_send_column.rrd") - - # Connect the viewer's root to the "world" frame. - rec.log("/", rr.CoordinateFrame("world"), static=True) - - # Load all URDF files from directory if provided - if args.urdf_dir: - for urdf_path in args.urdf_dir.glob("*.urdf"): - rec.log_file_from_path(urdf_path, static=True) - rec.flush() # Ensure URDFs finish loading before processing messages - - # State for deduplicating static logs - logged_static_transforms: set[tuple[str, str]] = set() - logged_static_calibrations: set[str] = set() - - # Collectors for time-series data - video_collectors: dict[str, ColumnCollector] = {} - transform_collectors: dict[str, ColumnCollector] = {} - dynamic_collectors: dict[str, ColumnCollector] = {} - - # region: conversion_loop - with open(path_to_mcap, "rb") as f: - reader = make_reader(f, decoder_factories=[DecoderFactory()]) - for _schema, channel, message, proto_msg in reader.iter_decoded_messages(): - msg = McapMessage( - topic=channel.topic, - log_time_ns=message.log_time, - publish_time_ns=message.publish_time, - proto_msg=proto_msg, - ) - - # Static-only: camera calibration - if camera_calibration(rec, msg, logged_static_calibrations): - continue - - # Time-series: compressed video - if frame := compressed_video(msg): - entity_path = msg.topic - if entity_path not in video_collectors: - video_collectors[entity_path] = ColumnCollector(entity_path, rr.VideoStream) - rec.log(entity_path, rr.VideoStream(codec=frame.codec), static=True) - video_collectors[entity_path].append( - indexes={"message_log_time": frame.log_time_ns, "message_publish_time": frame.publish_time_ns}, - sample=frame.data, - ) - continue - - # Time-series: transforms (static transforms logged inside handler) - if transforms := transform_msg(rec, msg, logged_static_transforms): - for t in transforms: - if t.entity_path not in transform_collectors: - transform_collectors[t.entity_path] = ColumnCollector(t.entity_path, rr.Transform3D) - transform_collectors[t.entity_path].append( - indexes={"message_log_time": t.log_time_ns, "message_publish_time": t.publish_time_ns}, - translation=t.translation, - quaternion=rr.Quaternion(xyzw=t.quaternion), - parent_frame=t.parent_frame, - child_frame=t.child_frame, - ) - continue - - # Fallback: any unhandled message as DynamicArchetype - archetype_name, entity_path, components = implicit_collect(msg) - if entity_path not in dynamic_collectors: - dynamic_collectors[entity_path] = ColumnCollector( - entity_path, rr.DynamicArchetype, archetype_name=archetype_name - ) - dynamic_collectors[entity_path].append( - indexes={"message_log_time": msg.log_time_ns, "message_publish_time": msg.publish_time_ns}, - **components, - ) - - # Send all collected time-series data using columnar API - send_collected_columns(rec, video_collectors, transform_collectors, dynamic_collectors) -# endregion: conversion_loop diff --git a/docs/snippets/all/howto/dataframe_operations.py b/docs/snippets/all/howto/dataframe_operations.py index d9af933f1f4d..8f8efa14b271 100644 --- a/docs/snippets/all/howto/dataframe_operations.py +++ b/docs/snippets/all/howto/dataframe_operations.py @@ -1,4 +1,4 @@ -"""Demonstrate common dataframe operations with Rerun Data Platform.""" +"""Demonstrate common dataframe operations with a catalog server.""" # region: setup from __future__ import annotations @@ -13,13 +13,17 @@ import rerun as rr -sample_5_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +sample_5_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +) server = rr.server.Server(datasets={"sample_dataset": sample_5_path}) CATALOG_URL = server.url() client = rr.catalog.CatalogClient(CATALOG_URL) dataset = client.get_dataset(name="sample_dataset") -observations = dataset.filter_contents(["/observation/**"]).reader(index="real_time") +observations = dataset.filter_contents(["/observation/**"]).reader( + index="real_time" +) # endregion: setup # region: group_by @@ -39,7 +43,8 @@ # region: join_query joints = dataset.filter_contents(["/observation/joint_positions"]) -# Find the earliest joint position in each episode (cast to unix epoch nanoseconds for easier math later) +# Find the earliest joint position in each episode (cast to unix epoch +# nanoseconds for easier math later) joint_min_t = ( joints .reader(index="real_time") @@ -53,7 +58,8 @@ cameras = dataset.filter_contents(["/camera/**"]) -# Find the earliest camera frame in each episode (cast to unix epoch nanoseconds for easier math later) +# Find the earliest camera frame in each episode (cast to unix epoch +# nanoseconds for easier math later) camera_min_t = ( cameras .reader(index="real_time") @@ -82,11 +88,23 @@ THRESHOLD_S = 1 NANO_S = 1_000_000_000 outliers = delta_t.filter( - dfn.Expr.between(col("start_delta_t"), -THRESHOLD_S * NANO_S, THRESHOLD_S * NANO_S, negated=True), + dfn.Expr.between( + col("start_delta_t"), + -THRESHOLD_S * NANO_S, + THRESHOLD_S * NANO_S, + negated=True, + ), +) +outliers = outliers.with_column( + "start_delta_t_s", col("start_delta_t") / 1_000_000_000.0 ) -outliers = outliers.with_column("start_delta_t_s", col("start_delta_t") / 1_000_000_000.0) -print(f"{outliers.count()=}\n", f"{joint_min_t.count()=}\n", f"{camera_min_t.count()=}", sep="") +print( + f"{outliers.count()=}\n", + f"{joint_min_t.count()=}\n", + f"{camera_min_t.count()=}", + sep="", +) # endregion: join_query # region: sub_episodes @@ -120,25 +138,37 @@ light_slice = light_slice.with_column( "prev_gripper_open", F.lag( - col("gripper_open"), default_value=False, partition_by=[col("rerun_segment_id")], order_by=[col("real_time")] + col("gripper_open"), + default_value=False, + partition_by=[col("rerun_segment_id")], + order_by=[col("real_time")], ), ) light_slice = light_slice.with_column( "gripper_change", - col("gripper_open").cast(pa.int8()) - col("prev_gripper_open").cast(pa.int8()), + col("gripper_open").cast(pa.int8()) + - col("prev_gripper_open").cast(pa.int8()), ) slice_times = light_slice.with_column( "start", - F.case(col("gripper_change")).when(lit(1), col("real_time")).otherwise(lit(None)), + F + .case(col("gripper_change")) + .when(lit(1), col("real_time")) + .otherwise(lit(None)), ).with_column( "end", - F.case(col("gripper_change")).when(lit(-1), col("real_time")).otherwise(lit(None)), + F + .case(col("gripper_change")) + .when(lit(-1), col("real_time")) + .otherwise(lit(None)), ) # Helper because pyarrow timestamps didn't have a nice min/max utility max_ts = pa.scalar(np.iinfo(np.int64).max, type=pa.timestamp("ns")) -min_ts = pa.scalar(np.iinfo(np.int64).min + 1_000_000_000, type=pa.timestamp("ns")) +min_ts = pa.scalar( + np.iinfo(np.int64).min + 1_000_000_000, type=pa.timestamp("ns") +) # This generates the column for the last observed start time slice_dense_times = ( @@ -158,7 +188,8 @@ .fill_null(value=max_ts, subset=["dense_start"]) ) -# This generates the column for the next observed end time (by finding the last_value in reversed order) +# This generates the column for the next observed end time (by finding the +# last_value in reversed order) slice_dense_times = slice_dense_times.with_column( "dense_end", F.last_value(col("end")).over( @@ -171,7 +202,9 @@ ), ).fill_null(value=min_ts, subset=["dense_end"]) -slice_dense_times = slice_dense_times.select("rerun_segment_id", "real_time", "dense_start", "dense_end") +slice_dense_times = slice_dense_times.select( + "rerun_segment_id", "real_time", "dense_start", "dense_end" +) sub_episodes = slice_dense_times.filter( dfn.Expr.between(col("real_time"), col("dense_start"), col("dense_end")), diff --git a/docs/snippets/all/howto/dataframe_performance.py b/docs/snippets/all/howto/dataframe_performance.py index 045f42778a09..f4dff6ca2e04 100644 --- a/docs/snippets/all/howto/dataframe_performance.py +++ b/docs/snippets/all/howto/dataframe_performance.py @@ -13,14 +13,19 @@ RRD_PATH = TMP_FILE.name # region: get_df -sample_video_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample" +sample_video_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample" +) server = rr.server.Server(datasets={"video_dataset": sample_video_path}) # Using OSS server for demonstration but in practice replace with # the URL of your cloud instance CATALOG_URL = server.url() client = rr.catalog.CatalogClient(CATALOG_URL) dataset = client.get_dataset(name="video_dataset") -df = dataset.filter_contents(["/compressed_images/**", "/raw_images/**"]).reader(index="log_time") +df = dataset.filter_contents([ + "/compressed_images/**", + "/raw_images/**", +]).reader(index="log_time") # endregion: get_df # region: to_list_bad @@ -48,12 +53,14 @@ rec.set_time("log_time", timestamp=second_to_last_timestamp) rec.log("/events", rr.AnyValues(flag=True)) -dataset.register(Path(RRD_PATH).as_uri(), layer_name="event_layer") +dataset.register([Path(RRD_PATH).as_uri()], layer_name="event_layer") # Read dataframe including new sparse layer -df_with_flag = dataset.filter_contents(["/compressed_images/**", "/raw_images/**", "/events/**"]).reader( - index="log_time" -) +df_with_flag = dataset.filter_contents([ + "/compressed_images/**", + "/raw_images/**", + "/events/**", +]).reader(index="log_time") # This filter only looks at the single row in events df_with_flag.filter(col("/events:flag").is_not_null()) diff --git a/docs/snippets/all/howto/dataloader.py b/docs/snippets/all/howto/dataloader.py new file mode 100644 index 000000000000..03cf879c34fc --- /dev/null +++ b/docs/snippets/all/howto/dataloader.py @@ -0,0 +1,161 @@ +"""Stream a Rerun catalog into PyTorch with the experimental dataloader.""" + +from __future__ import annotations + +from pathlib import Path + +import torch +import torch.multiprocessing +from torch import nn + +import rerun as rr + +# Rerun's tokio runtime is not fork-safe, so DataLoader workers must use +# `spawn`. Set this before constructing any DataLoader, even with +# `num_workers=0`, so bumping the worker count later doesn't deadlock on the +# first catalog call. +torch.multiprocessing.set_start_method("spawn", force=True) + +# In a real workflow you'd start a long-running OSS server (`rerun server`) +# and point a `CatalogClient` at it. For this self-contained snippet we use +# a short-lived in-process server and the DROID sample dataset shipped with +# the repo. +sample_5_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +) +server = rr.server.Server() +rrd_paths = list(sample_5_path.glob("*.rrd")) + +# region: register +client = rr.catalog.CatalogClient(server.url()) +dataset = client.create_dataset("my_robot_data", exist_ok=True) + +uris = [f"file://{p.resolve()}" for p in rrd_paths] +dataset.register(uris).wait() +# endregion: register + +# region: describe_sample +from rerun.experimental.dataloader import ( + DataSource, + Field, + FixedRateSampling, + NumericDecoder, + RerunIterableDataset, +) + +source = DataSource( + dataset=client.get_dataset("my_robot_data"), + segments=[ + "ILIAD_50aee79f_2023_07_12_20h_55m_08s", + "ILIAD_5e938e3b_2023_07_20_10h_40m_10s", + ], +) + +fields = { + "state": Field( + "/observation/joint_positions:Scalars:scalars", decode=NumericDecoder() + ), + "action": Field( + "/action/joint_positions:Scalars:scalars", decode=NumericDecoder() + ), +} + +ds = RerunIterableDataset( + source=source, + index="real_time", + fields=fields, + timeline_sampling=FixedRateSampling(rate_hz=15.0), +) +# endregion: describe_sample + + +# region: window +# Each sample now carries the next 50 action steps instead of a single value. +# Offsets are in the index timeline's native unit: integer steps for integer +# indices, or nanoseconds for timestamp indices (use multiples of the +# FixedRateSampling period). +windowed_action = Field( + "/action/joint_positions:Scalars:scalars", + decode=NumericDecoder(), + window=(1, 50), +) +# endregion: window + + +# region: video_decoder +# Decode a compressed video stream as part of each sample. +# `keyframe_interval` must be at least the actual GOP length. For timestamp +# timelines, `fps_estimate` should also approximate the true frame rate. +from rerun.experimental.dataloader import VideoFrameDecoder + +image_field = Field( + "/camera/wrist:VideoStream:sample", + decode=VideoFrameDecoder( + codec="h264", keyframe_interval=500, fps_estimate=15.0 + ), +) +# endregion: video_decoder + + +# region: dataloader +from torch.utils.data import DataLoader + +from rerun.experimental.dataloader import RerunMapDataset + + +def my_collate( + samples: list[dict[str, torch.Tensor]], +) -> dict[str, torch.Tensor]: + # Drop samples that landed outside the underlying data (FixedRateSampling + # may overshoot the end of a segment by one grid point). + samples = [ + s for s in samples if s["state"].numel() > 0 and s["action"].numel() > 0 + ] + return { + "state": torch.stack([s["state"] for s in samples]).float(), + "action": torch.stack([s["action"] for s in samples]).float(), + } + + +loader = DataLoader( + ds, + batch_size=8, + num_workers=0, + shuffle=isinstance(ds, RerunMapDataset), # iterable shuffles internally + collate_fn=my_collate, +) +# endregion: dataloader + + +# A one-layer stand-in for the actual policy. The point of the snippet is +# the dataloader, not the model. +class TinyPolicy(nn.Module): + def __init__(self, state_dim: int = 7, action_dim: int = 7) -> None: + super().__init__() + self.linear = nn.Linear(state_dim, action_dim) + + def forward( + self, batch: dict[str, torch.Tensor] + ) -> tuple[torch.Tensor, dict[str, float]]: + prediction = self.linear(batch["state"]) + loss = nn.functional.mse_loss(prediction, batch["action"]) + return loss, {} + + +policy = TinyPolicy() +optimizer = torch.optim.AdamW(policy.parameters(), lr=1e-4) +device = torch.device("cpu") +policy.to(device) +epochs = 1 + +# region: train +for epoch in range(epochs): + if isinstance(ds, RerunIterableDataset): + ds.set_epoch(epoch) + for batch in loader: + batch = {k: v.to(device) for k, v in batch.items()} + loss, _ = policy.forward(batch) + loss.backward() + optimizer.step() + optimizer.zero_grad() +# endregion: train diff --git a/docs/snippets/all/howto/dataset_resampling.py b/docs/snippets/all/howto/dataset_resampling.py index dba263bff76d..fd11479a8c61 100644 --- a/docs/snippets/all/howto/dataset_resampling.py +++ b/docs/snippets/all/howto/dataset_resampling.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 -- ASCII output tables below need wide lines """Sample snippets highlighting common performance-related improvements""" import tempfile @@ -11,7 +12,9 @@ RRD_PATH = TMP_FILE.name # region: get_dataset -sample_dataset_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "dataset" +sample_dataset_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "dataset" +) server = rr.server.Server(datasets={"dataset": sample_dataset_path}) # Using OSS server for demonstration but in practice replace with # the URL of your cloud instance @@ -25,7 +28,13 @@ dataset .get_index_ranges() .select( - "rerun_segment_id", "time_1:start", "time_1:end", "time_2:start", "time_2:end", "time_3:start", "time_3:end" + "rerun_segment_id", + "time_1:start", + "time_1:end", + "time_2:start", + "time_2:end", + "time_3:start", + "time_3:end", ) .sort("rerun_segment_id") .show() @@ -41,7 +50,13 @@ "/obj2:Points3D:positions", "/obj3:Points3D:positions", ] -(dataset.reader(index=time_index).select(*columns_of_interest).sort("rerun_segment_id", time_index).show()) +( + dataset + .reader(index=time_index) + .select(*columns_of_interest) + .sort("rerun_segment_id", time_index) + .show() +) # +----------------------------------+--------+--------------------------+--------------------------+--------------------------+ # | rerun_segment_id | time_3 | /obj1:Points3D:positions | /obj2:Points3D:positions | /obj3:Points3D:positions | @@ -58,12 +73,19 @@ # region: resampled_data resample_column = "/obj3:Points3D:positions" times_of_interest = ( - dataset.reader(index=time_index).filter(col(resample_column).is_not_null()).select("rerun_segment_id", time_index) + dataset + .reader(index=time_index) + .filter(col(resample_column).is_not_null()) + .select("rerun_segment_id", time_index) ) ( dataset - .reader(index=time_index, using_index_values=times_of_interest, fill_latest_at=True) + .reader( + index=time_index, + using_index_values=times_of_interest, + fill_latest_at=True, + ) .select(*columns_of_interest) .sort("rerun_segment_id", time_index) .show() diff --git a/docs/snippets/all/howto/dual_color_point_cloud.py b/docs/snippets/all/howto/dual_color_point_cloud.py index 30c182ca5db0..dd4089859bb9 100644 --- a/docs/snippets/all/howto/dual_color_point_cloud.py +++ b/docs/snippets/all/howto/dual_color_point_cloud.py @@ -1,8 +1,8 @@ """ -Demonstrates how to visualize the same point cloud with two different color schemes. +Visualize the same point cloud with two different color schemes. -Two custom archetypes (using Rerun's Color component type) are logged on the same entity, -then a blueprint maps each color set to a separate 3D view. +Two custom archetypes (using Rerun's Color component type) are logged on the +same entity, then a blueprint maps each color set to a separate 3D view. """ from __future__ import annotations @@ -11,7 +11,10 @@ import rerun as rr import rerun.blueprint as rrb -from rerun.blueprint.datatypes import ComponentSourceKind, VisualizerComponentMapping +from rerun.blueprint.datatypes import ( + ComponentSourceKind, + VisualizerComponentMapping, +) rr.init("rerun_example_custom_color_archetypes", spawn=True) @@ -21,7 +24,11 @@ theta = rng.uniform(0, 2 * np.pi, N) # angle around the ring phi = rng.uniform(0, 2 * np.pi, N) # angle around the tube tube = 3.0 + np.cos(phi) # major radius 3, minor radius 1 -positions = np.column_stack([tube * np.cos(theta), tube * np.sin(theta), np.sin(phi)]) +positions = np.column_stack([ + tube * np.cos(theta), + tube * np.sin(theta), + np.sin(phi), +]) # --- Color scheme 1: height (z-coordinate), cool-to-warm --- z_norm = (np.sin(phi) + 1.0) / 2.0 @@ -36,8 +43,12 @@ theta_norm = theta / (2 * np.pi) spin_rgba = np.column_stack([ np.interp(theta_norm, [0, 0.25, 0.5, 0.75, 1], [0, 120, 255, 200, 0]), # R - np.interp(theta_norm, [0, 0.25, 0.5, 0.75, 1], [200, 40, 140, 220, 200]), # G - np.interp(theta_norm, [0, 0.25, 0.5, 0.75, 1], [200, 200, 50, 60, 200]), # B + np.interp( + theta_norm, [0, 0.25, 0.5, 0.75, 1], [200, 40, 140, 220, 200] + ), # G + np.interp( + theta_norm, [0, 0.25, 0.5, 0.75, 1], [200, 200, 50, 60, 200] + ), # B np.full(N, 255), ]).astype(np.uint8) @@ -46,8 +57,13 @@ rr.log( "pointcloud", rr.Points3D(positions, radii=0.06), - rr.DynamicArchetype("HeightColors", components={"colors": rr.components.ColorBatch(height_rgba)}), - rr.DynamicArchetype("SpinColors", components={"colors": rr.components.ColorBatch(spin_rgba)}), + rr.DynamicArchetype( + "HeightColors", + components={"colors": rr.components.ColorBatch(height_rgba)}, + ), + rr.DynamicArchetype( + "SpinColors", components={"colors": rr.components.ColorBatch(spin_rgba)} + ), ) # endregion: log_custom_archetypes diff --git a/docs/snippets/all/howto/dual_color_point_cloud.rs b/docs/snippets/all/howto/dual_color_point_cloud.rs index b248932e3859..e08a9f397b8c 100644 --- a/docs/snippets/all/howto/dual_color_point_cloud.rs +++ b/docs/snippets/all/howto/dual_color_point_cloud.rs @@ -12,7 +12,8 @@ use rerun::blueprint::VisualizableArchetype as _; fn colormap(t: f64, stops: &[(f64, [u8; 3])]) -> rerun::components::Color { for i in 0..stops.len() - 1 { if t <= stops[i + 1].0 { - let frac = ((t - stops[i].0) / (stops[i + 1].0 - stops[i].0)) as f32; + let frac = + ((t - stops[i].0) / (stops[i + 1].0 - stops[i].0)) as f32; let [r0, g0, b0] = stops[i].1.map(|c| c as f32); let [r1, g1, b1] = stops[i + 1].1.map(|c| c as f32); return rerun::components::Color::from_rgb( @@ -27,8 +28,10 @@ fn colormap(t: f64, stops: &[(f64, [u8; 3])]) -> rerun::components::Color { } fn main() -> Result<(), Box> { - let rec = - rerun::RecordingStreamBuilder::new("rerun_example_custom_color_archetypes").spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_custom_color_archetypes", + ) + .spawn()?; // --- Generate a torus point cloud --- let n = 8_000; @@ -37,9 +40,7 @@ fn main() -> Result<(), Box> { let theta: Vec = (0..n).map(|_| rng.random_range(0.0..TAU)).collect(); // angle around ring let phi: Vec = (0..n).map(|_| rng.random_range(0.0..TAU)).collect(); // angle around tube - let positions: Vec<[f32; 3]> = theta - .iter() - .zip(&phi) + let positions: Vec<[f32; 3]> = std::iter::zip(&theta, &phi) .map(|(&t, &p)| { let r = 3.0 + p.cos(); [(r * t.cos()) as f32, (r * t.sin()) as f32, p.sin() as f32] @@ -65,19 +66,27 @@ fn main() -> Result<(), Box> { (0.75, [200, 220, 60]), (1.0, [0, 200, 200]), ]; - let spin_colors: Vec<_> = theta.iter().map(|&t| colormap(t / TAU, &cyclic)).collect(); + let spin_colors: Vec<_> = + theta.iter().map(|&t| colormap(t / TAU, &cyclic)).collect(); // region: log_custom_archetypes // --- Log positions and both color sets in one call --- rec.log( "pointcloud", &[ - &rerun::Points3D::new(positions).with_radii([rerun::components::Radius::from(0.06)]) + &rerun::Points3D::new(positions) + .with_radii([rerun::components::Radius::from(0.06)]) as &dyn rerun::AsComponents, &rerun::DynamicArchetype::new("HeightColors") - .with_component::("colors", height_colors), + .with_component::( + "colors", + height_colors, + ), &rerun::DynamicArchetype::new("SpinColors") - .with_component::("colors", spin_colors), + .with_component::( + "colors", + spin_colors, + ), ], )?; // endregion: log_custom_archetypes diff --git a/docs/snippets/all/howto/layers.py b/docs/snippets/all/howto/layers.py index 82e2f6296818..546cc6ed8aca 100644 --- a/docs/snippets/all/howto/layers.py +++ b/docs/snippets/all/howto/layers.py @@ -16,7 +16,9 @@ import rerun as rr -sample_5_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +sample_5_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +) server = rr.server.Server(datasets={"sample_dataset": sample_5_path}) client = server.client() @@ -38,14 +40,20 @@ from datafusion import col # Query action (commanded) and observation (actual) joint positions -joints = dataset.filter_contents(["/action/joint_positions", "/observation/joint_positions"]).reader(index="real_time") +joints = dataset.filter_contents([ + "/action/joint_positions", + "/observation/joint_positions", +]).reader(index="real_time") # Compute tracking error: L2 norm of (commanded - actual) joint positions -segment_ids = pa.table(joints.select("rerun_segment_id").distinct())["rerun_segment_id"].to_numpy() +segment_ids = pa.table(joints.select("rerun_segment_id").distinct())[ + "rerun_segment_id" +].to_numpy() rrd_paths = [] for seg_id in segment_ids: - # Filter to this segment and collect as a PyArrow table for efficient extraction to NumPy + # Filter to this segment and collect as a PyArrow table for efficient + # extraction to NumPy segment_data = pa.table( joints.filter(col("rerun_segment_id") == seg_id).select( "real_time", @@ -56,8 +64,12 @@ timestamps = segment_data["real_time"].to_numpy() - actions = np.vstack(segment_data["/action/joint_positions:Scalars:scalars"].to_numpy()) - observations = np.vstack(segment_data["/observation/joint_positions:Scalars:scalars"].to_numpy()) + actions = np.vstack( + segment_data["/action/joint_positions:Scalars:scalars"].to_numpy() + ) + observations = np.vstack( + segment_data["/observation/joint_positions:Scalars:scalars"].to_numpy() + ) # Compute L2 tracking error per timestep tracking_error = np.linalg.norm(actions - observations, axis=1) @@ -66,7 +78,9 @@ rrd_path = TMP_DIR / f"{seg_id}_tracking_error.rrd" rrd_paths.append(rrd_path) - with rr.RecordingStream(application_id="rerun_example_tracking_error", recording_id=seg_id) as rec: + with rr.RecordingStream( + application_id="rerun_example_tracking_error", recording_id=seg_id + ) as rec: rec.save(rrd_path) rr.send_columns( "/derived/tracking_error", @@ -76,7 +90,9 @@ # Register derived RRDs as a new layer -dataset.register([p.as_uri() for p in rrd_paths], layer_name="tracking_error").wait() +dataset.register( + [p.as_uri() for p in rrd_paths], layer_name="tracking_error" +).wait() # endregion: add_tracking_error # region: check_layer_names @@ -96,12 +112,18 @@ # Query the tracking error we just added and compute a quality metric from datafusion import functions as F -tracking = dataset.filter_contents(["/derived/tracking_error"]).reader(index="real_time") +tracking = dataset.filter_contents(["/derived/tracking_error"]).reader( + index="real_time" +) quality_stats = pa.table( tracking .aggregate( col("rerun_segment_id"), - [F.avg(col("/derived/tracking_error:Scalars:scalars")[0]).alias("mean_error")], + [ + F.avg(col("/derived/tracking_error:Scalars:scalars")[0]).alias( + "mean_error" + ) + ], ) .with_column("tracking_good", col("mean_error") < 0.13) .select("rerun_segment_id", "tracking_good") @@ -109,11 +131,15 @@ # Create RRDs with just the property rrd_paths = [] -for seg_id, tracking_good in zip(quality_stats["rerun_segment_id"], quality_stats["tracking_good"]): +for seg_id, tracking_good in zip( + quality_stats["rerun_segment_id"], quality_stats["tracking_good"] +): rrd_path = TMP_DIR / f"{seg_id}_quality.rrd" rrd_paths.append(rrd_path) - with rr.RecordingStream(application_id="rerun_example_quality", recording_id=seg_id) as rec: + with rr.RecordingStream( + application_id="rerun_example_quality", recording_id=seg_id + ) as rec: rec.save(rrd_path) rec.send_property("quality", rr.AnyValues(tracking_good=tracking_good)) @@ -136,16 +162,15 @@ print(segment_table) # endregion: verify -# region: manifest -manifest = ( +# region: list_layers +layers = ( dataset - .manifest() + .segment_table() .select( "rerun_segment_id", - "rerun_layer_name", - "property:quality:tracking_good", + "rerun_layer_names", ) - .sort("rerun_segment_id", "rerun_layer_name") + .sort("rerun_segment_id") ) -print(manifest) -# endregion: manifest +print(layers) +# endregion: list_layers diff --git a/docs/snippets/all/howto/lerobot_export.py b/docs/snippets/all/howto/lerobot_export.py index 82514efd47e1..ce56063a91f0 100644 --- a/docs/snippets/all/howto/lerobot_export.py +++ b/docs/snippets/all/howto/lerobot_export.py @@ -14,16 +14,20 @@ from pathlib import Path -from lerobot.datasets.lerobot_dataset import LeRobotDataset # type: ignore[import-untyped,import-not-found] -from rerun_export.lerobot.converter import convert_dataframe_to_episode -from rerun_export.lerobot.feature_inference import infer_features -from rerun_export.lerobot.types import LeRobotConversionConfig, VideoSpec +from lerobot.datasets.lerobot_dataset import ( + LeRobotDataset, # type: ignore[import-untyped,import-not-found] +) +from rerun_lerobot.converter import convert_dataframe_to_episode +from rerun_lerobot.feature_inference import infer_features +from rerun_lerobot.types import LeRobotConversionConfig, VideoSpec import rerun as rr # Start a server with RRD recordings # In practice, you would point this to your directory of RRD files -sample_5_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +sample_5_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +) server = rr.server.Server(datasets={"robot_dataset": sample_5_path}) client = server.client() @@ -59,8 +63,9 @@ # For this example, we assume a static instruction instructions = "/language_instruction:TextDocument:text" -# Specify video streams to include in the dataset -# Each stream needs a key (camera identifier) and entity path where the VideoStream is logged +# Specify video streams to include in the dataset. +# Each stream needs a key (camera identifier) and entity path where the +# VideoStream is logged videos = [ VideoSpec(key="ext1", path="/camera/ext1", video_format="h264"), VideoSpec(key="ext2", path="/camera/ext2", video_format="h264"), @@ -70,10 +75,12 @@ # Configure the conversion parameters # This maps Rerun's flexible data model to LeRobot's standardized format config = LeRobotConversionConfig( - fps=15, # Target framerate for the dataset + fps=15, # Target frame rate for the dataset index_column="real_time", # Timeline to use for alignment - action="/action/joint_positions:Scalars:scalars", # Fully qualified action column - state="/observation/joint_positions:Scalars:scalars", # Fully qualified state column + # Fully qualified action column + action="/action/joint_positions:Scalars:scalars", + # Fully qualified state column + state="/observation/joint_positions:Scalars:scalars", task=instructions, # Task description column videos=videos, # Video streams to include ) @@ -102,7 +109,7 @@ # region: export_episode # Convert the recording to a LeRobot episode -# This aligns all time series to the target framerate, extracts video frames, +# This aligns all time series to the target frame rate, extracts video frames, # and writes the episode data in LeRobot's Parquet format print("Creating episode") diff --git a/docs/snippets/all/howto/load_mcap.rs b/docs/snippets/all/howto/load_mcap.rs index ea626d76b91c..b7db6cce94b4 100644 --- a/docs/snippets/all/howto/load_mcap.rs +++ b/docs/snippets/all/howto/load_mcap.rs @@ -4,7 +4,8 @@ fn main() -> Result<(), Box> { let path_to_mcap = std::env::args().nth(2).ok_or("Missing MCAP file")?; // Initialize the SDK and give our recording a unique name - let rec = rerun::RecordingStreamBuilder::new("rerun_example_load_mcap").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_load_mcap") + .spawn()?; // Load the MCAP file rec.log_file_from_path(path_to_mcap, None, false)?; diff --git a/docs/snippets/all/howto/load_urdf.py b/docs/snippets/all/howto/load_urdf.py index ae6ab5a29402..208c3fcafbc1 100644 --- a/docs/snippets/all/howto/load_urdf.py +++ b/docs/snippets/all/howto/load_urdf.py @@ -19,7 +19,8 @@ joint_axis = [0, 0, 1] # comes from URDF joint_angle = 1.216 # radians origin_xyz = [0, 0, 0.1] # comes from URDF - # Make sure that `parent_frame` and `child_frame` match the joint's frame IDs in the URDF file. + # Make sure that `parent_frame` and `child_frame` match the joint's + # frame IDs in the URDF file. rec.log( "transforms", rr.Transform3D( diff --git a/docs/snippets/all/howto/micro_batching.py b/docs/snippets/all/howto/micro_batching.py index e2cfb6390d5a..7230a537664f 100644 --- a/docs/snippets/all/howto/micro_batching.py +++ b/docs/snippets/all/howto/micro_batching.py @@ -1,7 +1,7 @@ """ Shows how to configure micro-batching directly from code. -Check out for more information. +Check out for more info. """ from datetime import timedelta @@ -21,6 +21,7 @@ rec = rr.RecordingStream("rerun_example_micro_batching", batcher_config=config) rec.spawn() -# These 10 log calls are guaranteed be batched together, and end up in the same chunk. +# These 10 log calls are guaranteed be batched together, and end up in the +# same chunk. for i in range(10): rec.log("logs", rr.TextLog(f"log #{i}")) diff --git a/docs/snippets/all/howto/micro_batching.rs b/docs/snippets/all/howto/micro_batching.rs index 7f2acbeadba7..f91ac5a2ac93 100644 --- a/docs/snippets/all/howto/micro_batching.rs +++ b/docs/snippets/all/howto/micro_batching.rs @@ -7,14 +7,16 @@ fn main() -> Result<(), Box> { // * RERUN_FLUSH_NUM_BYTES=<+inf> // * RERUN_FLUSH_NUM_ROWS=10 // * RERUN_FLUSH_TICK_SECS=10 - let mut config = rerun::log::ChunkBatcherConfig::from_env().unwrap_or_default(); + let mut config = + rerun::log::ChunkBatcherConfig::from_env().unwrap_or_default(); config.flush_num_bytes = u64::MAX; config.flush_num_rows = 10; config.flush_tick = std::time::Duration::from_secs(10); - let rec = rerun::RecordingStreamBuilder::new("rerun_example_micro_batching") - .batcher_config(config) - .spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_micro_batching") + .batcher_config(config) + .spawn()?; // These 10 log calls are guaranteed be batched together, and end up in the same chunk. for i in 0..10 { diff --git a/docs/snippets/all/howto/optimize_chunks.py b/docs/snippets/all/howto/optimize_chunks.py new file mode 100644 index 000000000000..a3c8c04fb4d2 --- /dev/null +++ b/docs/snippets/all/howto/optimize_chunks.py @@ -0,0 +1,29 @@ +"""Compact an MCAP recording in-process via the Chunk Processing API.""" + +from __future__ import annotations + +from pathlib import Path + +from rerun.experimental import McapReader, OptimizationProfile + +mcap_path = ( + Path(__file__).resolve().parents[4] + / "tests" + / "assets" + / "mcap" + / "trossen_transfer_cube.mcap" +) +output_path = Path("trossen_compacted.rrd") + +# region: optimize +( + McapReader(mcap_path) + .stream() + .collect(optimize=OptimizationProfile.OBJECT_STORE) + .write_rrd( + output_path, + application_id="rerun_example_optimize", + recording_id=mcap_path.stem, + ) +) +# endregion: optimize diff --git a/docs/snippets/all/howto/query-and-transform/segment_url.py b/docs/snippets/all/howto/query-and-transform/segment_url.py index d893c3be34de..275bba73f184 100644 --- a/docs/snippets/all/howto/query-and-transform/segment_url.py +++ b/docs/snippets/all/howto/query-and-transform/segment_url.py @@ -12,7 +12,9 @@ import rerun as rr from rerun.utilities.datafusion.functions.url_generation import segment_url -sample_5_path = Path(__file__).parents[5] / "tests" / "assets" / "rrd" / "sample_5" +sample_5_path = ( + Path(__file__).parents[5] / "tests" / "assets" / "rrd" / "sample_5" +) server = rr.server.Server(datasets={"sample_dataset": sample_5_path}) client = server.client() @@ -35,7 +37,11 @@ [t + timedelta(milliseconds=500) for t in event_times], type=pa.timestamp("ns"), ), - "entity_path": ["/camera/rgb", "/observation/joint_positions", "/observation/gripper_state"], + "entity_path": [ + "/camera/rgb", + "/observation/joint_positions", + "/observation/gripper_state", + ], }, ) @@ -51,15 +57,22 @@ # endregion: basic # region: timestamp -ts = view.segment_table(join_meta=meta_df).select("rerun_segment_id", "event_time") +ts = view.segment_table(join_meta=meta_df).select( + "rerun_segment_id", "event_time" +) ts = ts.sort("rerun_segment_id") -ts = ts.with_column("url", segment_url(dataset, timestamp="event_time", timeline_name="real_time")) +ts = ts.with_column( + "url", + segment_url(dataset, timestamp="event_time", timeline_name="real_time"), +) for url in ts.select("url").to_pydict()["url"]: print(url) # endregion: timestamp # region: time_range -tr = view.segment_table(join_meta=meta_df).select("rerun_segment_id", "range_start", "range_end") +tr = view.segment_table(join_meta=meta_df).select( + "rerun_segment_id", "range_start", "range_end" +) tr = tr.sort("rerun_segment_id") tr = tr.with_column( "url", @@ -75,7 +88,9 @@ # endregion: time_range # region: selection -sel = view.segment_table(join_meta=meta_df).select("rerun_segment_id", "entity_path") +sel = view.segment_table(join_meta=meta_df).select( + "rerun_segment_id", "entity_path" +) sel = sel.sort("rerun_segment_id") sel = sel.with_column("url", segment_url(dataset, selection="entity_path")) for url in sel.select("url").to_pydict()["url"]: @@ -103,7 +118,9 @@ # endregion: combined # region: expressions -expr = view.segment_table(join_meta=meta_df).select("rerun_segment_id", "event_time") +expr = view.segment_table(join_meta=meta_df).select( + "rerun_segment_id", "event_time" +) expr = expr.sort("rerun_segment_id") expr = expr.with_column( "url", diff --git a/docs/snippets/all/howto/query_images.py b/docs/snippets/all/howto/query_images.py index 03d0739c4101..6fac122d83ec 100644 --- a/docs/snippets/all/howto/query_images.py +++ b/docs/snippets/all/howto/query_images.py @@ -13,13 +13,18 @@ import rerun as rr -sample_video_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample" +sample_video_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample" +) server = rr.server.Server(datasets={"video_dataset": sample_video_path}) CATALOG_URL = server.url() client = rr.catalog.CatalogClient(CATALOG_URL) dataset = client.get_dataset(name="video_dataset") -df = dataset.filter_contents(["/compressed_images/**", "/raw_images/**"]).reader(index="log_time") +df = dataset.filter_contents([ + "/compressed_images/**", + "/raw_images/**", +]).reader(index="log_time") times = pa.table(df.select("log_time"))["log_time"].to_numpy() # endregion: setup @@ -34,11 +39,19 @@ # region: raw_image content_column = "/raw_images:Image:buffer" format_column = "/raw_images:Image:format" -row = df.filter(col("log_time") == times[0]).select(content_column, format_column) +row = df.filter(col("log_time") == times[0]).select( + content_column, format_column +) table = pa.table(row) format_details = table[format_column][0][0] flattened_image = table[content_column].to_numpy()[0][0] -num_channels = rr.datatypes.color_model.ColorModel.auto(int(format_details["color_model"].as_py())).num_channels() -image = flattened_image.reshape(format_details["height"].as_py(), format_details["width"].as_py(), num_channels) +num_channels = rr.datatypes.color_model.ColorModel.auto( + int(format_details["color_model"].as_py()) +).num_channels() +image = flattened_image.reshape( + format_details["height"].as_py(), + format_details["width"].as_py(), + num_channels, +) print(f"{image.shape=}") # endregion: raw_image diff --git a/docs/snippets/all/howto/query_video_keyframes.py b/docs/snippets/all/howto/query_video_keyframes.py index 02e61964611f..f24f401425ba 100644 --- a/docs/snippets/all/howto/query_video_keyframes.py +++ b/docs/snippets/all/howto/query_video_keyframes.py @@ -24,7 +24,9 @@ import rerun as rr -sample_video_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample" +sample_video_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample" +) server = rr.server.Server(datasets={"video_dataset": sample_video_path}) client = server.client() @@ -73,7 +75,9 @@ # Make sure the timeline matches the original video stream timeline = "log_time" time_column = rr.TimeColumn(timeline=timeline, timestamp=keyframe_times) -content = rr.DynamicArchetype.columns(archetype="KeyframeData", components={"is_keyframe": keyframe_values}) +content = rr.DynamicArchetype.columns( + archetype="KeyframeData", components={"is_keyframe": keyframe_values} +) # Write to a new file as a layer layer_path = TMP_DIR / "keyframe_layer.rrd" @@ -85,13 +89,14 @@ rec.send_columns("/video_stream", indexes=[time_column], columns=[*content]) # Register the layer with the dataset -dataset.register(layer_path.as_uri(), layer_name="keyframes") +dataset.register([layer_path.as_uri()], layer_name="keyframes") print(f"Registered keyframe layer at {layer_path}") # endregion: add_keyframe_column # region: query_with_keyframes # Query using keyframe information for efficient random access -# Assume we've already added keyframe information via the preprocessing step above +# Assume we've already added keyframe information via the preprocessing step +# above target_frame_index = 42 target_time = times[target_frame_index] @@ -100,11 +105,19 @@ keyframe_column = "/video_stream:is_keyframe" full_df = dataset.filter_contents(["/video_stream/**"]).reader(index="log_time") -# Query to find the most recent keyframe at or before the target time -# Since we only log when is_keyframe=True, any row with this column present is a keyframe -keyframe_slice = full_df.filter((col("log_time") <= target_time) & col(keyframe_column).is_not_null()) +# Query to find the most recent keyframe at or before the target time. +# Since we only log when is_keyframe=True, any row with this column present +# is a keyframe +keyframe_slice = full_df.filter( + (col("log_time") <= target_time) & col(keyframe_column).is_not_null() +) closest_keyframe_df = keyframe_slice.aggregate( - [], [F.last_value(col("log_time"), order_by=[col("log_time")]).alias("latest_keyframe")] + [], + [ + F.last_value(col("log_time"), order_by=[col("log_time")]).alias( + "latest_keyframe" + ) + ], ) keyframe_result = pa.table(closest_keyframe_df) @@ -114,14 +127,22 @@ start_frame_idx = np.searchsorted(times, start_time) frames_saved = target_frame_index - start_frame_idx -print(f"Found keyframe at frame {start_frame_idx}, saved decoding {frames_saved} frames") +print( + f"Found keyframe at frame {start_frame_idx}, " + f"saved decoding {frames_saved} frames" +) # Query only the video samples from keyframe to target (much more efficient!) -efficient_video_df = df.filter(col("log_time").between(start_time, target_time)).select("log_time", video_column) +efficient_video_df = df.filter( + col("log_time").between(start_time, target_time) +).select("log_time", video_column) efficient_table = pa.table(efficient_video_df) frames_to_decode = len(efficient_table) -print(f"Decoding {frames_to_decode} frames (vs {target_frame_index + 1} without keyframe info)") +print( + f"Decoding {frames_to_decode} frames " + f"(vs {target_frame_index + 1} without keyframe info)" +) # Now decode just this smaller range samples = efficient_table[video_column].to_numpy() @@ -136,7 +157,9 @@ # Decode to the target frame frame = None -for packet, time in zip(container.demux(video_stream), sample_times, strict=False): +for packet, time in zip( + container.demux(video_stream), sample_times, strict=False +): packet.time_base = Fraction(1, 1_000_000_000) packet.pts = int(time - sample_times[0]) packet.dts = packet.pts @@ -145,5 +168,8 @@ if isinstance(frame, av.VideoFrame): image = np.asarray(frame.to_image()) - print(f"Efficiently decoded frame {target_frame_index} with shape: {image.shape}") + print( + f"Efficiently decoded frame {target_frame_index} " + f"with shape: {image.shape}" + ) # endregion: query_with_keyframes diff --git a/docs/snippets/all/howto/query_videos.py b/docs/snippets/all/howto/query_videos.py index 58ee96eebf0a..3460cf340e2b 100644 --- a/docs/snippets/all/howto/query_videos.py +++ b/docs/snippets/all/howto/query_videos.py @@ -23,7 +23,9 @@ import rerun as rr -sample_video_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample" +sample_video_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "video_sample" +) server = rr.server.Server(datasets={"video_dataset": sample_video_path}) CATALOG_URL = server.url() @@ -35,10 +37,15 @@ # region: check_codec codec_column = "/video_stream:VideoStream:codec" -num_codec_matches = df.select(col(codec_column)[0] == rr.VideoCodec.H264.value).count() +num_codec_matches = df.select( + col(codec_column)[0] == rr.VideoCodec.H264.value +).count() if num_codec_matches != df.select(codec_column).count(): - raise ValueError(f"Expected H.264 codec {rr.VideoCodec.H264.value}, got {df.select(codec_column).limit(1)}") + raise ValueError( + f"Expected H.264 codec {rr.VideoCodec.H264.value}, " + f"got {df.select(codec_column).limit(1)}" + ) # endregion: check_codec # region: decode_frame @@ -48,7 +55,9 @@ # Query all samples up to and including the target frame. # We need to decode from the start (or a keyframe) to reach our target. selected_time = times[selected_frame_index] -video_df = df.filter(col("log_time") <= selected_time).select("log_time", video_column) +video_df = df.filter(col("log_time") <= selected_time).select( + "log_time", video_column +) pa_table = pa.table(video_df) # Concatenate samples into a byte buffer @@ -66,7 +75,9 @@ # Decode all frames up to our target, keeping only the last one frame = None -for packet, time in zip(container.demux(video_stream), sample_times, strict=False): +for packet, time in zip( + container.demux(video_stream), sample_times, strict=False +): packet.time_base = Fraction(1, 1_000_000_000) # Timestamps in nanoseconds packet.pts = int(time - start_time) packet.dts = packet.pts # No B-frames, so dts == pts @@ -87,7 +98,9 @@ all_samples = pa_table["/video_stream:VideoStream:sample"] # Concatenate samples into a single byte buffer -sample_bytes = np.concatenate([sample[0] for sample in all_samples.to_numpy()]).tobytes() +sample_bytes = np.concatenate([ + sample[0] for sample in all_samples.to_numpy() +]).tobytes() sample_bytes_io = BytesIO(sample_bytes) # Setup input container (H.264 Annex B stream) @@ -101,7 +114,9 @@ # Remux packets with correct timestamps start_time = all_times.chunk(0)[0] -for packet, time in zip(input_container.demux(input_stream), all_times, strict=False): +for packet, time in zip( + input_container.demux(input_stream), all_times, strict=False +): packet.time_base = Fraction(1, 1_000_000_000) packet.pts = int(time.value - start_time.value) packet.dts = packet.pts diff --git a/docs/snippets/all/howto/screenshot.py b/docs/snippets/all/howto/screenshot.py index 46ba21497ae2..70407c763181 100644 --- a/docs/snippets/all/howto/screenshot.py +++ b/docs/snippets/all/howto/screenshot.py @@ -4,16 +4,19 @@ import rerun.blueprint as rrb from rerun.experimental import ViewerClient -# Setup a viewer with a known blueprint. -rr.init("rerun_example_screenshot", spawn=True) -view = rrb.Spatial3DView(name="my blue 3D", background=[100, 149, 237]) -rr.send_blueprint(view) +# Spawn a headless viewer; the client owns its lifetime. +with ViewerClient.spawn(headless=True) as viewer: + rec = rr.RecordingStream("rerun_example_screenshot") + rec.connect_grpc(url=viewer.url) -# Connect to a local viewer. -viewer = ViewerClient() + view = rrb.Spatial3DView(name="my blue 3D", background=[100, 149, 237]) + rec.send_blueprint(view) -# Screenshot the entire viewer. -viewer.save_screenshot("entire_viewer.jpg") + # Screenshot the entire viewer. + viewer.save_screenshot("entire_viewer.jpg") -# Screenshot only the view we created earlier. -viewer.save_screenshot("my_view.png", view_id=view.id) + # Screenshot only the view we created earlier. + viewer.save_screenshot("my_view.png", view_id=view.id) + + # Disconnect the RecordingStream before the headless viewer shuts down. + rec.disconnect() diff --git a/docs/snippets/all/howto/send_table.py b/docs/snippets/all/howto/send_table.py index 2f3e6abbbe80..560e0f877cb7 100644 --- a/docs/snippets/all/howto/send_table.py +++ b/docs/snippets/all/howto/send_table.py @@ -4,11 +4,15 @@ from rerun.experimental import ViewerClient -client = ViewerClient(addr="rerun+http://0.0.0.0:9876/proxy") +client = ViewerClient.connect(url="rerun+http://127.0.0.1:9876/proxy") client.send_table( "Hello from Python", pa.RecordBatch.from_pydict({ "id": [1, 2, 3], - "url": ["https://www.rerun.io", "https://github.com/rerun-io/rerun", "https://crates.io/crates/rerun"], + "url": [ + "https://www.rerun.io", + "https://github.com/rerun-io/rerun", + "https://crates.io/crates/rerun", + ], }), ) diff --git a/docs/snippets/all/howto/serve_web_viewer.py b/docs/snippets/all/howto/serve_web_viewer.py index b22f4f7c6efd..3dfb7c3cccb4 100644 --- a/docs/snippets/all/howto/serve_web_viewer.py +++ b/docs/snippets/all/howto/serve_web_viewer.py @@ -1,4 +1,4 @@ -"""Demonstrates how to log data to a gRPC server and connect the web viewer to it.""" +"""Log data to a gRPC server and connect the web viewer to it.""" import time @@ -14,7 +14,8 @@ # Log some data to the gRPC server. rr.log("data", rr.Boxes3D(half_sizes=[2.0, 2.0, 1.0])) -# Keep server running. If we cancel it too early, data may never arrive in the browser. +# Keep server running. If we cancel it too early, data may never arrive in +# the browser. try: while True: time.sleep(1) diff --git a/docs/snippets/all/howto/serve_web_viewer.rs b/docs/snippets/all/howto/serve_web_viewer.rs index 3a96fdbffa39..36548a38889d 100644 --- a/docs/snippets/all/howto/serve_web_viewer.rs +++ b/docs/snippets/all/howto/serve_web_viewer.rs @@ -5,16 +5,19 @@ fn main() -> Result<(), Box> { // Start a gRPC server and use it as log sink. - let rec = rerun::RecordingStreamBuilder::new("rerun_example_serve_web_viewer").serve_grpc()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_serve_web_viewer") + .serve_grpc()?; // Connect the web viewer to the gRPC server and open it in the browser. // Feature check here is for the in-repository build. // For your own code remove it and enable the `web_viewer` feature on the `rerun` crate. #[cfg(feature = "web_viewer")] - let _server_guard = rerun::serve_web_viewer(rerun::web_viewer::WebViewerConfig { - connect_to: vec!["rerun+http://localhost/proxy".to_owned()], - ..Default::default() - })?; + let _server_guard = + rerun::serve_web_viewer(rerun::web_viewer::WebViewerConfig { + connect_to: vec!["rerun+http://localhost/proxy".to_owned()], + ..Default::default() + })?; // Log some data to the gRPC server. rec.log("data", &rerun::Boxes3D::from_half_sizes([(2.0, 2.0, 1.0)]))?; diff --git a/docs/snippets/all/howto/set_sinks.cpp b/docs/snippets/all/howto/set_sinks.cpp index 1e865bc73504..fb79fc47d762 100644 --- a/docs/snippets/all/howto/set_sinks.cpp +++ b/docs/snippets/all/howto/set_sinks.cpp @@ -14,9 +14,14 @@ int main(int argc, char* argv[]) { .exit_on_failure(); // Create some data using the `grid` utility function. - std::vector points = grid3d(-10.f, 10.f, 10); - std::vector colors = grid3d(0, 255, 10); + std::vector points = + grid3d(-10.f, 10.f, 10); + std::vector colors = + grid3d(0, 255, 10); // Log the "my_points" entity with our data, using the `Points3D` archetype. - rec.log("my_points", rerun::Points3D(points).with_colors(colors).with_radii({0.5f})); + rec.log( + "my_points", + rerun::Points3D(points).with_colors(colors).with_radii({0.5f}) + ); } diff --git a/docs/snippets/all/howto/set_sinks.rs b/docs/snippets/all/howto/set_sinks.rs index 80cb80c40928..78524d5e41a7 100644 --- a/docs/snippets/all/howto/set_sinks.rs +++ b/docs/snippets/all/howto/set_sinks.rs @@ -3,12 +3,13 @@ use rerun::{demo_util::grid, external::glam}; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_attach_sinks").set_sinks(( - // Connect to a local viewer using the default URL. - rerun::sink::GrpcSink::default(), - // Write data to a `data.rrd` file in the current directory. - rerun::sink::FileSink::new("data.rrd")?, - ))?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_attach_sinks") + .set_sinks(( + // Connect to a local viewer using the default URL. + rerun::sink::GrpcSink::default(), + // Write data to a `data.rrd` file in the current directory. + rerun::sink::FileSink::new("data.rrd")?, + ))?; // Create some data using the `grid` utility function. let points = grid(glam::Vec3::splat(-10.0), glam::Vec3::splat(10.0), 10); diff --git a/docs/snippets/all/howto/state_remapping.py b/docs/snippets/all/howto/state_remapping.py new file mode 100644 index 000000000000..f39e50c6bd66 --- /dev/null +++ b/docs/snippets/all/howto/state_remapping.py @@ -0,0 +1,53 @@ +""" +Visualize an arbitrary component as state by remapping `StateChange.state`. + +⚠️TODO(#12600): The API for component mappings is still evolving, so this +example may change in the future. +""" + +from __future__ import annotations + +import rerun as rr +import rerun.blueprint as rrb +from rerun.blueprint.datatypes import ( + ComponentSourceKind, + VisualizerComponentMapping, +) + +rr.init("rerun_example_state_remapping", spawn=True) + +# region: custom_data +# Log a robot mode as a plain string component — note that this is *not* a +# `StateChange`, just an arbitrary string logged via `AnyValues`. It shows up on +# the entity as the component `AnyValues:mode`. +modes = ["booting", "idle", "driving", "idle", "charging"] +for step, mode in enumerate(modes): + rr.set_time("step", sequence=step) + rr.log("robot", rr.AnyValues(mode=mode)) +# endregion: custom_data + +# region: blueprint +# Remap the state-timeline visualizer's `StateChange:state` input to read from +# the custom `AnyValues:mode` component instead of an actual `StateChange`. +# Any string, boolean, or numeric component can be visualized this way. +blueprint = rrb.Blueprint( + rrb.StateTimelineView( + origin="/", + name="Robot mode", + overrides={ + "robot": [ + rr.StateChange.from_fields().visualizer( + mappings=[ + VisualizerComponentMapping( + target="StateChange:state", + source_kind=ComponentSourceKind.SourceComponent, + source_component="AnyValues:mode", + ), + ], + ), + ], + }, + ), +) +rr.send_blueprint(blueprint) +# endregion: blueprint diff --git a/docs/snippets/all/howto/state_timeline.cpp b/docs/snippets/all/howto/state_timeline.cpp new file mode 100644 index 000000000000..8c99f87c256e --- /dev/null +++ b/docs/snippets/all/howto/state_timeline.cpp @@ -0,0 +1,39 @@ +// Demonstrates the experimental state timeline view: logging state changes and customizing display. + +#include + +int main(int argc, char* argv[]) { + const auto rec = + rerun::RecordingStream("rerun_example_howto_state_timeline"); + rec.spawn().exit_on_failure(); + + // region: state_config + // Customize how each state value is displayed (label, color, visibility). + // Log as static so the configuration applies for the entire recording. + rec.log_static( + "door", + rerun::StateConfiguration() + .with_values({"open", "closed"}) + .with_labels({"Open", "Closed"}) + .with_colors({0x4CAF50FF, 0xEF5350FF}) + ); + // endregion: state_config + + // region: log_changes + // Log state transitions for two entities. Each call marks the start of a new state; + // the previous state implicitly ends. The `/door` lane uses the `StateConfiguration` + // above, while `/window` gets default styling (raw value as label, hashed color). + rec.set_time_sequence("step", 0); + rec.log("door", rerun::StateChange().with_state({"open"})); + rec.log("window", rerun::StateChange().with_state({"closed"})); + + rec.set_time_sequence("step", 1); + rec.log("door", rerun::StateChange().with_state({"closed"})); + + rec.set_time_sequence("step", 3); + rec.log("window", rerun::StateChange().with_state({"open"})); + + rec.set_time_sequence("step", 4); + rec.log("door", rerun::StateChange().with_state({"open"})); + // endregion: log_changes +} diff --git a/docs/snippets/all/howto/state_timeline.py b/docs/snippets/all/howto/state_timeline.py new file mode 100644 index 000000000000..0e99356e5768 --- /dev/null +++ b/docs/snippets/all/howto/state_timeline.py @@ -0,0 +1,49 @@ +"""Demonstrates the experimental state timeline view.""" + +import rerun as rr +import rerun.blueprint as rrb + +rr.init("rerun_example_howto_state_timeline", spawn=True) + +# region: state_config +# Customize how each state value is displayed (label, color, visibility). +# Log as static so the configuration applies for the entire recording. +rr.log( + "door", + rr.StateConfiguration( + values=["open", "closed"], + labels=["Open", "Closed"], + colors=[0x4CAF50FF, 0xEF5350FF], + ), + static=True, +) +# endregion: state_config + +# region: log_changes +# Log state transitions for two entities. Each call marks the start of a new +# state; the previous state implicitly ends. The `/door` lane uses the +# `StateConfiguration` above, while `/window` gets default styling (raw value +# as label, hashed color). +rr.set_time("step", sequence=0) +rr.log("door", rr.StateChange(state="open")) +rr.log("window", rr.StateChange(state="closed")) + +rr.set_time("step", sequence=1) +rr.log("door", rr.StateChange(state="closed")) + +rr.set_time("step", sequence=3) +rr.log("window", rr.StateChange(state="open")) + +rr.set_time("step", sequence=4) +rr.log("door", rr.StateChange(state="open")) +# endregion: log_changes + +# region: blueprint +# Place a state timeline view at the root. The viewer will create one +# automatically as soon as it sees `StateChange` data, but the blueprint API +# lets you control the origin, name, and layout explicitly. +blueprint = rrb.Blueprint( + rrb.StateTimelineView(origin="/", name="Doors and windows"), +) +rr.send_blueprint(blueprint) +# endregion: blueprint diff --git a/docs/snippets/all/howto/state_timeline.rs b/docs/snippets/all/howto/state_timeline.rs new file mode 100644 index 000000000000..c938b90cf193 --- /dev/null +++ b/docs/snippets/all/howto/state_timeline.rs @@ -0,0 +1,40 @@ +//! Demonstrates the experimental state timeline view: logging state changes and customizing display. + +fn main() -> Result<(), Box> { + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_howto_state_timeline", + ) + .spawn()?; + + // region: state_config + // Customize how each state value is displayed (label, color, visibility). + // Log as static so the configuration applies for the entire recording. + rec.log_static( + "door", + &rerun::StateConfiguration::new() + .with_values(["open", "closed"]) + .with_labels(["Open", "Closed"]) + .with_colors([0x4CAF50FFu32, 0xEF5350FFu32]), + )?; + // endregion: state_config + + // region: log_changes + // Log state transitions for two entities. Each call marks the start of a new state; + // the previous state implicitly ends. The `/door` lane uses the `StateConfiguration` + // above, while `/window` gets default styling (raw value as label, hashed color). + rec.set_time_sequence("step", 0); + rec.log("door", &rerun::StateChange::single("open"))?; + rec.log("window", &rerun::StateChange::single("closed"))?; + + rec.set_time_sequence("step", 1); + rec.log("door", &rerun::StateChange::single("closed"))?; + + rec.set_time_sequence("step", 3); + rec.log("window", &rerun::StateChange::single("open"))?; + + rec.set_time_sequence("step", 4); + rec.log("door", &rerun::StateChange::single("open"))?; + // endregion: log_changes + + Ok(()) +} diff --git a/docs/snippets/all/howto/sub_dataset.py b/docs/snippets/all/howto/sub_dataset.py index 4c86371e9a4d..76eec72fc54c 100644 --- a/docs/snippets/all/howto/sub_dataset.py +++ b/docs/snippets/all/howto/sub_dataset.py @@ -6,12 +6,15 @@ from pathlib import Path import pyarrow as pa +import pyarrow.compute as pc from datafusion import col, lit from datafusion import functions as F import rerun as rr -sample_5_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +sample_5_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +) server = rr.server.Server(datasets={"sample_dataset": sample_5_path}) CATALOG_URL = server.url() @@ -27,21 +30,25 @@ def create_sub_dataset( name: str, segment_ids: list[str], ) -> rr.catalog.DatasetEntry: - """Create a new dataset containing a subset of segments from an existing dataset.""" + """Create a new dataset with a subset of segments from another dataset.""" - # Query the manifest for storage URLs of the selected segments - manifest = pa.table( + # Look up the storage URLs of the selected segments. + selected = pa.table( source - .manifest() - .filter(F.in_list(col("rerun_segment_id"), [lit(s) for s in segment_ids])) - .select("rerun_storage_url", "rerun_layer_name") + .segment_table() + .filter( + F.in_list(col("rerun_segment_id"), [lit(s) for s in segment_ids]) + ) + .select("rerun_storage_urls", "rerun_layer_names") ) sub_dataset = client.create_dataset(name) - if manifest.num_rows > 0: - uris = manifest.column("rerun_storage_url").to_pylist() - layers = manifest.column("rerun_layer_name").to_pylist() + # Flatten the per-segment lists into the (url, layer) pairs to register. + uris = pc.list_flatten(selected.column("rerun_storage_urls")).to_pylist() + layers = pc.list_flatten(selected.column("rerun_layer_names")).to_pylist() + + if uris: sub_dataset.register(uris, layer_name=layers).wait() return sub_dataset @@ -52,7 +59,12 @@ def create_sub_dataset( # region: select_segments # View available segments print("Available segments:") -print(source_dataset.segment_table().select("rerun_segment_id").sort("rerun_segment_id")) +print( + source_dataset + .segment_table() + .select("rerun_segment_id") + .sort("rerun_segment_id") +) # Select a subset — here we pick the first 3 segments. all_segment_ids = source_dataset.segment_ids() @@ -60,24 +72,32 @@ def create_sub_dataset( # endregion: select_segments # region: create -sub_dataset = create_sub_dataset(client, source_dataset, "my_experiment", subset_ids) +sub_dataset = create_sub_dataset( + client, source_dataset, "my_experiment", subset_ids +) # endregion: create # region: verify print("\nSub-dataset segments:") -print(sub_dataset.segment_table().select("rerun_segment_id", "rerun_layer_names").sort("rerun_segment_id")) +print( + sub_dataset + .segment_table() + .select("rerun_segment_id", "rerun_layer_names") + .sort("rerun_segment_id") +) -print("\nSub-dataset manifest:") +print("\nSub-dataset storage URLs:") print( sub_dataset - .manifest() - .select("rerun_segment_id", "rerun_layer_name", "rerun_storage_url") - .sort("rerun_segment_id", "rerun_layer_name") + .segment_table() + .select("rerun_segment_id", "rerun_layer_names", "rerun_storage_urls") + .sort("rerun_segment_id") ) # endregion: verify # region: cleanup # When done experimenting, delete the sub-dataset. -# This only removes the dataset entry — the underlying RRD storage is not affected. +# This only removes the dataset entry — the underlying RRD storage is not +# affected. sub_dataset.delete() # endregion: cleanup diff --git a/docs/snippets/all/howto/time_alignment.py b/docs/snippets/all/howto/time_alignment.py index b7e39281a4b2..f27099242c81 100644 --- a/docs/snippets/all/howto/time_alignment.py +++ b/docs/snippets/all/howto/time_alignment.py @@ -10,7 +10,9 @@ import rerun as rr -sample_5_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +sample_5_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +) server = rr.server.Server(datasets={"sample_dataset": sample_5_path}) CATALOG_URL = server.url() @@ -19,12 +21,16 @@ # endregion: setup # region: extract_timepoints -view = dataset.filter_segments("ILIAD_sbd7d2c6_2023_12_24_16h_20m_37s").filter_contents("/observation/joint_positions") +view = dataset.filter_segments( + "ILIAD_sbd7d2c6_2023_12_24_16h_20m_37s" +).filter_contents("/observation/joint_positions") ranges = view.get_index_ranges().to_arrow_table() min_time = ranges["real_time:start"].to_numpy().flatten() max_time = ranges["real_time:end"].to_numpy().flatten() -desired_timestamps = np.arange(min_time[0], max_time[0], np.timedelta64(100, "ms")) # 10Hz +desired_timestamps = np.arange( + min_time[0], max_time[0], np.timedelta64(100, "ms") +) # 10Hz # endregion: extract_timepoints # region: time_align @@ -35,7 +41,11 @@ dataset .filter_segments("ILIAD_sbd7d2c6_2023_12_24_16h_20m_37s") .filter_contents(["/observation/joint_positions", "/camera/ext1/**"]) - .reader(index="real_time", using_index_values=desired_timestamps, fill_latest_at=True) + .reader( + index="real_time", + using_index_values=desired_timestamps, + fill_latest_at=True, + ) ) # Filter out partially sparse rows (since one column may start before the other) diff --git a/docs/snippets/all/howto/view_operations.py b/docs/snippets/all/howto/view_operations.py index b3e4dcf7a695..1abfc235432b 100644 --- a/docs/snippets/all/howto/view_operations.py +++ b/docs/snippets/all/howto/view_operations.py @@ -10,7 +10,9 @@ import rerun as rr -sample_5_path = Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +sample_5_path = ( + Path(__file__).parents[4] / "tests" / "assets" / "rrd" / "sample_5" +) server = rr.server.Server(datasets={"sample_dataset": sample_5_path}) CATALOG_URL = server.url() @@ -23,13 +25,19 @@ episode = "ILIAD_50aee79f_2023_07_12_20h_55m_08s" start = 1689220508 end = start + 5 -filtered_view = dataset.filter_segments(episode).filter_contents("/observation/**") +filtered_view = dataset.filter_segments(episode).filter_contents( + "/observation/**" +) filtered_df = filtered_view.reader(index="real_time") -filtered_df = filtered_df.filter((col("real_time") >= start) & (col("real_time") < end)) +filtered_df = filtered_df.filter( + (col("real_time") >= start) & (col("real_time") < end) +) # endregion: filtering # region: static_data -instructions = dataset.filter_contents("/language_instruction/**").reader(index=None) +instructions = dataset.filter_contents("/language_instruction/**").reader( + index=None +) # Sort to ensure documented output is always correct instructions = instructions.sort("/language_instruction:TextDocument:text") diff --git a/docs/snippets/all/howto/visualization/load_blueprint.cpp b/docs/snippets/all/howto/visualization/load_blueprint.cpp index 8d3df3667480..eb8b18463176 100644 --- a/docs/snippets/all/howto/visualization/load_blueprint.cpp +++ b/docs/snippets/all/howto/visualization/load_blueprint.cpp @@ -16,7 +16,8 @@ int main(int argc, char* argv[]) { std::string path_to_rrd = argv[1]; std::string path_to_rbl = argv[2]; - const auto rec = rerun::RecordingStream("rerun_example_dataframe_view_query_external"); + const auto rec = + rerun::RecordingStream("rerun_example_dataframe_view_query_external"); rec.spawn().exit_on_failure(); // Log the files diff --git a/docs/snippets/all/howto/visualization/load_blueprint.rs b/docs/snippets/all/howto/visualization/load_blueprint.rs index 2e13a27cae77..ca923d20345d 100644 --- a/docs/snippets/all/howto/visualization/load_blueprint.rs +++ b/docs/snippets/all/howto/visualization/load_blueprint.rs @@ -10,11 +10,21 @@ fn main() -> Result<(), Box> { let path_to_rrd = &args[1]; let path_to_rbl = &args[2]; - let rec = rerun::RecordingStreamBuilder::new("rerun_example_dataframe_view_query_external") - .spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_dataframe_view_query_external", + ) + .spawn()?; - rec.log_file_from_path(path_to_rrd, None /* prefix */, false /* static */)?; - rec.log_file_from_path(path_to_rbl, None /* prefix */, false /* static */)?; + rec.log_file_from_path( + path_to_rrd, + None, /* prefix */ + false, /* static */ + )?; + rec.log_file_from_path( + path_to_rbl, + None, /* prefix */ + false, /* static */ + )?; Ok(()) } diff --git a/docs/snippets/all/howto/visualization/save_blueprint.py b/docs/snippets/all/howto/visualization/save_blueprint.py index a5b0659730a4..5db9825dac4f 100644 --- a/docs/snippets/all/howto/visualization/save_blueprint.py +++ b/docs/snippets/all/howto/visualization/save_blueprint.py @@ -1,4 +1,4 @@ -"""Craft a blueprint with the python API and save it to a file for future use.""" +"""Craft a blueprint with the python API and save it to file.""" import sys diff --git a/docs/snippets/all/migration/log_tick_enabled.cpp b/docs/snippets/all/migration/log_tick_enabled.cpp new file mode 100644 index 000000000000..67cae8d51813 --- /dev/null +++ b/docs/snippets/all/migration/log_tick_enabled.cpp @@ -0,0 +1,2 @@ +rec.set_log_tick_enabled(true); +rec.set_log_time_enabled(false); diff --git a/docs/snippets/all/migration/log_tick_enabled.py b/docs/snippets/all/migration/log_tick_enabled.py new file mode 100644 index 000000000000..46268721024e --- /dev/null +++ b/docs/snippets/all/migration/log_tick_enabled.py @@ -0,0 +1,7 @@ +import rerun as rr + +rr.set_log_tick_enabled(True) # opt in to `log_tick` on the active recording +rr.set_log_time_enabled(False) # opt out of `log_time` on the active recording + +rec = rr.RecordingStream("rerun_example_my_app") +rec.set_log_tick_enabled(True) # …or on a specific recording diff --git a/docs/snippets/all/migration/log_tick_enabled.rs b/docs/snippets/all/migration/log_tick_enabled.rs new file mode 100644 index 000000000000..67cae8d51813 --- /dev/null +++ b/docs/snippets/all/migration/log_tick_enabled.rs @@ -0,0 +1,2 @@ +rec.set_log_tick_enabled(true); +rec.set_log_time_enabled(false); diff --git a/docs/snippets/all/migration/transactional_transforms.py b/docs/snippets/all/migration/transactional_transforms.py index 7b263d391f98..303c4bbaef89 100644 --- a/docs/snippets/all/migration/transactional_transforms.py +++ b/docs/snippets/all/migration/transactional_transforms.py @@ -4,5 +4,6 @@ # Note that we explicitly only set the scale here: # Previously, this would have meant that we keep the translation. -# However, in 0.27 the Viewer will no longer show apply the previous translation regardless. +# However, in 0.27 the Viewer will no longer show apply the previous +# translation regardless. rr.log("simple", rr.Transform3D.from_fields(scale=2)) diff --git a/docs/snippets/all/quick_start/quick_start_connect.cpp b/docs/snippets/all/quick_start/quick_start_connect.cpp index f50906f5e00f..7c4d445dc1ae 100644 --- a/docs/snippets/all/quick_start/quick_start_connect.cpp +++ b/docs/snippets/all/quick_start/quick_start_connect.cpp @@ -5,13 +5,19 @@ using namespace rerun::demo; int main(int argc, char* argv[]) { // Create a new `RecordingStream` which sends data over gRPC to the viewer process. - const auto rec = rerun::RecordingStream("rerun_example_quick_start_connect"); + const auto rec = + rerun::RecordingStream("rerun_example_quick_start_connect"); rec.connect_grpc().exit_on_failure(); // Create some data using the `grid` utility function. - std::vector points = grid3d(-10.f, 10.f, 10); - std::vector colors = grid3d(0, 255, 10); + std::vector points = + grid3d(-10.f, 10.f, 10); + std::vector colors = + grid3d(0, 255, 10); // Log the "my_points" entity with our data, using the `Points3D` archetype. - rec.log("my_points", rerun::Points3D(points).with_colors(colors).with_radii({0.5f})); + rec.log( + "my_points", + rerun::Points3D(points).with_colors(colors).with_radii({0.5f}) + ); } diff --git a/docs/snippets/all/quick_start/quick_start_connect.rs b/docs/snippets/all/quick_start/quick_start_connect.rs index b4e4b2c69c27..b13bed25a270 100644 --- a/docs/snippets/all/quick_start/quick_start_connect.rs +++ b/docs/snippets/all/quick_start/quick_start_connect.rs @@ -5,7 +5,8 @@ use rerun::{demo_util::grid, external::glam}; fn main() -> Result<(), Box> { // Create a new `RecordingStream` which sends data over gRPC to the viewer process. let rec = - rerun::RecordingStreamBuilder::new("rerun_example_quick_start_connect").connect_grpc()?; + rerun::RecordingStreamBuilder::new("rerun_example_quick_start_connect") + .connect_grpc()?; // Create some data using the `grid` utility function. let points = grid(glam::Vec3::splat(-10.0), glam::Vec3::splat(10.0), 10); diff --git a/docs/snippets/all/quick_start/quick_start_spawn.cpp b/docs/snippets/all/quick_start/quick_start_spawn.cpp index b0f08f6414d4..ea318fe52223 100644 --- a/docs/snippets/all/quick_start/quick_start_spawn.cpp +++ b/docs/snippets/all/quick_start/quick_start_spawn.cpp @@ -9,9 +9,14 @@ int main(int argc, char* argv[]) { rec.spawn().exit_on_failure(); // Create some data using the `grid` utility function. - std::vector points = grid3d(-10.f, 10.f, 10); - std::vector colors = grid3d(0, 255, 10); + std::vector points = + grid3d(-10.f, 10.f, 10); + std::vector colors = + grid3d(0, 255, 10); // Log the "my_points" entity with our data, using the `Points3D` archetype. - rec.log("my_points", rerun::Points3D(points).with_colors(colors).with_radii({0.5f})); + rec.log( + "my_points", + rerun::Points3D(points).with_colors(colors).with_radii({0.5f}) + ); } diff --git a/docs/snippets/all/quick_start/quick_start_spawn.rs b/docs/snippets/all/quick_start/quick_start_spawn.rs index 2ebc74a7a3cd..b7bd91fe45e8 100644 --- a/docs/snippets/all/quick_start/quick_start_spawn.rs +++ b/docs/snippets/all/quick_start/quick_start_spawn.rs @@ -4,7 +4,9 @@ use rerun::{demo_util::grid, external::glam}; fn main() -> Result<(), Box> { // Create a new `RecordingStream` which stores data in memory. - let rec = rerun::RecordingStreamBuilder::new("rerun_example_quick_start_spawn").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_quick_start_spawn") + .spawn()?; // Create some data using the `grid` utility function. let points = grid(glam::Vec3::splat(-10.0), glam::Vec3::splat(10.0), 10); diff --git a/docs/snippets/all/ruff.toml b/docs/snippets/all/ruff.toml new file mode 100644 index 000000000000..54266063ca86 --- /dev/null +++ b/docs/snippets/all/ruff.toml @@ -0,0 +1,9 @@ +extend = "../../../pyproject.toml" + +# Keep snippets tight so they look nice on our web page: +line-length = 80 + +[lint] +# Flag any line — including comments — over `line-length`. Ruff's formatter +# rewraps code but leaves comments alone, so we need the lint rule to catch them. +extend-select = ["E501"] diff --git a/docs/snippets/all/tutorials/annotation_context.cpp b/docs/snippets/all/tutorials/annotation_context.cpp index 86e032951041..0c9c203b8ea4 100644 --- a/docs/snippets/all/tutorials/annotation_context.cpp +++ b/docs/snippets/all/tutorials/annotation_context.cpp @@ -1,7 +1,8 @@ #include int main(int argc, char* argv[]) { - const auto rec = rerun::RecordingStream("rerun_example_annotation_context_connections"); + const auto rec = + rerun::RecordingStream("rerun_example_annotation_context_connections"); rec.spawn().exit_on_failure(); // Annotation context with two classes, using two labeled classes, of which ones defines a @@ -17,9 +18,10 @@ int main(int argc, char* argv[]) { // Annotation context with simple keypoints & keypoint connections. std::vector keypoint_annotations; for (uint16_t i = 0; i < 10; ++i) { - keypoint_annotations.push_back( - rerun::AnnotationInfo(i, rerun::Rgba32(0, static_cast(28 * i), 0)) - ); + keypoint_annotations.push_back(rerun::AnnotationInfo( + i, + rerun::Rgba32(0, static_cast(28 * i), 0) + )); } std::vector keypoint_connections; diff --git a/docs/snippets/all/tutorials/annotation_context.py b/docs/snippets/all/tutorials/annotation_context.py index 62c4d6d94472..b7b1a0904126 100644 --- a/docs/snippets/all/tutorials/annotation_context.py +++ b/docs/snippets/all/tutorials/annotation_context.py @@ -2,7 +2,8 @@ rr.init("rerun_example_annotation_context_connections") -# Annotation context with two classes, using two labeled classes, of which ones defines a color. +# Annotation context with two classes, using two labeled classes, of which +# ones defines a color. rr.log( "masks", # Applies to all entities below "masks". rr.AnnotationContext( @@ -19,7 +20,9 @@ "detections", # Applies to all entities below "detections". rr.ClassDescription( info=rr.AnnotationInfo(0, label="Snake"), - keypoint_annotations=[rr.AnnotationInfo(id=i, color=(0, 28 * i, 0)) for i in range(10)], + keypoint_annotations=[ + rr.AnnotationInfo(id=i, color=(0, 28 * i, 0)) for i in range(10) + ], keypoint_connections=[(i, i + 1) for i in range(9)], ), static=True, diff --git a/docs/snippets/all/tutorials/annotation_context.rs b/docs/snippets/all/tutorials/annotation_context.rs index 7d8e8311ce0e..5052b977504a 100644 --- a/docs/snippets/all/tutorials/annotation_context.rs +++ b/docs/snippets/all/tutorials/annotation_context.rs @@ -4,8 +4,10 @@ use rerun::{ }; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_annotation_context_connections") - .spawn()?; + let rec = rerun::RecordingStreamBuilder::new( + "rerun_example_annotation_context_connections", + ) + .spawn()?; // Annotation context with two classes, using two labeled classes, of which ones defines a // color. @@ -13,7 +15,11 @@ fn main() -> Result<(), Box> { "masks", // Applies to all entities below "masks". &AnnotationContext::new([ ClassDescriptionMapElem::from((0, "Background")), - ClassDescriptionMapElem::from((1, "Person", Rgba32::from_rgb(255, 0, 0))), + ClassDescriptionMapElem::from(( + 1, + "Person", + Rgba32::from_rgb(255, 0, 0), + )), ]), )?; diff --git a/docs/snippets/all/tutorials/any_values.cpp b/docs/snippets/all/tutorials/any_values.cpp index 973fd0b7174c..eb2012822c51 100644 --- a/docs/snippets/all/tutorials/any_values.cpp +++ b/docs/snippets/all/tutorials/any_values.cpp @@ -18,7 +18,7 @@ arrow::Status run_main() { auto confidences = rerun::ComponentBatch::from_arrow_array( std::move(arrow_array), rerun::ComponentDescriptor("confidence") - .with_component_type(rerun::Loggable::ComponentType) + .with_component_type(rerun::Loggable::ComponentType) ); arrow::StringBuilder description_builder; @@ -29,7 +29,7 @@ arrow::Status run_main() { rerun::ComponentDescriptor("description") .with_component_type( - rerun::Loggable::ComponentType + rerun::Loggable::ComponentType ) ); // URIs will become clickable links @@ -42,7 +42,9 @@ arrow::Status run_main() { ); arrow::StringBuilder repository_builder; - ARROW_RETURN_NOT_OK(repository_builder.Append("https://github.com/rerun-io/rerun")); + ARROW_RETURN_NOT_OK( + repository_builder.Append("https://github.com/rerun-io/rerun") + ); ARROW_RETURN_NOT_OK(repository_builder.Finish(&arrow_array)); auto repository = rerun::ComponentBatch::from_arrow_array( std::move(arrow_array), diff --git a/docs/snippets/all/tutorials/any_values.py b/docs/snippets/all/tutorials/any_values.py index f1c06322af4c..42d297cd00dc 100644 --- a/docs/snippets/all/tutorials/any_values.py +++ b/docs/snippets/all/tutorials/any_values.py @@ -13,6 +13,10 @@ repository="https://github.com/rerun-io/rerun", ) # Using Rerun's builtin components. - .with_component_override("confidence", rr.components.ScalarBatch._COMPONENT_TYPE, [1.2, 3.4, 5.6]) - .with_component_override("description", rr.components.TextBatch._COMPONENT_TYPE, "Bla bla bla…"), + .with_component_override( + "confidence", rr.components.ScalarBatch._COMPONENT_TYPE, [1.2, 3.4, 5.6] + ) + .with_component_override( + "description", rr.components.TextBatch._COMPONENT_TYPE, "Bla bla bla…" + ), ) diff --git a/docs/snippets/all/tutorials/any_values.rs b/docs/snippets/all/tutorials/any_values.rs index 0efdfe83fe23..66d5421843c5 100644 --- a/docs/snippets/all/tutorials/any_values.rs +++ b/docs/snippets/all/tutorials/any_values.rs @@ -5,7 +5,8 @@ use std::sync::Arc; use rerun::external::arrow; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_any_values").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_any_values") + .spawn()?; let any_values = rerun::AnyValues::default() // Using arbitrary Arrow data. @@ -22,8 +23,14 @@ fn main() -> Result<(), Box> { ])), ) // Using Rerun's builtin components. - .with_component::("confidence", [1.2, 3.4, 5.6]) - .with_component::("description", vec!["Bla bla bla…"]); + .with_component::( + "confidence", + [1.2, 3.4, 5.6], + ) + .with_component::( + "description", + vec!["Bla bla bla…"], + ); rec.log("any_values", &any_values)?; diff --git a/docs/snippets/all/tutorials/custom-recording-id.cpp b/docs/snippets/all/tutorials/custom-recording-id.cpp index e88d9d0f1fa3..0198d5cb6fff 100644 --- a/docs/snippets/all/tutorials/custom-recording-id.cpp +++ b/docs/snippets/all/tutorials/custom-recording-id.cpp @@ -1 +1,3 @@ -const auto rec = rerun::RecordingStream("rerun_example_shared_recording", "my_shared_recording"); +const auto rec = rerun::RecordingStream( + "rerun_example_shared_recording", "my_shared_recording" +); diff --git a/docs/snippets/all/tutorials/custom_data.cpp b/docs/snippets/all/tutorials/custom_data.cpp index 5d801e3c9ba7..198257ed0936 100644 --- a/docs/snippets/all/tutorials/custom_data.cpp +++ b/docs/snippets/all/tutorials/custom_data.cpp @@ -37,20 +37,27 @@ struct CustomPoints3D { template <> struct rerun::AsComponents { - static Result> as_batches(const CustomPoints3D& archetype) { - auto batches = AsComponents::as_batches(archetype.points) - .value_or_throw() - .to_vector(); + static Result> as_batches( + const CustomPoints3D& archetype + ) { + auto batches = + AsComponents::as_batches(archetype.points) + .value_or_throw() + .to_vector(); // Add custom confidence components if present. if (archetype.confidences) { auto descriptor = rerun::ComponentDescriptor("user.CustomPoints3D:confidences") .or_with_archetype("user.CustomPoints3D") - .or_with_component_type(rerun::Loggable::ComponentType); - batches.push_back( - ComponentBatch::from_loggable(*archetype.confidences, descriptor).value_or_throw() - ); + .or_with_component_type( + rerun::Loggable::ComponentType + ); + batches.push_back(ComponentBatch::from_loggable( + *archetype.confidences, + descriptor + ) + .value_or_throw()); } return rerun::take_ownership(std::move(batches)); diff --git a/docs/snippets/all/tutorials/custom_data.py b/docs/snippets/all/tutorials/custom_data.py index 58639099e3af..3ac8345c4575 100644 --- a/docs/snippets/all/tutorials/custom_data.py +++ b/docs/snippets/all/tutorials/custom_data.py @@ -24,9 +24,11 @@ def as_arrow_array(self) -> pa.Array: class CustomPoints3D(rr.AsComponents): # type: ignore[misc] - """A custom archetype that extends Rerun's builtin `Points3D` archetype with a custom component.""" + """A custom archetype extending the builtin `Points3D` with extra data.""" - def __init__(self: Any, positions: npt.ArrayLike, confidences: npt.ArrayLike) -> None: + def __init__( + self: Any, positions: npt.ArrayLike, confidences: npt.ArrayLike + ) -> None: self.points3d = rr.Points3D(positions) self.confidences = ConfidenceBatch(confidences).described( rr.ComponentDescriptor( @@ -38,8 +40,10 @@ def __init__(self: Any, positions: npt.ArrayLike, confidences: npt.ArrayLike) -> def as_component_batches(self) -> list[rr.DescribedComponentBatch]: return [ - *self.points3d.as_component_batches(), # The components from Points3D - self.confidences, # Custom confidence data + # The components from Points3D + *self.points3d.as_component_batches(), + # Custom confidence data + self.confidences, ] @@ -58,12 +62,16 @@ def log_custom_data() -> None: rr.log( "right/my_polarized_point_cloud", - CustomPoints3D(positions=point_grid, confidences=np.arange(0, len(point_grid))), + CustomPoints3D( + positions=point_grid, confidences=np.arange(0, len(point_grid)) + ), ) def main() -> None: - parser = argparse.ArgumentParser(description="Logs rich data using the Rerun SDK.") + parser = argparse.ArgumentParser( + description="Logs rich data using the Rerun SDK." + ) rr.script_add_args(parser) args = parser.parse_args() diff --git a/docs/snippets/all/tutorials/custom_data.rs b/docs/snippets/all/tutorials/custom_data.rs index 827d415f33ca..86b4a137d6cb 100644 --- a/docs/snippets/all/tutorials/custom_data.rs +++ b/docs/snippets/all/tutorials/custom_data.rs @@ -19,27 +19,27 @@ struct CustomPoints3D { impl rerun::AsComponents for CustomPoints3D { fn as_serialized_batches(&self) -> Vec { - self.points3d - .as_serialized_batches() - .into_iter() - .chain( - std::iter::once(self.confidences.as_ref().and_then(|batch| { - batch.serialized(ComponentDescriptor { - archetype: Some("user.CustomPoints3D".into()), - component: "user.CustomPoints3D:confidences".into(), - component_type: Some(::name()), - }) - })) - .flatten(), - ) - .collect() + std::iter::chain( + self.points3d.as_serialized_batches(), + std::iter::once(self.confidences.as_ref().and_then(|batch| { + batch.serialized(ComponentDescriptor { + archetype: Some("user.CustomPoints3D".into()), + component: "user.CustomPoints3D:confidences".into(), + component_type: Some( + ::name(), + ), + }) + })) + .flatten(), + ) + .collect() } } // --- /// A custom [`rerun::Component`] that is backed by a builtin [`rerun::Float32`] scalar. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, rerun::SizeBytes)] struct Confidence(rerun::Float32); impl From for Confidence { @@ -48,13 +48,6 @@ impl From for Confidence { } } -impl rerun::SizeBytes for Confidence { - #[inline] - fn heap_size_bytes(&self) -> u64 { - 0 - } -} - impl rerun::Loggable for Confidence { #[inline] fn arrow_datatype() -> arrow::datatypes::DataType { @@ -63,12 +56,16 @@ impl rerun::Loggable for Confidence { #[inline] fn to_arrow_opt<'a>( - data: impl IntoIterator>>>, + data: impl IntoIterator< + Item = Option>>, + >, ) -> re_sdk_types::SerializationResult where Self: 'a, { - rerun::Float32::to_arrow_opt(data.into_iter().map(|opt| opt.map(Into::into).map(|c| c.0))) + rerun::Float32::to_arrow_opt( + data.into_iter().map(|opt| opt.map(Into::into).map(|c| c.0)), + ) } } @@ -82,7 +79,8 @@ impl rerun::Component for Confidence { // --- fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_custom_data").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_custom_data") + .spawn()?; rec.log( "left/my_confident_point_cloud", @@ -104,7 +102,9 @@ fn main() -> Result<(), Box> { glam::Vec3::splat(5.0), 3, )), - confidences: Some((0..27).map(|i| i as f32).map(Into::into).collect()), + confidences: Some( + (0..27).map(|i| i as f32).map(Into::into).collect(), + ), }, )?; diff --git a/docs/snippets/all/tutorials/data_out.py b/docs/snippets/all/tutorials/data_out.py index 3fc8f135db61..b6bd10a63d9c 100644 --- a/docs/snippets/all/tutorials/data_out.py +++ b/docs/snippets/all/tutorials/data_out.py @@ -9,11 +9,13 @@ # endregion: imports -# ---------------------------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Load and prepare the data repo_root = Path(__file__).parent.parent.parent.parent.parent -example_rrd = repo_root / "tests" / "assets" / "rrd" / "examples" / "face_tracking.rrd" +example_rrd = ( + repo_root / "tests" / "assets" / "rrd" / "examples" / "face_tracking.rrd" +) assert example_rrd.exists(), f"Example RRD not found at {example_rrd}" # region: launch_server server = rr.server.Server(datasets={"tutorial": [example_rrd]}) @@ -38,11 +40,13 @@ # convert the "jawOpen" column to a flat list of floats print(pd_df) # region: explode_jaw -pd_df["jawOpen"] = pd_df["/blendshapes/0/jawOpen:Scalars:scalars"].explode().astype(float) +pd_df["jawOpen"] = ( + pd_df["/blendshapes/0/jawOpen:Scalars:scalars"].explode().astype(float) +) print(pd_df["jawOpen"][160:180]) # endregion: explode_jaw -# ---------------------------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Analyze the data # region: filter_jaw @@ -52,10 +56,12 @@ # endregion: filter_jaw -# ---------------------------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Log the data back to the viewer -application_id = rr.recording.load_recording(example_rrd).application_id() +application_id = ( + rr.experimental.RrdReader(example_rrd).recordings()[0].application_id +) # Connect to the viewer # region: connect_viewer @@ -79,6 +85,8 @@ rr.send_columns( target_entity, indexes=[rr.TimeColumn("frame_nr", sequence=pd_df["frame_nr"])], - columns=rr.Boxes2D.columns(labels=np.where(pd_df["jawOpenState"], "OPEN", "CLOSE")), + columns=rr.Boxes2D.columns( + labels=np.where(pd_df["jawOpenState"], "OPEN", "CLOSE") + ), ) # endregion: log_labels diff --git a/docs/snippets/all/tutorials/dna.cpp b/docs/snippets/all/tutorials/dna.cpp new file mode 100644 index 000000000000..7017a9ea59b9 --- /dev/null +++ b/docs/snippets/all/tutorials/dna.cpp @@ -0,0 +1,154 @@ +// The DNA-abacus example from the Log and Ingest tutorial. + +// region: imports +#include +#include + +#include // std::generate +#include +#include + +// endregion: imports + +int main(int argc, char* argv[]) { + using namespace rerun::demo; + using namespace std::chrono_literals; + + // region: init + const auto rec = rerun::RecordingStream("rerun_example_dna_abacus"); + rec.spawn().exit_on_failure(); + // endregion: init + + // The fix for the latest-at lesson — see "Latest-at semantics" in the tutorial. + // region: latest_at_fix + rec.set_time_duration("stable_time", 0s); + // endregion: latest_at_fix + + constexpr size_t NUM_POINTS = 100; + + // region: first_points + std::vector points1, points2; + std::vector colors1, colors2; + color_spiral(NUM_POINTS, 2.0f, 0.02f, 0.0f, 0.1f, points1, colors1); + color_spiral(NUM_POINTS, 2.0f, 0.02f, TAU * 0.5f, 0.1f, points2, colors2); + + rec.log( + "dna/structure/left", + rerun::Points3D(points1).with_colors(colors1).with_radii({0.08f}) + ); + rec.log( + "dna/structure/right", + rerun::Points3D(points2).with_colors(colors2).with_radii({0.08f}) + ); + // endregion: first_points + + // region: scaffolding + std::vector lines; + for (size_t i = 0; i < points1.size(); ++i) { + lines.emplace_back(rerun::LineStrip3D({points1[i].xyz, points2[i].xyz}) + ); + } + + rec.log( + "dna/structure/scaffolding", + rerun::LineStrips3D(lines).with_colors(rerun::Color(128, 128, 128)) + ); + // endregion: scaffolding + + // region: beads + std::default_random_engine gen; + std::uniform_real_distribution dist(0.0f, 1.0f); + std::vector offsets(NUM_POINTS); + std::generate(offsets.begin(), offsets.end(), [&] { return dist(gen); }); + + std::vector beads_positions(lines.size()); + std::vector beads_colors(lines.size()); + + for (size_t i = 0; i < lines.size(); ++i) { + auto c = + static_cast(bounce_lerp(80.0f, 230.0f, offsets[i] * 2.0f)); + beads_positions[i] = rerun::Position3D( + bounce_lerp( + lines[i].points[0].x(), + lines[i].points[1].x(), + offsets[i] + ), + bounce_lerp( + lines[i].points[0].y(), + lines[i].points[1].y(), + offsets[i] + ), + bounce_lerp( + lines[i].points[0].z(), + lines[i].points[1].z(), + offsets[i] + ) + ); + beads_colors[i] = rerun::Color(c, c, c); + } + + rec.log( + "dna/structure/scaffolding/beads", + rerun::Points3D(beads_positions) + .with_colors(beads_colors) + .with_radii({0.06f}) + ); + // endregion: beads + + // region: time_loop + for (int t = 0; t < 400; t++) { + auto time = std::chrono::duration(t) * 0.01f; + + rec.set_time_duration("stable_time", time); + + for (size_t i = 0; i < lines.size(); ++i) { + float time_offset = time.count() + offsets[i]; + auto c = static_cast( + bounce_lerp(80.0f, 230.0f, time_offset * 2.0f) + ); + + beads_positions[i] = rerun::Position3D( + bounce_lerp( + lines[i].points[0].x(), + lines[i].points[1].x(), + time_offset + ), + bounce_lerp( + lines[i].points[0].y(), + lines[i].points[1].y(), + time_offset + ), + bounce_lerp( + lines[i].points[0].z(), + lines[i].points[1].z(), + time_offset + ) + ); + beads_colors[i] = rerun::Color(c, c, c); + } + + rec.log( + "dna/structure/scaffolding/beads", + rerun::Points3D(beads_positions) + .with_colors(beads_colors) + .with_radii({0.06f}) + ); + } + // endregion: time_loop + + // region: transform_loop + for (int t = 0; t < 400; t++) { + auto time = std::chrono::duration(t) * 0.01f; + + rec.set_time_duration("stable_time", time); + + rec.log( + "dna/structure", + rerun::archetypes::Transform3D(rerun::RotationAxisAngle( + {0.0f, 0.0f, 1.0f}, + rerun::Angle::radians(time.count() / 4.0f * TAU) + )) + ); + } + // endregion: transform_loop +} diff --git a/docs/snippets/all/tutorials/dna.py b/docs/snippets/all/tutorials/dna.py new file mode 100644 index 000000000000..2b3feb118e07 --- /dev/null +++ b/docs/snippets/all/tutorials/dna.py @@ -0,0 +1,101 @@ +"""The DNA-abacus example from the Log and Ingest tutorial.""" + +# region: imports +from math import tau + +import numpy as np + +import rerun as rr +from rerun.utilities import bounce_lerp, build_color_spiral + +# endregion: imports + + +def main() -> None: + # region: init + rr.init("rerun_example_dna_abacus", spawn=True) + # endregion: init + + # The fix for the latest-at lesson — see "Latest-at semantics" in tutorial. + # region: latest_at_fix + rr.set_time("stable_time", duration=0) + # endregion: latest_at_fix + + NUM_POINTS = 100 + + # region: first_points + points1, colors1 = build_color_spiral(NUM_POINTS) + points2, colors2 = build_color_spiral(NUM_POINTS, angular_offset=tau * 0.5) + + rr.log( + "dna/structure/left", rr.Points3D(points1, colors=colors1, radii=0.08) + ) + rr.log( + "dna/structure/right", rr.Points3D(points2, colors=colors2, radii=0.08) + ) + # endregion: first_points + + # region: scaffolding + rr.log( + "dna/structure/scaffolding", + rr.LineStrips3D( + np.stack((points1, points2), axis=1), colors=[128, 128, 128] + ), + ) + # endregion: scaffolding + + # region: beads + offsets = np.random.rand(NUM_POINTS) + beads = [ + bounce_lerp(points1[n], points2[n], offsets[n]) + for n in range(NUM_POINTS) + ] + colors = [ + [int(bounce_lerp(80, 230, offsets[n] * 2))] for n in range(NUM_POINTS) + ] + rr.log( + "dna/structure/scaffolding/beads", + rr.Points3D(beads, radii=0.06, colors=np.repeat(colors, 3, axis=-1)), + ) + # endregion: beads + + time_offsets = np.random.rand(NUM_POINTS) + + # region: time_loop + for i in range(400): + time = i * 0.01 + rr.set_time("stable_time", duration=time) + + times = np.repeat(time, NUM_POINTS) + time_offsets + beads = [ + bounce_lerp(points1[n], points2[n], times[n]) + for n in range(NUM_POINTS) + ] + colors = [ + [int(bounce_lerp(80, 230, times[n] * 2))] for n in range(NUM_POINTS) + ] + rr.log( + "dna/structure/scaffolding/beads", + rr.Points3D( + beads, radii=0.06, colors=np.repeat(colors, 3, axis=-1) + ), + ) + # endregion: time_loop + + # region: transform_loop + for i in range(400): + time = i * 0.01 + rr.set_time("stable_time", duration=time) + rr.log( + "dna/structure", + rr.Transform3D( + rotation=rr.RotationAxisAngle( + axis=[0, 0, 1], radians=time / 4.0 * tau + ) + ), + ) + # endregion: transform_loop + + +if __name__ == "__main__": + main() diff --git a/docs/snippets/all/tutorials/dna.rs b/docs/snippets/all/tutorials/dna.rs new file mode 100644 index 000000000000..c092a8f229b2 --- /dev/null +++ b/docs/snippets/all/tutorials/dna.rs @@ -0,0 +1,123 @@ +//! The DNA-abacus example from the Log and Ingest tutorial. + +// region: imports +use std::f32::consts::TAU; + +use itertools::Itertools as _; +use rand::Rng as _; +use rerun::{ + demo_util::{bounce_lerp, color_spiral}, + external::glam, +}; +// endregion: imports + +fn main() -> Result<(), Box> { + // region: init + let rec = rerun::RecordingStreamBuilder::new("rerun_example_dna_abacus") + .spawn()?; + // endregion: init + + // The fix for the latest-at lesson — see "Latest-at semantics" in the tutorial. + // region: latest_at_fix + rec.set_duration_secs("stable_time", 0.0); + // endregion: latest_at_fix + + const NUM_POINTS: usize = 100; + + // region: first_points + let (points1, colors1) = color_spiral(NUM_POINTS, 2.0, 0.02, 0.0, 0.1); + let (points2, colors2) = + color_spiral(NUM_POINTS, 2.0, 0.02, TAU * 0.5, 0.1); + + rec.log( + "dna/structure/left", + &rerun::Points3D::new(points1.iter().copied()) + .with_colors(colors1) + .with_radii([0.08]), + )?; + rec.log( + "dna/structure/right", + &rerun::Points3D::new(points2.iter().copied()) + .with_colors(colors2) + .with_radii([0.08]), + )?; + // endregion: first_points + + // region: scaffolding + let lines: Vec<[glam::Vec3; 2]> = std::iter::zip(&points1, &points2) + .map(|(&p1, &p2)| (p1, p2).into()) + .collect_vec(); + + rec.log( + "dna/structure/scaffolding", + &rerun::LineStrips3D::new(lines.iter().copied()) + .with_colors([rerun::Color::from_rgb(128, 128, 128)]), + )?; + // endregion: scaffolding + + // region: beads + let mut rng = rand::rng(); + let offsets = (0..NUM_POINTS).map(|_| rng.random::()).collect_vec(); + + let beads = std::iter::zip(&lines, &offsets) + .map(|(&[p1, p2], &offset)| bounce_lerp(p1, p2, offset)) + .collect_vec(); + let colors = offsets + .iter() + .map(|&offset| bounce_lerp(80.0, 230.0, offset * 2.0) as u8) + .map(|c| rerun::Color::from_rgb(c, c, c)) + .collect_vec(); + + rec.log( + "dna/structure/scaffolding/beads", + &rerun::Points3D::new(beads) + .with_colors(colors) + .with_radii([0.06]), + )?; + // endregion: beads + + // region: time_loop + for i in 0..400 { + let time = i as f32 * 0.01; + + rec.set_duration_secs("stable_time", time); + + let times = offsets.iter().map(|offset| time + offset).collect_vec(); + let beads = std::iter::zip(&lines, ×) + .map(|(&[p1, p2], &time)| bounce_lerp(p1, p2, time)) + .collect_vec(); + let colors = times + .iter() + .map(|time| bounce_lerp(80.0, 230.0, time * 2.0) as u8) + .map(|c| rerun::Color::from_rgb(c, c, c)) + .collect_vec(); + + rec.log( + "dna/structure/scaffolding/beads", + &rerun::Points3D::new(beads) + .with_colors(colors) + .with_radii([0.06]), + )?; + } + // endregion: time_loop + + // region: transform_loop + for i in 0..400 { + let time = i as f32 * 0.01; + + rec.set_duration_secs("stable_time", time); + + rec.log( + "dna/structure", + &rerun::archetypes::Transform3D::from_rotation( + rerun::RotationAxisAngle::new( + glam::Vec3::Z, + rerun::Angle::from_radians(time / 4.0 * TAU), + ), + ), + )?; + } + // endregion: transform_loop + + Ok(()) +} diff --git a/docs/snippets/all/tutorials/dna_connect_grpc.cpp b/docs/snippets/all/tutorials/dna_connect_grpc.cpp new file mode 100644 index 000000000000..25e14a99eee0 --- /dev/null +++ b/docs/snippets/all/tutorials/dna_connect_grpc.cpp @@ -0,0 +1,11 @@ +// The DNA-abacus example, connecting to a separately-running viewer over gRPC. + +#include + +int main(int argc, char* argv[]) { + // Connect to the viewer running at the default URL. + const auto rec = rerun::RecordingStream("rerun_example_dna_abacus"); + rec.connect_grpc().exit_on_failure(); + + // … log data as in the spawn-based example … +} diff --git a/docs/snippets/all/tutorials/dna_connect_grpc.py b/docs/snippets/all/tutorials/dna_connect_grpc.py new file mode 100644 index 000000000000..24c2e65c193f --- /dev/null +++ b/docs/snippets/all/tutorials/dna_connect_grpc.py @@ -0,0 +1,8 @@ +"""DNA-abacus example, connecting to a separately-running viewer over gRPC.""" + +import rerun as rr + +rr.init("rerun_example_dna_abacus") +rr.connect_grpc() # connect to the viewer running at the default URL + +# … log data as in the spawn-based example … diff --git a/docs/snippets/all/tutorials/dna_connect_grpc.rs b/docs/snippets/all/tutorials/dna_connect_grpc.rs new file mode 100644 index 000000000000..fafd3f24166d --- /dev/null +++ b/docs/snippets/all/tutorials/dna_connect_grpc.rs @@ -0,0 +1,11 @@ +//! The DNA-abacus example, connecting to a separately-running viewer over gRPC. + +fn main() -> Result<(), Box> { + // Connect to the viewer running at the default URL. + let _rec = rerun::RecordingStreamBuilder::new("rerun_example_dna_abacus") + .connect_grpc()?; + + // … log data as in the spawn-based example … + + Ok(()) +} diff --git a/docs/snippets/all/tutorials/dynamic_archetype.cpp b/docs/snippets/all/tutorials/dynamic_archetype.cpp index 23b07d18cf29..3a3867c9aef6 100644 --- a/docs/snippets/all/tutorials/dynamic_archetype.cpp +++ b/docs/snippets/all/tutorials/dynamic_archetype.cpp @@ -18,7 +18,7 @@ arrow::Status run_main() { auto confidences = rerun::ComponentBatch::from_arrow_array( std::move(arrow_array), rerun::ComponentDescriptor("MyArchetype:confidence") - .with_component_type(rerun::Loggable::ComponentType) + .with_component_type(rerun::Loggable::ComponentType) .with_archetype("MyArchetype") ); @@ -30,7 +30,7 @@ arrow::Status run_main() { rerun::ComponentDescriptor("MyArchetype:description") .with_component_type( - rerun::Loggable::ComponentType + rerun::Loggable::ComponentType ) .with_archetype("MyArchetype") ); @@ -40,15 +40,19 @@ arrow::Status run_main() { ARROW_RETURN_NOT_OK(homepage_builder.Finish(&arrow_array)); auto homepage = rerun::ComponentBatch::from_arrow_array( std::move(arrow_array), - rerun::ComponentDescriptor("MyArchetype:homepage").with_archetype("MyArchetype") + rerun::ComponentDescriptor("MyArchetype:homepage") + .with_archetype("MyArchetype") ); arrow::StringBuilder repository_builder; - ARROW_RETURN_NOT_OK(repository_builder.Append("https://github.com/rerun-io/rerun")); + ARROW_RETURN_NOT_OK( + repository_builder.Append("https://github.com/rerun-io/rerun") + ); ARROW_RETURN_NOT_OK(repository_builder.Finish(&arrow_array)); auto repository = rerun::ComponentBatch::from_arrow_array( std::move(arrow_array), - rerun::ComponentDescriptor("MyArchetype:repository").with_archetype("MyArchetype") + rerun::ComponentDescriptor("MyArchetype:repository") + .with_archetype("MyArchetype") ); rec.log("new_archetype", confidences, description, homepage, repository); diff --git a/docs/snippets/all/tutorials/dynamic_archetype.py b/docs/snippets/all/tutorials/dynamic_archetype.py index 3db6932e4cc2..020f48837573 100644 --- a/docs/snippets/all/tutorials/dynamic_archetype.py +++ b/docs/snippets/all/tutorials/dynamic_archetype.py @@ -16,6 +16,10 @@ }, ) # Using Rerun's builtin components. - .with_component_override("confidence", rr.components.ScalarBatch._COMPONENT_TYPE, [1.2, 3.4, 5.6]) - .with_component_override("description", rr.components.TextBatch._COMPONENT_TYPE, "Bla bla bla…"), + .with_component_override( + "confidence", rr.components.ScalarBatch._COMPONENT_TYPE, [1.2, 3.4, 5.6] + ) + .with_component_override( + "description", rr.components.TextBatch._COMPONENT_TYPE, "Bla bla bla…" + ), ) diff --git a/docs/snippets/all/tutorials/dynamic_archetype.rs b/docs/snippets/all/tutorials/dynamic_archetype.rs index 4f03ebb0a59d..ae2accf9ec1f 100644 --- a/docs/snippets/all/tutorials/dynamic_archetype.rs +++ b/docs/snippets/all/tutorials/dynamic_archetype.rs @@ -5,7 +5,9 @@ use std::sync::Arc; use rerun::external::arrow; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_dynamic_archetype").spawn()?; + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_dynamic_archetype") + .spawn()?; let new_archetype = rerun::DynamicArchetype::new("MyArchetype") // Using arbitrary Arrow data. @@ -22,8 +24,14 @@ fn main() -> Result<(), Box> { ])), ) // Using Rerun's builtin components. - .with_component::("confidence", [1.2, 3.4, 5.6]) - .with_component::("description", vec!["Bla bla bla…"]); + .with_component::( + "confidence", + [1.2, 3.4, 5.6], + ) + .with_component::( + "description", + vec!["Bla bla bla…"], + ); rec.log("new_archetype", &new_archetype)?; diff --git a/docs/snippets/all/tutorials/extra_values.cpp b/docs/snippets/all/tutorials/extra_values.cpp index 562f6ee6c102..96a7e68dc3bb 100644 --- a/docs/snippets/all/tutorials/extra_values.cpp +++ b/docs/snippets/all/tutorials/extra_values.cpp @@ -8,14 +8,18 @@ arrow::Status run_main() { const auto rec = rerun::RecordingStream("rerun_example_extra_values"); rec.spawn().exit_on_failure(); - auto points = rerun::Points2D({{-1.0f, -1.0f}, {-1.0f, 1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}}); + auto points = rerun::Points2D( + {{-1.0f, -1.0f}, {-1.0f, 1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}} + ); std::shared_ptr arrow_array; arrow::DoubleBuilder confidences_builder; ARROW_RETURN_NOT_OK(confidences_builder.AppendValues({0.3, 0.4, 0.5, 0.6})); ARROW_RETURN_NOT_OK(confidences_builder.Finish(&arrow_array)); - auto confidences = - rerun::ComponentBatch::from_arrow_array(std::move(arrow_array), "confidence"); + auto confidences = rerun::ComponentBatch::from_arrow_array( + std::move(arrow_array), + "confidence" + ); rec.log("extra_values", points, confidences); diff --git a/docs/snippets/all/tutorials/extra_values.py b/docs/snippets/all/tutorials/extra_values.py index 9c4809e31f03..21dde7ec2f26 100644 --- a/docs/snippets/all/tutorials/extra_values.py +++ b/docs/snippets/all/tutorials/extra_values.py @@ -14,4 +14,10 @@ ) # Set view bounds: -rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1.5, 1.5], y_range=[-1.5, 1.5]))) +rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D( + x_range=[-1.5, 1.5], y_range=[-1.5, 1.5] + ) + ) +) diff --git a/docs/snippets/all/tutorials/extra_values.rs b/docs/snippets/all/tutorials/extra_values.rs index 6eda76ce86b1..7f5b5fc4b84e 100644 --- a/docs/snippets/all/tutorials/extra_values.rs +++ b/docs/snippets/all/tutorials/extra_values.rs @@ -5,9 +5,15 @@ use std::sync::Arc; use rerun::external::arrow; fn main() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_extra_values").spawn()?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_extra_values") + .spawn()?; - let points = rerun::Points2D::new([(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)]); + let points = rerun::Points2D::new([ + (-1.0, -1.0), + (-1.0, 1.0), + (1.0, -1.0), + (1.0, 1.0), + ]); let confidences = rerun::AnyValues::default().with_component_from_data( "confidence", Arc::new(arrow::array::Float64Array::from(vec![0.3, 0.4, 0.5, 0.6])), diff --git a/docs/snippets/all/tutorials/getting_started.py b/docs/snippets/all/tutorials/getting_started.py new file mode 100644 index 000000000000..e6ac875787a2 --- /dev/null +++ b/docs/snippets/all/tutorials/getting_started.py @@ -0,0 +1,104 @@ +"""Getting Started workflow: Catalog SDK regions (Python only). + +The Log step lives in `tutorials/getting_started_log` because it uses only the +Logging SDK and is therefore available in Python, Rust, and C++. +""" + +import math +import os +import tempfile +from pathlib import Path + +import torch.multiprocessing + +import rerun as rr + +# Rerun's tokio runtime is not fork-safe; DataLoader workers must use `spawn`. +torch.multiprocessing.set_start_method("spawn", force=True) + +# Run from a fresh temp dir so the .rrd files this snippet writes don't +# collide with other snippets executing in parallel from the same cwd. +os.chdir(tempfile.mkdtemp()) + +# Materialize the .rrd that the catalog regions below register against. +# Same code as the Log step's three-language snippet, repeated here so this +# file runs end-to-end. +with rr.RecordingStream( + "rerun_example_getting_started", recording_id="run-1" +) as _rec: + _rec.save("run-1.rrd") + for _t in range(10): + _rec.set_time("step", sequence=_t) + _rec.log("/arm/shoulder", rr.Scalars(math.sin(_t * 0.5))) + _rec.log("/arm/elbow", rr.Scalars(math.cos(_t * 0.5))) + +# Start an in-process catalog server on a random port so this snippet runs +# end-to-end. In a real workflow you'd run `rerun server` in a separate +# terminal, which is what the docs show. +_server = rr.server.Server() +server_url = _server.url() + + +# region: setup +# `server_url` is the catalog URL — defaults to "rerun+http://127.0.0.1:51234" +# when running `rerun server` locally. +client = rr.catalog.CatalogClient(server_url) +# endregion: setup + + +# region: ingest +dataset = client.create_dataset("demo", exist_ok=True) +dataset.register([Path("run-1.rrd").absolute().as_uri()]).wait() +# endregion: ingest + + +# region: annotate +with rr.RecordingStream( + "rerun_example_getting_started", recording_id="run-1" +) as ann: + ann.save("run-1-properties.rrd") + ann.send_property( + "episode", rr.AnyValues(success=True, task="pick_and_place") + ) + +dataset.register( + [Path("run-1-properties.rrd").absolute().as_uri()], layer_name="properties" +).wait() +# endregion: annotate + + +# region: query +df = dataset.filter_contents(["/arm/**"]).reader(index="step") +print( + df.select( + "rerun_segment_id", + "/arm/shoulder:Scalars:scalars", + "/arm/elbow:Scalars:scalars", + ) +) +# endregion: query + +# region: train +from torch.utils.data import DataLoader + +from rerun.experimental.dataloader import ( + DataSource, + Field, + NumericDecoder, + RerunIterableDataset, +) + +ds = RerunIterableDataset( + source=DataSource(dataset=dataset), + index="step", + fields={ + "shoulder": Field( + "/arm/shoulder:Scalars:scalars", decode=NumericDecoder() + ), + "elbow": Field("/arm/elbow:Scalars:scalars", decode=NumericDecoder()), + }, +) + +for batch in DataLoader(ds, batch_size=4): + print(batch) +# endregion: train diff --git a/docs/snippets/all/tutorials/getting_started_convert.py b/docs/snippets/all/tutorials/getting_started_convert.py new file mode 100644 index 000000000000..5c4845e68f30 --- /dev/null +++ b/docs/snippets/all/tutorials/getting_started_convert.py @@ -0,0 +1,7 @@ +from rerun.experimental import McapReader + +McapReader("input.mcap").stream().write_rrd( + "run-1.rrd", + application_id="rerun_example_getting_started", + recording_id="run-1", +) diff --git a/docs/snippets/all/tutorials/getting_started_log.cpp b/docs/snippets/all/tutorials/getting_started_log.cpp new file mode 100644 index 000000000000..6a212aa2ea1a --- /dev/null +++ b/docs/snippets/all/tutorials/getting_started_log.cpp @@ -0,0 +1,16 @@ +#include + +#include + +int main(int argc, char* argv[]) { + const auto rec = + rerun::RecordingStream("rerun_example_getting_started", "run-1"); + rec.save("run-1.rrd").exit_on_failure(); + + for (int t = 0; t < 10; ++t) { + rec.set_time_sequence("step", t); + const auto tf = static_cast(t); + rec.log("/arm/shoulder", rerun::Scalars(std::sin(tf * 0.5))); + rec.log("/arm/elbow", rerun::Scalars(std::cos(tf * 0.5))); + } +} diff --git a/docs/snippets/all/tutorials/getting_started_log.py b/docs/snippets/all/tutorials/getting_started_log.py new file mode 100644 index 000000000000..5d789fbc327b --- /dev/null +++ b/docs/snippets/all/tutorials/getting_started_log.py @@ -0,0 +1,12 @@ +import math + +import rerun as rr + +with rr.RecordingStream( + "rerun_example_getting_started", recording_id="run-1", send_properties=False +) as rec: + rec.save("run-1.rrd") + for t in range(10): + rec.set_time("step", sequence=t) + rec.log("/arm/shoulder", rr.Scalars(math.sin(t * 0.5))) + rec.log("/arm/elbow", rr.Scalars(math.cos(t * 0.5))) diff --git a/docs/snippets/all/tutorials/getting_started_log.rs b/docs/snippets/all/tutorials/getting_started_log.rs new file mode 100644 index 000000000000..f6b362ed334f --- /dev/null +++ b/docs/snippets/all/tutorials/getting_started_log.rs @@ -0,0 +1,15 @@ +fn main() -> Result<(), Box> { + let rec = + rerun::RecordingStreamBuilder::new("rerun_example_getting_started") + .recording_id("run-1") + .save("run-1.rrd")?; + + for t in 0..10 { + rec.set_time_sequence("step", t); + let tf = t as f64; + rec.log("/arm/shoulder", &rerun::Scalars::single((tf * 0.5).sin()))?; + rec.log("/arm/elbow", &rerun::Scalars::single((tf * 0.5).cos()))?; + } + + Ok(()) +} diff --git a/docs/snippets/all/tutorials/visualization/save_blueprint.py b/docs/snippets/all/tutorials/visualization/save_blueprint.py index 6c6896915f92..a015cbe8056a 100644 --- a/docs/snippets/all/tutorials/visualization/save_blueprint.py +++ b/docs/snippets/all/tutorials/visualization/save_blueprint.py @@ -1,4 +1,4 @@ -"""Craft an example blueprint with the python API and save it to a file for future use.""" +"""Craft an example blueprint with the python API and save it to a file.""" import sys diff --git a/docs/snippets/all/views/bar_chart.py b/docs/snippets/all/views/bar_chart.py index b542d883f883..7cb0d81439c3 100644 --- a/docs/snippets/all/views/bar_chart.py +++ b/docs/snippets/all/views/bar_chart.py @@ -11,7 +11,9 @@ rrb.BarChartView( origin="bar_chart", name="Bar Chart", - background=rrb.archetypes.PlotBackground(color=[50, 0, 50, 255], show_grid=False), + background=rrb.archetypes.PlotBackground( + color=[50, 0, 50, 255], show_grid=False + ), ), collapse_panels=True, ) diff --git a/docs/snippets/all/views/dataframe.py b/docs/snippets/all/views/dataframe.py index 11b96de2201b..a5ba3b08be9e 100644 --- a/docs/snippets/all/views/dataframe.py +++ b/docs/snippets/all/views/dataframe.py @@ -25,7 +25,13 @@ timeline="t", filter_by_range=(rr.TimeInt(seconds=0), rr.TimeInt(seconds=20)), filter_is_not_null="/trig/tan_sparse:Scalar", - select=["t", "log_tick", "/trig/sin:Scalar", "/trig/cos:Scalar", "/trig/tan_sparse:Scalar"], + select=[ + "t", + "log_tick", + "/trig/sin:Scalar", + "/trig/cos:Scalar", + "/trig/tan_sparse:Scalar", + ], entity_order=["/trig/cos", "/trig/sin", "/trig/tan_sparse"], auto_scroll=True, ), diff --git a/docs/snippets/all/views/graph.py b/docs/snippets/all/views/graph.py index 2c454a44e0ce..ef79f9b7450e 100644 --- a/docs/snippets/all/views/graph.py +++ b/docs/snippets/all/views/graph.py @@ -20,7 +20,9 @@ origin="/", name="Graph", # Note that this translates the viewbox. - visual_bounds=rrb.VisualBounds2D(x_range=[-150, 150], y_range=[-50, 150]), + visual_bounds=rrb.VisualBounds2D( + x_range=[-150, 150], y_range=[-50, 150] + ), background=rrb.archetypes.GraphBackground(color=[30, 10, 10]), ), collapse_panels=True, diff --git a/docs/snippets/all/views/map.py b/docs/snippets/all/views/map.py index eaff31b61497..9371bc186edc 100644 --- a/docs/snippets/all/views/map.py +++ b/docs/snippets/all/views/map.py @@ -5,7 +5,13 @@ rr.init("rerun_example_map_view", spawn=True) -rr.log("points", rr.GeoPoints(lat_lon=[[47.6344, 19.1397], [47.6334, 19.1399]], radii=rr.Radius.ui_points(20.0))) +rr.log( + "points", + rr.GeoPoints( + lat_lon=[[47.6344, 19.1397], [47.6334, 19.1399]], + radii=rr.Radius.ui_points(20.0), + ), +) # Create a map view to display the chart. blueprint = rrb.Blueprint( diff --git a/docs/snippets/all/views/spatial2d.py b/docs/snippets/all/views/spatial2d.py index 789a60828df6..6e36017eac2b 100644 --- a/docs/snippets/all/views/spatial2d.py +++ b/docs/snippets/all/views/spatial2d.py @@ -11,8 +11,15 @@ n = 150 angle = np.linspace(0, 10 * np.pi, n) spiral_radius = np.linspace(0.0, 3.0, n) ** 2 -positions = np.column_stack((np.cos(angle) * spiral_radius, np.sin(angle) * spiral_radius)) -colors = np.dstack((np.linspace(255, 255, n), np.linspace(255, 0, n), np.linspace(0, 255, n)))[0].astype(int) +positions = np.column_stack(( + np.cos(angle) * spiral_radius, + np.sin(angle) * spiral_radius, +)) +colors = np.dstack(( + np.linspace(255, 255, n), + np.linspace(255, 0, n), + np.linspace(0, 255, n), +))[0].astype(int) radii = np.linspace(0.01, 0.7, n) rr.log("points", rr.Points2D(positions, colors=colors, radii=radii)) diff --git a/docs/snippets/all/views/spatial3d.py b/docs/snippets/all/views/spatial3d.py index 02c2d3df261a..1de2a8310946 100644 --- a/docs/snippets/all/views/spatial3d.py +++ b/docs/snippets/all/views/spatial3d.py @@ -34,12 +34,19 @@ ), # Configure the line grid. line_grid=rrb.LineGrid3D( - visible=True, # The grid is enabled by default, but you can hide it with this property. + # The grid is enabled by default, but you can hide it. + visible=True, spacing=0.1, # Makes the grid more fine-grained. - # By default, the plane is inferred from view coordinates setup, but you can set arbitrary planes. + # By default, the plane is inferred from view coordinates setup, + # but you can set arbitrary planes. plane=rr.components.Plane3D.XY.with_distance(-5.0), stroke_width=2.0, # Makes the grid lines twice as thick as usual. - color=[255, 255, 255, 128], # Colors the grid a half-transparent white. + color=[ + 255, + 255, + 255, + 128, + ], # Colors the grid a half-transparent white. ), spatial_information=rrb.SpatialInformation( target_frame="tf#/", diff --git a/docs/snippets/all/views/state_timeline.py b/docs/snippets/all/views/state_timeline.py new file mode 100644 index 000000000000..e56370e721a1 --- /dev/null +++ b/docs/snippets/all/views/state_timeline.py @@ -0,0 +1,26 @@ +# Use a blueprint to show a StateTimelineView. + +import rerun as rr +import rerun.blueprint as rrb + +rr.init("rerun_example_state_timeline", spawn=True) + +rr.set_time("step", sequence=0) +rr.log("door", rr.StateChange(state="open")) + +rr.set_time("step", sequence=1) +rr.log("door", rr.StateChange(state="closed")) + +rr.set_time("step", sequence=2) +rr.log("door", rr.StateChange(state="open")) + +# Create a state timeline view to display the state transitions. +blueprint = rrb.Blueprint( + rrb.StateTimelineView( + origin="/", + name="State Transitions", + ), + collapse_panels=True, +) + +rr.send_blueprint(blueprint) diff --git a/docs/snippets/all/views/status.py b/docs/snippets/all/views/status.py deleted file mode 100644 index 2331e963e1de..000000000000 --- a/docs/snippets/all/views/status.py +++ /dev/null @@ -1,26 +0,0 @@ -# Use a blueprint to show a StatusView. - -import rerun as rr -import rerun.blueprint as rrb - -rr.init("rerun_example_status", spawn=True) - -rr.set_time("step", sequence=0) -rr.log("door", rr.Status(status="open")) - -rr.set_time("step", sequence=1) -rr.log("door", rr.Status(status="closed")) - -rr.set_time("step", sequence=2) -rr.log("door", rr.Status(status="open")) - -# Create a status view to display the status transitions. -blueprint = rrb.Blueprint( - rrb.StatusView( - origin="/", - name="Status Transitions", - ), - collapse_panels=True, -) - -rr.send_blueprint(blueprint) diff --git a/docs/snippets/all/views/tensor.py b/docs/snippets/all/views/tensor.py index c48120b9fad7..640340376f19 100644 --- a/docs/snippets/all/views/tensor.py +++ b/docs/snippets/all/views/tensor.py @@ -25,11 +25,15 @@ rr.TensorDimensionIndexSelection(dimension=2, index=4), rr.TensorDimensionIndexSelection(dimension=3, index=5), ], - # Show a slider for dimension 2 only. If not specified, all dimensions in `indices` will have sliders. + # Show a slider for dimension 2 only. If not specified, all + # dimensions in `indices` will have sliders. slider=[2], ), - # Set a scalar mapping with a custom colormap, gamma and magnification filter. - scalar_mapping=rrb.TensorScalarMapping(colormap="turbo", gamma=1.5, mag_filter="linear"), + # Set a scalar mapping with a custom colormap, gamma and + # magnification filter. + scalar_mapping=rrb.TensorScalarMapping( + colormap="turbo", gamma=1.5, mag_filter="linear" + ), # Fill the view, ignoring aspect ratio. view_fit="fill", ), diff --git a/docs/snippets/all/views/text_log.py b/docs/snippets/all/views/text_log.py index 018918e6654c..02882337754a 100644 --- a/docs/snippets/all/views/text_log.py +++ b/docs/snippets/all/views/text_log.py @@ -6,12 +6,17 @@ rr.init("rerun_example_text_log", spawn=True) rr.set_time("time", sequence=0) -rr.log("log/status", rr.TextLog("Application started.", level=rr.TextLogLevel.INFO)) +rr.log( + "log/status", rr.TextLog("Application started.", level=rr.TextLogLevel.INFO) +) rr.set_time("time", sequence=5) rr.log("log/other", rr.TextLog("A warning.", level=rr.TextLogLevel.WARN)) for i in range(10): rr.set_time("time", sequence=i) - rr.log("log/status", rr.TextLog(f"Processing item {i}.", level=rr.TextLogLevel.INFO)) + rr.log( + "log/status", + rr.TextLog(f"Processing item {i}.", level=rr.TextLogLevel.INFO), + ) # Create a text view that displays all logs. blueprint = rrb.Blueprint( diff --git a/docs/snippets/all/views/timeseries.py b/docs/snippets/all/views/timeseries.py index c5c8ef462c66..949dc6a0a42f 100644 --- a/docs/snippets/all/views/timeseries.py +++ b/docs/snippets/all/views/timeseries.py @@ -8,9 +8,21 @@ rr.init("rerun_example_timeseries", spawn=True) # Log some trigonometric functions -rr.log("trig/sin", rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)"), static=True) -rr.log("trig/cos", rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)"), static=True) -rr.log("trig/cos_scaled", rr.SeriesLines(colors=[0, 0, 255], names="cos(0.01t) scaled"), static=True) +rr.log( + "trig/sin", + rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)"), + static=True, +) +rr.log( + "trig/cos", + rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)"), + static=True, +) +rr.log( + "trig/cos_scaled", + rr.SeriesLines(colors=[0, 0, 255], names="cos(0.01t) scaled"), + static=True, +) for t in range(int(math.pi * 4 * 100.0)): rr.set_time("timeline0", sequence=t) rr.set_time("timeline1", duration=t) @@ -30,13 +42,15 @@ plot_legend=rrb.PlotLegend(visible=False), # Set time different time ranges for different timelines. time_ranges=[ - # Sliding window depending on the time cursor for the first timeline. + # Sliding window depending on the time cursor for the + # first timeline. rrb.VisibleTimeRange( "timeline0", start=rrb.TimeRangeBoundary.cursor_relative(seq=-100), end=rrb.TimeRangeBoundary.cursor_relative(), ), - # Time range from some point to the end of the timeline for the second timeline. + # Time range from some point to the end of the timeline + # for the second timeline. rrb.VisibleTimeRange( "timeline1", start=rrb.TimeRangeBoundary.absolute(seconds=300.0), @@ -48,14 +62,18 @@ origin="/trig", axis_x=rrb.TimeAxis( view_range=rr.TimeRange( - start=rrb.TimeRangeBoundary.cursor_relative(seconds=-100), + start=rrb.TimeRangeBoundary.cursor_relative( + seconds=-100 + ), end=rrb.TimeRangeBoundary.cursor_relative(seconds=100), ), zoom_lock=True, ), # Configure the legend. plot_legend=rrb.PlotLegend(visible=True), - background=rrb.archetypes.PlotBackground(color=[128, 128, 128], show_grid=False), + background=rrb.archetypes.PlotBackground( + color=[128, 128, 128], show_grid=False + ), ), ] ), diff --git a/docs/snippets/build.rs b/docs/snippets/build.rs index 0715537c8dd8..2a44b4536ec2 100644 --- a/docs/snippets/build.rs +++ b/docs/snippets/build.rs @@ -15,8 +15,10 @@ use std::path::Path; use itertools::Itertools as _; fn main() { - let crate_path = - Path::new(&re_build_tools::get_and_track_env_var("CARGO_MANIFEST_DIR").unwrap()).to_owned(); + let crate_path = Path::new( + &re_build_tools::get_and_track_env_var("CARGO_MANIFEST_DIR").unwrap(), + ) + .to_owned(); let all_path = crate_path.join("all"); let src_path = crate_path.join("src"); let snippets_path = src_path.join("snippets"); @@ -38,15 +40,18 @@ fn main() { let path = entry.path(); if let Some(extension) = path.extension() { if extension == "rs" { - let snippet_name = path.file_stem().unwrap().to_str().unwrap().to_owned(); + let snippet_name = + path.file_stem().unwrap().to_str().unwrap().to_owned(); let contents = fs::read_to_string(&path).unwrap(); // TODO(#4047): some snippets lack a main, they should come with their necessary stub code commented out so that we can re-add it here. if contents.contains("fn main()") { // Patch the source code so we can call into `main` and pass arguments to it: - let contents = - contents.replace("fn main()", "pub fn main(_args: &[String])"); + let contents = contents.replace( + "fn main()", + "pub fn main(_args: &[String])", + ); let contents = contents.replace( "let args = std::env::args().collect::>();", "let args = _args;", @@ -57,16 +62,21 @@ fn main() { path.to_str().unwrap().replace('\\', "/"), ); - let target_path = snippets_path.join(format!("{snippet_name}.rs")); + let target_path = + snippets_path.join(format!("{snippet_name}.rs")); println!("{}", target_path.display()); - re_build_tools::write_file_if_necessary(target_path, contents.as_bytes()) - .expect("failed to write snippet??"); + re_build_tools::write_file_if_necessary( + target_path, + contents.as_bytes(), + ) + .expect("failed to write snippet??"); snippets.push(snippet_name); } } else if extension == "png" { // Files used by the snippets, e.g. via `include_bytes`. Copy them: - let target_path = snippets_path.join(path.file_name().unwrap()); + let target_path = + snippets_path.join(path.file_name().unwrap()); fs::copy(&path, &target_path).unwrap(); } } @@ -87,6 +97,8 @@ fn main() { ${MODS} + // The generated match arm grows with every snippet, so we have exceeded clippy's 600-line limit. + #[expect(clippy::too_many_lines)] pub fn run() { let args: Vec = std::env::args().skip(1).collect(); @@ -135,8 +147,12 @@ fn main() { .join(",\n"), ); - let source = re_build_tools::rustfmt_str(&source).expect("Failed to format"); + let source = + re_build_tools::rustfmt_str(&source).expect("Failed to format"); - re_build_tools::write_file_if_necessary(snippets_path.join("mod.rs"), source.as_bytes()) - .unwrap(); + re_build_tools::write_file_if_necessary( + snippets_path.join("mod.rs"), + source.as_bytes(), + ) + .unwrap(); } diff --git a/docs/snippets/compare_snippet_output.py b/docs/snippets/compare_snippet_output.py index 070559f7bdf3..a42b6c449703 100755 --- a/docs/snippets/compare_snippet_output.py +++ b/docs/snippets/compare_snippet_output.py @@ -97,22 +97,46 @@ def main() -> None: parser.add_argument("--no-py", action="store_true", help="Skip Python tests") parser.add_argument("--no-cpp", action="store_true", help="Skip C++ tests") # We don't allow skipping Rust - it is what we compare to at the moment. - parser.add_argument("--no-py-build", action="store_true", help="Skip building rerun-sdk for Python") + parser.add_argument( + "--no-py-build", + action="store_true", + help="Skip building rerun-sdk for Python", + ) parser.add_argument( "--no-cpp-build", action="store_true", help="Skip cmake configure and ahead of time build for rerun_c & rerun_prebuilt_cpp", ) parser.add_argument("--full-dump", action="store_true", help="Dump both rrd files as tables") - parser.add_argument("--release", action="store_true", help="Run cargo invocations with --release") - parser.add_argument("--target", type=str, default=None, help="Target used for cargo invocations") - parser.add_argument("--target-dir", type=str, default=None, help="Target directory used for cargo invocations") + parser.add_argument( + "--release", + action="store_true", + help="Run cargo invocations with --release", + ) + parser.add_argument( + "--target", + type=str, + default=None, + help="Target used for cargo invocations", + ) + parser.add_argument( + "--target-dir", + type=str, + default=None, + help="Target directory used for cargo invocations", + ) parser.add_argument( "--write-missing-backward-assets", action="store_true", help="Add any missing asset files to tests/assets/rrd/snippets", ) - parser.add_argument("example", nargs="*", type=str, default=None, help="Run only the specified example(s)") + parser.add_argument( + "example", + nargs="*", + type=str, + default=None, + help="Run only the specified example(s)", + ) args = parser.parse_args() @@ -129,7 +153,15 @@ def main() -> None: build_python_sdk(build_env) # Use uv to install the snippet dependencies run( - ["uv", "sync", "--group", "snippets", "--inexact", "--no-install-package", "rerun-sdk"], + [ + "uv", + "sync", + "--group", + "snippets", + "--inexact", + "--no-install-package", + "rerun-sdk", + ], env=build_env, ) @@ -143,7 +175,12 @@ def main() -> None: build_cpp_snippets() # Always build rust since we use it as the baseline for comparison. - build_rust_snippets(build_env=build_env, release=args.release, target=args.target, target_dir=args.target_dir) + build_rust_snippets( + build_env=build_env, + release=args.release, + target=args.target, + target_dir=args.target_dir, + ) examples = [] if len(args.example) > 0: @@ -224,7 +261,9 @@ def main() -> None: # They should be the same! try: if backwards_path.exists(): - run_comparison(backwards_path, baseline_path, args.full_dump) + # Older backward-compatibility assets were recorded back when `log_tick` was + # injected by default. It is now opt-in, so ignore it in the comparison. + run_comparison(backwards_path, baseline_path, args.full_dump, ignore_timelines=["log_tick"]) elif args.write_missing_backward_assets: print(f"Writing new backwards-compatibility file to {backwards_path}…") backwards_path.parent.mkdir(parents=True, exist_ok=True) @@ -289,13 +328,22 @@ def run_example(example: Example, language: str, args: argparse.Namespace) -> No elif language == "py": run_python(example) elif language == "rust": - run_prebuilt_rust(example, release=args.release, target=args.target, target_dir=args.target_dir) + run_prebuilt_rust( + example, + release=args.release, + target=args.target, + target_dir=args.target_dir, + ) else: raise AssertionError(f"Unknown language: {language}") def build_rust_snippets( - *, build_env: dict[str, str], release: bool, target: str | None, target_dir: str | None + *, + build_env: dict[str, str], + release: bool, + target: str | None, + target_dir: str | None, ) -> None: print("----------------------------------------------------------") print("Building snippets for Rust…") @@ -356,7 +404,13 @@ def run_python(example: Example) -> str: return output_path -def run_prebuilt_rust(example: Example, *, release: bool, target: str | None, target_dir: str | None) -> str: +def run_prebuilt_rust( + example: Example, + *, + release: bool, + target: str | None, + target_dir: str | None, +) -> str: output_path = example.output_path("rust") extension = ".exe" if os.name == "nt" else "" @@ -387,7 +441,11 @@ def run_prebuilt_cpp(example: Example) -> str: output_path = example.output_path("cpp") extension = ".exe" if os.name == "nt" else "" - cmd = [f"./build/debug/docs/snippets/snippets{extension}", example.name, *example.extra_args()] + cmd = [ + f"./build/debug/docs/snippets/snippets{extension}", + example.name, + *example.extra_args(), + ] env = None if str(example) not in OPT_OUT_BACKWARDS_CHECK: env = roundtrip_env(save_path=output_path) diff --git a/docs/snippets/rustfmt.toml b/docs/snippets/rustfmt.toml new file mode 100644 index 000000000000..ca2a183b7104 --- /dev/null +++ b/docs/snippets/rustfmt.toml @@ -0,0 +1,2 @@ +# Keep snippets tight so they look nice on our web page: +max_width = 80 diff --git a/docs/snippets/snippets.toml b/docs/snippets/snippets.toml index 90b575b98391..d91ff704436a 100644 --- a/docs/snippets/snippets.toml +++ b/docs/snippets/snippets.toml @@ -17,11 +17,13 @@ [snippets_ref.snippets.opt_out] # Migration snippets rarely make sense as part of the snippet index. "migration/log_line" = ["cpp", "rust", "py"] +"migration/log_tick_enabled" = ["cpp", "rust", "py"] "migration/transactional_transforms" = ["cpp", "rust", "py"] # These archetypes will ignore the associated snippets in the snippet index. [snippets_ref.archetypes.opt_out] "DataframeQuery" = ["howto/visualization/save_blueprint"] +"Tensor" = ["howto/dataloader"] # snippet uses `torch.Tensor`, not Rerun's `Tensor` archetype # These components will ignore the associated snippets in the snippet index. [snippets_ref.components.opt_out] @@ -34,7 +36,7 @@ [snippets_ref] features = [ [ - "Query Data Platform", + "Catalog server", [ "howto/dataframe_operations", "howto/dataframe_performance", @@ -125,12 +127,6 @@ features = [ "howto/send_table", ], ], - [ - "Convert custom MCAP Protobuf", - [ - "howto/convert_mcap_protobuf", - ], - ], ] # -------------------------------------------------------------------------------------------------- @@ -139,14 +135,22 @@ features = [ backwards_check = [ "archetypes/image_advanced", # Uses Pillow to encode a PNG; Pillow encodes differently on different OSes, so the bytes in the RRD won't match the checked-in reference. "archetypes/video_stream_synthetic", # Video encodes differently on CI :( + "concepts/build_chunk", # Python-only experimental API; no reference RRD checked in + "concepts/build_chunk_from_record_batch", # Python-only experimental API; builds and prints chunks, generates no rrd "concepts/explicit_recording", # The file path differs locally and on CI - "howto/convert_mcap_protobuf", # Processes video from MCAP, non-deterministic encoding - "howto/convert_mcap_protobuf_send_column", - # Dataplatform examples don't generate rrds + "concepts/send_dataframe", # Python-only API; no reference RRD checked in + "concepts/chunk_processing", # Python-only experimental API; output rrd may shift across versions + "concepts/chunk_processing_intro", # Python-only experimental API; output rrd may shift across versions + "concepts/chunk_processing_query", # Python-only experimental API; queries in-process, generates no rrd + "concepts/rrd_format", # Python-only experimental API; output rrd may shift across versions + "howto/optimize_chunks", # Compacts a video-bearing MCAP; chunk layout may shift across optimizer versions + # Catalog server examples don't generate rrds "concepts/query-and-transform/dataframe_query_example", "concepts/query-and-transform/segment_properties", "howto/dataframe_operations", "howto/dataframe_performance", + "howto/dataloader", + "tutorials/getting_started", "howto/dataset_resampling", "howto/layers", "howto/lerobot_export", @@ -175,6 +179,30 @@ backwards_check = [ "cpp", "rust", ] +"concepts/build_chunk" = [ # Python-only experimental API + "cpp", + "rust", +] +"concepts/build_chunk_from_record_batch" = [ # Python-only experimental API + "cpp", + "rust", +] +"concepts/chunk_processing" = [ # `rerun.experimental` is Python-only + "cpp", + "rust", +] +"concepts/chunk_processing_intro" = [ # `rerun.experimental` is Python-only + "cpp", + "rust", +] +"concepts/chunk_processing_query" = [ # `rerun.experimental` is Python-only + "cpp", + "rust", +] +"concepts/rrd_format" = [ # `rerun.experimental` is Python-only + "cpp", + "rust", +] "concepts/explicit_recording" = [ # python-specific check "cpp", "rust", @@ -184,11 +212,15 @@ backwards_check = [ "cpp", "rust", ] -"concepts/send_recording" = [ +"concepts/send_chunks" = [ "py", # Requires context (an RRD file to be exported by the user) "cpp", # Not implemented for C++ "rust", # Requires context (an RRD file to be exported by the user) ] +"concepts/send_dataframe" = [ # Python-only API + "cpp", + "rust", +] "concepts/static/log_static" = [ # pseudo-code "py", "cpp", @@ -252,18 +284,18 @@ backwards_check = [ "cpp", # TODO(#2353): Doesn't exist "rust", # Runs indefinitely. ] -"howto/convert_mcap_protobuf" = [ +"howto/component_mapping" = [ "cpp", # Not implemented - "rust", # Not implemented ] -"howto/convert_mcap_protobuf_send_column" = [ - "cpp", # Not implemented - "rust", # Not implemented +"howto/state_remapping" = [ + "cpp", # Blueprint component-mapping API is Python-only + "rust", # Blueprint component-mapping API is Python-only ] -"howto/component_mapping" = [ +"howto/dataframe_operations" = [ "cpp", # Not implemented + "rust", # Not implemented ] -"howto/dataframe_operations" = [ +"howto/dataloader" = [ "cpp", # Not implemented "rust", # Not implemented ] @@ -295,6 +327,10 @@ backwards_check = [ "howto/micro_batching" = [ "cpp", # TODO(#10661): Doesn't exist ] +"howto/optimize_chunks" = [ + "cpp", # `rerun.experimental` is Python-only + "rust", # `rerun.experimental` is Python-only +] "howto/query_images" = [ "cpp", # Not implemented "rust", # Not implemented @@ -343,10 +379,18 @@ backwards_check = [ "cpp", # Missing examples "rust", # Missing examples ] +"archetypes/grid_map_pose" = [ + "cpp", # Missing examples + "rust", # Missing examples +] "archetypes/entity_behavior" = [ "cpp", # Blueprint API doesn't exist for C++/Rust "rust", # Blueprint API doesn't exist for C++/Rust ] +"archetypes/line_strips3d_time_window" = [ + "cpp", # Python-only blueprint example + "rust", # Python-only blueprint example +] "archetypes/pinhole_projections" = [ "cpp", # Blueprint API doesn't exist for C++/Rust "rust", # Blueprint API doesn't exist for C++/Rust @@ -365,6 +409,11 @@ backwards_check = [ "rust", "py", ] +"migration/log_tick_enabled" = [ # Not a complete example -- just a couple of method calls + "cpp", + "rust", + "py", +] "migration/transactional_transforms" = [ # Not a complete example -- just a couple of log lines "cpp", "rust", @@ -410,6 +459,25 @@ backwards_check = [ "rust", "py", ] +"tutorials/dna" = [ # Calls spawn() with non-deterministic RNG output — see tutorials/dna_connect_grpc for a CI-friendly variant + "cpp", + "rust", + "py", +] +"tutorials/dna_connect_grpc" = [ # Requires a separately-running viewer + "cpp", + "rust", + "py", +] +"tutorials/getting_started" = [ + "cpp", # Not implemented + "rust", # Not implemented +] +"tutorials/getting_started_convert" = [ + "py", # Requires an input.mcap file + "cpp", # Not implemented + "rust", # Not implemented +] "tutorials/fixed_window_plot" = [ "cpp", # Not implemented "rust", # Not implemented @@ -536,12 +604,27 @@ quick_start = [ # These examples don't have exactly the same implementation. "py", "rust", ] -"howto/convert_mcap_protobuf" = [ # Uses external MCAP file, depends on mcap_protobuf package +"concepts/chunk_processing" = [ # Python-only; nothing to compare across languages "cpp", "py", "rust", ] -"howto/convert_mcap_protobuf_send_column" = [ # Uses external MCAP file, depends on mcap_protobuf package +"concepts/chunk_processing_intro" = [ # Python-only; nothing to compare across languages + "cpp", + "py", + "rust", +] +"concepts/chunk_processing_query" = [ # Python-only; nothing to compare across languages + "cpp", + "py", + "rust", +] +"concepts/rrd_format" = [ # Python-only; nothing to compare across languages + "cpp", + "py", + "rust", +] +"howto/optimize_chunks" = [ # Python-only; nothing to compare across languages "cpp", "py", "rust", @@ -607,7 +690,7 @@ quick_start = [ # These examples don't have exactly the same implementation. # `$config_dir` will be replaced with the absolute path of `docs/snippets`. [extra_args] -"concepts/send_recording" = ["$config_dir/../../tests/assets/rrd/dna.rrd"] +"concepts/send_chunks" = ["$config_dir/../../tests/assets/rrd/dna.rrd"] "archetypes/asset3d_simple" = ["$config_dir/../../tests/assets/mesh/cube.glb"] "archetypes/asset3d_out_of_tree" = ["$config_dir/../../tests/assets/mesh/cube.glb"] "archetypes/encoded_depth_image" = ["$config_dir/../../tests/assets/encoded_depth_image.rvl"] @@ -616,7 +699,3 @@ quick_start = [ # These examples don't have exactly the same implementation. ] "archetypes/video_manual_frames" = ["$config_dir/../../tests/assets/video/Sintel_1080_10s_av1.mp4"] "howto/load_mcap" = ["$config_dir/../../tests/assets/mcap/r2b_galileo.mcap"] -"howto/convert_mcap_protobuf" = ["$config_dir/../../tests/assets/mcap/trossen_transfer_cube.mcap"] -"howto/convert_mcap_protobuf_send_column" = [ - "$config_dir/../../tests/assets/mcap/trossen_transfer_cube.mcap", -] diff --git a/examples/cpp/log_file/README.md b/examples/cpp/log_file/README.md index b0da094739ea..0798e795c62a 100644 --- a/examples/cpp/log_file/README.md +++ b/examples/cpp/log_file/README.md @@ -2,7 +2,7 @@ title = "Log file example" --> -Demonstrates how to log any file from the SDK using the [`Importer`](https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview?speculative-link) machinery. +Demonstrates how to log any file from the SDK using the [`Importer`](https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview) machinery. To build it from a checkout of the repository (requires a Rust toolchain): ```bash diff --git a/examples/manifest.toml b/examples/manifest.toml index 8e02c8ca77b7..34690b90d3cc 100644 --- a/examples/manifest.toml +++ b/examples/manifest.toml @@ -33,7 +33,9 @@ Examples related to robotics, autonomous systems, and interfacing with sensor ha examples = [ # display order, most interesting first "droid_dataset", + "droid_semantic_search", "animated_urdf", + "robot_data_preprocessing", "rerun_export", "ros_node", "chess_robby_fischer", @@ -163,6 +165,7 @@ examples = [ "multiprocess_logging", "multithreading", "plots", + "state_timeline", "live_scrolling_plot", "raw_mesh", "air_traffic_data", @@ -191,6 +194,7 @@ examples = [ "extend_viewer_ui", "external_importer", "graph_lattice", + "table_grid_with_flags", "incremental_logging", "lenses", "minimal_serve", @@ -198,6 +202,7 @@ examples = [ "shared_recording", "spawn_viewer", "stdio", + "table_blueprints", "table_zoo", "template", "viewer_callbacks", diff --git a/examples/notebook/notebook/requirements.txt b/examples/notebook/notebook/requirements.txt index 9efe559767a8..8d1e396330fa 100644 --- a/examples/notebook/notebook/requirements.txt +++ b/examples/notebook/notebook/requirements.txt @@ -7,4 +7,4 @@ rerun-sdk[notebook] # See e.g. https://github.com/jupyter/notebook/issues/6721 jupyter_client<8 pyzmq<25 -tornado<=6.3.3 +tornado<=6.5.5 diff --git a/examples/notebook/notebook_callbacks/README.md b/examples/notebook/notebook_callbacks/README.md index 3c37080227ba..e298d9330b25 100644 --- a/examples/notebook/notebook_callbacks/README.md +++ b/examples/notebook/notebook_callbacks/README.md @@ -24,7 +24,7 @@ Check out the [minimal notebook example](https://rerun.io/examples/integrations/ This notebook spins up a colorful point cloud and pipes it into the viewer so you can experiment with callbacks in real time. As the camera, timeline, and selection change, `Viewer.on_event` emits rich event payloads that we translate into friendly [`ipywidgets`](https://ipywidgets.readthedocs.io/) readouts. -Scrub the timeline, pick individual points, or activate entire views to see how each interaction updates the labels—handy for building responsive dashboards or debugging custom tooling around the Rerun Viewer. +Scrub the timeline, pick individual points, or activate entire views to see how each interaction updates the labels — handy for building responsive dashboards or debugging custom tooling around the Rerun Viewer. ## Running in Jupyter diff --git a/examples/notebook/notebook_neural_field_2d/requirements.txt b/examples/notebook/notebook_neural_field_2d/requirements.txt index c924719fc2d2..1d273c614642 100644 --- a/examples/notebook/notebook_neural_field_2d/requirements.txt +++ b/examples/notebook/notebook_neural_field_2d/requirements.txt @@ -8,4 +8,4 @@ torch # See e.g. https://github.com/jupyter/notebook/issues/6721 jupyter_client<8 pyzmq<25 -tornado<=6.3.3 +tornado<=6.5.5 diff --git a/examples/notebook/notebook_viewer/notebook_viewer.ipynb b/examples/notebook/notebook_viewer/notebook_viewer.ipynb index 6dbda40626e6..cb9529e7e884 100644 --- a/examples/notebook/notebook_viewer/notebook_viewer.ipynb +++ b/examples/notebook/notebook_viewer/notebook_viewer.ipynb @@ -19,8 +19,7 @@ " blueprint=\"hidden\",\n", " selection=\"hidden\",\n", " time=\"collapsed\",\n", - ")\n", - "v" + ")" ] } ], diff --git a/examples/python/arkit_scenes/arkit_scenes/__main__.py b/examples/python/arkit_scenes/arkit_scenes/__main__.py index de734c8917f2..c5f75c9be8a1 100755 --- a/examples/python/arkit_scenes/arkit_scenes/__main__.py +++ b/examples/python/arkit_scenes/arkit_scenes/__main__.py @@ -214,7 +214,7 @@ def log_arkit(recording_path: Path, include_highres: bool) -> None: vertex_positions=mesh.vertices, # type: ignore[attr-defined] vertex_colors=mesh.visual.vertex_colors, # type: ignore[attr-defined] triangle_indices=mesh.faces, # type: ignore[attr-defined] - face_rendering="Back", # We want to hide the front facing faces, but the dataset uses mostly clockwise winding order which is the opposite of what Rerun assumes (CCW). + face_rendering="Front", # We want to hide the front facing faces, but the dataset uses mostly clockwise winding order which is the opposite of what Rerun assumes (CCW). ), static=True, ) diff --git a/examples/python/camera_video_stream/camera_video_stream.py b/examples/python/camera_video_stream/camera_video_stream.py index 742a56aff8fb..eb1a29f4d95a 100755 --- a/examples/python/camera_video_stream/camera_video_stream.py +++ b/examples/python/camera_video_stream/camera_video_stream.py @@ -23,7 +23,7 @@ def setup_camera_input(video_device: str | None = None) -> av.container.InputCon return av.open( video_device, format="avfoundation", - container_options={"framerate": "30"}, # `avfoundation` fails if the framerate is not set. + container_options={"framerate": "30"}, # `avfoundation` fails if the frame rate is not set. ) elif platform.system() == "Windows": if video_device is None: diff --git a/examples/python/controlnet/pyproject.toml b/examples/python/controlnet/pyproject.toml index 73118bf60ec5..f30eb8a2be1d 100644 --- a/examples/python/controlnet/pyproject.toml +++ b/examples/python/controlnet/pyproject.toml @@ -7,7 +7,7 @@ dependencies = [ "accelerate", "opencv-python", "pillow", - "diffusers==0.27.2", + "diffusers<0.39", "numpy", "torch", # this will use the version defined in the uv workspace "transformers", diff --git a/examples/python/dataloader/README.md b/examples/python/dataloader/README.md index 6b1b86ca74cc..5bf2c4427636 100644 --- a/examples/python/dataloader/README.md +++ b/examples/python/dataloader/README.md @@ -1,27 +1,26 @@ -Train a [LeRobot](https://github.com/huggingface/lerobot) ACT policy using Rerun's experimental PyTorch dataloader, streaming trajectory data directly from a Rerun Data Platform catalog. +Train a [LeRobot](https://github.com/huggingface/lerobot) ACT policy using Rerun's experimental PyTorch dataloader, streaming trajectory data directly from a Rerun catalog. -## Background +For an explanation of the dataloader API and how the example fits together, see the [Train PyTorch models with the Rerun dataloader](https://rerun.io/docs/howto/train) how-to guide. -The Rerun Data Platform stores multimodal robot data (video streams, scalar signals, poses, …) as time-indexed recordings. -The `rerun.experimental.dataloader` module exposes those recordings as a PyTorch-style `Dataset`, so you can plug them straight into a standard `DataLoader` and training loop. +## Run the code -This example shows how to: +### 1. Install dependencies -- register a LeRobot dataset (from HuggingFace Hub) to a local Rerun Data Platform instance -- build a `RerunDataset` that decodes video frames and scalar columns on the fly -- use the `Column.window` feature to fetch future action chunks in a single query per batch -- train an [ACT](https://tonyzhaozh.github.io/aloha/) (Action Chunking Transformer) policy on the resulting batches +This example has its own `uv` project, separate from the workspace `.venv`, because LeRobot requires +Python >=3.12 while the workspace supports older versions. -## Run the code +**Standalone** (sparse-checkout of just this directory, no local Rerun build): -### 1. Install dependencies +```bash +uv sync --no-sources --no-dev +``` -This example has its own `uv` project, separate from the workspace `.venv`, because LeRobot pins an -incompatible `rerun-sdk`. From the repo root: +**Monorepo dev** (full repo checkout, editable local `rerun-sdk`): ```bash cd examples/python/dataloader -RERUN_ALLOW_MISSING_BIN=1 uv sync # builds local rerun-sdk + installs lerobot into ./.venv +RERUN_ALLOW_MISSING_BIN=1 uv sync +uv pip install ../../../rerun_py/rerun_dev_fixup ``` Then either `source .venv/bin/activate` or prefix subsequent commands with `uv run`. @@ -65,24 +64,14 @@ uv run python train.py \ --batch-size 8 \ --num-workers 8 \ --lr 1e-5 \ - --checkpoint-dir act_checkpoint + --checkpoint-dir act_checkpoint \ + --dataset-style iterable # or "map" ``` Pass `--num-segments 0` to train on all segments in the dataset. -### 4b. Train with traces +### Training with traces ```sh -TELEMETRY_ENABLED=true OTEL_SDK_ENABLED=true uv run python train.py -``` - -### Iterable vs. Map-style dataset - -Pass `--dataset-style` to pick the PyTorch dataset class: - -- `iterable` (default) uses `RerunIterableDataset` — in-order streaming with shuffling and cross-worker partitioning handled internally. -- `map` uses `RerunMapDataset` — random access by global index, so it plugs into PyTorch's sampler ecosystem (`DistributedSampler`, `WeightedRandomSampler`, `SubsetRandomSampler`, …). - -```bash -uv run python train.py --dataset-style map +TELEMETRY_ENABLED=true OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4317 uv run python train.py ``` diff --git a/examples/python/dataloader/prepare_dataset.py b/examples/python/dataloader/prepare_dataset.py index 07c7307136a0..c7e34466108e 100644 --- a/examples/python/dataloader/prepare_dataset.py +++ b/examples/python/dataloader/prepare_dataset.py @@ -4,7 +4,7 @@ 1. Downloads a LeRobot dataset from HuggingFace Hub. 2. Loads it into Rerun via the built-in LeRobot importer (`log_file_from_path`). 3. Splits the resulting archive into one RRD per episode. -4. Registers the per-episode RRDs to a Rerun Data Platform instance. +4. Registers the per-episode RRDs to a catalog server instance. """ @@ -46,8 +46,6 @@ def lerobot_to_combined_rrd(dataset_dir: Path, combined_rrd: Path) -> None: with rr.RecordingStream(APPLICATION_ID) as rec: rec.save(str(combined_rrd)) rec.log_file_from_path(str(dataset_dir)) - rec.flush() - rec.disconnect() def split_into_episode_rrds(combined_rrd: Path, rrd_dir: Path) -> list[Path]: @@ -57,25 +55,23 @@ def split_into_episode_rrds(combined_rrd: Path, rrd_dir: Path) -> list[Path]: """ rrd_dir.mkdir(parents=True, exist_ok=True) - archive = rr.recording.load_archive(str(combined_rrd)) - recordings = archive.all_recordings() + reader = rr.experimental.RrdReader(str(combined_rrd)) + recordings = reader.recordings() print(f"Archive contains {len(recordings)} recordings") episode_paths: list[Path] = [] - for recording in recordings: + for entry in recordings: + store = reader.store(store=entry) # Skip metadata-only recordings (e.g. the "root" recording that only carries properties). - if not recording.schema().entity_paths(): + if not store.schema().entity_paths(): continue - episode_id = _zero_pad_episode_id(recording.recording_id()) + episode_id = _zero_pad_episode_id(entry.recording_id) rrd_path = rrd_dir / f"{episode_id}.rrd" - rec = rr.RecordingStream(APPLICATION_ID, recording_id=episode_id, send_properties=False) - rec.save(str(rrd_path)) - rr.send_recording(recording, recording=rec) - rec.flush() - # Disconnect to ensure footers are written. - rec.disconnect() + with rr.RecordingStream(APPLICATION_ID, recording_id=episode_id, send_properties=False) as rec: + rec.save(str(rrd_path)) + rec.send_chunks(store) episode_paths.append(rrd_path) print(f" wrote {rrd_path} ({rrd_path.stat().st_size / (1024 * 1024):.1f} MB)") @@ -88,7 +84,7 @@ def register_to_catalog( catalog_url: str, dataset_name: str, ) -> None: - """Register per-episode RRDs to a Rerun Data Platform instance. + """Register per-episode RRDs to a catalog server instance. Uses absolute file:// URIs so the catalog can read the RRDs directly from the local filesystem. """ diff --git a/examples/python/dataloader/pyproject.toml b/examples/python/dataloader/pyproject.toml index 73f9d2160faa..5d2209ffc50c 100644 --- a/examples/python/dataloader/pyproject.toml +++ b/examples/python/dataloader/pyproject.toml @@ -2,13 +2,15 @@ name = "dataloader" version = "0.1.0" readme = "README.md" -requires-python = ">=3.10,<3.13" -dependencies = ["rerun-sdk[dataloader,dataplatform,tracing]", "huggingface-hub<1.0", "lerobot==0.4"] +requires-python = ">=3.12,<3.13" +dependencies = [ + "rerun-sdk[dataloader,catalog,tracing]", + "huggingface-hub>=1.0", + "lerobot[dataset]==0.6.0", +] [dependency-groups] -# rerun-dev-fixup installs the .pth shim that makes `import rerun` resolve to the -# editable `rerun_py/rerun_sdk/` tree — same trick the workspace .venv uses. -dev = ["mypy==1.19.1", "rerun-dev-fixup"] +dev = ["mypy==1.19.1"] [tool.rerun-example] # Picked up by scripts/ci/isolated_examples.py and the `py-lint-isolated-examples` pixi task. @@ -17,13 +19,23 @@ isolated = true [tool.uv] # The example is flat scripts, not a wheel — skip project build, just sync deps. package = false -# Ignore LeRobot's pinned rerun-sdk so resolution picks the local source below. -# Scoped to this project — does not affect the root workspace. -override-dependencies = ["rerun-sdk[dataloader,dataplatform,tracing]"] +# pyarrow 24.0.0 segfaults rerun on import and ships an incomplete py.typed that breaks mypy. +# Isolated examples don't inherit the workspace root's constraint-dependencies, so pin it here. +# pyarrow arrives transitively (via rerun-sdk), hence a constraint rather than a direct dependency. +constraint-dependencies = ["pyarrow>=23.0.1,<24"] +# Default `uv sync` uses the in-repo editable rerun-sdk (monorepo dev mode). +# After syncing, also run `uv pip install ../../../rerun_py/rerun_dev_fixup` to +# install the .pth shim that makes `import rerun` resolve to the editable source tree. +# +# Standalone users (e.g. sparse-checkout of just this example) run instead: +# uv sync --no-sources --no-dev +# That ignores the path source below and resolves `rerun-sdk` from PyPI. +# rerun-dev-fixup is intentionally absent from this file: uv 0.7.x resolves all +# dependency groups and extras unconditionally, so any path-only package here would +# block standalone `--no-sources` resolution. [tool.uv.sources] rerun-sdk = { path = "../../../rerun_py", editable = true } -rerun-dev-fixup = { path = "../../../rerun_py/rerun_dev_fixup", editable = false } # Merged onto the shared base at `../_isolated/mypy.ini` by # scripts/ci/isolated_examples.py — list the untyped third-party libs this diff --git a/examples/python/dataloader/train.py b/examples/python/dataloader/train.py index c22981356af2..4a76d801f486 100644 --- a/examples/python/dataloader/train.py +++ b/examples/python/dataloader/train.py @@ -3,7 +3,7 @@ Demonstrates how to stream robot trajectory data from Rerun's catalog into an imitation learning policy (Action Chunking Transformers). -The Rerun dataloader's Column.window feature fetches future action chunks in a single query per batch. +The Rerun dataloader's Field.window feature fetches future action chunks in a single query per batch. """ from __future__ import annotations @@ -11,6 +11,7 @@ import argparse import time from pathlib import Path +from typing import cast import torch import torch.nn.functional as F @@ -22,8 +23,8 @@ from rerun._tracing import tracing_scope, with_tracing from rerun.catalog import CatalogClient from rerun.experimental.dataloader import ( - Column, DataSource, + Field, NumericDecoder, RerunIterableDataset, RerunMapDataset, @@ -41,7 +42,8 @@ EPOCHS = 5 BATCH_SIZE = 8 LR = 1e-5 -NUM_WORKERS = 8 +NUM_WORKERS = 4 +FETCH_SIZE = 256 class CollateFn: @@ -52,13 +54,17 @@ def __init__(self, chunk_size: int, state_dim: int) -> None: self.state_dim = state_dim @with_tracing("CollateFn") - def __call__(self, samples: list[dict[str, torch.Tensor]]) -> dict[str, torch.Tensor]: - batch_size = len(samples) + def __call__(self, samples: list[dict[str, torch.Tensor | None]]) -> dict[str, torch.Tensor]: + # `VideoFrameDecoder` returns `None` when a target precedes the first keyframe; filter those out. + complete: list[dict[str, torch.Tensor]] = [ + cast("dict[str, torch.Tensor]", s) for s in samples if all(s[f"image_{cam}"] is not None for cam in CAMERAS) + ] + batch_size = len(complete) - states = torch.stack([s["state"] for s in samples]).float() + states = torch.stack([s["state"] for s in complete]).float() # Future action chunks: reshape windowed flat tensors - actions = torch.stack([s["action"].reshape(self.chunk_size, self.state_dim) for s in samples]).float() + actions = torch.stack([s["action"].reshape(self.chunk_size, self.state_dim) for s in complete]).float() batch: dict[str, torch.Tensor] = { "observation.state": states, @@ -67,7 +73,7 @@ def __call__(self, samples: list[dict[str, torch.Tensor]]) -> dict[str, torch.Te } # Per-camera images: (3, H, W) uint8 -> float in [0, 1], resized to (IMAGE_H, IMAGE_W) for cam, key in zip(CAMERAS, IMAGE_KEYS): - imgs = torch.stack([s[f"image_{cam}"] for s in samples]).float() / 255.0 + imgs = torch.stack([s[f"image_{cam}"] for s in complete]).float() / 255.0 batch[key] = F.interpolate(imgs, size=(IMAGE_H, IMAGE_W), mode="bilinear", align_corners=False) return batch @@ -88,6 +94,12 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--epochs", type=int, default=EPOCHS, help="Number of training epochs") parser.add_argument("--batch-size", type=int, default=BATCH_SIZE, help="Training batch size") parser.add_argument("--num-workers", type=int, default=NUM_WORKERS, help="DataLoader worker processes") + parser.add_argument( + "--fetch-size", + type=int, + default=FETCH_SIZE, + help="Samples fetched per server query for the iterable dataset", + ) parser.add_argument("--lr", type=float, default=LR, help="Learning rate") parser.add_argument( "--dataset-style", @@ -120,22 +132,22 @@ def main() -> None: source = DataSource(dataset_entry, segments=segments) - columns = { - "state": Column("/observation.state:Scalars:scalars", decode=NumericDecoder()), - "action": Column( + fields = { + "state": Field("/observation.state:Scalars:scalars", decode=NumericDecoder()), + "action": Field( "/action:Scalars:scalars", decode=NumericDecoder(), window=(1, CHUNK_SIZE), ), - "image_laptop": Column( + "image_laptop": Field( "/observation.images.laptop:VideoStream:sample", decode=VideoFrameDecoder(codec="av1", keyframe_interval=2), ), - "image_phone": Column( + "image_phone": Field( "/observation.images.phone:VideoStream:sample", decode=VideoFrameDecoder(codec="av1", keyframe_interval=2), ), - "image_side": Column( + "image_side": Field( "/observation.images.side:VideoStream:sample", decode=VideoFrameDecoder(codec="av1", keyframe_interval=2), ), @@ -143,13 +155,15 @@ def main() -> None: ds: RerunIterableDataset | RerunMapDataset if args.dataset_style == "map": - ds = RerunMapDataset(source=source, index="frame_index", columns=columns) + ds = RerunMapDataset(source=source, index="frame_index", fields=fields) else: - ds = RerunIterableDataset(source=source, index="frame_index", columns=columns, fetch_size=512) + ds = RerunIterableDataset(source=source, index="frame_index", fields=fields, fetch_size=args.fetch_size) print(f"Using {args.dataset_style} dataset with {len(ds)} samples (after window trimming)") # IterableDataset doesn't support indexing, so probe shape via iteration. - state_dim = next(iter(ds))["state"].shape[0] + state_tensor = next(iter(ds))["state"] + assert state_tensor is not None # NumericDecoder never returns None + state_dim = state_tensor.shape[0] action_dim = state_dim print(f"Dimensions: {state_dim=}, {action_dim=}") diff --git a/examples/python/dataloader/uv.lock b/examples/python/dataloader/uv.lock index b5e7d002ce0c..ebe11319d9fc 100644 --- a/examples/python/dataloader/uv.lock +++ b/examples/python/dataloader/uv.lock @@ -1,46 +1,18 @@ version = 1 revision = 3 -requires-python = ">=3.10, <3.13" +requires-python = "==3.12.*" resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", + "platform_machine == 'arm64' and sys_platform == 'darwin'", + "(platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')", + "sys_platform == 'win32'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", + "platform_machine == 'arm64' and sys_platform == 'linux'", + "platform_machine != 'arm64' and sys_platform == 'darwin'", + "(platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", ] [manifest] -overrides = [{ name = "rerun-sdk", extras = ["dataloader", "dataplatform", "tracing"], editable = "../../../rerun_py" }] - -[[package]] -name = "accelerate" -version = "1.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "safetensors" }, - { name = "torch" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" }, -] +constraints = [{ name = "pyarrow", specifier = ">=23.0.1,<24" }] [[package]] name = "aiohappyeyeballs" @@ -53,71 +25,38 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, + { name = "typing-extensions" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, - { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, - { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, - { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, - { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, - { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, - { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, - { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, - { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, - { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, - { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, - { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, + { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, + { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, + { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, + { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, ] [[package]] @@ -134,21 +73,16 @@ wheels = [ ] [[package]] -name = "annotated-types" -version = "0.7.0" +name = "anyio" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, ] - -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, ] [[package]] @@ -166,20 +100,6 @@ version = "15.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e9/c3/83e6e73d1592bc54436eae0bc61704ae0cff0c3cfbde7b58af9ed67ebb49/av-15.1.0.tar.gz", hash = "sha256:39cda2dc810e11c1938f8cb5759c41d6b630550236b3365790e67a313660ec85", size = 3774192, upload-time = "2025-08-30T04:41:56.076Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/6a/91e3e68ae0d1b53b480ec69a96f2ae820fb007bc60e6b821741f31c7ba4e/av-15.1.0-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:cf067b66cee2248220b29df33b60eb4840d9e7b9b75545d6b922f9c41d88c4ee", size = 21781685, upload-time = "2025-08-30T04:39:13.118Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6d/afa951b9cb615c3bc6d95c4eed280c6cefb52c006f4e15e79043626fab39/av-15.1.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:26426163d96fc3bde9a015ba4d60da09ef848d9284fe79b4ca5e60965a008fc5", size = 26962481, upload-time = "2025-08-30T04:39:16.875Z" }, - { url = "https://files.pythonhosted.org/packages/3c/42/0c384884235c42c439cef28cbd129e4624ad60229119bf3c6c6020805119/av-15.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:92f524541ce74b8a12491d8934164a5c57e983da24826547c212f60123de400b", size = 37571839, upload-time = "2025-08-30T04:39:20.325Z" }, - { url = "https://files.pythonhosted.org/packages/25/c0/5c967b0872fce1add80a8f50fa7ce11e3e3e5257c2b079263570bc854699/av-15.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:659f9d6145fb2c58e8b31907283b6ba876570f5dd6e7e890d74c09614c436c8e", size = 39070227, upload-time = "2025-08-30T04:39:24.079Z" }, - { url = "https://files.pythonhosted.org/packages/e2/81/e333056d49363c35a74b828ed5f87c96dfbcc1a506b49d79a31ac773b94d/av-15.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:07a8ae30c0cfc3132eff320a6b27d18a5e0dda36effd0ae28892888f4ee14729", size = 39619362, upload-time = "2025-08-30T04:39:27.7Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ae/50cc2af1bf68452cbfec8d1b2554c18f6d167c8ba6d7ad7707797dfd1541/av-15.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e33a76e38f03bb5de026b9f66ccf23dc01ddd2223221096992cb52ac22e62538", size = 40371627, upload-time = "2025-08-30T04:39:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/381edf1779106dd31c9ef1ac9842f643af4465b8a87cbc278d3eaa76229a/av-15.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa4bf12bdce20edc2a3b13a2776c474c5ab63e1817d53793714504476eeba82e", size = 31340369, upload-time = "2025-08-30T04:39:34.774Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/4e44cf6939be7aba96a4abce024e1be11ba7539ecac74d09369b8c03aa05/av-15.1.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b785948762a8d45fc58fc24a20251496829ace1817e9a7a508a348d6de2182c3", size = 21767323, upload-time = "2025-08-30T04:39:37.989Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f6/a946544cdb49f6d892d2761b1d61a8bc6ce912fe57ba06769bdc640c0a7f/av-15.1.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:9c7131494a3a318612b4ee4db98fe5bc50eb705f6b6536127c7ab776c524fd8b", size = 26946268, upload-time = "2025-08-30T04:39:40.601Z" }, - { url = "https://files.pythonhosted.org/packages/70/7c/b33513c0af73d0033af59a98f035b521c5b93445a6af7e9efbf41a6e8383/av-15.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2b9623ae848625c59213b610c8665817924f913580c7c5c91e0dc18936deb00d", size = 38062118, upload-time = "2025-08-30T04:39:43.928Z" }, - { url = "https://files.pythonhosted.org/packages/5e/95/31b7fb34f9fea7c7389240364194f4f56ad2d460095038cc720f50a90bb3/av-15.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c8ef597087db560514617143532b1fafc4825ebb2dda9a22418f548b113a0cc7", size = 39571086, upload-time = "2025-08-30T04:39:47.109Z" }, - { url = "https://files.pythonhosted.org/packages/e7/b0/7b0b45474a4e90c35c11d0032947d8b3c7386872957ce29c6f12add69a74/av-15.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08eac47a90ebae1e2bd5935f400dd515166019bab4ff5b03c4625fa6ac3a0a5e", size = 40112634, upload-time = "2025-08-30T04:39:50.981Z" }, - { url = "https://files.pythonhosted.org/packages/aa/04/038b94bc9a1ee10a451c867d4a2fc91e845f83bfc2dae9df25893abcb57f/av-15.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d3f66ff200ea166e606cb3c5cb1bd2fc714effbec2e262a5d67ce60450c8234a", size = 40878695, upload-time = "2025-08-30T04:39:54.493Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3d/9f8f96c0deeaaf648485a3dbd1699b2f0580f2ce8a36cb616c0138ba7615/av-15.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:57b99544d91121b8bea570e4ddf61700f679a6b677c1f37966bc1a22e1d4cd5c", size = 31335683, upload-time = "2025-08-30T04:39:57.861Z" }, { url = "https://files.pythonhosted.org/packages/d1/58/de78b276d20db6ffcd4371283df771721a833ba525a3d57e753d00a9fe79/av-15.1.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:40c5df37f4c354ab8190c6fd68dab7881d112f527906f64ca73da4c252a58cee", size = 21760991, upload-time = "2025-08-30T04:40:00.801Z" }, { url = "https://files.pythonhosted.org/packages/56/cc/45f85775304ae60b66976360d82ba5b152ad3fd91f9267d5020a51e9a828/av-15.1.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:af455ce65ada3d361f80c90c810d9bced4db5655ab9aa513024d6c71c5c476d5", size = 26953097, upload-time = "2025-08-30T04:40:03.998Z" }, { url = "https://files.pythonhosted.org/packages/f3/f8/2d781e5e71d02fc829487e775ccb1185e72f95340d05f2e84eb57a11e093/av-15.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86226d2474c80c3393fa07a9c366106029ae500716098b72b3ec3f67205524c3", size = 38319710, upload-time = "2025-08-30T04:40:07.701Z" }, @@ -204,38 +124,6 @@ version = "3.4.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, @@ -257,14 +145,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.2" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -313,19 +201,19 @@ wheels = [ [[package]] name = "datafusion" -version = "52.3.0" +version = "53.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyarrow" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/d4/a5ad7b665a80008901892fde61dc667318db0652a955d706ddca3a224b5a/datafusion-52.3.0.tar.gz", hash = "sha256:2e8b02ad142b1a0d673f035d96a0944a640ac78275003d7e453cee4afe4a20a4", size = 205026, upload-time = "2026-03-16T10:54:07.739Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/2b/0f96f12b70839c93930c4e17d767fc32b6c77d548c78784128049e944701/datafusion-53.0.0.tar.gz", hash = "sha256:ba9a5ec06b5453fbd8710d6aeeb515a8bcac4b6c140e254409bb53a5f322ef22", size = 224267, upload-time = "2026-04-13T00:45:02.686Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/63/1bb0737988cefa77274b459d64fa4b57ba4cf755639a39733e9581b5d599/datafusion-52.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a73f02406b2985b9145dd97f8221a929c9ef3289a8ba64c6b52043e240938528", size = 31503230, upload-time = "2026-03-16T10:53:50.312Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/ea3b79239953c3044d19d8e9581015da025b6640796db03799e435b17910/datafusion-52.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:118a1f0add6a3f91fcbc90c71819fe08750e2981637d5e7b346e099e94a20b8b", size = 28159497, upload-time = "2026-03-16T10:53:54.032Z" }, - { url = "https://files.pythonhosted.org/packages/24/c8/7d325feb4b7509ae03857fd7e164e95ec72e8c9f3dfd3178ec7f80d53977/datafusion-52.3.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:253ce7aee5fe84bd6ee290c20608114114bdb5115852617f97d3855d36ad9341", size = 30769154, upload-time = "2026-03-16T10:53:57.835Z" }, - { url = "https://files.pythonhosted.org/packages/37/ee/478689c69b3cb1ccabb2d52feac0c181f6cdf20b51a81df35344b1dab9a6/datafusion-52.3.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2af3469d2f06959bec88579ab107a72f965de18b32e607069bbdd0b859ed8dbb", size = 33060335, upload-time = "2026-03-16T10:54:01.715Z" }, - { url = "https://files.pythonhosted.org/packages/1c/48/01906ab5c1a70373c6874ac5192d03646fa7b94d9ff06e3f676cb6b0f43f/datafusion-52.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:9fb35738cf4dbff672dbcfffc7332813024cb0ad2ab8cda1fb90b9054277ab0c", size = 33765807, upload-time = "2026-03-16T10:54:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/4c/60e052813d81f1ffe3123ead013dbdd2cf961daa576cb9056cbb80228e6b/datafusion-53.0.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a0bd1a98d736571321416dc4ed361a9d1225da1ec9f6c5fad818d75f547697a7", size = 35774913, upload-time = "2026-04-13T00:44:46.235Z" }, + { url = "https://files.pythonhosted.org/packages/6e/59/beabe5301df3338d8206446cd624079e43bdad46e20377a6336017fb6ccf/datafusion-53.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ce186a8d2405afd67e11e2fb75715019f16b00d070b8d0da89d8aa61cc74c8b5", size = 32667118, upload-time = "2026-04-13T00:44:50.269Z" }, + { url = "https://files.pythonhosted.org/packages/ae/94/636ab61ade98395daea6e733e225e9c7beef111c7c5b575ac851513e203c/datafusion-53.0.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:288a00a7ef03e2807a4667683f7560efd80d60ed1d41696ac15ca9ded14c8251", size = 35585824, upload-time = "2026-04-13T00:44:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/b9f4889209af02f8d14bccb0e6f0519c329b072bc4d2595025a1303f144c/datafusion-53.0.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8fef0004f0161fcfc556c025a7201f9cc3169aa3adb97a86419ebb34182d9efb", size = 38083690, upload-time = "2026-04-13T00:44:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1a/ea4831fc6aeefedbcf186c9f6a273d507b1787c03cbb905bded7e1149a6a/datafusion-53.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:4c8410f5f659b926677be6c7d443bbc05d825c078c970b7d8cf977ebcf948314", size = 38120687, upload-time = "2026-04-13T00:45:00.633Z" }, ] [[package]] @@ -334,85 +222,48 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "huggingface-hub" }, - { name = "lerobot" }, - { name = "rerun-sdk", extra = ["dataloader", "dataplatform", "tracing"] }, + { name = "lerobot", extra = ["dataset"] }, + { name = "rerun-sdk", extra = ["catalog", "dataloader", "tracing"] }, ] [package.dev-dependencies] dev = [ { name = "mypy" }, - { name = "rerun-dev-fixup" }, ] [package.metadata] requires-dist = [ - { name = "huggingface-hub", specifier = "<1.0" }, - { name = "lerobot", specifier = "==0.4" }, - { name = "rerun-sdk", extras = ["dataloader", "dataplatform", "tracing"], editable = "../../../rerun_py" }, + { name = "huggingface-hub", specifier = ">=1.0" }, + { name = "lerobot", extras = ["dataset"], specifier = "==0.6.0" }, + { name = "rerun-sdk", extras = ["dataloader", "catalog", "tracing"], editable = "../../../rerun_py" }, ] [package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = "==1.19.1" }, - { name = "rerun-dev-fixup", directory = "../../../rerun_py/rerun_dev_fixup" }, -] +dev = [{ name = "mypy", specifier = "==1.19.1" }] [[package]] name = "datasets" -version = "4.1.1" +version = "4.8.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dill" }, { name = "filelock" }, { name = "fsspec", extra = ["http"] }, + { name = "httpx" }, { name = "huggingface-hub" }, { name = "multiprocess" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "packaging" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, { name = "pyarrow" }, { name = "pyyaml" }, { name = "requests" }, { name = "tqdm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/a4/73f8e6ef52c535e1d20d5b2ca83bfe6de399d8b8b8a61ccc8d63d60735aa/datasets-4.1.1.tar.gz", hash = "sha256:7d8d5ba8b12861d2c44bfff9c83484ebfafff1ff553371e5901a8d3aab5450e2", size = 579324, upload-time = "2025-09-18T13:14:27.108Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/c8/09012ac195a0aab58755800d2efdc0e7d5905053509f12cb5d136c911cda/datasets-4.1.1-py3-none-any.whl", hash = "sha256:62e4f6899a36be9ec74a7e759a6951253cc85b3fcfa0a759b0efa8353b149dac", size = 503623, upload-time = "2025-09-18T13:14:25.111Z" }, -] - -[[package]] -name = "deepdiff" -version = "8.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "orderly-set" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/50/767448e792d41bfb6094ee317a355c1cb221dca24b2e178e2203bbea2a77/deepdiff-8.6.2.tar.gz", hash = "sha256:186dcbd181e4d76cef11ab05f802d0056c5d6083c5a6748c1473e9d7481e183e", size = 634860, upload-time = "2026-03-18T17:16:33.785Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/5f/c52bd1255db763d0cdcb7084d2e90c42119cb229302c56bdf1d0aa78abd2/deepdiff-8.6.2-py3-none-any.whl", hash = "sha256:4d22034a866c3928303a9332c279362f714192d9305bac17c498720d095fd1b4", size = 91979, upload-time = "2026-03-18T17:16:32.171Z" }, -] - -[[package]] -name = "diffusers" -version = "0.35.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "huggingface-hub" }, - { name = "importlib-metadata" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, - { name = "regex" }, - { name = "requests" }, - { name = "safetensors" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/03/68/288ca23c7c05c73e87ffe5efffc282400ac9b017f7a9bb03883f4310ea15/diffusers-0.35.2.tar.gz", hash = "sha256:30ecd552303edfcfe1724573c3918a8462ee3ab4d529bdbd4c0045f763affded", size = 3366711, upload-time = "2025-10-15T04:05:17.213Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/34/14cd8e76f907f7d4dca2334cfeec9f81d30fd15c25a015f99aaea694eaed/datasets-4.8.5.tar.gz", hash = "sha256:0f0c1c3d56ffff2c93b2f4c63c95bac94f3d7e8621aea2a2a576275233bba772", size = 605649, upload-time = "2026-04-27T15:43:57.384Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/2e/38d9824f8c6bb048c5ba21c6d4da54c29c162a46b58b3ef907a360a76d3e/diffusers-0.35.2-py3-none-any.whl", hash = "sha256:d50d5e74fdd6dcf55e5c1d304bc52cc7c2659abd1752740d736d7b54078b4db5", size = 4121649, upload-time = "2025-10-15T04:05:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/65/99/00f3196036501b53032c4b1ab8337a0b978dee832ed276dae3815df4e8b5/datasets-4.8.5-py3-none-any.whl", hash = "sha256:5079900781719c0e063a8efdd2cd95a31ad0c63209178669cd23cf1b926149ff", size = 528973, upload-time = "2026-04-27T15:43:53.702Z" }, ] [[package]] @@ -449,12 +300,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, ] -[[package]] -name = "evdev" -version = "1.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/f5/397b61091120a9ca5001041dd7bf76c385b3bfd67a0e5bcb74b852bd22a4/evdev-1.9.3.tar.gz", hash = "sha256:2c140e01ac8437758fa23fe5c871397412461f42d421aa20241dc8fe8cfccbc9", size = 32723, upload-time = "2026-02-05T21:54:24.987Z" } - [[package]] name = "farama-notifications" version = "0.0.4" @@ -479,38 +324,6 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, - { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, - { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, - { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, - { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, - { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, - { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, - { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, - { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, - { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, - { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, - { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, @@ -544,30 +357,6 @@ http = [ { name = "aiohttp" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.47" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c1/bd/50db468e9b1310529a19fce651b3b0e753b5c07954d486cba31bbee9a5d5/gitpython-3.1.47.tar.gz", hash = "sha256:dba27f922bd2b42cb54c87a8ab3cb6beb6bf07f3d564e21ac848913a05a8a3cd", size = 216978, upload-time = "2026-04-22T02:44:44.059Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl", hash = "sha256:489f590edfd6d20571b2c0e72c6a6ac6915ee8b8cd04572330e3842207a78905", size = 209547, upload-time = "2026-04-22T02:44:41.271Z" }, -] - [[package]] name = "googleapis-common-protos" version = "1.74.0" @@ -589,26 +378,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/cd/bb7b7e54084a344c03d68144450da7ddd5564e51a298ae1662de65f48e2d/grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c", size = 6050363, upload-time = "2026-03-30T08:46:20.894Z" }, - { url = "https://files.pythonhosted.org/packages/16/02/1417f5c3460dea65f7a2e3c14e8b31e77f7ffb730e9bfadd89eda7a9f477/grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388", size = 12026037, upload-time = "2026-03-30T08:46:25.144Z" }, - { url = "https://files.pythonhosted.org/packages/43/98/c910254eedf2cae368d78336a2de0678e66a7317d27c02522392f949b5c6/grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02", size = 6602306, upload-time = "2026-03-30T08:46:27.593Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f8/88ca4e78c077b2b2113d95da1e1ab43efd43d723c9a0397d26529c2c1a56/grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc", size = 7301535, upload-time = "2026-03-30T08:46:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f9/96/f28660fe2fe0f153288bf4a04e4910b7309d442395135c88ed4f5b3b8b40/grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a", size = 6808669, upload-time = "2026-03-30T08:46:31.984Z" }, - { url = "https://files.pythonhosted.org/packages/47/eb/3f68a5e955779c00aeef23850e019c1c1d0e032d90633ba49c01ad5a96e0/grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9", size = 7409489, upload-time = "2026-03-30T08:46:34.684Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a7/d2f681a4bfb881be40659a309771f3bdfbfdb1190619442816c3f0ffc079/grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199", size = 8423167, upload-time = "2026-03-30T08:46:36.833Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/29b4589c204959aa35ce5708400a05bba72181807c45c47b3ec000c39333/grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81", size = 7846761, upload-time = "2026-03-30T08:46:40.091Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d2/ed143e097230ee121ac5848f6ff14372dba91289b10b536d54fb1b7cbae7/grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069", size = 4156534, upload-time = "2026-03-30T08:46:42.026Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c9/df8279bb49b29409995e95efa85b72973d62f8aeff89abee58c91f393710/grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58", size = 4889869, upload-time = "2026-03-30T08:46:44.219Z" }, - { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, - { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, - { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, - { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, - { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, @@ -628,8 +397,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, { name = "farama-notifications" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/59/653a9417d98ed3e29ef9734ba52c3495f6c6823b8d5c0c75369f25111708/gymnasium-1.2.3.tar.gz", hash = "sha256:2b2cb5b5fbbbdf3afb9f38ca952cc48aa6aa3e26561400d940747fda3ad42509", size = 829230, upload-time = "2025-12-18T16:51:10.234Z" } @@ -638,110 +406,85 @@ wheels = [ ] [[package]] -name = "hf-transfer" -version = "0.1.9" +name = "h11" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8ce3b2b180a089d4d6b25b1d0d232d016704cb852104/hf_transfer-0.1.9.tar.gz", hash = "sha256:035572865dab29d17e783fbf1e84cf1cb24f3fcf8f1b17db1cfc7fdf139f02bf", size = 25201, upload-time = "2025-01-07T10:05:12.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/f5/461d2e5f307e5048289b1168d5c642ae3bb2504e88dff1a38b92ed990a21/hf_transfer-0.1.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e66acf91df4a8b72f60223059df3003062a5ae111757187ed1a06750a30e911b", size = 1393046, upload-time = "2025-01-07T10:04:51.003Z" }, - { url = "https://files.pythonhosted.org/packages/41/ba/8d9fd9f1083525edfcb389c93738c802f3559cb749324090d7109c8bf4c2/hf_transfer-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:8669dbcc7a3e2e8d61d42cd24da9c50d57770bd74b445c65123291ca842a7e7a", size = 1348126, upload-time = "2025-01-07T10:04:45.712Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a2/cd7885bc9959421065a6fae0fe67b6c55becdeda4e69b873e52976f9a9f0/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd0167c4407a3bc4cdd0307e65ada2294ec04f1813d8a69a5243e379b22e9d8", size = 3728604, upload-time = "2025-01-07T10:04:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2e/a072cf196edfeda3310c9a5ade0a0fdd785e6154b3ce24fc738c818da2a7/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee8b10afedcb75f71091bcc197c526a6ebf5c58bbbadb34fdeee6160f55f619f", size = 3064995, upload-time = "2025-01-07T10:04:18.663Z" }, - { url = "https://files.pythonhosted.org/packages/c2/84/aec9ef4c0fab93c1ea2b1badff38c78b4b2f86f0555b26d2051dbc920cde/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5828057e313de59300dd1abb489444bc452efe3f479d3c55b31a8f680936ba42", size = 3580908, upload-time = "2025-01-07T10:04:32.834Z" }, - { url = "https://files.pythonhosted.org/packages/29/63/b560d39651a56603d64f1a0212d0472a44cbd965db2fa62b99d99cb981bf/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc6bd19e1cc177c66bdef15ef8636ad3bde79d5a4f608c158021153b4573509d", size = 3400839, upload-time = "2025-01-07T10:04:26.122Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d8/f87ea6f42456254b48915970ed98e993110521e9263472840174d32c880d/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca9bfb89e6f8f281890cc61a8aff2d3cecaff7e1a4d275574d96ca70098557", size = 3552664, upload-time = "2025-01-07T10:04:40.123Z" }, - { url = "https://files.pythonhosted.org/packages/d6/56/1267c39b65fc8f4e2113b36297320f102718bf5799b544a6cbe22013aa1d/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89a23f58b7b7effbc047b8ca286f131b17728c99a9f972723323003ffd1bb916", size = 4073732, upload-time = "2025-01-07T10:04:55.624Z" }, - { url = "https://files.pythonhosted.org/packages/82/1a/9c748befbe3decf7cb415e34f8a0c3789a0a9c55910dea73d581e48c0ce5/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:dc7fff1345980d6c0ebb92c811d24afa4b98b3e07ed070c8e38cc91fd80478c5", size = 3390096, upload-time = "2025-01-07T10:04:59.98Z" }, - { url = "https://files.pythonhosted.org/packages/72/85/4c03da147b6b4b7cb12e074d3d44eee28604a387ed0eaf7eaaead5069c57/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a6bd16c667ebe89a069ca163060127a794fa3a3525292c900b8c8cc47985b0d", size = 3664743, upload-time = "2025-01-07T10:05:05.416Z" }, - { url = "https://files.pythonhosted.org/packages/e7/6e/e597b04f753f1b09e6893075d53a82a30c13855cbaa791402695b01e369f/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2fde99d502093ade3ab1b53f80da18480e9902aa960dab7f74fb1b9e5bc5746", size = 3695243, upload-time = "2025-01-07T10:05:11.411Z" }, - { url = "https://files.pythonhosted.org/packages/09/89/d4e234727a26b2546c8fb70a276cd924260d60135f2165bf8b9ed67bb9a4/hf_transfer-0.1.9-cp38-abi3-win32.whl", hash = "sha256:435cc3cdc8524ce57b074032b8fd76eed70a4224d2091232fa6a8cef8fd6803e", size = 1086605, upload-time = "2025-01-07T10:05:18.873Z" }, - { url = "https://files.pythonhosted.org/packages/a1/14/f1e15b851d1c2af5b0b1a82bf8eb10bda2da62d98180220ba6fd8879bb5b/hf_transfer-0.1.9-cp38-abi3-win_amd64.whl", hash = "sha256:16f208fc678911c37e11aa7b586bc66a37d02e636208f18b6bc53d29b5df40ad", size = 1160240, upload-time = "2025-01-07T10:05:14.324Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "hf-xet" -version = "1.4.3" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, - { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, - { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, ] [[package]] -name = "huggingface-hub" -version = "0.35.3" +name = "httpcore" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "tqdm" }, - { name = "typing-extensions" }, + { name = "certifi" }, + { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/7e/a0a97de7c73671863ca6b3f61fa12518caf35db37825e43d63a70956738c/huggingface_hub-0.35.3.tar.gz", hash = "sha256:350932eaa5cc6a4747efae85126ee220e4ef1b54e29d31c3b45c5612ddf0b32a", size = 461798, upload-time = "2025-09-29T14:29:58.625Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/a0/651f93d154cb72323358bf2bbae3e642bdb5d2f1bfc874d096f7cb159fa0/huggingface_hub-0.35.3-py3-none-any.whl", hash = "sha256:0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba", size = 564262, upload-time = "2025-09-29T14:29:55.813Z" }, -] - -[package.optional-dependencies] -cli = [ - { name = "inquirerpy" }, -] -hf-transfer = [ - { name = "hf-transfer" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] -name = "idna" -version = "3.12" +name = "httpx" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/12/2948fbe5513d062169bd91f7d7b1cd97bc8894f32946b71fa39f6e63ca0c/idna-3.12.tar.gz", hash = "sha256:724e9952cc9e2bd7550ea784adb098d837ab5267ef67a1ab9cf7846bdbdd8254", size = 194350, upload-time = "2026-04-21T13:32:48.916Z" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/b2/acc33950394b3becb2b664741a0c0889c7ef9f9ffbfa8d47eddb53a50abd/idna-3.12-py3-none-any.whl", hash = "sha256:60ffaa1858fac94c9c124728c24fcde8160f3fb4a7f79aa8cdd33a9d1af60a67", size = 68634, upload-time = "2026-04-21T13:32:47.403Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] -name = "imageio" -version = "2.37.3" +name = "huggingface-hub" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/8f/999e4dda11c6187c78f090eac00895a47e11a0049308f07579bcb7aa3aa2/huggingface_hub-1.23.0.tar.gz", hash = "sha256:c04997fb8bbdace1e57b7703d30ed7678af51f70d00d241819ff411b92ae9a88", size = 919163, upload-time = "2026-07-09T14:49:32.315Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, -] - -[package.optional-dependencies] -ffmpeg = [ - { name = "imageio-ffmpeg" }, - { name = "psutil" }, + { url = "https://files.pythonhosted.org/packages/f1/ce/13b2ba57838b8db1e6bd033c1b21ce0b9f6153b87d4e4939f77074e41eb0/huggingface_hub-1.23.0-py3-none-any.whl", hash = "sha256:b1d604788f5adc7f0eb246e03e0ec19011ca06e38400218c347dccc3dffa64a2", size = 770336, upload-time = "2026-07-09T14:49:30.597Z" }, ] [[package]] -name = "imageio-ffmpeg" -version = "0.6.0" +name = "idna" +version = "3.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/bd/c3343c721f2a1b0c9fc71c1aebf1966a3b7f08c2eea8ed5437a2865611d6/imageio_ffmpeg-0.6.0.tar.gz", hash = "sha256:e2556bed8e005564a9f925bb7afa4002d82770d6b08825078b7697ab88ba1755", size = 25210, upload-time = "2025-01-16T21:34:32.747Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/58/87ef68ac83f4c7690961bce288fd8e382bc5f1513860fc7f90a9c1c1c6bf/imageio_ffmpeg-0.6.0-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.whl", hash = "sha256:9d2baaf867088508d4a3458e61eeb30e945c4ad8016025545f66c4b5aaef0a61", size = 24932969, upload-time = "2025-01-16T21:34:20.464Z" }, - { url = "https://files.pythonhosted.org/packages/40/5c/f3d8a657d362cc93b81aab8feda487317da5b5d31c0e1fdfd5e986e55d17/imageio_ffmpeg-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b1ae3173414b5fc5f538a726c4e48ea97edc0d2cdc11f103afee655c463fa742", size = 21113891, upload-time = "2025-01-16T21:34:00.277Z" }, - { url = "https://files.pythonhosted.org/packages/33/e7/1925bfbc563c39c1d2e82501d8372734a5c725e53ac3b31b4c2d081e895b/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1d47bebd83d2c5fc770720d211855f208af8a596c82d17730aa51e815cdee6dc", size = 25632706, upload-time = "2025-01-16T21:33:53.475Z" }, - { url = "https://files.pythonhosted.org/packages/a0/2d/43c8522a2038e9d0e7dbdf3a61195ecc31ca576fb1527a528c877e87d973/imageio_ffmpeg-0.6.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c7e46fcec401dd990405049d2e2f475e2b397779df2519b544b8aab515195282", size = 29498237, upload-time = "2025-01-16T21:34:13.726Z" }, - { url = "https://files.pythonhosted.org/packages/a0/13/59da54728351883c3c1d9fca1710ab8eee82c7beba585df8f25ca925f08f/imageio_ffmpeg-0.6.0-py3-none-win32.whl", hash = "sha256:196faa79366b4a82f95c0f4053191d2013f4714a715780f0ad2a68ff37483cc2", size = 19652251, upload-time = "2025-01-16T21:34:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, + { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, ] [[package]] @@ -756,19 +499,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] -[[package]] -name = "inquirerpy" -version = "0.3.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pfzy" }, - { name = "prompt-toolkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/73/7570847b9da026e07053da3bbe2ac7ea6cde6bb2cbd3c7a5a950fa0ae40b/InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e", size = 44431, upload-time = "2022-06-27T23:11:20.598Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/ff/3b59672c47c6284e8005b42e84ceba13864aa0f39f067c973d1af02f5d91/InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4", size = 67677, upload-time = "2022-06-27T23:11:17.723Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -795,36 +525,40 @@ wheels = [ [[package]] name = "lerobot" -version = "0.4.0" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "accelerate" }, - { name = "av" }, { name = "cmake" }, - { name = "datasets" }, - { name = "deepdiff" }, - { name = "diffusers" }, { name = "draccus" }, { name = "einops" }, { name = "gymnasium" }, - { name = "huggingface-hub", extra = ["cli", "hf-transfer"] }, - { name = "imageio", extra = ["ffmpeg"] }, - { name = "jsonlines" }, + { name = "huggingface-hub" }, + { name = "numpy" }, { name = "opencv-python-headless" }, { name = "packaging" }, - { name = "pynput" }, - { name = "pyserial" }, - { name = "rerun-sdk", extra = ["dataloader", "dataplatform", "tracing"] }, + { name = "pillow" }, + { name = "requests" }, + { name = "safetensors" }, { name = "setuptools" }, { name = "termcolor" }, { name = "torch" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, { name = "torchvision" }, - { name = "wandb" }, + { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/51/f125de006b655a23a3326e96bed28678751b4c5239664350ea11e5c59948/lerobot-0.4.0.tar.gz", hash = "sha256:8e464a33825b343209d9a13646da1f05f74fc8f0174db976f4b232bc6950ab6a", size = 558414, upload-time = "2025-10-23T18:38:31.216Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/18/b2999bbb52399d404ddcf05645536d3902bd65575ba0ad6bb7bef990a723/lerobot-0.6.0.tar.gz", hash = "sha256:6cad660816fdb72570ea7345ecb23bf0f74d36324e2d7c816a260d31a0c29186", size = 1367952, upload-time = "2026-07-06T10:42:05.281Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/76/187b2aae3ceed15370a801206e78a491e80ff27cd4bad6416983c2335f18/lerobot-0.4.0-py3-none-any.whl", hash = "sha256:69d407fe7e3aad23e2bbf1e89466e514b31efab539114da4d8811aabf6df86b1", size = 735079, upload-time = "2025-10-23T18:38:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/5d/20/9a96311c19e9d256e65584ca83c49c5782d0f204836e84ceeb420d4d493e/lerobot-0.6.0-py3-none-any.whl", hash = "sha256:b38a564fbc441d98380576863bf68635dde5fc2c42ddc2a39d0486640dc9e9a8", size = 1743768, upload-time = "2026-07-06T10:42:03.165Z" }, +] + +[package.optional-dependencies] +dataset = [ + { name = "av" }, + { name = "datasets" }, + { name = "jsonlines" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "torchcodec", version = "0.5", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "torchcodec", version = "0.11.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or sys_platform == 'win32'" }, ] [[package]] @@ -833,31 +567,6 @@ version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/4a/c64265d71b84030174ff3ac2cd16d8b664072afab8c41fccd8e2ee5a6f8d/librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443", size = 67529, upload-time = "2026-04-09T16:04:27.373Z" }, - { url = "https://files.pythonhosted.org/packages/23/b1/30ca0b3a8bdac209a00145c66cf42e5e7da2cc056ffc6ebc5c7b430ddd34/librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c", size = 70248, upload-time = "2026-04-09T16:04:28.758Z" }, - { url = "https://files.pythonhosted.org/packages/fa/fc/c6018dc181478d6ac5aa24a5846b8185101eb90894346db239eb3ea53209/librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e", size = 202184, upload-time = "2026-04-09T16:04:29.893Z" }, - { url = "https://files.pythonhosted.org/packages/bf/58/d69629f002203370ef41ea69ff71c49a2c618aec39b226ff49986ecd8623/librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285", size = 212926, upload-time = "2026-04-09T16:04:31.126Z" }, - { url = "https://files.pythonhosted.org/packages/cc/55/01d859f57824e42bd02465c77bec31fa5ef9d8c2bcee702ccf8ef1b9f508/librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2", size = 225664, upload-time = "2026-04-09T16:04:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/9b/02/32f63ad0ef085a94a70315291efe1151a48b9947af12261882f8445b2a30/librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce", size = 219534, upload-time = "2026-04-09T16:04:33.667Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5a/9d77111a183c885acf3b3b6e4c00f5b5b07b5817028226499a55f1fedc59/librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f", size = 227322, upload-time = "2026-04-09T16:04:34.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/05d700c93063753e12ab230b972002a3f8f3b9c95d8a980c2f646c8b6963/librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236", size = 223407, upload-time = "2026-04-09T16:04:36.22Z" }, - { url = "https://files.pythonhosted.org/packages/c0/26/26c3124823c67c987456977c683da9a27cc874befc194ddcead5f9988425/librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38", size = 221302, upload-time = "2026-04-09T16:04:37.62Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/c7cc2be5cf4ff7b017d948a789256288cb33a517687ff1995e72a7eea79f/librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b", size = 243893, upload-time = "2026-04-09T16:04:38.909Z" }, - { url = "https://files.pythonhosted.org/packages/62/d3/da553d37417a337d12660450535d5fd51373caffbedf6962173c87867246/librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774", size = 55375, upload-time = "2026-04-09T16:04:40.148Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5a/46fa357bab8311b6442a83471591f2f9e5b15ecc1d2121a43725e0c529b8/librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8", size = 62581, upload-time = "2026-04-09T16:04:41.452Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, - { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, - { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, - { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, - { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, - { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, - { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, - { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, - { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, - { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, @@ -879,28 +588,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -936,47 +623,8 @@ wheels = [ name = "multidict" version = "6.7.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, - { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, - { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, - { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, - { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, - { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, - { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, - { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, - { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, - { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, - { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, - { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, - { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, @@ -1007,8 +655,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1", size = 1772603, upload-time = "2024-01-28T18:52:34.85Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/76/6e712a2623d146d314f17598df5de7224c85c0060ef63fd95cc15a25b3fa/multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee", size = 134980, upload-time = "2024-01-28T18:52:15.731Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ab/1e6e8009e380e22254ff539ebe117861e5bdb3bff1fc977920972237c6c7/multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec", size = 134982, upload-time = "2024-01-28T18:52:17.783Z" }, { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824, upload-time = "2024-01-28T18:52:26.062Z" }, { url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a", size = 143519, upload-time = "2024-01-28T18:52:28.115Z" }, { url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e", size = 146741, upload-time = "2024-01-28T18:52:29.395Z" }, @@ -1024,23 +670,10 @@ dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, @@ -1059,39 +692,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "networkx" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, -] - [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", -] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, @@ -1101,34 +705,8 @@ wheels = [ name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", -] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, @@ -1139,61 +717,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - -[[package]] -name = "numpy" -version = "2.4.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, - { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, - { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, - { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, - { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, - { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, - { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, - { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, - { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, - { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, ] [[package]] @@ -1235,7 +758,7 @@ name = "nvidia-cudnn-cu12" version = "9.5.1.17" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386, upload-time = "2024-10-25T19:54:26.39Z" }, @@ -1246,7 +769,7 @@ name = "nvidia-cufft-cu12" version = "11.3.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632, upload-time = "2024-11-20T17:41:32.357Z" }, @@ -1275,9 +798,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790, upload-time = "2024-11-20T17:43:43.211Z" }, @@ -1289,7 +812,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367, upload-time = "2024-11-20T17:44:54.824Z" }, @@ -1334,8 +857,7 @@ name = "opencv-python-headless" version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } wheels = [ @@ -1429,15 +951,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/6c/5e86fa1759a525ef91c2d8b79d668574760ff3f900d114297765eb8786cb/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489", size = 231619, upload-time = "2026-04-09T14:38:32.394Z" }, ] -[[package]] -name = "orderly-set" -version = "5.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -1451,34 +964,14 @@ wheels = [ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux'", -] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, - { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, - { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, @@ -1488,49 +981,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, ] -[[package]] -name = "pandas" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, - { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, - { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, - { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, - { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, - { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, -] - [[package]] name = "pathspec" version = "1.0.4" @@ -1540,43 +990,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] -[[package]] -name = "pfzy" -version = "0.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/5a/32b50c077c86bfccc7bed4881c5a2b823518f5450a30e639db5d3711952e/pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1", size = 8396, upload-time = "2022-01-28T02:26:17.946Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537, upload-time = "2022-01-28T02:26:16.047Z" }, -] - [[package]] name = "pillow" version = "12.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, - { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, @@ -1588,34 +1007,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.9.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] @@ -1624,36 +1015,6 @@ version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, - { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, - { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, - { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, - { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, - { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, - { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, @@ -1705,215 +1066,17 @@ wheels = [ [[package]] name = "pyarrow" -version = "24.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", size = 35076681, upload-time = "2026-04-21T08:51:46.845Z" }, - { url = "https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", size = 36684260, upload-time = "2026-04-21T08:51:53.642Z" }, - { url = "https://files.pythonhosted.org/packages/57/02/9b9320e673dd8a99411fac78690f3df92f6dd6f59754c750110bca66d64e/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", size = 45698566, upload-time = "2026-04-21T10:46:02.133Z" }, - { url = "https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", size = 48835562, upload-time = "2026-04-21T10:46:10.278Z" }, - { url = "https://files.pythonhosted.org/packages/a5/63/097510448e47e4091faa41c43ba92f97cecaab8f4535b56a3d149578f634/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", size = 49394997, upload-time = "2026-04-21T10:46:18.08Z" }, - { url = "https://files.pythonhosted.org/packages/60/6b/c047d6222ab279024a062742d1807e2fbaf27bba88a98637299ff47b9236/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", size = 51911424, upload-time = "2026-04-21T10:46:25.347Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ba/464cc70761c2a525d97ebd84e21c31ebd47f3ef4bdcee117009f51c46f24/pyarrow-24.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", size = 27251730, upload-time = "2026-04-21T10:46:30.913Z" }, - { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, - { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, - { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, - { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, - { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, -] - -[[package]] -name = "pydantic" -version = "2.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/98/b50eb9a411e87483b5c65dba4fa430a06bac4234d3403a40e5a9905ebcd0/pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", size = 2108971, upload-time = "2026-04-20T14:43:51.945Z" }, - { url = "https://files.pythonhosted.org/packages/08/4b/f364b9d161718ff2217160a4b5d41ce38de60aed91c3689ebffa1c939d23/pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", size = 1949588, upload-time = "2026-04-20T14:44:10.386Z" }, - { url = "https://files.pythonhosted.org/packages/8f/8b/30bd03ee83b2f5e29f5ba8e647ab3c456bf56f2ec72fdbcc0215484a0854/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", size = 1975986, upload-time = "2026-04-20T14:43:57.106Z" }, - { url = "https://files.pythonhosted.org/packages/3c/54/13ccf954d84ec275d5d023d5786e4aa48840bc9f161f2838dc98e1153518/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", size = 2055830, upload-time = "2026-04-20T14:44:15.499Z" }, - { url = "https://files.pythonhosted.org/packages/be/0e/65f38125e660fdbd72aa858e7dfae893645cfa0e7b13d333e174a367cd23/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", size = 2222340, upload-time = "2026-04-20T14:41:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/d1/88/f3ab7739efe0e7e80777dbb84c59eb98518e3f57ea433206194c2e425272/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", size = 2280727, upload-time = "2026-04-20T14:41:30.461Z" }, - { url = "https://files.pythonhosted.org/packages/2a/6d/c228219080817bec4982f9531cadb18da6aaa770fdeb114f49c237ac2c9f/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", size = 2092158, upload-time = "2026-04-20T14:44:07.305Z" }, - { url = "https://files.pythonhosted.org/packages/0f/b1/525a16711e7c6d61635fac3b0bd54600b5c5d9f60c6fc5aaab26b64a2297/pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", size = 2116626, upload-time = "2026-04-20T14:42:34.118Z" }, - { url = "https://files.pythonhosted.org/packages/ef/7c/17d30673351439a6951bf54f564cf2443ab00ae264ec9df00e2efd710eb5/pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", size = 2160691, upload-time = "2026-04-20T14:41:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/86/66/af8adbcbc0886ead7f1a116606a534d75a307e71e6e08226000d51b880d2/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", size = 2182543, upload-time = "2026-04-20T14:40:48.886Z" }, - { url = "https://files.pythonhosted.org/packages/b0/37/6de71e0f54c54a4190010f57deb749e1ddf75c568ada3b1320b70067f121/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", size = 2324513, upload-time = "2026-04-20T14:42:36.121Z" }, - { url = "https://files.pythonhosted.org/packages/51/b1/9fc74ce94f603d5ef59ff258ca9c2c8fb902fb548d340a96f77f4d1c3b7f/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", size = 2361853, upload-time = "2026-04-20T14:43:24.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/d0/4c652fc592db35f100279ee751d5a145aca1b9a7984b9684ba7c1b5b0535/pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", size = 1980465, upload-time = "2026-04-20T14:44:46.239Z" }, - { url = "https://files.pythonhosted.org/packages/27/b8/a920453c38afbe1f355e1ea0b0d94a0a3e0b0879d32d793108755fa171d5/pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", size = 2073884, upload-time = "2026-04-20T14:43:01.201Z" }, - { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, - { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, - { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, - { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, - { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, - { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, - { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, - { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, - { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, - { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, - { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, - { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, - { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, - { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, - { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, - { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, - { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, - { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, - { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, - { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, - { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, - { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, - { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, - { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, - { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, - { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, - { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, -] - -[[package]] -name = "pynput" -version = "1.8.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "evdev", marker = "'linux' in sys_platform" }, - { name = "pyobjc-framework-applicationservices", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, - { name = "python-xlib", marker = "'linux' in sys_platform" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/c3/dccf44c68225046df5324db0cc7d563a560635355b3e5f1d249468268a6f/pynput-1.8.1.tar.gz", hash = "sha256:70d7c8373ee98911004a7c938742242840a5628c004573d84ba849d4601df81e", size = 82289, upload-time = "2025-03-17T17:12:01.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/4f/ac3fa906ae8a375a536b12794128c5efacade9eaa917a35dfd27ce0c7400/pynput-1.8.1-py2.py3-none-any.whl", hash = "sha256:42dfcf27404459ca16ca889c8fb8ffe42a9fe54f722fd1a3e130728e59e768d2", size = 91693, upload-time = "2025-03-17T17:12:00.094Z" }, -] - -[[package]] -name = "pyobjc-core" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/bf/3dbb1783388da54e650f8a6b88bde03c101d9ba93dfe8ab1b1873f1cd999/pyobjc_core-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93418e79c1655f66b4352168f8c85c942707cb1d3ea13a1da3e6f6a143bacda7", size = 676748, upload-time = "2025-11-14T09:30:50.023Z" }, - { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, - { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, -] - -[[package]] -name = "pyobjc-framework-applicationservices" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-coretext", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/9d/3cf36e7b08832e71f5d48ddfa1047865cf2dfc53df8c0f2a82843ea9507a/pyobjc_framework_applicationservices-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4fd1b008757182b9e2603a63c6ffa930cc412fab47294ec64260ab3f8ec695d", size = 32791, upload-time = "2025-11-14T09:36:05.576Z" }, - { url = "https://files.pythonhosted.org/packages/17/86/d07eff705ff909a0ffa96d14fc14026e9fc9dd716233648c53dfd5056b8e/pyobjc_framework_applicationservices-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bdddd492eeac6d14ff2f5bd342aba29e30dffa72a2d358c08444da22129890e2", size = 32784, upload-time = "2025-11-14T09:36:08.755Z" }, - { url = "https://files.pythonhosted.org/packages/37/a7/55fa88def5c02732c4b747606ff1cbce6e1f890734bbd00f5596b21eaa02/pyobjc_framework_applicationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8f6e2fb3b3e9214ab4864ef04eee18f592b46a986c86ea0113448b310520532", size = 32835, upload-time = "2025-11-14T09:36:11.855Z" }, -] - -[[package]] -name = "pyobjc-framework-cocoa" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/aa/2b2d7ec3ac4b112a605e9bd5c5e5e4fd31d60a8a4b610ab19cc4838aa92a/pyobjc_framework_cocoa-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9b880d3bdcd102809d704b6d8e14e31611443aa892d9f60e8491e457182fdd48", size = 383825, upload-time = "2025-11-14T09:40:28.354Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, - { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, -] - -[[package]] -name = "pyobjc-framework-coretext" -version = "12.1" +version = "23.0.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1c/ddecc72a672d681476c668bcedcfb8ade16383c028eac566ac7458fb91ef/pyobjc_framework_coretext-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1c8315dcef6699c2953461d97117fe81402f7c29cff36d2950dacce028a362fd", size = 29987, upload-time = "2025-11-14T09:46:58.028Z" }, - { url = "https://files.pythonhosted.org/packages/f0/81/7b8efc41e743adfa2d74b92dec263c91bcebfb188d2a8f5eea1886a195ff/pyobjc_framework_coretext-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f6742ba5b0bb7629c345e99eff928fbfd9e9d3d667421ac1a2a43bdb7ba9833", size = 29990, upload-time = "2025-11-14T09:47:01.206Z" }, - { url = "https://files.pythonhosted.org/packages/cd/0f/ddf45bf0e3ba4fbdc7772de4728fd97ffc34a0b5a15e1ab1115b202fe4ae/pyobjc_framework_coretext-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d246fa654bdbf43bae3969887d58f0b336c29b795ad55a54eb76397d0e62b93c", size = 30108, upload-time = "2025-11-14T09:47:04.228Z" }, -] - -[[package]] -name = "pyobjc-framework-quartz" -version = "12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/f4/50c42c84796886e4d360407fb629000bb68d843b2502c88318375441676f/pyobjc_framework_quartz-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c6f312ae79ef8b3019dcf4b3374c52035c7c7bc4a09a1748b61b041bb685a0ed", size = 217799, upload-time = "2025-11-14T09:59:32.62Z" }, - { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" }, -] - -[[package]] -name = "pyserial" -version = "3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, ] [[package]] @@ -1928,18 +1091,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] -[[package]] -name = "python-xlib" -version = "0.33" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, -] - [[package]] name = "pytz" version = "2026.1.post1" @@ -1955,24 +1106,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -1997,63 +1130,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/ca/6a2cc3a73170d10b5af1f1613baa2ed1f8f46f62dd0bfab2bffd2c2fe260/pyyaml_include-1.4.1-py3-none-any.whl", hash = "sha256:323c7f3a19c82fbc4d73abbaab7ef4f793e146a13383866831631b26ccc7fb00", size = 19079, upload-time = "2024-03-25T14:56:41.274Z" }, ] -[[package]] -name = "regex" -version = "2026.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/59/fd98f8fd54b3feaa76a855324c676c17668c5a1121ec91b7ec96b01bf865/regex-2026.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:74fa82dcc8143386c7c0392e18032009d1db715c25f4ba22d23dc2e04d02a20f", size = 489403, upload-time = "2026-04-03T20:52:39.742Z" }, - { url = "https://files.pythonhosted.org/packages/6c/64/d0f222f68e3579d50babf0e4fcc9c9639ef0587fecc00b15e1e46bfc32fa/regex-2026.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a85b620a388d6c9caa12189233109e236b3da3deffe4ff11b84ae84e218a274f", size = 291208, upload-time = "2026-04-03T20:52:42.943Z" }, - { url = "https://files.pythonhosted.org/packages/16/7f/3fab9709b0b0060ba81a04b8a107b34147cd14b9c5551b772154d6505504/regex-2026.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2895506ebe32cc63eeed8f80e6eae453171cfccccab35b70dc3129abec35a5b8", size = 289214, upload-time = "2026-04-03T20:52:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/14/bc/f5dcf04fd462139dcd75495c02eee22032ef741cfa151386a39c3f5fc9b5/regex-2026.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6780f008ee81381c737634e75c24e5a6569cc883c4f8e37a37917ee79efcafd9", size = 785505, upload-time = "2026-04-03T20:52:46.35Z" }, - { url = "https://files.pythonhosted.org/packages/37/36/8a906e216d5b4de7ec3788c1d589b45db40c1c9580cd7b326835cfc976d4/regex-2026.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88e9b048345c613f253bea4645b2fe7e579782b82cac99b1daad81e29cc2ed8e", size = 852129, upload-time = "2026-04-03T20:52:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/a5/bb/bad2d79be0917a6ef31f5e0f161d9265cb56fd90a3ae1d2e8d991882a48b/regex-2026.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be061028481186ba62a0f4c5f1cc1e3d5ab8bce70c89236ebe01023883bc903b", size = 899578, upload-time = "2026-04-03T20:52:50.61Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b9/7cd0ceb58cd99c70806241636640ae15b4a3fe62e22e9b99afa67a0d7965/regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2228c02b368d69b724c36e96d3d1da721561fb9cc7faa373d7bf65e07d75cb5", size = 793634, upload-time = "2026-04-03T20:52:53Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fb/c58e3ea40ed183806ccbac05c29a3e8c2f88c1d3a66ed27860d5cad7c62d/regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0540e5b733618a2f84e9cb3e812c8afa82e151ca8e19cf6c4e95c5a65198236f", size = 786210, upload-time = "2026-04-03T20:52:54.713Z" }, - { url = "https://files.pythonhosted.org/packages/54/a9/53790fc7a6c948a7be2bc7214fd9cabdd0d1ba561b0f401c91f4ff0357f0/regex-2026.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf9b1b2e692d4877880388934ac746c99552ce6bf40792a767fd42c8c99f136d", size = 769930, upload-time = "2026-04-03T20:52:56.825Z" }, - { url = "https://files.pythonhosted.org/packages/e3/3c/29ca44729191c79f5476538cd0fa04fa2553b3c45508519ecea4c7afa8f6/regex-2026.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:011bb48bffc1b46553ac704c975b3348717f4e4aa7a67522b51906f99da1820c", size = 774892, upload-time = "2026-04-03T20:52:58.934Z" }, - { url = "https://files.pythonhosted.org/packages/3e/db/6ae74ef8a4cfead341c367e4eed45f71fb1aaba35827a775eed4f1ba4f74/regex-2026.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8512fcdb43f1bf18582698a478b5ab73f9c1667a5b7548761329ef410cd0a760", size = 848816, upload-time = "2026-04-03T20:53:00.684Z" }, - { url = "https://files.pythonhosted.org/packages/53/9a/f7f2c1c6b610d7c6de1c3dc5951effd92c324b1fde761af2044b4721020f/regex-2026.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:867bddc63109a0276f5a31999e4c8e0eb7bbbad7d6166e28d969a2c1afeb97f9", size = 758363, upload-time = "2026-04-03T20:53:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/dd/55/e5386d393bbf8b43c8b084703a46d635e7b2bdc6e0f5909a2619ea1125f1/regex-2026.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1b9a00b83f3a40e09859c78920571dcb83293c8004079653dd22ec14bbfa98c7", size = 837122, upload-time = "2026-04-03T20:53:03.727Z" }, - { url = "https://files.pythonhosted.org/packages/01/da/cc78710ea2e60b10bacfcc9beb18c67514200ab03597b3b2b319995785c2/regex-2026.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e355be718caf838aa089870259cf1776dc2a4aa980514af9d02c59544d9a8b22", size = 782140, upload-time = "2026-04-03T20:53:05.608Z" }, - { url = "https://files.pythonhosted.org/packages/a2/5f/c7bcba41529105d6c2ca7080ecab7184cd00bee2e1ad1fdea80e618704ea/regex-2026.4.4-cp310-cp310-win32.whl", hash = "sha256:33bfda9684646d323414df7abe5692c61d297dbb0530b28ec66442e768813c59", size = 266225, upload-time = "2026-04-03T20:53:07.342Z" }, - { url = "https://files.pythonhosted.org/packages/eb/26/a745729c2c49354ec4f4bce168f29da932ca01b4758227686cc16c7dde1b/regex-2026.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:0709f22a56798457ae317bcce42aacee33c680068a8f14097430d9f9ba364bee", size = 278393, upload-time = "2026-04-03T20:53:08.65Z" }, - { url = "https://files.pythonhosted.org/packages/87/8b/4327eeb9dbb4b098ebecaf02e9f82b79b6077beeb54c43d9a0660cf7c44c/regex-2026.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:ee9627de8587c1a22201cb16d0296ab92b4df5cdcb5349f4e9744d61db7c7c98", size = 270470, upload-time = "2026-04-03T20:53:10.018Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7a/617356cbecdb452812a5d42f720d6d5096b360d4a4c1073af700ea140ad2/regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6", size = 489415, upload-time = "2026-04-03T20:53:11.645Z" }, - { url = "https://files.pythonhosted.org/packages/20/e6/bf057227144d02e3ba758b66649e87531d744dda5f3254f48660f18ae9d8/regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87", size = 291205, upload-time = "2026-04-03T20:53:13.289Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3b/637181b787dd1a820ba1c712cee2b4144cd84a32dc776ca067b12b2d70c8/regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8", size = 289225, upload-time = "2026-04-03T20:53:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/05/21/bac05d806ed02cd4b39d9c8e5b5f9a2998c94c3a351b7792e80671fa5315/regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada", size = 792434, upload-time = "2026-04-03T20:53:17.414Z" }, - { url = "https://files.pythonhosted.org/packages/d9/17/c65d1d8ae90b772d5758eb4014e1e011bb2db353fc4455432e6cc9100df7/regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d", size = 861730, upload-time = "2026-04-03T20:53:18.903Z" }, - { url = "https://files.pythonhosted.org/packages/ad/64/933321aa082a2c6ee2785f22776143ba89840189c20d3b6b1d12b6aae16b/regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87", size = 906495, upload-time = "2026-04-03T20:53:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/01/ea/4c8d306e9c36ac22417336b1e02e7b358152c34dc379673f2d331143725f/regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4", size = 799810, upload-time = "2026-04-03T20:53:22.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/ce/7605048f00e1379eba89d610c7d644d8f695dc9b26d3b6ecfa3132b872ff/regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86", size = 774242, upload-time = "2026-04-03T20:53:25.015Z" }, - { url = "https://files.pythonhosted.org/packages/e9/77/283e0d5023fde22cd9e86190d6d9beb21590a452b195ffe00274de470691/regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59", size = 781257, upload-time = "2026-04-03T20:53:26.918Z" }, - { url = "https://files.pythonhosted.org/packages/8b/fb/7f3b772be101373c8626ed34c5d727dcbb8abd42a7b1219bc25fd9a3cc04/regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453", size = 854490, upload-time = "2026-04-03T20:53:29.065Z" }, - { url = "https://files.pythonhosted.org/packages/85/30/56547b80f34f4dd2986e1cdd63b1712932f63b6c4ce2f79c50a6cd79d1c2/regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80", size = 763544, upload-time = "2026-04-03T20:53:30.917Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2f/ce060fdfea8eff34a8997603532e44cdb7d1f35e3bc253612a8707a90538/regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b", size = 844442, upload-time = "2026-04-03T20:53:32.463Z" }, - { url = "https://files.pythonhosted.org/packages/e5/44/810cb113096a1dacbe82789fbfab2823f79d19b7f1271acecb7009ba9b88/regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f", size = 789162, upload-time = "2026-04-03T20:53:34.039Z" }, - { url = "https://files.pythonhosted.org/packages/20/96/9647dd7f2ecf6d9ce1fb04dfdb66910d094e10d8fe53e9c15096d8aa0bd2/regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351", size = 266227, upload-time = "2026-04-03T20:53:35.601Z" }, - { url = "https://files.pythonhosted.org/packages/33/80/74e13262460530c3097ff343a17de9a34d040a5dc4de9cf3a8241faab51c/regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735", size = 278399, upload-time = "2026-04-03T20:53:37.021Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/39f19f47f19dcefa3403f09d13562ca1c0fd07ab54db2bc03148f3f6b46a/regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54", size = 270473, upload-time = "2026-04-03T20:53:38.633Z" }, - { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, - { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, - { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, - { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, - { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, - { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, - { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, - { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, - { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, - { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, - { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, - { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, -] - [[package]] name = "requests" version = "2.33.1" @@ -2069,41 +1145,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] -[[package]] -name = "rerun-dev-fixup" -version = "0.1.0" -source = { directory = "../../../rerun_py/rerun_dev_fixup" } -dependencies = [ - { name = "sitecustomize-entrypoints" }, -] - -[package.metadata] -requires-dist = [{ name = "sitecustomize-entrypoints" }] - [[package]] name = "rerun-sdk" source = { editable = "../../../rerun_py" } dependencies = [ { name = "attrs" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "pillow" }, + { name = "psutil" }, { name = "pyarrow" }, { name = "typing-extensions" }, ] [package.optional-dependencies] +catalog = [ + { name = "datafusion" }, + { name = "pandas" }, +] dataloader = [ { name = "av" }, { name = "pillow" }, { name = "torch" }, { name = "torchvision" }, ] -dataplatform = [ - { name = "datafusion" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] tracing = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-grpc" }, @@ -2115,10 +1179,11 @@ requires-dist = [ { name = "attrs", specifier = ">=23.1.0" }, { name = "av", marker = "extra == 'dataloader'" }, { name = "av", marker = "extra == 'tests'", specifier = ">=14.2.0" }, - { name = "datafusion", marker = "extra == 'all'", specifier = "==52.3.0" }, - { name = "datafusion", marker = "extra == 'datafusion'", specifier = "==52.3.0" }, - { name = "datafusion", marker = "extra == 'dataplatform'", specifier = "==52.3.0" }, - { name = "datafusion", marker = "extra == 'tests'", specifier = "==52.3.0" }, + { name = "datafusion", marker = "extra == 'all'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'catalog'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'datafusion'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'dataplatform'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'tests'", specifier = "==53.0.0" }, { name = "inline-snapshot", marker = "extra == 'tests'", specifier = "==0.31.1" }, { name = "numpy", specifier = ">=2" }, { name = "opencv-python", marker = "extra == 'tests'", specifier = ">4.6" }, @@ -2126,14 +1191,16 @@ requires-dist = [ { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "extra == 'tracing'" }, { name = "opentelemetry-sdk", marker = "extra == 'tracing'" }, { name = "pandas", marker = "extra == 'all'", specifier = ">=2" }, + { name = "pandas", marker = "extra == 'catalog'", specifier = ">=2" }, { name = "pandas", marker = "extra == 'datafusion'", specifier = ">=2" }, { name = "pandas", marker = "extra == 'dataplatform'", specifier = ">=2" }, { name = "pandas", marker = "extra == 'tests'", specifier = ">=2" }, { name = "pillow", specifier = ">=8.0.0" }, { name = "pillow", marker = "extra == 'dataloader'", specifier = ">=8.0.0" }, { name = "polars", marker = "extra == 'tests'", specifier = "==1.36.1" }, + { name = "psutil", specifier = ">=7.0" }, { name = "pyarrow", specifier = ">=18.0.0" }, - { name = "pytest", marker = "extra == 'tests'", specifier = "==8.4.2" }, + { name = "pytest", marker = "extra == 'tests'", specifier = "==9.0.3" }, { name = "rerun-notebook", marker = "extra == 'all'", editable = "../../../rerun_notebook" }, { name = "rerun-notebook", marker = "extra == 'notebook'", editable = "../../../rerun_notebook" }, { name = "semver", marker = "extra == 'tests'", specifier = ">=3.0,<3.1" }, @@ -2145,7 +1212,7 @@ requires-dist = [ { name = "torchvision", marker = "extra == 'tests'" }, { name = "typing-extensions", specifier = ">=4.5" }, ] -provides-extras = ["all", "datafusion", "dataloader", "dataplatform", "notebook", "tests", "tracing"] +provides-extras = ["all", "catalog", "datafusion", "dataloader", "dataplatform", "notebook", "tests", "tracing"] [[package]] name = "safetensors" @@ -2167,23 +1234,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, - { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, -] - -[[package]] -name = "sentry-sdk" -version = "2.58.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" }, ] [[package]] @@ -2195,15 +1245,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, ] -[[package]] -name = "sitecustomize-entrypoints" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/88/c135ebdef2af797042a55fba89c79ca05b601f4dc36157a67a224d8090ca/sitecustomize-entrypoints-1.1.0.tar.gz", hash = "sha256:8bf9c9f3572e1709331f435a9bffd465320be77d65adda0f82b504594dc507b4", size = 24445, upload-time = "2023-04-10T16:59:55.551Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/11/412160ea4a304ee65babef67254833408d0ac7145334406bb1a8bff6f3cb/sitecustomize_entrypoints-1.1.0-py3-none-any.whl", hash = "sha256:ca80648c58d9ebcad31de46a4726213ae322fe829b285a0105da0eae3f74cb53", size = 25279, upload-time = "2023-04-10T16:59:56.824Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -2213,15 +1254,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "smmap" -version = "5.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, -] - [[package]] name = "sympy" version = "1.14.0" @@ -2252,33 +1284,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, -] - [[package]] name = "torch" version = "2.7.1" @@ -2287,8 +1292,7 @@ dependencies = [ { name = "filelock" }, { name = "fsspec" }, { name = "jinja2" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, @@ -2303,20 +1307,12 @@ dependencies = [ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "setuptools" }, { name = "sympy" }, { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/27/2e06cb52adf89fe6e020963529d17ed51532fc73c1e6d1b18420ef03338c/torch-2.7.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a103b5d782af5bd119b81dbcc7ffc6fa09904c423ff8db397a1e6ea8fd71508f", size = 99089441, upload-time = "2025-06-04T17:38:48.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/0a5b3aee977596459ec45be2220370fde8e017f651fecc40522fd478cb1e/torch-2.7.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:fe955951bdf32d182ee8ead6c3186ad54781492bf03d547d31771a01b3d6fb7d", size = 821154516, upload-time = "2025-06-04T17:36:28.556Z" }, - { url = "https://files.pythonhosted.org/packages/f9/91/3d709cfc5e15995fb3fe7a6b564ce42280d3a55676dad672205e94f34ac9/torch-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:885453d6fba67d9991132143bf7fa06b79b24352f4506fd4d10b309f53454162", size = 216093147, upload-time = "2025-06-04T17:39:38.132Z" }, - { url = "https://files.pythonhosted.org/packages/92/f6/5da3918414e07da9866ecb9330fe6ffdebe15cb9a4c5ada7d4b6e0a6654d/torch-2.7.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:d72acfdb86cee2a32c0ce0101606f3758f0d8bb5f8f31e7920dc2809e963aa7c", size = 68630914, upload-time = "2025-06-04T17:39:31.162Z" }, - { url = "https://files.pythonhosted.org/packages/11/56/2eae3494e3d375533034a8e8cf0ba163363e996d85f0629441fa9d9843fe/torch-2.7.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:236f501f2e383f1cb861337bdf057712182f910f10aeaf509065d54d339e49b2", size = 99093039, upload-time = "2025-06-04T17:39:06.963Z" }, - { url = "https://files.pythonhosted.org/packages/e5/94/34b80bd172d0072c9979708ccd279c2da2f55c3ef318eceec276ab9544a4/torch-2.7.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:06eea61f859436622e78dd0cdd51dbc8f8c6d76917a9cf0555a333f9eac31ec1", size = 821174704, upload-time = "2025-06-04T17:37:03.799Z" }, - { url = "https://files.pythonhosted.org/packages/50/9e/acf04ff375b0b49a45511c55d188bcea5c942da2aaf293096676110086d1/torch-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:8273145a2e0a3c6f9fd2ac36762d6ee89c26d430e612b95a99885df083b04e52", size = 216095937, upload-time = "2025-06-04T17:39:24.83Z" }, - { url = "https://files.pythonhosted.org/packages/5b/2b/d36d57c66ff031f93b4fa432e86802f84991477e522adcdffd314454326b/torch-2.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:aea4fc1bf433d12843eb2c6b2204861f43d8364597697074c8d38ae2507f8730", size = 68640034, upload-time = "2025-06-04T17:39:17.989Z" }, { url = "https://files.pythonhosted.org/packages/87/93/fb505a5022a2e908d81fe9a5e0aa84c86c0d5f408173be71c6018836f34e/torch-2.7.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ea1e518df4c9de73af7e8a720770f3628e7f667280bce2be7a16292697e3fa", size = 98948276, upload-time = "2025-06-04T17:39:12.852Z" }, { url = "https://files.pythonhosted.org/packages/56/7e/67c3fe2b8c33f40af06326a3d6ae7776b3e3a01daa8f71d125d78594d874/torch-2.7.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c33360cfc2edd976c2633b3b66c769bdcbbf0e0b6550606d188431c81e7dd1fc", size = 821025792, upload-time = "2025-06-04T17:34:58.747Z" }, { url = "https://files.pythonhosted.org/packages/a1/37/a37495502bc7a23bf34f89584fa5a78e25bae7b8da513bc1b8f97afb7009/torch-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:d8bf6e1856ddd1807e79dc57e54d3335f2b62e6f316ed13ed3ecfe1fc1df3d8b", size = 216050349, upload-time = "2025-06-04T17:38:59.709Z" }, @@ -2327,34 +1323,39 @@ wheels = [ name = "torchcodec" version = "0.5" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "platform_machine == 'arm64' and sys_platform == 'darwin'", + "(platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')", +] wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/b5/004a300528a1c7904a0bd41609fa16d7b3a1a9043061922911e9c553c628/torchcodec-0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f71cb7918a56196dd04fb456e259c1db83516d52320387fed8b16df5e95bc36d", size = 2810828, upload-time = "2025-07-23T17:43:51.435Z" }, - { url = "https://files.pythonhosted.org/packages/52/6e/d12c327338d9561f2ef0896218f163e6635d4ae1d688f1f28664d729b1ab/torchcodec-0.5-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f760e49803d7767e10577d3d20d1a9dd0ad5adebd1ecbf7ce941176e8c388f54", size = 1366065, upload-time = "2025-07-23T17:42:41.88Z" }, - { url = "https://files.pythonhosted.org/packages/84/9f/8e0ae96b2560050eb1a8780dabbe3d192cf99e62a5ce2a128684bfd6f5d6/torchcodec-0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d582981f251979d4432a1606d2bfbc8d56b98a701957afbaf4cafe7569b19c68", size = 3307718, upload-time = "2025-07-23T17:43:52.805Z" }, - { url = "https://files.pythonhosted.org/packages/d3/a1/cc2b7f72104cbb5f60f5f277f470e9d0172bffb354f74b0e13a02a241165/torchcodec-0.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:00252c2a138c655851cc7aa6e7ad6455c6513c9b2f93cec74563cba83196be8a", size = 1374385, upload-time = "2025-07-23T17:42:43.219Z" }, { url = "https://files.pythonhosted.org/packages/02/52/23145c2a6580f0753104f257067db0cf68e1ba105408777ec54ce686c1f0/torchcodec-0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:221c1123eefe31d57a41fe85b020306f685b6823dc70c3f4648215038fbae52c", size = 3493067, upload-time = "2025-07-23T17:43:54.519Z" }, { url = "https://files.pythonhosted.org/packages/65/38/dfe8aa3a71c4eb0fd930d0c6db661cc65d1f220b2225433207ceb15733c4/torchcodec-0.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:310dfbfd7c56e283e0a9bd7cb4b5815cf1251a7bdeb93991b14311a1d144137d", size = 1370294, upload-time = "2025-07-23T17:42:44.455Z" }, ] +[[package]] +name = "torchcodec" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'win32'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", + "platform_machine == 'arm64' and sys_platform == 'linux'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/85/3b41034b0f1289423745f918ace2a1e1e86b9c578c2e2461b6afcbb5354a/torchcodec-0.11.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f1aee486a84247fcaa67870ac5005aa8d382a9839e91e476fa71b5b3d9fda9b7", size = 2397532, upload-time = "2026-04-14T18:24:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/82/48/683114a4ed6b59f76b6919532a5db0f4068787be26bab92cc18a1dfa6794/torchcodec-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:3fd2d10e0e0a5f455c1c87dc1380b3bd43b77dd5eeeaf479470643b1c04a2dd2", size = 1921066, upload-time = "2026-04-14T18:24:57.102Z" }, +] + [[package]] name = "torchvision" version = "0.22.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "pillow" }, { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/15/2c/7b67117b14c6cc84ae3126ca6981abfa3af2ac54eb5252b80d9475fb40df/torchvision-0.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b47d8369ee568c067795c0da0b4078f39a9dfea6f3bc1f3ac87530dfda1dd56", size = 1947825, upload-time = "2025-06-04T17:43:15.523Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9f/c4dcf1d232b75e28bc37e21209ab2458d6d60235e16163544ed693de54cb/torchvision-0.22.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:990de4d657a41ed71680cd8be2e98ebcab55371f30993dc9bd2e676441f7180e", size = 2512611, upload-time = "2025-06-04T17:43:03.951Z" }, - { url = "https://files.pythonhosted.org/packages/e2/99/db71d62d12628111d59147095527a0ab492bdfecfba718d174c04ae6c505/torchvision-0.22.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3347f690c2eed6d02aa0edfb9b01d321e7f7cf1051992d96d8d196c39b881d49", size = 7485668, upload-time = "2025-06-04T17:43:09.453Z" }, - { url = "https://files.pythonhosted.org/packages/32/ff/4a93a4623c3e5f97e8552af0f9f81d289dcf7f2ac71f1493f1c93a6b973d/torchvision-0.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:86ad938f5a6ca645f0d5fb19484b1762492c2188c0ffb05c602e9e9945b7b371", size = 1707961, upload-time = "2025-06-04T17:43:13.038Z" }, - { url = "https://files.pythonhosted.org/packages/f6/00/bdab236ef19da050290abc2b5203ff9945c84a1f2c7aab73e8e9c8c85669/torchvision-0.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4addf626e2b57fc22fd6d329cf1346d474497672e6af8383b7b5b636fba94a53", size = 1947827, upload-time = "2025-06-04T17:43:10.84Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d0/18f951b2be3cfe48c0027b349dcc6fde950e3dc95dd83e037e86f284f6fd/torchvision-0.22.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:8b4a53a6067d63adba0c52f2b8dd2290db649d642021674ee43c0c922f0c6a69", size = 2514021, upload-time = "2025-06-04T17:43:07.608Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1a/63eb241598b36d37a0221e10af357da34bd33402ccf5c0765e389642218a/torchvision-0.22.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b7866a3b326413e67724ac46f1ee594996735e10521ba9e6cdbe0fa3cd98c2f2", size = 7487300, upload-time = "2025-06-04T17:42:58.349Z" }, - { url = "https://files.pythonhosted.org/packages/e5/73/1b009b42fe4a7774ba19c23c26bb0f020d68525c417a348b166f1c56044f/torchvision-0.22.1-cp311-cp311-win_amd64.whl", hash = "sha256:bb3f6df6f8fd415ce38ec4fd338376ad40c62e86052d7fc706a0dd51efac1718", size = 1707989, upload-time = "2025-06-04T17:43:14.332Z" }, { url = "https://files.pythonhosted.org/packages/02/90/f4e99a5112dc221cf68a485e853cc3d9f3f1787cb950b895f3ea26d1ea98/torchvision-0.22.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:153f1790e505bd6da123e21eee6e83e2e155df05c0fe7d56347303067d8543c5", size = 1947827, upload-time = "2025-06-04T17:43:11.945Z" }, { url = "https://files.pythonhosted.org/packages/25/f6/53e65384cdbbe732cc2106bb04f7fb908487e4fb02ae4a1613ce6904a122/torchvision-0.22.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:964414eef19459d55a10e886e2fca50677550e243586d1678f65e3f6f6bac47a", size = 2514576, upload-time = "2025-06-04T17:43:02.707Z" }, { url = "https://files.pythonhosted.org/packages/17/8b/155f99042f9319bd7759536779b2a5b67cbd4f89c380854670850f89a2f4/torchvision-0.22.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:699c2d70d33951187f6ed910ea05720b9b4aaac1dcc1135f53162ce7d42481d3", size = 7485962, upload-time = "2025-06-04T17:42:43.606Z" }, @@ -2378,11 +1379,9 @@ name = "triton" version = "3.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "setuptools", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/a9/549e51e9b1b2c9b854fd761a1d23df0ba2fbc60bd0c13b489ffa518cfcb7/triton-3.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e", size = 155600257, upload-time = "2025-05-29T23:39:36.085Z" }, - { url = "https://files.pythonhosted.org/packages/21/2f/3e56ea7b58f80ff68899b1dbe810ff257c9d177d288c6b0f55bf2fe4eb50/triton-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b", size = 155689937, upload-time = "2025-05-29T23:39:44.182Z" }, { url = "https://files.pythonhosted.org/packages/24/5f/950fb373bf9c01ad4eb5a8cd5eaf32cdf9e238c02f9293557a2129b9c4ac/triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9999e83aba21e1a78c1f36f21bce621b77bcaa530277a50484a7cb4a822f6e43", size = 155669138, upload-time = "2025-05-29T23:39:51.771Z" }, ] @@ -2408,18 +1407,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, ] -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - [[package]] name = "tzdata" version = "2026.1" @@ -2431,49 +1418,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "wandb" -version = "0.21.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "gitpython" }, - { name = "packaging" }, - { name = "platformdirs" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sentry-sdk" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/a8/aaa3f3f8e410f34442466aac10b1891b3084d35b98aef59ebcb4c0efb941/wandb-0.21.4.tar.gz", hash = "sha256:b350d50973409658deb455010fafcfa81e6be3470232e316286319e839ffb67b", size = 40175929, upload-time = "2025-09-11T21:14:29.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/6b/3a8d9db18a4c4568599a8792c0c8b1f422d9864c7123e8301a9477fbf0ac/wandb-0.21.4-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:c681ef7adb09925251d8d995c58aa76ae86a46dbf8de3b67353ad99fdef232d5", size = 18845369, upload-time = "2025-09-11T21:14:02.879Z" }, - { url = "https://files.pythonhosted.org/packages/60/e0/d7d6818938ec6958c93d979f9a90ea3d06bdc41e130b30f8cd89ae03c245/wandb-0.21.4-py3-none-macosx_12_0_arm64.whl", hash = "sha256:d35acc65c10bb7ac55d1331f7b1b8ab761f368f7b051131515f081a56ea5febc", size = 18339122, upload-time = "2025-09-11T21:14:06.455Z" }, - { url = "https://files.pythonhosted.org/packages/13/29/9bb8ed4adf32bed30e4d5df74d956dd1e93b6fd4bbc29dbe84167c84804b/wandb-0.21.4-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:765e66b57b7be5f393ecebd9a9d2c382c9f979d19cdee4a3f118eaafed43fca1", size = 19081975, upload-time = "2025-09-11T21:14:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/30/6e/4aa33bc2c56b70c0116e73687c72c7a674f4072442633b3b23270d2215e3/wandb-0.21.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06127ec49245d12fdb3922c1eca1ab611cefc94adabeaaaba7b069707c516cba", size = 18161358, upload-time = "2025-09-11T21:14:12.092Z" }, - { url = "https://files.pythonhosted.org/packages/f7/56/d9f845ecfd5e078cf637cb29d8abe3350b8a174924c54086168783454a8f/wandb-0.21.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48d4f65f1be5f5a25b868695e09cdbfe481678220df349a8c2cbed3992fb497f", size = 19602680, upload-time = "2025-09-11T21:14:14.987Z" }, - { url = "https://files.pythonhosted.org/packages/68/ea/237a3c2b679a35e02e577c5bf844d6a221a7d32925ab8d5230529e9f2841/wandb-0.21.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ebd11f78351a3ca22caa1045146a6d2ad9e62fed6d0de2e67a0db5710d75103a", size = 18166392, upload-time = "2025-09-11T21:14:17.478Z" }, - { url = "https://files.pythonhosted.org/packages/12/e3/dbf2c575c79c99d94f16ce1a2cbbb2529d5029a76348c1ddac7e47f6873f/wandb-0.21.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:595b9e77591a805653e05db8b892805ee0a5317d147ef4976353e4f1cc16ebdc", size = 19678800, upload-time = "2025-09-11T21:14:20.264Z" }, - { url = "https://files.pythonhosted.org/packages/fa/eb/4ed04879d697772b8eb251c0e5af9a4ff7e2cc2b3fcd4b8eee91253ec2f1/wandb-0.21.4-py3-none-win32.whl", hash = "sha256:f9c86eb7eb7d40c6441533428188b1ae3205674e80c940792d850e2c1fe8d31e", size = 18738950, upload-time = "2025-09-11T21:14:23.08Z" }, - { url = "https://files.pythonhosted.org/packages/c3/4a/86c5e19600cb6a616a45f133c26826b46133499cd72d592772929d530ccd/wandb-0.21.4-py3-none-win_amd64.whl", hash = "sha256:2da3d5bb310a9f9fb7f680f4aef285348095a4cc6d1ce22b7343ba4e3fffcd84", size = 18738953, upload-time = "2025-09-11T21:14:25.539Z" }, -] - -[[package]] -name = "wcwidth" -version = "0.6.0" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] @@ -2482,36 +1431,6 @@ version = "3.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, - { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, - { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, - { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, - { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, - { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, - { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, - { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, - { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, - { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, - { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, @@ -2527,11 +1446,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, ] [[package]] @@ -2545,42 +1459,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, - { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, - { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, - { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, - { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, - { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, - { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, - { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, - { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, - { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, - { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, - { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, - { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, - { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, - { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, - { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, - { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, - { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, - { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, - { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, - { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, - { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, - { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, diff --git a/examples/python/dataset_hierarchy_demo.py b/examples/python/dataset_hierarchy_demo.py deleted file mode 100644 index 1cf2f6e4db82..000000000000 --- a/examples/python/dataset_hierarchy_demo.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Demo: hierarchical dataset names using ':' as separator.""" - -from __future__ import annotations - -import rerun as rr - -server = rr.server.Server() -client = server.client() - -# Create datasets with hierarchical names using '.' as separator. -# These will appear as a collapsible tree in the recording panel. -DATASET_NAMES = [ - # Top-level (no hierarchy) - "standalone_dataset", - "raw_logs", - "scratchpad", - # One level of nesting - "robotics.lidar_scans", - "robotics.camera_feeds", - "robotics.imu_data", - "robotics.gps_traces", - "robotics.wheel_odometry", - # Two levels of nesting - "perception.detection.pedestrians", - "perception.detection.vehicles", - "perception.detection.cyclists", - "perception.detection.traffic_signs", - "perception.segmentation.semantic", - "perception.segmentation.instance", - "perception.segmentation.panoptic", - "perception.tracking.short_term", - "perception.tracking.long_term", - # Shared prefix with top-level sibling - "maps.indoor", - "maps.outdoor.parking_lot", - "maps.outdoor.highway", - "maps.outdoor.urban.downtown", - "maps.outdoor.urban.residential", - "maps.outdoor.rural.dirt_road", - # Three levels deep, multiple branches - "simulation.scenarios.highway.merge", - "simulation.scenarios.highway.exit", - "simulation.scenarios.intersection.unprotected_left", - "simulation.scenarios.intersection.four_way_stop", - "simulation.scenarios.parking.parallel", - "simulation.scenarios.parking.perpendicular", - "simulation.weather.rain", - "simulation.weather.snow", - "simulation.weather.fog", - # Benchmarks with versions - "benchmarks.kitti.v1", - "benchmarks.kitti.v2", - "benchmarks.nuscenes.mini", - "benchmarks.nuscenes.full", - "benchmarks.waymo.validation", - "benchmarks.waymo.test", - # Teams / ownership - "teams.planning.trajectories", - "teams.planning.behavior_trees", - "teams.control.pid_tuning", - "teams.control.mpc_experiments", - # Single-child folders (edge case) - "solo_folder.only_child", - # Deep chain (stress test) - "deep.a.b.c.d.leaf", -] - -for name in DATASET_NAMES: - client.create_dataset(name) - print(f"Created dataset: {name}") - -print(f"\nServer URL: {server.url()}") -print("Open the viewer and connect to this server to see the hierarchy.") -print("Press Ctrl+C to stop.") - -try: - import time - - while True: - time.sleep(1) -except KeyboardInterrupt: - pass -finally: - server.shutdown() diff --git a/examples/python/droid_semantic_search/.gitignore b/examples/python/droid_semantic_search/.gitignore new file mode 100644 index 000000000000..4fa97d2bc4ce --- /dev/null +++ b/examples/python/droid_semantic_search/.gitignore @@ -0,0 +1,10 @@ +# Local vector index built by `ingest.py` (LanceDB / Qdrant backends) +droid_lancedb/ +droid_qdrant/ +# DROID episodes downloaded by `prepare_dataset.py` (Hugging Face path) +data/ +# Optimized (keyframe-fixed) episode copies written by `prepare_dataset.py` +optimized/ +# uv-managed virtualenv for this isolated example +.venv/ +__pycache__/ diff --git a/examples/python/droid_semantic_search/README.md b/examples/python/droid_semantic_search/README.md new file mode 100644 index 000000000000..76af501bcc60 --- /dev/null +++ b/examples/python/droid_semantic_search/README.md @@ -0,0 +1,162 @@ + + +Find moments in robot demonstrations by describing them in plain language, and jump straight to the matching frame in the Rerun viewer. + + + + + + + DROID semantic frame search screenshot + + +This pulls frame embeddings from a Rerun dataset, indexes them in an external vector store, and resolves text queries back into deep-links that open the relevant frames in the viewer. +It ships two interchangeable backends — [LanceDB](https://lancedb.com) (default) and [Qdrant](https://qdrant.tech), both fully local on disk and selected with `--backend`. +They're worked examples of one pattern — embeddings out of Rerun, search in your vector database of choice, deep-links back into the viewer — that applies to any vector store. + +## What it shows off + +- Schema introspection and DataFusion content-filtered reads. +- The experimental video dataloader (`DataSource` / `Field` / `VideoFrameDecoder`) for streaming and decoding H.264 frames on demand. +- SigLIP-2 embeddings, with text and image features sharing one vector space. +- `segment_url` deep-links that focus the viewer on a specific frame. + +## How it works + +Three scripts: + +1. `prepare_dataset.py` — registers a few DROID episodes to your local catalog as a dataset (using the episodes bundled in the repo, or downloading them from the Hugging Face Hub when those aren't present). This is the data the other two scripts read. + +2. `ingest.py` — builds the index. For each camera it **auto-detects** the embedding source: + - **Read path:** if the dataset already has a `/camera/{role}/embedding` column (DROID registered with `--create-embeddings`), it reads those vectors directly via a DataFusion query — no model, no video decoding. + - **Compute path:** otherwise it streams the `VideoStream` through the dataloader, decodes frames, and embeds them with SigLIP-2. + + Either way it writes `(segment_id, camera, timestamp_ms, vector)` rows into the selected vector store: LanceDB by default, or Qdrant with `--backend qdrant`. + +3. `search.py` — embeds your example image or text prompt with the SigLIP-2 encoder, runs a vector search over that store (pass the same `--backend`), prints the ranked matches, and opens the best one in the viewer. + +Both scripts reach the store only through a small `VectorStore` interface in `vector_store.py`, so picking a backend is one `--backend` choice and adding one is a single subclass. +Each backend keeps its index in its own directory (`./droid_lancedb` vs `./droid_qdrant`, override with `--db-path`), so the `--backend` you pass to `search.py` must match the one `ingest.py` wrote. + +## Run the code + +### 1. Install dependencies + +This example has its own `uv` project, separate from the workspace `.venv`, because it needs the experimental `rerun-sdk[dataloader]` extras plus heavy ML deps (`transformers`, `lancedb`). + +**Standalone** (sparse-checkout of just this directory, no local Rerun build): + +```bash +uv sync --no-sources --no-dev +``` + +**Monorepo dev** (full repo checkout, editable local `rerun-sdk`): + +```bash +cd examples/python/droid_semantic_search +RERUN_ALLOW_MISSING_BIN=1 uv sync +uv pip install ../../../rerun_py/rerun_dev_fixup +``` + +The second command installs the `.pth` shim that points `import rerun` (and the `rerun` CLI) at the in-repo editable source tree. +It's a separate `uv pip install` rather than a dev-group dependency because uv resolves all dependency groups unconditionally, so a path-only package in `pyproject.toml` would break the standalone `--no-sources` resolution above. + +Then either `source .venv/bin/activate` or prefix subsequent commands with `uv run`. + +### 2. Start a local Rerun server + +This example reads data from a local open-source Rerun catalog server. +In a separate terminal, start one: + +```bash +rerun server +``` + +This serves a catalog at `rerun+http://127.0.0.1:51234` — the default the scripts use. +Leave it running; the steps below connect to it. + +### 3. Register a dataset + +Registers a few DROID episodes to the catalog as `droid:sample`: + +```bash +uv run python prepare_dataset.py +``` + +By default it auto-selects the source: + +- **Monorepo checkout** — it uses the episodes bundled in the repo at `tests/assets/rrd/sample_5` (via git-LFS), so there's nothing to download. If those are un-pulled LFS pointers, run `git lfs install && git lfs pull` first. +- **Standalone checkout** — when the bundled episodes aren't present, it downloads a few from [`rerun/droid_sample`](https://huggingface.co/datasets/rerun/droid_sample) on the Hugging Face Hub into `./data`. + +Useful flags: + +- `--source bundled|huggingface` to force a source (default `auto`). +- `--num-episodes N` to register more (or fewer) episodes; `0` for all (the full Hub dataset is ~3.3 GB). +- `--dataset-name` to register under a different name (pass the same name to `ingest.py`/`search.py`). +- `--no-optimize` to register episodes as-is (see the video decode yield note below). +- `--catalog-url ""` to skip registration. + +### 4. Build the search index + +Index a handful of segments (the exterior camera works well for scene-level search): + +```bash +uv run python ingest.py --num-segments 15 --cameras ext1 +``` + +These sample episodes ship video only (no pre-computed embeddings), so `ingest.py` takes the **compute path**: +the first run downloads the SigLIP-2 model (a few hundred MB) and then decodes and embeds frames — the slow step. +Start with a small `--num-segments` to keep it quick; raise it once you've seen it work. + +### 5. Search + +Search by text and open the best hit in the viewer: + +```bash +uv run python search.py "an open drawer full of tools" --top-k 5 +``` + +Or search by example image instead of text (same vector space): + +```bash +uv run python search.py --image ./query.jpg --top-k 5 +``` + +Both scripts default to LanceDB; pass `--backend qdrant` to use [Qdrant](https://qdrant.tech) instead. +Give the same `--backend` to `ingest.py` and `search.py`, since each writes to its own directory: + +```bash +uv run python ingest.py --backend qdrant --num-segments 15 --cameras ext1 +uv run python search.py --backend qdrant "an open drawer full of tools" --top-k 5 +``` + +Queries that discriminate well on DROID describe concrete, visible objects/scenes, e.g. `"a pink flower"`, `"a cardboard box"`, `"a white plastic bag"`, `"a robot arm over an empty table"`. + +Run `ingest.py --help` and `search.py --help` for the full flag list — index multiple cameras, change the sampling rate, widen the time selection around a hit, and more. + +## Scope and extension points + +- **Text or image queries.** Search by a text prompt or, with `--image `, by an example frame. SigLIP-2 puts text and image features in one space, so image-to-image search reuses the exact same index and ranking — only the query encoder differs. +- **Bring your own vector store.** Rerun supplies the embeddings (read from the dataset, or computed from platform-hosted video) and resolves matches back into viewer deep-links; search itself runs in an external store. This example ships two backends, LanceDB and Qdrant, behind the small `VectorStore` interface in `vector_store.py` — the same write-then-query flow drops onto any vector database, so adding a third is a single subclass. + +## Notes and gotchas + +- **SigLIP text tokenization.** SigLIP is trained with a fixed 64-token sequence and *must* be tokenized with `padding="max_length", max_length=64`. With dynamic padding the text embeddings are malformed and text→image retrieval collapses onto a single "hub" frame that wins every query. +- **Video decode yield.** DROID doesn't log the `VideoStream:is_keyframe` markers the decoder needs to seek, so without them only ~25 % of sampled frames decode. + `prepare_dataset.py` derives the markers up front (via `optimize`), which brings yield to ~100 %. + Optimized copies land in `./optimized`; the originals are untouched. + Skip it with `--no-optimize` if you'd rather register the raw episodes. + +## Files + +- `prepare_dataset.py` — register DROID sample episodes (bundled or downloaded) to the catalog. +- `ingest.py` — build the vector index from the dataset. +- `search.py` — query the index and open results in the viewer. +- `vector_store.py` — the `VectorStore` interface, with LanceDB and Qdrant backends. +- `embeddings.py` — SigLIP-2 helpers (adapted from the DROID loader's `embedding_util.py`). diff --git a/examples/python/droid_semantic_search/embeddings.py b/examples/python/droid_semantic_search/embeddings.py new file mode 100644 index 000000000000..42ec9453b794 --- /dev/null +++ b/examples/python/droid_semantic_search/embeddings.py @@ -0,0 +1,121 @@ +"""SigLIP-2 embedding helpers shared by `ingest.py` and `search.py`. + +These are trimmed copies of the helpers in the DROID loader +(`dataplatform/examples/droid/droid-loader/src/droid_loader/embedding_util.py`), +with the `Timer` instrumentation removed so this example stays standalone and +doesn't pull in the `droid_loader` package. The model is the same one the +loader uses to populate `/camera/{role}/embedding`, so query embeddings land in +the same vector space as any pre-computed frame embeddings. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import torch + +# Disable HF `tokenizers` (Rust) parallelism *before* importing transformers. Otherwise, +# once the SigLIP tokenizer has been used and the process later forks (e.g. a DataLoader +# worker), tokenizers prints "the current process just got forked, after parallelism has +# already been used". We only tokenize tiny queries, so parallelism buys nothing here. +# `setdefault` lets a caller still override via the real environment variable. +os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") + +from transformers import AutoModel, AutoProcessor # imported after TOKENIZERS_PARALLELISM is set (above) + +if TYPE_CHECKING: + from PIL.Image import Image + +# The concrete SigLIP-2 model/processor that `from_pretrained` returns. +EmbeddingModel = Any +EmbeddingProcessor = Any + +# Dual image/text encoder; image and text features share one space, so text +# queries retrieve image frames directly. 768-dim, L2-normalized output. +EMBEDDING_MODEL = "google/siglip2-base-patch16-224" + + +def _resolve_device(device: str | torch.device | None) -> torch.device: + if device is not None: + return torch.device(device) + if torch.cuda.is_available(): + return torch.device("cuda") + if torch.backends.mps.is_available(): + return torch.device("mps") + return torch.device("cpu") + + +def load_embedding_model( + cache_dir: str | Path | None = None, + use_fast: bool = True, +) -> tuple[EmbeddingModel, EmbeddingProcessor]: + """Load the SigLIP-2 model and its processor.""" + print(f"Loading model '{EMBEDDING_MODEL}'") + model = AutoModel.from_pretrained(EMBEDDING_MODEL, cache_dir=cache_dir) + processor = AutoProcessor.from_pretrained(EMBEDDING_MODEL, cache_dir=cache_dir, use_fast=use_fast) + return model, processor + + +def get_text_embeddings( + text: str | list[str], + model: EmbeddingModel, + processor: EmbeddingProcessor, + device: str | torch.device | None = None, +) -> torch.Tensor: + """Embed one or more strings into the SigLIP-2 space. + + Returns an L2-normalized `[N, 768]` CPU tensor (one row per input string). + """ + if isinstance(text, str): + text = [text] + if not text: + raise ValueError("Input 'text' must be a non-empty string or list of strings.") + + device = _resolve_device(device) + model = model.to(device) + model.eval() + + # SigLIP is trained with a fixed 64-token sequence; it MUST be tokenized with + # padding="max_length" (max_length=64). With dynamic padding="True" the text + # embeddings are malformed and text->image retrieval collapses onto a hub image. + inputs = processor(text=text, return_tensors="pt", padding="max_length", max_length=64, truncation=True).to(device) + with torch.inference_mode(): + # transformers 5.x: get_text_features returns the full encoder output, not a + # bare tensor — the embedding is its `pooler_output` (`[N, 768]`). + features = model.get_text_features(**inputs).pooler_output + normalized: torch.Tensor = features / features.norm(p=2, dim=-1, keepdim=True) + return normalized.cpu() + + +def compute_image_embeddings( + images: list[Image], + model: EmbeddingModel, + processor: EmbeddingProcessor, + device: str | torch.device | None = None, + batch_size: int = 64, +) -> torch.Tensor: + """Embed a list of PIL images into the SigLIP-2 space. + + Returns an L2-normalized `[len(images), 768]` CPU tensor. + """ + if not images: + raise ValueError("Input 'images' list cannot be empty.") + + device = _resolve_device(device) + model = model.to(device) + model.eval() + + all_embeddings: list[torch.Tensor] = [] + with torch.inference_mode(): + for start in range(0, len(images), batch_size): + batch = images[start : start + batch_size] + inputs = processor(images=batch, return_tensors="pt").to(device) + # transformers 5.x: get_image_features returns the full encoder output, not a + # bare tensor — the embedding is its `pooler_output` (`[N, 768]`). + features = model.get_image_features(**inputs).pooler_output + normalized = features / features.norm(p=2, dim=1, keepdim=True) + all_embeddings.append(normalized.cpu()) + + return torch.cat(all_embeddings, dim=0) diff --git a/examples/python/droid_semantic_search/ingest.py b/examples/python/droid_semantic_search/ingest.py new file mode 100644 index 000000000000..ed1e69db2ac5 --- /dev/null +++ b/examples/python/droid_semantic_search/ingest.py @@ -0,0 +1,330 @@ +"""Build a local vector index of DROID camera frames. + +For each requested camera the script auto-detects how to get embeddings: + +* **Read path** — if the dataset already has a `/camera/{role}/embedding` column + (DROID registered with `--create-embeddings`), read it straight out of the + catalog with a DataFusion query. No video decoding, no model needed. +* **Compute path** — otherwise stream the H.264 `VideoStream` via the + experimental dataloader, decode frames, and embed them with SigLIP-2. + +Either way we end up with a columnar `(segment_id, camera, timestamp_ms, vector)` +Arrow table, which we write to a local vector store (LanceDB or Qdrant, see +`--backend`) and index for ANN search. + +Run inside the rerun SDK venv, e.g.: + + pixi run uv run ../droid_semantic_search/ingest.py --num-segments 5 --cameras ext1 +""" + +from __future__ import annotations + +import argparse +import itertools +from collections.abc import Iterator +from typing import Any + +import numpy as np +import pyarrow as pa +import pyarrow.compute as pc +from vector_store import BACKENDS, DEFAULT_PATHS, open_store + +from rerun.catalog import CatalogClient + +# DROID camera roles, and the timeline everything is logged on. +ALL_CAMERAS = ("wrist", "ext1", "ext2") +TIMELINE = "real_time" + +# DROID is H.264, GOP size 64 at ~15 fps (see the droid-loader). These knobs are only a +# fallback for episodes registered with `--no-optimize` (no keyframe markers): the decoder +# then seeks by a fixed window, so we use 2x the GOP to make sure each window holds a keyframe. +DROID_CODEC = "h264" +DROID_KEYFRAME_INTERVAL = 128 +DROID_FPS_ESTIMATE = 15.0 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--catalog-url", default="rerun+http://127.0.0.1:51234", help="Rerun catalog URL") + parser.add_argument("--dataset", default="droid:sample", help="Dataset name in the catalog") + parser.add_argument("--token", default=None, help="Auth token (if the catalog requires one)") + parser.add_argument( + "--cameras", + default="ext1", + help="Comma-separated camera roles, or 'all'. Exterior cams (ext1/ext2) give better scene-level matches.", + ) + parser.add_argument("--num-segments", type=int, default=10, help="Number of segments to index (0 for all)") + parser.add_argument( + "--backend", + choices=BACKENDS, + default="lance", + help="Local vector store to write the index to.", + ) + parser.add_argument( + "--db-path", + default=None, + help="Directory for the local vector DB (default: ./droid_lancedb or ./droid_qdrant per backend).", + ) + parser.add_argument("--table", default="droid_frames", help="Table/collection name") + parser.add_argument( + "--rate-hz", + type=float, + default=2.0, + help="Compute-path only: frames per second to sample from each segment.", + ) + parser.add_argument( + "--fetch-batch", + type=int, + default=32, + help="Compute-path only: samples fetched per server round-trip.", + ) + parser.add_argument( + "--num-workers", + type=int, + default=0, + help="Compute-path only: DataLoader workers for fetching/decoding.", + ) + return parser.parse_args() + + +def resolve_cameras(arg: str) -> list[str]: + if arg.strip().lower() == "all": + return list(ALL_CAMERAS) + roles = [c.strip() for c in arg.split(",") if c.strip()] + unknown = [r for r in roles if r not in ALL_CAMERAS] + if unknown: + raise SystemExit(f"Unknown camera role(s) {unknown}; expected a subset of {ALL_CAMERAS} or 'all'.") + return roles + + +def cameras_with_embeddings(schema: object, cameras: list[str]) -> set[str]: + """Return the subset of *cameras* that already have an embedding entity in the dataset.""" + present = {p.strip("/") for p in schema.entity_paths()} # type: ignore[attr-defined] + return {role for role in cameras if f"camera/{role}/embedding" in present} + + +def _is_list_type(t: pa.DataType) -> bool: + return bool(pa.types.is_list(t) or pa.types.is_large_list(t) or pa.types.is_fixed_size_list(t)) + + +def _vector_dim(vectors: pa.Array) -> int: + """Embedding dimensionality of a (variable- or fixed-size) list array.""" + if pa.types.is_fixed_size_list(vectors.type): + return int(vectors.type.list_size) + return int(pc.max(pc.list_value_length(vectors)).as_py()) + + +def _embedding_table(role: str, segment_ids: pa.Array, timestamps_ms: pa.Array, vectors: pa.Array) -> pa.Table: + """Assemble the four index columns into one Arrow table. + + Both ingest paths funnel through here, so they share a single schema and + `pa.concat_tables` can stitch their results together with no per-row work. + """ + return pa.table( + { + "segment_id": segment_ids, + "camera": pa.array([role] * len(segment_ids), pa.string()), + "timestamp_ms": timestamps_ms, + "vector": vectors, + }, + ) + + +def _find_embedding_column(schema: pa.Schema, role: str) -> str: + """Locate the list-typed embedding column for *role* in a query result schema. + + The DROID loader logs embeddings via `rr.AnyValues(embeddings=...)`, so the + exact column name (e.g. `/camera/ext1/embedding:embeddings`) is derived by + the platform — discover it rather than hardcoding. + """ + candidates: list[str] = [f.name for f in schema if _is_list_type(f.type) and "embedding" in f.name.lower()] + if not candidates: + raise RuntimeError(f"No list-typed embedding column for camera '{role}'. Columns: {schema.names}") + for name in candidates: + if role in name: + return name + return candidates[0] + + +def read_embedding_table(dataset: object, segments: list[str], role: str) -> pa.Table | None: + """Read pre-computed embeddings for *role* out of the catalog. + + The query result is already columnar Arrow, so we stay in Arrow the whole + way — filter out missing rows and normalize the column types without ever + materializing Python row objects. + """ + view = dataset.filter_segments(segments).filter_contents( # type: ignore[attr-defined] + [f"/camera/{role}/embedding", f"/camera/{role}/embedding/**"], + ) + table = view.reader(index=TIMELINE).to_arrow_table() + + emb_col = _find_embedding_column(table.schema, role) + if "rerun_segment_id" not in table.schema.names or TIMELINE not in table.schema.names: + raise RuntimeError(f"Expected 'rerun_segment_id' and '{TIMELINE}' columns, got {table.schema.names}") + + # Columnar equivalent of the per-row `if vec is None or seg is None: continue`. + keep = pc.and_(pc.is_valid(table.column(emb_col)), pc.is_valid(table.column("rerun_segment_id"))) + table = table.filter(keep) + if table.num_rows == 0: + print(f" [{role}] no pre-computed embeddings") + return None + + vectors = table.column(emb_col).combine_chunks() + vectors = vectors.cast(pa.list_(pa.float32(), _vector_dim(vectors))) + segment_ids = table.column("rerun_segment_id").cast(pa.string()) + # DROID's index timeline is nanosecond timestamps; LanceDB just wants an int. + timestamps_ms = table.column(TIMELINE).cast(pa.timestamp("ms")).cast(pa.int64()) + + out = _embedding_table(role, segment_ids, timestamps_ms, vectors) + print(f" [{role}] read {out.num_rows} pre-computed embeddings") + return out + + +def _identity_collate(batch: list[Any]) -> list[Any]: + """Collate that leaves the list of per-sample dicts untouched (picklable for workers).""" + return batch + + +def compute_embedding_table( + dataset: object, + segments: list[str], + role: str, + *, + rate_hz: float, + fetch_batch: int, + num_workers: int, +) -> pa.Table | None: + """Decode frames for *role* and embed them with SigLIP-2.""" + # Heavy / optional deps are imported lazily so a pure read-path run stays light. + from embeddings import compute_image_embeddings, load_embedding_model + from PIL import Image + from torch.utils.data import DataLoader + from tqdm import tqdm + + from rerun.experimental.dataloader import ( + DataSource, + Field, + FixedRateSampling, + RerunMapDataset, + VideoFrameDecoder, + ) + + field_name = f"img_{role}" + source = DataSource(dataset, segments=segments) # type: ignore[arg-type] + fields = { + field_name: Field( + f"/camera/{role}:VideoStream:sample", + decode=VideoFrameDecoder( + codec=DROID_CODEC, + keyframe_interval=DROID_KEYFRAME_INTERVAL, + fps_estimate=DROID_FPS_ESTIMATE, + ), + ), + } + ds = RerunMapDataset( + source=source, + index=TIMELINE, + fields=fields, + timeline_sampling=FixedRateSampling(rate_hz=rate_hz), + ) + total = len(ds) + print(f" [{role}] decoding ~{total} frames at {rate_hz} Hz …") + + # The DataLoader earns its keep on the *fetch* side: `batch_size` batches the + # catalog round-trips, and `num_workers > 0` fans the CPU-bound video decode + # across worker processes. `shuffle=False` keeps indices in 0..N-1 order, which + # the running counter in `decoded_frames` relies on for the (segment, timestamp) + # pairing below. + loader = DataLoader( + ds, + batch_size=fetch_batch, + shuffle=False, + num_workers=num_workers, + collate_fn=_identity_collate, + ) + + def decoded_frames(pbar: tqdm[Any]) -> Iterator[tuple[Image.Image, str, int]]: + # The loader visits indices 0..N-1 in order, so a running counter pairs each + # decoded frame back to its (segment, timestamp) via `global_to_local`. The + # progress bar advances once per sample pulled from the loader (the slow, + # video-decoding step), including the ones we skip below. + global_idx = 0 + for batch in loader: + for sample in batch: + tensor = sample[field_name] + seg_meta, idx_val = ds.sample_index.global_to_local(global_idx) + global_idx += 1 + pbar.update(1) + if tensor is None: # target preceded the first keyframe; skip + continue + ts_ms = int(np.datetime64(idx_val).astype("datetime64[ms]").astype(np.int64)) # type: ignore[arg-type] + rgb = tensor.permute(1, 2, 0).cpu().numpy() # [C,H,W] uint8 -> [H,W,C] + yield Image.fromarray(rgb), seg_meta.segment_id, ts_ms + + # Embed the decoded stream chunk-by-chunk so peak memory stays at ~embed_batch + # frames rather than the whole role. Each chunk becomes one small Arrow table; + # `pa.concat_tables` stitches them at the end with no per-row work. + embed_batch = 64 + model, processor = load_embedding_model() + chunks: list[pa.Table] = [] + with tqdm(total=total, desc=f"[{role}] decode+embed", unit="frame") as pbar: + for chunk in itertools.batched(decoded_frames(pbar), embed_batch): # type: ignore[attr-defined, unused-ignore] + frames = [frame for frame, _, _ in chunk] + segs = [seg for _, seg, _ in chunk] + timestamps = [ts_ms for _, _, ts_ms in chunk] + vectors = compute_image_embeddings(frames, model, processor, batch_size=embed_batch).numpy() + _, dim = vectors.shape + vector_col = pa.FixedSizeListArray.from_arrays(pa.array(vectors.reshape(-1), pa.float32()), dim) + chunks.append( + _embedding_table(role, pa.array(segs, pa.string()), pa.array(timestamps, pa.int64()), vector_col), + ) + + if not chunks: + print(f" [{role}] no frames decoded") + return None + + out = pa.concat_tables(chunks) + print(f" [{role}] computed {out.num_rows} embeddings") + return out + + +def main() -> None: + args = parse_args() + cameras = resolve_cameras(args.cameras) + + client = CatalogClient(args.catalog_url, token=args.token) + dataset = client.get_dataset(args.dataset) + + all_segments = dataset.segment_ids() + segments = all_segments if args.num_segments == 0 else all_segments[: args.num_segments] + if not segments: + raise SystemExit(f"Dataset '{args.dataset}' has no segments.") + + have_emb = cameras_with_embeddings(dataset.schema(), cameras) + print(f"Indexing {len(segments)} segment(s); cameras={cameras}; pre-computed embeddings for {sorted(have_emb)}") + + tables: list[pa.Table] = [] + for role in cameras: + if role in have_emb: + table = read_embedding_table(dataset, segments, role) + else: + table = compute_embedding_table( + dataset, + segments, + role, + rate_hz=args.rate_hz, + fetch_batch=args.fetch_batch, + num_workers=args.num_workers, + ) + if table is not None: + tables.append(table) + + if not tables: + raise SystemExit("No embeddings produced; nothing to index.") + + db_path = args.db_path or DEFAULT_PATHS[args.backend] + open_store(args.backend, db_path, args.table).write(pa.concat_tables(tables)) + + +if __name__ == "__main__": + main() diff --git a/examples/python/droid_semantic_search/prepare_dataset.py b/examples/python/droid_semantic_search/prepare_dataset.py new file mode 100644 index 000000000000..c1e5b001ba47 --- /dev/null +++ b/examples/python/droid_semantic_search/prepare_dataset.py @@ -0,0 +1,222 @@ +"""Register a few DROID episodes to a local Rerun catalog so the rest of the example has data to index. + +Two sources, auto-selected (override with `--source`): + +* **Bundled** — the `tests/assets/rrd/sample_5` episodes shipped in the Rerun repo (via git-LFS). + Used automatically in a monorepo checkout: no download, works offline. +* **Hugging Face** — a few episodes from the [`rerun/droid_sample`](https://huggingface.co/datasets/rerun/droid_sample) + dataset, downloaded into `./data`. Used when the bundled episodes aren't available + (e.g. a standalone sparse-checkout of just this example). + +Either way the episodes are registered to the catalog as a dataset (default name `droid:sample`). +They carry H.264 `VideoStream`s but no pre-computed embeddings, so `ingest.py` will take its +(slower) compute path: decode frames and embed them with SigLIP-2. + +Episodes are optimized first to derive keyframe markers, so the compute path can decode +frames (DROID doesn't log the markers the decoder needs). Pass `--no-optimize` to skip. + +Run inside the rerun SDK venv, with a `rerun server` running in another terminal, e.g.: + + uv run python prepare_dataset.py +""" + +from __future__ import annotations + +import argparse +from dataclasses import replace +from pathlib import Path + +from tqdm import tqdm + +import rerun as rr +from rerun.experimental import OptimizationProfile, RrdReader + +# `tests/assets/rrd/sample_5`, relative to this file at `examples/python/droid_semantic_search/`. +BUNDLED_SAMPLE_DIR = Path(__file__).resolve().parents[3] / "tests" / "assets" / "rrd" / "sample_5" +DEFAULT_REPO_ID = "rerun/droid_sample" +DEFAULT_OUTPUT_DIR = Path(__file__).resolve().parent / "data" +DEFAULT_OPTIMIZED_DIR = Path(__file__).resolve().parent / "optimized" +DEFAULT_DATASET_NAME = "droid:sample" +DEFAULT_CATALOG_URL = "rerun+http://127.0.0.1:51234" + +_LFS_POINTER_PREFIX = b"version https://git-lfs.github.com/spec/v1" + + +def _is_lfs_pointer(path: Path) -> bool: + """True if *path* is an un-pulled git-LFS pointer file rather than the real RRD.""" + with path.open("rb") as f: + return f.read(len(_LFS_POINTER_PREFIX)) == _LFS_POINTER_PREFIX + + +def bundled_episode_paths() -> list[Path]: + """Return the bundled sample_5 RRDs (sorted), or an empty list if they're not available. + + Raises if the directory exists but the files are un-pulled git-LFS pointers — that's a + recoverable monorepo setup issue worth surfacing rather than silently working around. + """ + if not BUNDLED_SAMPLE_DIR.is_dir(): + return [] + rrds = sorted(BUNDLED_SAMPLE_DIR.glob("*.rrd")) + if not rrds: + return [] + if any(_is_lfs_pointer(p) for p in rrds): + raise SystemExit( + f"The bundled DROID episodes in {BUNDLED_SAMPLE_DIR} are un-pulled git-LFS pointers.\n" + "Fetch them with `git lfs install && git lfs pull`, or pass `--source huggingface` to download instead.", + ) + return rrds + + +def download_episodes(repo_id: str, num_episodes: int, dest: Path) -> list[Path]: + """Download the first *num_episodes* (0 for all) `.rrd` files from *repo_id* into *dest*. + + `snapshot_download` renders its own per-file progress bars, so the user sees the download advance. + """ + from huggingface_hub import HfApi, snapshot_download + + files = sorted(f for f in HfApi().list_repo_files(repo_id, repo_type="dataset") if f.endswith(".rrd")) + if not files: + raise SystemExit(f"No .rrd files found in '{repo_id}'.") + files = files if num_episodes == 0 else files[:num_episodes] + + print(f"Downloading {len(files)} episode(s) from '{repo_id}' to {dest} …") + local_dir = snapshot_download(repo_id=repo_id, repo_type="dataset", allow_patterns=files, local_dir=dest) + return [Path(local_dir) / f for f in files] + + +def resolve_episodes(source: str, *, num_episodes: int, repo_id: str, output_dir: Path) -> list[Path]: + """Pick the episode RRDs to register, per the requested *source* (`auto`/`bundled`/`huggingface`).""" + if source in ("auto", "bundled"): + bundled = bundled_episode_paths() + if bundled: + paths = bundled if num_episodes == 0 else bundled[:num_episodes] + print(f"Using {len(paths)} bundled episode(s) from {BUNDLED_SAMPLE_DIR}") + return paths + if source == "bundled": + raise SystemExit(f"No bundled episodes found at {BUNDLED_SAMPLE_DIR}; pass `--source huggingface`.") + print(f"Bundled episodes not found at {BUNDLED_SAMPLE_DIR}; downloading from the Hub instead.") + + return download_episodes(repo_id, num_episodes, output_dir) + + +def optimize_episodes(rrd_paths: list[Path], dest_dir: Path) -> list[Path]: + """Derive `VideoStream:is_keyframe` markers (DROID doesn't log them) so the decoder can seek. + + Writes a fixed copy of each episode to *dest_dir* and returns the new paths. IDs are + preserved, so catalog segment IDs and `segment_url` links are unchanged. + """ + dest_dir.mkdir(parents=True, exist_ok=True) + profile = replace(OptimizationProfile.OBJECT_STORE, fix_keyframe=True) + + optimized: list[Path] = [] + for src in tqdm(rrd_paths, desc="Optimizing", unit="episode"): + reader = RrdReader(src) + recordings = reader.recordings() + if len(recordings) != 1: + raise SystemExit(f"Expected one recording in {src}, found {len(recordings)}.") + entry = recordings[0] + + store = reader.stream(store=entry).collect(optimize=profile) + dst = dest_dir / src.name + store.write_rrd(dst, application_id=entry.application_id, recording_id=entry.recording_id) + optimized.append(dst) + + return optimized + + +def register_to_catalog(rrd_paths: list[Path], *, catalog_url: str, dataset_name: str) -> None: + """Register per-episode RRDs to a catalog server instance. + + Uses absolute `file://` URIs so the catalog can read the RRDs directly from the local filesystem. + Streams `iter_results()` so a progress bar advances as each segment finishes, rather than blocking + silently on `wait()`. + """ + print(f"\nRegistering {len(rrd_paths)} episode(s) to {catalog_url} as dataset '{dataset_name}' …") + client = rr.catalog.CatalogClient(catalog_url) + dataset = client.create_dataset(dataset_name, exist_ok=True) + + uris = [f"file://{p.resolve()}" for p in rrd_paths] + on_duplicate = rr.catalog.OnDuplicateSegmentLayer(rr.catalog.OnDuplicateSegmentLayer.REPLACE) + handle = dataset.register(uris, on_duplicate=on_duplicate) + + failures: list[str] = [] + for result in tqdm(handle.iter_results(), total=len(uris), desc="Registering", unit="segment"): + if result.is_error: + failures.append(f"{result.uri}: {result.error}") + + if failures: + joined = "\n ".join(failures) + raise SystemExit(f"Failed to register {len(failures)} of {len(uris)} episode(s):\n {joined}") + print(" registration done") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + "--source", + choices=("auto", "bundled", "huggingface"), + default="auto", + help="Where to get episodes: 'bundled' (in-repo sample_5), 'huggingface' (download), " + "or 'auto' (bundled if available, else download). Default: auto.", + ) + parser.add_argument( + "--repo-id", + default=DEFAULT_REPO_ID, + help=f"Hugging Face dataset repo id, for the download path (default: {DEFAULT_REPO_ID}).", + ) + parser.add_argument( + "--num-episodes", + type=int, + default=5, + help="Number of episodes to register (0 for all). The full Hub dataset is ~3.3 GB.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=DEFAULT_OUTPUT_DIR, + help=f"Directory to download episode RRDs into, for the download path (default: {DEFAULT_OUTPUT_DIR}).", + ) + parser.add_argument( + "--optimize", + default=True, + action=argparse.BooleanOptionalAction, + help="Derive keyframe markers before registering, so ingest.py can decode frames " + "(~100%% yield vs ~25%%). Use --no-optimize to register episodes as-is.", + ) + parser.add_argument( + "--optimized-dir", + type=Path, + default=DEFAULT_OPTIMIZED_DIR, + help=f"Directory to write optimized episode RRDs into (default: {DEFAULT_OPTIMIZED_DIR}).", + ) + parser.add_argument( + "--catalog-url", + default=DEFAULT_CATALOG_URL, + help="Rerun catalog URL to register episodes with. Pass an empty string to skip registration.", + ) + parser.add_argument( + "--dataset-name", + default=DEFAULT_DATASET_NAME, + help=f"Name of the dataset to create/use in the catalog (default: {DEFAULT_DATASET_NAME}).", + ) + args = parser.parse_args() + + rrd_paths = resolve_episodes( + args.source, + num_episodes=args.num_episodes, + repo_id=args.repo_id, + output_dir=args.output_dir, + ) + + if args.optimize: + print(f"\nOptimizing {len(rrd_paths)} episode(s) into {args.optimized_dir} …") + rrd_paths = optimize_episodes(rrd_paths, args.optimized_dir) + + if args.catalog_url: + register_to_catalog(rrd_paths, catalog_url=args.catalog_url, dataset_name=args.dataset_name) + else: + print(f"Skipping registration (empty --catalog-url). Episodes: {[str(p) for p in rrd_paths]}") + + +if __name__ == "__main__": + main() diff --git a/examples/python/droid_semantic_search/pyproject.toml b/examples/python/droid_semantic_search/pyproject.toml new file mode 100644 index 000000000000..bc7bebf8bced --- /dev/null +++ b/examples/python/droid_semantic_search/pyproject.toml @@ -0,0 +1,66 @@ +[project] +name = "droid_semantic_search" +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.12,<3.13" +dependencies = [ + # `catalog` brings datafusion + pandas; `dataloader` brings torch, torchvision, av, pillow. + "rerun-sdk[catalog,dataloader]", + "lancedb", # default local vector store + ANN index (--backend lance) + "qdrant-client", # alternative local vector store (--backend qdrant) + "transformers>=5.13.1", # SigLIP-2 model + processor + "pyarrow<24", # building the vector-index table + "huggingface-hub", # prepare_dataset.py: download DROID sample episodes from the Hub + "tqdm", # progress bars for download/register/ingest +] + +[dependency-groups] +dev = ["mypy==1.19.1", "types-tqdm"] + +[tool.rerun-example] +# Picked up by scripts/ci/isolated_examples.py and the `py-lint-isolated-examples` pixi task. +isolated = true + +[tool.uv] +# The example is flat scripts, not a wheel — skip project build, just sync deps. +package = false + +# Default `uv sync` uses the in-repo editable rerun-sdk (monorepo dev mode). +# After syncing, also run `uv pip install ../../../rerun_py/rerun_dev_fixup` to +# install the .pth shim that makes `import rerun` resolve to the editable source tree. +# +# Standalone users (e.g. sparse-checkout of just this example) run instead: +# uv sync --no-sources --no-dev +# That ignores the path source below and resolves `rerun-sdk` from PyPI. +# rerun-dev-fixup is intentionally absent from this file: uv 0.7.x resolves all +# dependency groups and extras unconditionally, so any path-only package here would +# block standalone `--no-sources` resolution. +[tool.uv.sources] +rerun-sdk = { path = "../../../rerun_py", editable = true } + +# Merged onto the shared base at `../_isolated/mypy.ini` by +# scripts/ci/isolated_examples.py — list the untyped third-party libs this +# example actually imports. +[[tool.mypy.overrides]] +module = [ + "lancedb.*", + "qdrant_client.*", + "torch.*", + "torchvision.*", + "av.*", + "PIL.*", + "huggingface_hub.*", + # pyarrow is pinned <24 (24.0.0 segfaults rerun on import); 23.x ships no py.typed, + # so all of pyarrow — including pyarrow.compute — is untyped to mypy. + "pyarrow.*", +] +ignore_missing_imports = true + +# transformers ships a `py.typed` marker but with incomplete stubs, so type-checking its +# internals yields false positives we can't fix from here: model methods are wrapped in +# `_Wrapped` descriptors that mistype `self` (`AutoModel has no attribute "to"`, +# `AutoProcessor not callable`, etc.). Skip following it — we only need our own usage typed. +[[tool.mypy.overrides]] +module = ["transformers.*"] +follow_imports = "skip" +ignore_missing_imports = true diff --git a/examples/python/droid_semantic_search/search.py b/examples/python/droid_semantic_search/search.py new file mode 100644 index 000000000000..17a4d88c61c2 --- /dev/null +++ b/examples/python/droid_semantic_search/search.py @@ -0,0 +1,128 @@ +"""Query the DROID frame index with a text prompt or example image and open the best hit in Rerun. + +Embeds the query with the SigLIP-2 text *or* image encoder (both share one +vector space, the same one the indexed frame embeddings live in), runs a cosine +nearest-neighbor search over the local vector store (LanceDB or Qdrant, see +`--backend`), prints the ranked matches, and mints a `segment_url` deep-link +that opens the top result focused on that frame in the Rerun viewer. + +Run inside the rerun SDK venv, e.g.: + + pixi run uv run ../droid_semantic_search/search.py "a robot gripper reaching for a cup" + pixi run uv run ../droid_semantic_search/search.py --image ./query.jpg +""" + +from __future__ import annotations + +import argparse +import webbrowser +from datetime import datetime, timedelta, timezone +from typing import cast + +from embeddings import ( + EmbeddingModel, + EmbeddingProcessor, + compute_image_embeddings, + get_text_embeddings, + load_embedding_model, +) +from vector_store import BACKENDS, DEFAULT_PATHS, open_store + +import rerun as rr +from rerun.catalog import CatalogClient + +TIMELINE = "real_time" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("query", nargs="?", help="Text prompt to search for") + parser.add_argument( + "--image", help="Path to an image to search by (image-to-image); mutually exclusive with the text query" + ) + parser.add_argument("--catalog-url", default="rerun+http://127.0.0.1:51234", help="Rerun catalog URL") + parser.add_argument("--dataset", default="droid:sample", help="Dataset name (used to mint the viewer link)") + parser.add_argument( + "--login", action="store_true", help="Authenticate with the catalog via rr.login() before connecting" + ) + parser.add_argument( + "--backend", + choices=BACKENDS, + default="lance", + help="Local vector store to query (must match what ingest.py wrote).", + ) + parser.add_argument( + "--db-path", + default=None, + help="Directory of the local vector DB (default: ./droid_lancedb or ./droid_qdrant per backend).", + ) + parser.add_argument("--table", default="droid_frames", help="Table/collection name") + parser.add_argument("--top-k", type=int, default=5, help="Number of matches to return") + parser.add_argument("--window-secs", type=float, default=2.0, help="Time window around the matched frame to show") + parser.add_argument( + "--open", + default=True, + action=argparse.BooleanOptionalAction, + help="Open the top hit in the viewer", + ) + return parser.parse_args() + + +def embed_image_query(path: str, model: EmbeddingModel, processor: EmbeddingProcessor) -> list[float]: + """Embed an image file for image-to-image search. + + Wired to the `--image` flag: text and image features share one SigLIP-2 + vector space, so a query image retrieves frames the same way a text prompt does. + """ + from PIL import Image + + image = Image.open(path).convert("RGB") + vector = compute_image_embeddings([image], model, processor).numpy()[0] + return cast("list[float]", vector.tolist()) + + +def viewer_url(dataset: object, segment_id: str, timestamp_ms: int, window_secs: float) -> str: + ts = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc) + half = timedelta(seconds=window_secs / 2) + return dataset.segment_url(segment_id, timeline=TIMELINE, start=ts - half, end=ts + half) # type: ignore[attr-defined, no-any-return] + + +def main() -> None: + args = parse_args() + + if bool(args.query) == bool(args.image): + raise SystemExit("Provide exactly one of: a text query or --image .") + + model, processor = load_embedding_model() + if args.image: + query_vec = embed_image_query(args.image, model, processor) + query_label = f"image {args.image}" + else: + query_vec = get_text_embeddings(args.query, model, processor).numpy()[0].tolist() + query_label = args.query + + db_path = args.db_path or DEFAULT_PATHS[args.backend] + hits = open_store(args.backend, db_path, args.table).search(query_vec, args.top_k) + if not hits: + raise SystemExit("No matches found — is the index populated? Run ingest.py first.") + + print(f'\nTop {len(hits)} matches for: "{query_label}"\n') + print(f"{'#':>2} {'sim':>5} {'camera':<6} {'timestamp (UTC)':<24} segment") + for rank, hit in enumerate(hits, start=1): + ts_iso = datetime.fromtimestamp(hit["timestamp_ms"] / 1000, tz=timezone.utc).isoformat(timespec="milliseconds") + print(f"{rank:>2} {hit['similarity']:>5.3f} {hit['camera']:<6} {ts_iso:<24} {hit['segment_id']}") + + # Mint a deep-link into the viewer for the best match. + if args.login: + rr.login() + client = CatalogClient(args.catalog_url) + dataset = client.get_dataset(args.dataset) + best = hits[0] + url = viewer_url(dataset, best["segment_id"], best["timestamp_ms"], args.window_secs) + print(f"\nTop hit in viewer:\n{url}") + if args.open: + webbrowser.open(url) + + +if __name__ == "__main__": + main() diff --git a/examples/python/droid_semantic_search/uv.lock b/examples/python/droid_semantic_search/uv.lock new file mode 100644 index 000000000000..fe21691a61c2 --- /dev/null +++ b/examples/python/droid_semantic_search/uv.lock @@ -0,0 +1,1300 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'emscripten'", + "sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "av" +version = "17.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/f0/8c8dca97ae0cf00e8e2a53bb5cb9aca5fd484f585ef3e9b412200aff3ebd/av-17.0.1.tar.gz", hash = "sha256:fbcbd4aa43bca6a8691816283112d1659a27f407bbeb66d1397023691339f5d4", size = 4411938, upload-time = "2026-04-18T17:12:34.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/82/e7007dcef7bd2d2c377e2e85977701384f42d19fc808c2ccb3a99eaf58f2/av-17.0.1-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:987f4f46ceae4da6c614dcbd2b8149be9dbf680c3bb7a6841c58af9cff4d9230", size = 23238802, upload-time = "2026-04-18T17:11:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/6b/aa/858b09a08ea6f83f91be44b5a5adad13ae8d9ac8b80fda27e73c24bfb160/av-17.0.1-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:d97f54e55b18a74912f479c1978aadd1341d38d892dee95bb5c2f2dccfa72f32", size = 18709338, upload-time = "2026-04-18T17:11:53.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8b/8de3fd21c4b0b74d44337421abeab0e71462337fb6a28fff888e0c356cbd/av-17.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e6eee84afa48d0e9321047cd3e4facd44b401493f6bdc753e2e1d1e7c9e6d13e", size = 34007351, upload-time = "2026-04-18T17:11:56.116Z" }, + { url = "https://files.pythonhosted.org/packages/02/28/167b291356c2cc315a2d62a95b0ceace72b5b0bf547de30b89313110f032/av-17.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c58c71bffd9383908c85695ac61d3184c668accb04a5bd1b262e0fb8d09f60a5", size = 36345295, upload-time = "2026-04-18T17:11:59.125Z" }, + { url = "https://files.pythonhosted.org/packages/04/fa/aae56f2ff2c204c408641e1120f5ca5ce9c3390cf5362245c6f1158704b5/av-17.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:42d6745d30a410ec9b22aef79a52a7ab5a001eb8f5adfd952946606a30983318", size = 35183754, upload-time = "2026-04-18T17:12:01.697Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bd/776046f27093aef80155a204ca7d82a887ae4ee72ba4ef8411b46ea7898c/av-17.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3ed6bcd7021fe55832f95b8ef78dd01a4cb21faf3cd71f1e1bf4f20bf100b278", size = 37430809, upload-time = "2026-04-18T17:12:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/3261bd2c6b7f6c0aa8379fc970d1ecf496330990b992ad28607785074268/av-17.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:9af524e8632a54032e361d6b88895bd3e7c6212ca560de60f5ccc525323c764c", size = 28889649, upload-time = "2026-04-18T17:12:07.04Z" }, + { url = "https://files.pythonhosted.org/packages/98/39/381104e427a0c7231d2ec0d25d538d58fc20fc0458846b95860d3ef8073b/av-17.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:50e58a473d65ea29b645e45c9fd8518a6783737135683ecc40571a91592bdfe4", size = 21918412, upload-time = "2026-04-18T17:12:09.312Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + +[[package]] +name = "datafusion" +version = "53.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyarrow" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/2b/0f96f12b70839c93930c4e17d767fc32b6c77d548c78784128049e944701/datafusion-53.0.0.tar.gz", hash = "sha256:ba9a5ec06b5453fbd8710d6aeeb515a8bcac4b6c140e254409bb53a5f322ef22", size = 224267, upload-time = "2026-04-13T00:45:02.686Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/4c/60e052813d81f1ffe3123ead013dbdd2cf961daa576cb9056cbb80228e6b/datafusion-53.0.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a0bd1a98d736571321416dc4ed361a9d1225da1ec9f6c5fad818d75f547697a7", size = 35774913, upload-time = "2026-04-13T00:44:46.235Z" }, + { url = "https://files.pythonhosted.org/packages/6e/59/beabe5301df3338d8206446cd624079e43bdad46e20377a6336017fb6ccf/datafusion-53.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ce186a8d2405afd67e11e2fb75715019f16b00d070b8d0da89d8aa61cc74c8b5", size = 32667118, upload-time = "2026-04-13T00:44:50.269Z" }, + { url = "https://files.pythonhosted.org/packages/ae/94/636ab61ade98395daea6e733e225e9c7beef111c7c5b575ac851513e203c/datafusion-53.0.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:288a00a7ef03e2807a4667683f7560efd80d60ed1d41696ac15ca9ded14c8251", size = 35585824, upload-time = "2026-04-13T00:44:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/b9f4889209af02f8d14bccb0e6f0519c329b072bc4d2595025a1303f144c/datafusion-53.0.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8fef0004f0161fcfc556c025a7201f9cc3169aa3adb97a86419ebb34182d9efb", size = 38083690, upload-time = "2026-04-13T00:44:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1a/ea4831fc6aeefedbcf186c9f6a273d507b1787c03cbb905bded7e1149a6a/datafusion-53.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:4c8410f5f659b926677be6c7d443bbc05d825c078c970b7d8cf977ebcf948314", size = 38120687, upload-time = "2026-04-13T00:45:00.633Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "droid-semantic-search" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "huggingface-hub" }, + { name = "lancedb" }, + { name = "pyarrow" }, + { name = "qdrant-client" }, + { name = "rerun-sdk", extra = ["catalog", "dataloader"] }, + { name = "tqdm" }, + { name = "transformers" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "types-tqdm" }, +] + +[package.metadata] +requires-dist = [ + { name = "huggingface-hub" }, + { name = "lancedb" }, + { name = "pyarrow", specifier = "<24" }, + { name = "qdrant-client" }, + { name = "rerun-sdk", extras = ["catalog", "dataloader"], editable = "../../../rerun_py" }, + { name = "tqdm" }, + { name = "transformers", specifier = ">=5.13.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = "==1.19.1" }, + { name = "types-tqdm" }, +] + +[[package]] +name = "filelock" +version = "3.29.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "grpcio" +version = "1.81.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/07/9a979c81738863a738dc23d65177056e71fbb2db817740ed870b33434e7a/grpcio-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8b39472beafc0bdcafc4c8c73ad082ebfdb449d566897a61e7acb4fa88089115", size = 6053264, upload-time = "2026-06-11T12:45:21.017Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/539706ca0d3bd40dbad583dc56fd883da941f37556b629132da5762781b9/grpcio-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:12b7524c88d4026d3dcb7b0ebe16b6714f3b4af402ddd0f0639ab064a00c87c3", size = 12052560, upload-time = "2026-06-11T12:45:23.652Z" }, + { url = "https://files.pythonhosted.org/packages/e0/44/f257b7e0bd69c93b06c6cb8ac8d1b901ccb42bedabd83c1a4c77a71f8810/grpcio-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1e123f9b37edb8375fd74130d1f69c944bbf0a7b06761ae7211154b8759e94d2", size = 6595983, upload-time = "2026-06-11T12:45:26.963Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f3/19782aa04c960968bef8c5539329d8e3bbc3364e2e46d19eb5e5cc5e43b7/grpcio-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2c2e2ae6867c2966b8daccc836d54a13218e0007e9a490aeb81dd05be64d22d7", size = 7303455, upload-time = "2026-06-11T12:45:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8c/dea020b6d91508cd84463917a63149ec196ee7db505d032ae43fcb3303b9/grpcio-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:766bc7c9a9c340342f4c864ccbda8e78111e4751f13b895812b9c148fb79e9d0", size = 6809167, upload-time = "2026-06-11T12:45:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/3030dd940408083bd32cd95d634777a71605ade4887154d93e8a89244946/grpcio-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b259a04a737cb3496be0901328eb8b7552ed8df4865d8c8f1cf1bffcfc0776a3", size = 7412536, upload-time = "2026-06-11T12:45:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/e0/dd/1172a9e42b168edcafefad6115346ef619a3fc02158bb170e66ced24bcdd/grpcio-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:85b10a45b8993d195c4f3ff57025b8d1e11834909ee475c403bfa60cb4caefaf", size = 8408276, upload-time = "2026-06-11T12:45:37.78Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/71437c7f3596e5246155c515852795a85a1a8d228190212432b13b97a95d/grpcio-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8ea1936c26b99999b27479853039a7f34713f56c49375ad52b38535ec93a796c", size = 7849660, upload-time = "2026-06-11T12:45:40.627Z" }, + { url = "https://files.pythonhosted.org/packages/65/40/7debc0da45d2efebafb82da75644be347497fe4ee250514b8cd3b86ae8bf/grpcio-1.81.1-cp312-cp312-win32.whl", hash = "sha256:a185a04039df6cae8648bc8ab6d6fde7bf94f7188ecf7828e76ac52eef1e41d6", size = 4185819, upload-time = "2026-06-11T12:45:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8fe3ba5ed462067774ebc1f9c7f26aa7ebcc280ddd476be107153de1339e/grpcio-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ad74f8bb1a18963914c5452d289422830b39459e8776ebbcd207be1fbfb1d94", size = 4930461, upload-time = "2026-06-11T12:45:45.775Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/27/629cfe58c582f92ded066c4a07d1a057ff617118ab7973200f770bd853cb/huggingface_hub-1.19.0.tar.gz", hash = "sha256:fd771622182d40977272a923953ee3b1b13538f9f8a7f5d78398f10af0f1c0bd", size = 824721, upload-time = "2026-06-11T12:33:18.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a5/558da89f66464d8d0229ff497e8b8666977de2d8cf48c28a2862ecf1250f/huggingface_hub-1.19.0-py3-none-any.whl", hash = "sha256:1dc72e1f6b4d6df6b30eb72e57d00514ef453d660f04af2b87f0e67267f31ee0", size = 693398, upload-time = "2026-06-11T12:33:16.695Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "lance-namespace" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lance-namespace-urllib3-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/fd/3a8731b2ed83ba198b15b5963c6df4836736057f23206107b0ab4a5f57fd/lance_namespace-0.8.2.tar.gz", hash = "sha256:78cd6ad2f2764bccded1d8b64474419cc5571956b68a23ad2770977ddaeb03a1", size = 11281, upload-time = "2026-06-05T04:46:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/cb/7f3cc83b8b35a27a27539c3086562d11010f10ca113808ce1078308ca5c0/lance_namespace-0.8.2-py3-none-any.whl", hash = "sha256:6531a4d8b95f201835b954a949f890d03cbc3124aca5f1dd21d999157a08935f", size = 13113, upload-time = "2026-06-05T04:46:27.781Z" }, +] + +[[package]] +name = "lance-namespace-urllib3-client" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/98/a0bb656a4f2d5989e1267a62acbb5a9ed8eb15ac45fbfe380b5a59dba642/lance_namespace_urllib3_client-0.8.2.tar.gz", hash = "sha256:82f0a5c9b6b7fde67326d6038b89ed807e8d14692e461246f1a7df5c36b804d6", size = 222291, upload-time = "2026-06-05T04:46:24.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/58/6a993bf50375170547d0e0bfe9189cc9b378b89482dc2c7bb75ef170a49a/lance_namespace_urllib3_client-0.8.2-py3-none-any.whl", hash = "sha256:cb8dc098fcd42f848eb5206fb49ebc3b5f162ee32b5c4155a5048ffd30a7cd37", size = 364909, upload-time = "2026-06-05T04:46:26.504Z" }, +] + +[[package]] +name = "lancedb" +version = "0.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "lance-namespace" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/2f/d5a4b2a5bb1f800936c76a6d8a4daf127a86fcab621eeb70b574a5adc774/lancedb-0.33.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4eaf6fa7c2eac619208f1d396f4de635ee0f535673067118a31c1181575c48b", size = 48338115, upload-time = "2026-05-28T20:37:55.88Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/31787b93a856b2c31382c7771dc22fb05575b70b87c9efe454269f4f0948/lancedb-0.33.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c6c2402ed2744245ae76c4167c0461da0a7a80f1608e0ec491c1548ea2b4302", size = 51162262, upload-time = "2026-05-28T20:37:59.101Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/081cc29f8e06bf12191b99ab3fe702aceebdb0914476b821a8c0445cacc8/lancedb-0.33.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ebf1ffad811e6254a93931a79489ba1f21f48564bdfa06abae846f5fcaaf3e8", size = 54381368, upload-time = "2026-05-28T20:38:02.2Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bd/e0f4bd621f10ecf96a801b0166e87799ed7ca5a9dbabcef9a6c766a58ef3/lancedb-0.33.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:13da39f80adfea59e5831fe64e4166b2d70a2f843e6507bf644c4fe4c350087c", size = 51188986, upload-time = "2026-05-28T20:38:05.375Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1a/a8647a432ac6aa59cdce1fc061a7050ea4278bcab364539b78af2ecf72d2/lancedb-0.33.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:21b712825f0a00225e8974a41352c4ea84b0899ef8c23b17f672fadc38bd8346", size = 54440958, upload-time = "2026-05-28T20:38:08.474Z" }, + { url = "https://files.pythonhosted.org/packages/08/6c/d0cc8da784cd7ed3b4940a5d1f3e7702e2d99a0a348ba81a376eed782810/lancedb-0.33.0-cp39-abi3-win_amd64.whl", hash = "sha256:4ba78c6202b0f6c2ce8edc7aa470e550d2da56271c7cbdd10428613f1f7126f9", size = 58751944, upload-time = "2026-05-28T20:38:11.549Z" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, +] + +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyarrow" +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "qdrant-client" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "httpx", extra = ["http2"] }, + { name = "numpy" }, + { name = "portalocker" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/45/5b1bdd15a3c7730eefb9c113600829e20d689b82b5a23f9e07d107094004/qdrant_client-1.18.0.tar.gz", hash = "sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4", size = 352580, upload-time = "2026-05-11T14:12:38.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/10/c437bd2ac41ef30d3019063e6ce537dc111e9214473b337ee88f7fa6359a/qdrant_client-1.18.0-py3-none-any.whl", hash = "sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd", size = 398126, upload-time = "2026-05-11T14:12:36.998Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, +] + +[[package]] +name = "rerun-sdk" +source = { editable = "../../../rerun_py" } +dependencies = [ + { name = "attrs" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "psutil" }, + { name = "pyarrow" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +catalog = [ + { name = "datafusion" }, + { name = "pandas" }, +] +dataloader = [ + { name = "av" }, + { name = "pillow" }, + { name = "torch" }, + { name = "torchvision" }, +] + +[package.metadata] +requires-dist = [ + { name = "attrs", specifier = ">=23.1.0" }, + { name = "av", marker = "extra == 'dataloader'" }, + { name = "av", marker = "extra == 'tests'", specifier = ">=14.2.0" }, + { name = "datafusion", marker = "extra == 'all'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'catalog'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'datafusion'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'dataplatform'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'tests'", specifier = "==53.0.0" }, + { name = "inline-snapshot", marker = "extra == 'tests'", specifier = "==0.31.1" }, + { name = "numpy", specifier = ">=2" }, + { name = "opencv-python", marker = "extra == 'tests'", specifier = ">4.6" }, + { name = "opentelemetry-api", marker = "extra == 'tracing'" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "extra == 'tracing'" }, + { name = "opentelemetry-sdk", marker = "extra == 'tracing'" }, + { name = "pandas", marker = "extra == 'all'", specifier = ">=2" }, + { name = "pandas", marker = "extra == 'catalog'", specifier = ">=2" }, + { name = "pandas", marker = "extra == 'datafusion'", specifier = ">=2" }, + { name = "pandas", marker = "extra == 'dataplatform'", specifier = ">=2" }, + { name = "pandas", marker = "extra == 'tests'", specifier = ">=2" }, + { name = "pillow", specifier = ">=8.0.0" }, + { name = "pillow", marker = "extra == 'dataloader'", specifier = ">=8.0.0" }, + { name = "polars", marker = "extra == 'tests'", specifier = "==1.36.1" }, + { name = "psutil", specifier = ">=7.0" }, + { name = "pyarrow", specifier = ">=18.0.0" }, + { name = "pytest", marker = "extra == 'tests'", specifier = "==9.0.3" }, + { name = "rerun-notebook", marker = "extra == 'all'", editable = "../../../rerun_notebook" }, + { name = "rerun-notebook", marker = "extra == 'notebook'", editable = "../../../rerun_notebook" }, + { name = "semver", marker = "extra == 'tests'", specifier = ">=3.0,<3.1" }, + { name = "syrupy", marker = "extra == 'tests'", specifier = "==5.0.0" }, + { name = "tomli", marker = "extra == 'tests'", specifier = "==2.0.1" }, + { name = "torch", marker = "extra == 'dataloader'", specifier = ">=2.5" }, + { name = "torch", marker = "extra == 'tests'", specifier = ">=2.5" }, + { name = "torchvision", marker = "extra == 'dataloader'" }, + { name = "torchvision", marker = "extra == 'tests'" }, + { name = "typing-extensions", specifier = ">=4.5" }, +] +provides-extras = ["all", "catalog", "datafusion", "dataloader", "dataplatform", "notebook", "tests", "tracing"] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "torch" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/bb/285d643f254731294c9b595a007eac39db4600a98682d7bca688f42ca164/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", size = 88010197, upload-time = "2026-05-13T14:55:35.414Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c2/64b06cbb7830fb3cd9be13e1158b31a3f36b68e6a209105ee3c9d9480be0/torch-2.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b958caff4a14d3a3b0b2dfc6a378f64dda9728a9dad28c08a0db9ce4dafb549", size = 122988114, upload-time = "2026-05-13T14:54:42.153Z" }, +] + +[[package]] +name = "torchvision" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c8/5cd91932f7f3671b0743dc4ae1a4c16b1d0b45bf4087976277d325bda718/torchvision-0.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1a6dd742a150645126df9e0b2e449874c1d635897c773b322c2e067e98382dfe", size = 1758824, upload-time = "2026-05-13T14:57:15.227Z" }, + { url = "https://files.pythonhosted.org/packages/d9/36/7fb7d19477b3d93283b52fea11fa8ee30ab9064a08c97b4a6b91445e26cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65772ff3ec4f4f5d680e30019835555dd239e7fefee4b0a846375fe1cb1592ef", size = 7831034, upload-time = "2026-05-13T14:57:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/dfd894c3f8b01b5b33fde990f0159c1926ebc7b6e2c4193e2efb7da3c4cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7a9966a088d06b4cf6c610e03be62de469efa6f2cd2e7c7eed8e925ed6af59ac", size = 7579774, upload-time = "2026-05-13T14:56:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0c/722e989f9cf026e97ef7cb24a9bb1859e099f72d247ae35388fb89729f73/torchvision-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c037709072ca9b19750c0cbe9e8bb6f91c9a1be1befa26df33e281deccbd8c7", size = 4021073, upload-time = "2026-05-13T14:57:00.848Z" }, +] + +[[package]] +name = "tqdm" +version = "4.68.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/1e/a5e2602851e2a8c49372e3ed4d8437fa43c996941899af7aea6e30173a3d/tqdm-4.68.0.tar.gz", hash = "sha256:c627124266fe7904cabb70e88a940d75a06b889a0b11680307a67c18ce094f19", size = 170209, upload-time = "2026-06-05T13:17:20.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/51/ab57af723f38a041c5027040fc6fa93a9a066ca7294fc380c1c0810813b2/tqdm-4.68.0-py3-none-any.whl", hash = "sha256:b79a3ae57db4c870a55352e43abb33b329557cd75b1483028909286ff22a2c03", size = 78247, upload-time = "2026-06-05T13:17:18.272Z" }, +] + +[[package]] +name = "transformers" +version = "5.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/f7/418169401560cec2b61512e6bf37b0cfb4c8e27700cfa868a1de073cb65d/transformers-5.13.1.tar.gz", hash = "sha256:1e2452d6778a7482158df5d5dacf6bf775d5b2fdcfce33caaf7f6b0e5f3e3397", size = 9196891, upload-time = "2026-07-11T09:15:50.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/47/54eacf96b5c835bbd6ca631aa2740e7705ed63d9e3a8afd2d2cc6d09cae5/transformers-5.13.1-py3-none-any.whl", hash = "sha256:53f0ea8aa397e29244c2377ba981bcaf0c87adcf44fbdd447ef6306522afcacd", size = 11503977, upload-time = "2026-07-11T09:15:46.801Z" }, +] + +[[package]] +name = "triton" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260518" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, +] + +[[package]] +name = "types-tqdm" +version = "4.68.0.20260608" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/e0/3facccb1ff69970c73fca7a8028286c233d4c1312c475a65fb3d896f56d9/types_tqdm-4.68.0.20260608.tar.gz", hash = "sha256:e1dfddf8770fbc30ecaf95ae57c286397831235064308f7dfc2b1d6684a76107", size = 18470, upload-time = "2026-06-08T06:26:06.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/e8/61d95bfd49d1609fb8e8c5e06f4a094183411988a6f448873f5de6602499/types_tqdm-4.68.0.20260608-py3-none-any.whl", hash = "sha256:450a6e7e9e9b604928968927c414b32970e40091213c4180e1ed470905b13eff", size = 24858, upload-time = "2026-06-08T06:26:05.741Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/examples/python/droid_semantic_search/vector_store.py b/examples/python/droid_semantic_search/vector_store.py new file mode 100644 index 000000000000..42092012f965 --- /dev/null +++ b/examples/python/droid_semantic_search/vector_store.py @@ -0,0 +1,161 @@ +"""Pluggable local vector-store backends for the DROID frame index. + +`ingest.py` and `search.py` talk to a vector store only through the small +`VectorStore` interface here, so the same code path works whether you pick +LanceDB (`--backend lance`) or Qdrant (`--backend qdrant`). Both run fully +locally on disk — no server to stand up — so the example stays one-command. +Adding another store (Pinecone, pgvector, Milvus, …) is a third subclass. + +The index is written from the columnar `(segment_id, camera, timestamp_ms, +vector)` Arrow table that `ingest.py` assembles. Searches return hits carrying +those three metadata fields plus a `similarity` in `[-1, 1]` (cosine), +normalized here so callers never have to know which backend's distance/score +convention is in play. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + import pyarrow as pa + +BACKENDS = ("lance", "qdrant") + +# Per-backend default on-disk location, used when `--db-path` is omitted. +DEFAULT_PATHS = { + "lance": "./droid_lancedb", + "qdrant": "./droid_qdrant", +} + + +class VectorStore(ABC): + """A local on-disk vector index over `(segment_id, camera, timestamp_ms, vector)` rows.""" + + @abstractmethod + def write(self, table: pa.Table) -> None: + """(Over)write the index from *table*, replacing any existing table/collection. + + *table* has columns `segment_id` (string), `camera` (string), + `timestamp_ms` (int64), and `vector` (fixed-size list of float32). + """ + + @abstractmethod + def search(self, vector: list[float], top_k: int) -> list[dict[str, Any]]: + """Cosine nearest-neighbor search. + + Returns up to *top_k* hit dicts, each with `segment_id`, `camera`, + `timestamp_ms`, and a cosine `similarity` (higher is closer). + """ + + +def open_store(backend: str, path: str, table: str) -> VectorStore: + if backend == "lance": + return LanceStore(path, table) + if backend == "qdrant": + return QdrantStore(path, table) + raise ValueError(f"Unknown backend '{backend}'; expected one of {BACKENDS}.") + + +class LanceStore(VectorStore): + """LanceDB backend. Returns cosine *distance*, which we flip to similarity.""" + + def __init__(self, path: str, table: str) -> None: + self._path = path + self._table = table + + def write(self, table: pa.Table) -> None: + import lancedb + + dim = table.schema.field("vector").type.list_size + + db = lancedb.connect(self._path) + tbl = db.create_table(self._table, data=table, mode="overwrite") + print(f"Wrote {table.num_rows} rows ({dim}-dim) to LanceDB table '{self._table}' in {self._path}") + + # An ANN index needs enough rows to train; small demo tables fall back to + # brute-force search, which is exact and plenty fast at this scale. + try: + tbl.create_index(metric="cosine", vector_column_name="vector") + print("Built ANN index (cosine).") + except Exception as exc: + print(f"Skipped ANN index ({exc}); brute-force cosine search will be used.") + + def search(self, vector: list[float], top_k: int) -> list[dict[str, Any]]: + import lancedb + + tbl = lancedb.connect(self._path).open_table(self._table) + hits = tbl.search(vector).metric("cosine").limit(top_k).to_list() + return [ + { + "segment_id": h["segment_id"], + "camera": h["camera"], + "timestamp_ms": h["timestamp_ms"], + "similarity": 1.0 - float(h["_distance"]), # cosine distance -> similarity + } + for h in hits + ] + + +class QdrantStore(VectorStore): + """Qdrant backend in local (embedded) mode. Returns cosine *score* directly.""" + + def __init__(self, path: str, collection: str) -> None: + self._path = path + self._collection = collection + + def write(self, table: pa.Table) -> None: + from qdrant_client import QdrantClient, models + + dim = table.schema.field("vector").type.list_size + segment_ids = table.column("segment_id").to_pylist() + cameras = table.column("camera").to_pylist() + timestamps_ms = table.column("timestamp_ms").to_pylist() + vectors = table.column("vector").to_pylist() + + client = QdrantClient(path=self._path) + + # Mirror Lance's overwrite semantics: drop any prior collection first. + if client.collection_exists(self._collection): + client.delete_collection(self._collection) + client.create_collection( + collection_name=self._collection, + vectors_config=models.VectorParams(size=dim, distance=models.Distance.COSINE), + ) + + points = [ + models.PointStruct( + id=i, + vector=vectors[i], + payload={ + "segment_id": segment_ids[i], + "camera": cameras[i], + "timestamp_ms": timestamps_ms[i], + }, + ) + for i in range(table.num_rows) + ] + client.upsert(collection_name=self._collection, points=points) + print(f"Wrote {table.num_rows} rows ({dim}-dim) to Qdrant collection '{self._collection}' in {self._path}") + + def search(self, vector: list[float], top_k: int) -> list[dict[str, Any]]: + from qdrant_client import QdrantClient + + client = QdrantClient(path=self._path) + result = client.query_points( + collection_name=self._collection, + query=vector, + limit=top_k, + with_payload=True, + ) + return [ + { + "segment_id": payload["segment_id"], + "camera": payload["camera"], + "timestamp_ms": payload["timestamp_ms"], + "similarity": float(point.score), # Qdrant cosine score is already a similarity + } + for point in result.points + if (payload := point.payload) is not None + ] diff --git a/examples/python/graph_lattice/graph_lattice.py b/examples/python/graph_lattice/graph_lattice.py index 569b58d16b8c..8acc6e236970 100755 --- a/examples/python/graph_lattice/graph_lattice.py +++ b/examples/python/graph_lattice/graph_lattice.py @@ -14,7 +14,7 @@ # Graph Lattice This is a minimal example that logs a graph (node-link diagram) that represents a lattice. -In this example, the node positions—and therefore the graph layout—are computed by Rerun internally. +In this example, the node positions — and therefore the graph layout — are computed by Rerun internally. The full source code for this example is available [on GitHub](https://github.com/rerun-io/rerun/blob/latest/examples/python/graph_lattice). diff --git a/examples/python/graphs/graphs.py b/examples/python/graphs/graphs.py index 89e0be59d128..0992e58859af 100755 --- a/examples/python/graphs/graphs.py +++ b/examples/python/graphs/graphs.py @@ -30,7 +30,7 @@ DESCRIPTION = """ # Graphs This example shows various graph visualizations that you can create using Rerun. -In this example, the node positions—and therefore the graph layout—are computed by Rerun internally using a force-based layout algorithm. +In this example, the node positions — and therefore the graph layout — are computed by Rerun internally using a force-based layout algorithm. You can modify how these graphs look by changing the parameters of the force-based layout algorithm in the selection panel. diff --git a/examples/python/log_file/README.md b/examples/python/log_file/README.md index 055e93b77458..af51ffdca78d 100644 --- a/examples/python/log_file/README.md +++ b/examples/python/log_file/README.md @@ -5,7 +5,7 @@ thumbnail = "https://static.rerun.io/log_file/d86e525cce547cd2dde8e2d7619c01bd3b thumbnail_dimensions = [480, 480] --> -Demonstrates how to log any file from the SDK using the [`Importer`](https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview?speculative-link) machinery. +Demonstrates how to log any file from the SDK using the [`Importer`](https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview) machinery. Usage: ```bash diff --git a/examples/python/log_file/log_file.py b/examples/python/log_file/log_file.py index 573c2824dd0b..88f3c01383e2 100755 --- a/examples/python/log_file/log_file.py +++ b/examples/python/log_file/log_file.py @@ -2,7 +2,7 @@ """ Demonstrates how to log any file from the SDK using the `Importer` machinery. -See for more information. +See for more information. """ from __future__ import annotations diff --git a/examples/python/mast3r_slam/README.md b/examples/python/mast3r_slam/README.md index 21579b6b8ca3..0b59858f9e62 100644 --- a/examples/python/mast3r_slam/README.md +++ b/examples/python/mast3r_slam/README.md @@ -10,7 +10,7 @@ https://vimeo.com/1064055355?autoplay=1&loop=1&autopause=0&background=1&muted=1& ## Background -Mast3r-slam is a realtime monocular slam system that is based on Mast3r, a two view 3D reconstruction and matching prior. Equipped with this strong prior, the system is robust on in-the-wild video sequences despite making no assumption on a fixed or parametric camera model beyond a unique camera center. It introduces efficient methods for pointmap matching, camera tracking and local fusion, graph construction and loop closure, and second-order global optimisation. With known calibration, a simple modification to the system achieves state-of-the-art performance across various benchmarks. +Mast3r-slam is a realtime monocular slam system that is based on Mast3r, a two view 3D reconstruction and matching prior. Equipped with this strong prior, the system is robust on in-the-wild video sequences despite making no assumption on a fixed or parametric camera model beyond a unique camera center. It introduces efficient methods for pointmap matching, camera tracking and local fusion, graph construction and loop closure, and second-order global optimization. With known calibration, a simple modification to the system achieves state-of-the-art performance across various benchmarks. ## Run the code diff --git a/examples/python/objectron/objectron/proto/a_r_capture_metadata.proto b/examples/python/objectron/objectron/proto/a_r_capture_metadata.proto index 95dc82d778e8..5c38a89e1316 100644 --- a/examples/python/objectron/objectron/proto/a_r_capture_metadata.proto +++ b/examples/python/objectron/objectron/proto/a_r_capture_metadata.proto @@ -562,8 +562,8 @@ message ARMeshGeometry { } message Face { - /// Indices of vertices defining the face from correspondent array of parent - /// message. A typical face is triangular. + // Indices of vertices defining the face from correspondent array of parent + // message. A typical face is triangular. repeated int32 vertex_indices = 1 [packed = true]; } diff --git a/examples/python/plots/plots.py b/examples/python/plots/plots.py index 4ac7fd35aa28..ffee77257833 100755 --- a/examples/python/plots/plots.py +++ b/examples/python/plots/plots.py @@ -118,6 +118,50 @@ def log_classification() -> None: ) +def log_states() -> None: + # Configure how each raw state value is displayed (label, color). This is + # time-independent, so we log it as static. + rr.log( + "states/trend", + rr.StateConfiguration( + values=["rising", "falling"], + labels=["Rising", "Falling"], + colors=[0x4CAF50FF, 0xEF5350FF], + ), + static=True, + ) + rr.log( + "states/level", + rr.StateConfiguration( + values=["low", "mid", "high"], + # Wrapped as `np.uint32` so that a length-3 list isn't mistaken for a single RGB color. + colors=np.array([0x5C6BC0FF, 0x9E9E9EFF, 0xFFB300FF], dtype=np.uint32), + ), + static=True, + ) + + # Derive discrete states from the same sine wave as `log_trig`, and log a + # `StateChange` whenever a state transition happens. The state timeline view + # displays these as horizontal colored lanes over time. + trend = None + level = None + for t in range(int(tau * 2 * 100.0)): + rr.set_time("frame_nr", sequence=t) + + sin_of_t = sin(float(t) / 100.0) + cos_of_t = cos(float(t) / 100.0) + + new_trend = "rising" if cos_of_t >= 0.0 else "falling" + if new_trend != trend: + trend = new_trend + rr.log("states/trend", rr.StateChange(state=trend)) + + new_level = "high" if sin_of_t > 0.5 else "low" if sin_of_t < -0.5 else "mid" + if new_level != level: + level = new_level + rr.log("states/level", rr.StateChange(state=level)) + + def main() -> None: parser = argparse.ArgumentParser( description="demonstrates how to integrate python's native `logging` with the Rerun SDK", @@ -152,10 +196,15 @@ def main() -> None: }, ), ), - rrb.TimeSeriesView( - name="Spiral", - origin="/spiral", - overrides={"spiral": rr.SeriesLines.from_fields(names=["0.01t cos(0.01t)", "0.01t sin(0.01t)"])}, # type: ignore[arg-type] + rrb.Horizontal( + rrb.TimeSeriesView( + name="Spiral", + origin="/spiral", + overrides={ + "spiral": rr.SeriesLines.from_fields(names=["0.01t cos(0.01t)", "0.01t sin(0.01t)"]) + }, # type: ignore[arg-type] + ), + rrb.StateTimelineView(name="States", origin="/states"), ), row_shares=[2, 1], ), @@ -174,6 +223,7 @@ def main() -> None: log_trig() log_spiral() log_classification() + log_states() rr.script_teardown(args) diff --git a/examples/python/prompt_depth_anything/README.md b/examples/python/prompt_depth_anything/README.md index 309a98c0eb5d..483851f18c18 100644 --- a/examples/python/prompt_depth_anything/README.md +++ b/examples/python/prompt_depth_anything/README.md @@ -10,7 +10,7 @@ thumbnail_dimensions = [480, 275] https://vimeo.com/1052753560?autoplay=1&loop=1&autopause=0&background=1&muted=1&ratio=2802:1790 ## Background -Prompt Depth Anything builds on DepthAnythingV2 by leveraging a low-resolution “prompt” depth map captured from an iPhone LiDAR along with its corresponding image to generate metric depth maps at resolutions up to 4K. This approach benefits applications that require high-resolution, metric, and multi-view consistent depth—such as 3D reconstruction and generalized robotic grasping. In this example, you can use the output from a raw Polycam scan to produce high-resolution depth maps for downstream applications. +Prompt Depth Anything builds on DepthAnythingV2 by leveraging a low-resolution “prompt” depth map captured from an iPhone LiDAR along with its corresponding image to generate metric depth maps at resolutions up to 4K. This approach benefits applications that require high-resolution, metric, and multi-view consistent depth — such as 3D reconstruction and generalized robotic grasping. In this example, you can use the output from a raw Polycam scan to produce high-resolution depth maps for downstream applications. ## Run the code diff --git a/examples/python/rerun_export/README.md b/examples/python/rerun_export/README.md index 0eff138ff5bc..11167350227a 100644 --- a/examples/python/rerun_export/README.md +++ b/examples/python/rerun_export/README.md @@ -1,5 +1,6 @@ + +This example demonstrates how Rerun's [chunk processing API](https://rerun.io/docs/concepts/logging-and-ingestion/chunk-processing-api) can be used to assemble a robot recording from multiple file sources, including preprocessing to modify or augment the data. + + + + + + + + + +## Introduction + +### Input data + +While the example uses simulated data, it's intentionally designed to cover real-world challenges that should sound familiar to most roboticists: + +- incomplete data requiring preprocessing +- custom data types +- bugs in the recorded data +- data spread across multiple files in different formats + +Specifically, we use a recording of a dual-robot-arm setup, consisting of: + +| `episode.mcap` | `offsets.json` | URDF files | +| --- | --- | --- | +| Base recording (videos, sensors, …). | Static world offsets for each robot. | Robot & scene models as [URDF](https://en.wikipedia.org/wiki/URDF). +| Some cameras have wrong parameters.
No dynamic 3D transforms were recorded,
only joint states in a custom Protobuf schema. | Saved outside of base recording. | `robot.urdf`, `scene.urdf`, mesh data | + +### Goals + +Our task is to handle and process all the different data sources: + +- read, convert and fix MCAP data +- compute 3D transforms using MCAP joint states and URDF +- handle URDFs + - add `scene.urdf` and 2x `robot.urdf` + - modify visual meshes with a custom color & transparency per robot +- add static transforms from JSON + +…and merge them into one coherent recording. + +## Processing pipeline + +Solving such a task in an elegant way requires a non-trivial amount of engineering, but Rerun's [chunk processing API](https://rerun.io/docs/concepts/logging-and-ingestion/chunk-processing-api) gives us all the tools to properly structure the pipeline: + + + + + + + + + + + +The example code implements this pipeline and contains several explanatory comments. +We recommend reading the concept explanations below, before going through the `main()` function of [`robot_data_preprocessing.py`](robot_data_preprocessing.py) to understand the code structure. + +> ℹ️ Note that we create two separate RRD files in this example. +For the Rerun Viewer or Catalog, both *physical* files form one [*logical* recording](https://rerun.io/docs/concepts/logging-and-ingestion/recordings#logical-vs-physical-recordings) since they specify the same recording ID. + +### Chunk streams + +[Chunks](https://rerun.io/docs/concepts/logging-and-ingestion/chunks) are the core datastructure of Rerun. +In this example, we use [chunk _streams_](https://rerun.io/docs/concepts/logging-and-ingestion/chunk-processing-api) as the "glue" of our pipeline. + +In a nutshell, `LazyChunkStream`s allow us to define how `Chunk`s get routed through filtering, transformation and output steps. +As the name suggests, these streams are lazily evaluated. +We use an expressive Python API to define the pipeline, but the final execution happens in a multithreaded, GIL-free execution engine written in Rust for maximum efficiency. + +In this example, we use the following sources that can emit `LazyChunkStream`s: +* `McapReader.stream()` for the MCAP recording +* `UrdfTree.stream()` for the URDF models +* manually constructed `LazyChunkStream` for the custom JSON file, using [`Chunk.from_columns(…)`](https://rerun.io/docs/concepts/logging-and-ingestion/chunks#sending-actual-chunks-sendchunks) + +### Lenses + +[Lenses](https://rerun.io/docs/concepts/query-and-transform/lenses) allow us to modify the chunks' components via [`MutateLens`](https://rerun.io/docs/concepts/query-and-transform/lenses#mutate-lenses), or to derive completely new components from them via [`DeriveLens`](https://rerun.io/docs/concepts/query-and-transform/lenses#derive-lenses). +In both cases, we use [`Selector`](https://rerun.io/docs/concepts/query-and-transform/lenses#selectors)s to extract component fields we're interested in, and pipe them through custom transformation functions. + +#### `MutateLens` example + +A simple `MutateLens` used in this example is the one that fixes the swapped `Pinhole:resolution` component of the external camera streams: +```python +mcap_stream.lenses( + MutateLens( + "Pinhole:resolution", + Selector(".").pipe( + lambda resolution: pa.array( + [(height, width) for width, height in resolution.to_pylist()], type=resolution.type + ) + ), + ), + content=["/external/cam_low", "/external/cam_high"], + output_mode="forward_unmatched", +) +``` +The `content` filter makes sure that this lens only gets applied to the external camera entities, while the `output_mode` makes sure we forward the other pinhole entities that don't match unchanged (here: the robot cameras that don't require the fix). + +#### `DeriveLens` example + +A more complex lens setup is required for the forward kinematics, i.e. to compute the 3D transforms from joint values (angles, distances). +For this we need the recorded joint states from the MCAP, as well as the URDF for the kinematic structure. + +Our MCAP file contains joint states encoded in a custom Protobuf schema: +```proto +message JointState { + google.protobuf.Timestamp timestamp = 1; + repeated string joint_names = 2; + repeated double joint_positions = 3; + repeated double joint_velocities = 4; + repeated double joint_efforts = 5; +} +``` +This custom schema is not part of the [directly supported message types](https://rerun.io/docs/concepts/logging-and-ingestion/mcap/message-formats) of the MCAP importer (like e.g. the video streams). But thanks to [schema reflection](https://rerun.io/docs/concepts/logging-and-ingestion/mcap/message-formats#schema-reflection), we still get chunks with queryable Rerun components that we can process in our streams. + +Each input row of joint states contains `N` joint values that map to `N` 3D transforms, for which we want to have a dedicated output row with [`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d) each. +Due to this input-to-output row length mismatch, we use two sequential lenses: +1. For each joint state message… + * select the joint names and values + * use [`UrdfTree.compute_joint_transform_batches`](https://ref.rerun.io/docs/python/stable/urdf/#rerun.urdf.UrdfTree.compute_joint_transform_batches) + * output a single row with a list of `N` 3D transforms. +2. Scatter each computed row into `N` rows with `Transform3D` component columns. + +#### Others + +Besides fixing camera data and computing forward kinematics, we also apply lenses for smaller things like URDF model colorization. + +Finally, the streams are merged and written to two RRD files with the same recording ID to form layers of a single [logical recording](https://rerun.io/docs/concepts/logging-and-ingestion/recordings#logical-vs-physical-recordings). +We use two RRDs for demonstration purposes, but merging into a single RRD would be also possible. + +See the code for all implementation details. + +## Run the code + +```bash +pip install -e examples/python/robot_data_preprocessing +python -m robot_data_preprocessing +``` + +The resulting RRDs can be opened in the viewer: +```bash +rerun examples/python/robot_data_preprocessing/output/*.rrd +``` +Since we use consistent recording IDs, the two output RRD layers show up as a single recording. + + + + +## Summary + +We showed how a non-trivial robotics problem can be solved through a structured data pipeline. +The chunk processing API provides the tools to build such custom pipelines in a compact manner (here: < 200 lines of Python code) while having a powerful execution engine under the hood. + +We also demonstrated how recording IDs can be used to structure RRDs into logical recordings, allowing also to potentially add more layers (e.g. for metadata or extra sensor data). + +Documentation links for further reading: +* [Chunk processing API](https://rerun.io/docs/concepts/logging-and-ingestion/chunk-processing-api) +* [Lenses API](https://rerun.io/docs/concepts/query-and-transform/lenses) +* [Recordings](https://rerun.io/docs/concepts/logging-and-ingestion/recordings) +* [Working with MCAP](https://rerun.io/docs/howto/logging-and-ingestion/mcap) +* [Loading URDF models](https://rerun.io/docs/howto/logging-and-ingestion/urdf) + +## Going further + +In a real-world setting, this kind of processing would be only the first step of data curation, to finalize multiple raw recordings before ingesting them to central storage. + +With Rerun, this would mean registering a dataset to a [catalog server](https://rerun.io/docs/concepts/how-does-rerun-work#catalog-server) (either via Rerun Hub for enterprise scalability, or using the open-source `rerun server` for small-scale local development). +This enables e.g. to perform [queries across recordings](https://rerun.io/docs/concepts/query-and-transform/dataframe-queries) for analytics or to export training data. diff --git a/examples/python/robot_data_preprocessing/input_data/episode.mcap b/examples/python/robot_data_preprocessing/input_data/episode.mcap new file mode 100644 index 000000000000..b8f90d3ec152 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/episode.mcap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c27a3f89b57d662ba6a3146c9fd8e37fb1566320b46dbb35d5e2e28670a9707a +size 33133865 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/angled_extrusion.stl b/examples/python/robot_data_preprocessing/input_data/meshes/angled_extrusion.stl new file mode 100644 index 000000000000..a9fc652b5870 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/angled_extrusion.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10e6e10a1a5c1efcf743884f7f0dad14c6cc3e15790695bfe5b7038c43f8a4c4 +size 448184 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/base_link.stl b/examples/python/robot_data_preprocessing/input_data/meshes/base_link.stl new file mode 100644 index 000000000000..2a3569984797 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/base_link.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3540af6e3e1883f101417a8c8a0e3f6de829e2aaa965428cab7b3135c46c645 +size 34084 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/camera_mount_d405.stl b/examples/python/robot_data_preprocessing/input_data/meshes/camera_mount_d405.stl new file mode 100644 index 000000000000..11df3350926f --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/camera_mount_d405.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:463d04b9b507e1c8c070e5545eb1b62a4ce9a6d9b7d207675a9596a084b0dc4b +size 673284 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/carriage_left.stl b/examples/python/robot_data_preprocessing/input_data/meshes/carriage_left.stl new file mode 100644 index 000000000000..a168257e281e --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/carriage_left.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9cb543c0258cb01fc10d408595585ba9171389061cb218c6d53fbcaf36783ce7 +size 140684 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/carriage_right.stl b/examples/python/robot_data_preprocessing/input_data/meshes/carriage_right.stl new file mode 100644 index 000000000000..b8c6e6041e6a --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/carriage_right.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b22a0e420f0265c4fbed1238f8550f460fe6b6b70e931b1699c657626373de5 +size 140684 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/corner_bracket.stl b/examples/python/robot_data_preprocessing/input_data/meshes/corner_bracket.stl new file mode 100644 index 000000000000..2952455e8db2 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/corner_bracket.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75e40b0b807f51d238638cd44e787413d8590c1fcd28dff7beaa549c126f1fd0 +size 234284 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/d405_solid.stl b/examples/python/robot_data_preprocessing/input_data/meshes/d405_solid.stl new file mode 100644 index 000000000000..7841d5a2d7c8 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/d405_solid.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a9a84dd9c9a67687e5ad0b37a7ed334dbfea6b0f995fef28524cc8da5f21ed2 +size 1242284 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_1000.stl b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_1000.stl new file mode 100644 index 000000000000..3108d43efe81 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_1000.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a8b99ef84ffd7e91382af9aa733f815a7747c207f64e0499c5889a58d58c8de +size 40084 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_1220.stl b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_1220.stl new file mode 100644 index 000000000000..603001c5faee --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_1220.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b5203c92dfe03b94ed5a679c6cec1f61e48d6d202b5fa03abb032d63313676e +size 40084 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_150.stl b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_150.stl new file mode 100644 index 000000000000..7e946c8f0a41 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_150.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39029c2f95faef3dab1340e4beec221d6866e694e4df0b22795eed6f00b5f893 +size 40084 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_2040_1000.stl b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_2040_1000.stl new file mode 100644 index 000000000000..313f218eff65 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_2040_1000.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acaff3be15f686064b6ec8b985758866b2bf51aed0a9b793d8c8242dde43ee0a +size 84084 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_2040_880.stl b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_2040_880.stl new file mode 100644 index 000000000000..da81df1cf0c8 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_2040_880.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bb04a77ca20a14178aa59f8ef1ef50d26695e748639677b24f0be11efc016d8 +size 84084 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_600.stl b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_600.stl new file mode 100644 index 000000000000..527416edda5e --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/extrusion_600.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25c87bd6a975f1a561e547e8c490ffe4b59d8a962634548242b1f4a5c6607268 +size 40084 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/gripper_left.stl b/examples/python/robot_data_preprocessing/input_data/meshes/gripper_left.stl new file mode 100644 index 000000000000..03625341195a --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/gripper_left.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63ac36c2f4ab9d75341b857212edfa71f45b4eff66068b2dac4c509155cda6c8 +size 285284 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/gripper_right.stl b/examples/python/robot_data_preprocessing/input_data/meshes/gripper_right.stl new file mode 100644 index 000000000000..c5c004ca6860 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/gripper_right.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64540fa6c62d8358336bc3cddfe2aebb40c4323822737544edb67b1ad25800fc +size 282984 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/link_1.stl b/examples/python/robot_data_preprocessing/input_data/meshes/link_1.stl new file mode 100644 index 000000000000..98f38c7aa55f --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/link_1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd3231c1656b55b871e777825897056368f9934230998c4d92931c2a7eca4150 +size 29884 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/link_2.stl b/examples/python/robot_data_preprocessing/input_data/meshes/link_2.stl new file mode 100644 index 000000000000..37a256f318c0 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/link_2.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:633fb1643f22b9c1f9b6b8c5d16c4b1216a77a3c216efd0e70d5138492c7a44d +size 64484 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/link_3.stl b/examples/python/robot_data_preprocessing/input_data/meshes/link_3.stl new file mode 100644 index 000000000000..54c64a416473 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/link_3.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a9fe629c68a475409ecd536761e6cebcdc5a6c858fddbe01bc266017c079c0 +size 46284 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/link_4.stl b/examples/python/robot_data_preprocessing/input_data/meshes/link_4.stl new file mode 100644 index 000000000000..50dbbdd4f26f --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/link_4.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:413868d347309dff92d67d45144a7311ef833354c0ea1c26bea0f450d3909dc7 +size 78484 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/link_5.stl b/examples/python/robot_data_preprocessing/input_data/meshes/link_5.stl new file mode 100644 index 000000000000..19f8f05ed190 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/link_5.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a140433a98d95c9dfe2e3ced4c581cd9fc97a1b3f84a2d3a13f14bfa8b10fdae +size 124984 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/link_6.stl b/examples/python/robot_data_preprocessing/input_data/meshes/link_6.stl new file mode 100644 index 000000000000..0d54ce4223ce --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/link_6.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d72ff84f6a405796834e2f297fbe453557fdd7a8e31691cd6f1110ef0aecca69 +size 51084 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/overhead_mount.stl b/examples/python/robot_data_preprocessing/input_data/meshes/overhead_mount.stl new file mode 100644 index 000000000000..8cb300de4b5b --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/overhead_mount.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54d031aae8599f7ae69adaa834bd5435928484e5815ce4acce964522a115c2db +size 1899284 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/tablelegs.obj b/examples/python/robot_data_preprocessing/input_data/meshes/tablelegs.obj new file mode 100644 index 000000000000..2bc961c02a9a --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/tablelegs.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2514d9d8cd8974e29d95116d9f5b50c1beba5e746e3955f6ccb5ccb594ac6796 +size 10174 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/tabletop.obj b/examples/python/robot_data_preprocessing/input_data/meshes/tabletop.obj new file mode 100644 index 000000000000..66a85ef9e1ff --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/tabletop.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d88abdc152ebe3dabec44100fcbcbb179c132b1b903c97220225a8ba0232687 +size 4702 diff --git a/examples/python/robot_data_preprocessing/input_data/meshes/wormseye_mount.stl b/examples/python/robot_data_preprocessing/input_data/meshes/wormseye_mount.stl new file mode 100644 index 000000000000..f881713fb18e --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/meshes/wormseye_mount.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c939865a1adfc8a148817d6e12d8a04541a192ab9b6af37c94e3cae8330aa64 +size 1955684 diff --git a/examples/python/robot_data_preprocessing/input_data/offsets.json b/examples/python/robot_data_preprocessing/input_data/offsets.json new file mode 100644 index 000000000000..350efb5f3d40 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/offsets.json @@ -0,0 +1,16 @@ +{ + "transforms": [ + { + "parent": "world", + "child": "left_base_link", + "translation": [-0.4575, -0.019, 0.02], + "quaternion_xyzw": [0.0, 0.0, 0.0, 1.0] + }, + { + "parent": "world", + "child": "right_base_link", + "translation": [0.4575, -0.019, 0.02], + "quaternion_xyzw": [0.0, 0.0, -1.0, 0.0] + } + ] +} diff --git a/examples/python/robot_data_preprocessing/input_data/robot.urdf b/examples/python/robot_data_preprocessing/input_data/robot.urdf new file mode 100644 index 000000000000..0aa2ee81de17 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/robot.urdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5773d3a0b306bedebe2fa174fd234d331d81988dcf32042ad93065ce74508ce +size 12818 diff --git a/examples/python/robot_data_preprocessing/input_data/scene.urdf b/examples/python/robot_data_preprocessing/input_data/scene.urdf new file mode 100644 index 000000000000..fa2a2264b781 --- /dev/null +++ b/examples/python/robot_data_preprocessing/input_data/scene.urdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:673d769291613be4abcc286a7a2cd213b5e28de8d9238cd0d69bf6b7d0e49e8a +size 18386 diff --git a/examples/python/robot_data_preprocessing/pyproject.toml b/examples/python/robot_data_preprocessing/pyproject.toml new file mode 100644 index 000000000000..b7c507d8526a --- /dev/null +++ b/examples/python/robot_data_preprocessing/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "robot_data_preprocessing" +version = "0.1.0" +readme = "README.md" +dependencies = ["pyarrow", "rerun-sdk"] + +[project.scripts] +robot_data_preprocessing = "robot_data_preprocessing:main" + +[tool.rerun-example] +skip = true + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/python/robot_data_preprocessing/robot_data_preprocessing.py b/examples/python/robot_data_preprocessing/robot_data_preprocessing.py new file mode 100644 index 000000000000..a97f44a1cab6 --- /dev/null +++ b/examples/python/robot_data_preprocessing/robot_data_preprocessing.py @@ -0,0 +1,196 @@ +""" +Demonstrates how to use Rerun's chunk processing API to assemble a robot recording +from multiple file sources (MCAP, custom data, URDF, …): + +- fix recording errors +- add external static data +- compute joint transforms using URDF +- insert URDF assets +- … + +The resulting merged stream is saved to an RRD file, which can be +opened in the Rerun viewer or registered to a dataset catalog. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pyarrow as pa + +import rerun as rr +from rerun.experimental import Chunk, DeriveLens, LazyChunkStream, McapReader, MutateLens, OptimizationProfile, Selector +from rerun.urdf import UrdfTree + +PARENT_DIR = Path(__file__).parent +DATA_DIR = PARENT_DIR / "input_data" +OUTPUT_DIR = PARENT_DIR / "output" + + +def json_transforms_stream(json_path: Path) -> LazyChunkStream: + """Loads transform data saved in JSON as a chunk stream of static Transform3D.""" + with json_path.open() as f: + transforms = json.load(f)["transforms"] + + chunk = Chunk.from_columns( + "/tf_static/robot_offsets", + indexes=[], + columns=rr.Transform3D.columns( + translation=[transform["translation"] for transform in transforms], + quaternion=[transform["quaternion_xyzw"] for transform in transforms], + parent_frame=[transform["parent"] for transform in transforms], + child_frame=[transform["child"] for transform in transforms], + ), + ) + return LazyChunkStream.from_iter([chunk]) + + +def change_albedo_factor_lens(new_albedo: rr.components.AlbedoFactor) -> MutateLens: + """Replaces Asset3D albedo factors with a fixed color.""" + + return MutateLens( + "Asset3D:albedo_factor", + Selector(".").pipe(lambda old_albedo: pa.array([new_albedo] * len(old_albedo), type=old_albedo.type)), + ) + + +def joints_batch_lens(robot_urdf: UrdfTree, to_entity: str = "/tmp") -> DeriveLens: + """Computes intermediate transform batches from each joint state message using the URDF.""" + return DeriveLens("schemas.proto.JointState:message", output_entity=to_entity).to_component( + "rerun.urdf.JointTransformBatch", + Selector(".").pipe( + lambda joint_state_messages: robot_urdf.compute_joint_transform_batches( + names=Selector(".joint_names").execute(joint_state_messages), + values=Selector(".joint_positions").execute(joint_state_messages), + ) + ), + ) + + +def output_transforms_lens() -> DeriveLens: + """Scatters transform batches into final Transform3D rows per joint.""" + return ( + DeriveLens("rerun.urdf.JointTransformBatch", output_entity="/tf", scatter=True) + .to_component( + rr.Transform3D.descriptor_translation(), + Selector(".[].translation"), + ) + .to_component( + rr.Transform3D.descriptor_quaternion(), + Selector(".[].quaternion"), + ) + .to_component( + rr.Transform3D.descriptor_parent_frame(), + Selector(".[].parent_frame"), + ) + .to_component( + rr.Transform3D.descriptor_child_frame(), + Selector(".[].child_frame"), + ) + ) + + +def main() -> None: + """Run the main chunk-processing pipeline for this example.""" + OUTPUT_DIR.mkdir(exist_ok=True) + + # Create a chunk stream from the MCAP file. + # The reader uses Rerun's MCAP importer (like the viewer or `rerun mcap convert` CLI), + # so we get Rerun components that we can process in-stream. + mcap_stream = McapReader(DATA_DIR / "episode.mcap").stream() + + # The world-to-base transform offsets of the two robots are stored in a separate JSON file. + robot_offsets_stream = json_transforms_stream(DATA_DIR / "offsets.json") + + # Load the same robot URDF twice, with distinct entity path and frame name prefixes for each robot. + robot_urdf_left = UrdfTree.from_file_path( + DATA_DIR / "robot.urdf", + entity_path_prefix="robot_left", + frame_prefix="left_", + static_transform_entity_path="/tf_static/left_robot", + ) + robot_urdf_right = UrdfTree.from_file_path( + DATA_DIR / "robot.urdf", + entity_path_prefix="robot_right", + frame_prefix="right_", + static_transform_entity_path="/tf_static/right_robot", + ) + # Load the scene URDF (table & external cameras). + scene_urdf = UrdfTree.from_file_path(DATA_DIR / "scene.urdf", static_transform_entity_path="/tf_static/scene") + + # The external camera calibration in our example MCAP has swapped width/height. + # We can fix this with a MutateLens. + mcap_stream = mcap_stream.lenses( + MutateLens( + "Pinhole:resolution", + Selector(".").pipe( + lambda resolution: pa.array( + [(height, width) for width, height in resolution.to_pylist()], type=resolution.type + ) + ), + ), + content=["/external/cam_low", "/external/cam_high"], + output_mode="forward_unmatched", + ) + + # For each robot, compute the joint transforms in batches and convert to the final Transform3D chunks. + # We keep the original joint states in the stream ("forward_all") while dropping the temporary batch values. + mcap_stream = ( + mcap_stream + .lenses(joints_batch_lens(robot_urdf_left), content="/robot_left/joint_states", output_mode="forward_all") + .lenses(output_transforms_lens(), content="/tmp", output_mode="drop_unmatched") + .lenses(joints_batch_lens(robot_urdf_right), content="/robot_right/joint_states", output_mode="forward_all") + .lenses(output_transforms_lens(), content="/tmp", output_mode="drop_unmatched") + ) + + # We also modify each robot's visual meshes to have custom colors / transparency by mutating the albedo factor. + robot_urdf_left_stream = robot_urdf_left.stream().lenses( + change_albedo_factor_lens(rr.components.AlbedoFactor([80, 120, 175, 125])), + content="/robot_left/wxai/visual_geometries/**", + output_mode="forward_unmatched", + ) + robot_urdf_right_stream = robot_urdf_right.stream().lenses( + change_albedo_factor_lens(rr.components.AlbedoFactor([200, 120, 90, 125])), + content="/robot_right/wxai/visual_geometries/**", + output_mode="forward_unmatched", + ) + + # Drop the collision meshes from each URDF. + # (you can also disable them in the viewer, but here we demonstrate how to drop them entirely) + robot_urdf_left_stream = robot_urdf_left_stream.drop(content="/robot_left/wxai/collision_geometries/**") + robot_urdf_right_stream = robot_urdf_right_stream.drop(content="/robot_right/wxai/collision_geometries/**") + + # Merge the streams in logical groups (base recording and URDF data). + # (alternatively we could also merge everything in one stream here, if desired) + data_stream = LazyChunkStream.merge( + mcap_stream, + robot_offsets_stream, + ) + urdf_stream = LazyChunkStream.merge( + robot_urdf_left_stream, + robot_urdf_right_stream, + scene_urdf.stream(), + ) + + # Run the pipeline, materialize into a ChunkStore and optimize it before writing to an RRD. + # Here we use an optimization profile suited for object-store (query & stream applications). + data_stream.collect(optimize=OptimizationProfile.OBJECT_STORE).write_rrd( + OUTPUT_DIR / "data.rrd", + application_id="rerun_example_robot_data_preprocessing", + recording_id="episode", + ) + # Write also the URDF streams to an RRD. + # Note how we use the same `recording_id` here to group the two RRD layers into the same logical recording. + # https://rerun.io/docs/concepts/logging-and-ingestion/recordings#logical-vs-physical-recordings + urdf_stream.collect(optimize=OptimizationProfile.OBJECT_STORE).write_rrd( + OUTPUT_DIR / "urdf.rrd", + application_id="rerun_example_robot_data_preprocessing", + recording_id="episode", + ) + + print(f"\nWrote output RRDs to: {OUTPUT_DIR}") + + +if __name__ == "__main__": + main() diff --git a/examples/python/ros_node/README.md b/examples/python/ros_node/README.md index 847b394ad7e0..95fcbb7baacc 100644 --- a/examples/python/ros_node/README.md +++ b/examples/python/ros_node/README.md @@ -1,8 +1,8 @@ A minimal example of creating a ROS node that subscribes to topics and converts the messages to Rerun log calls. @@ -10,15 +10,15 @@ A minimal example of creating a ROS node that subscribes to topics and converts The solution here is mostly a toy example to show how ROS concepts can be mapped to Rerun. - Rerun viewer showing data streamed from the example ROS node - - - - + Rerun viewer showing data streamed from the example ROS node + + + + ## Used Rerun types -[`Image`](https://www.rerun.io/docs/reference/types/archetypes/image), [`DepthImage`](https://rerun.io/docs/reference/types/archetypes/depth_image), [`Pinhole`](https://www.rerun.io/docs/reference/types/archetypes/pinhole), [`Transform3D`](https://www.rerun.io/docs/reference/types/archetypes/transform3d), [`Points3D`](https://www.rerun.io/docs/reference/types/archetypes/points3d), [`LineStrips3D`](https://www.rerun.io/docs/reference/types/archetypes/line_strips3d), [`Scalars`](https://www.rerun.io/docs/reference/types/archetypes/scalars) +[`Image`](https://www.rerun.io/docs/reference/types/archetypes/image), [`DepthImage`](https://rerun.io/docs/reference/types/archetypes/depth_image), [`Pinhole`](https://www.rerun.io/docs/reference/types/archetypes/pinhole), [`Transform3D`](https://www.rerun.io/docs/reference/types/archetypes/transform3d), [`GridMap`](https://www.rerun.io/docs/reference/types/archetypes/grid_map), [`Points3D`](https://www.rerun.io/docs/reference/types/archetypes/points3d), [`LineStrips3D`](https://www.rerun.io/docs/reference/types/archetypes/line_strips3d), [`Scalars`](https://www.rerun.io/docs/reference/types/archetypes/scalars) ## Background The [Robot Operating System (ROS)](https://www.ros.org) helps build robot applications through software libraries and tools. diff --git a/examples/python/ros_node/main.py b/examples/python/ros_node/main.py index 93767bbf89f3..5dba8daed687 100755 --- a/examples/python/ros_node/main.py +++ b/examples/python/ros_node/main.py @@ -19,13 +19,14 @@ import numpy as np import rerun as rr # pip install rerun-sdk +from rerun.components import Colormap try: import cv_bridge import laser_geometry import rclpy from image_geometry import PinholeCameraModel - from nav_msgs.msg import Odometry + from nav_msgs.msg import OccupancyGrid, Odometry from numpy.lib.recfunctions import structured_to_unstructured from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.node import Node @@ -70,6 +71,26 @@ def __init__(self) -> None: self.subscribe("/rgbd_camera/image", Image, self.image_callback) self.subscribe("/rgbd_camera/depth_image", Image, self.depth_callback) self.subscribe("/robot_description", String, self.urdf_callback, latching=True) + self.subscribe( + "/map", + OccupancyGrid, + lambda grid: self.occupancy_grid_callback("/map", grid, Colormap.RvizMap, draw_order=1.0), + latching=True, + ) + self.subscribe( + "/global_costmap/costmap", + OccupancyGrid, + lambda grid: self.occupancy_grid_callback( + "/global_costmap_costmap", grid, Colormap.RvizCostmap, draw_order=2.0, opacity=0.75 + ), + ) + self.subscribe( + "/local_costmap/costmap", + OccupancyGrid, + lambda grid: self.occupancy_grid_callback( + "/local_costmap_costmap", grid, Colormap.RvizCostmap, draw_order=3.0, opacity=0.75 + ), + ) def subscribe( self, topic: str, msg_type: type, callback: Callable[[rclpy.MsgT], None], latching: bool = False @@ -143,6 +164,58 @@ def depth_callback(self, img: Image) -> None: rr.log("rgbd_camera/depth_image", depth_image) rr.log("rgbd_camera/depth_image", rr.CoordinateFrame(frame=img.header.frame_id + "_image_plane")) + def occupancy_grid_callback( + self, + entity_path: str, + grid: OccupancyGrid, + colormap: rr.components.Colormap, + draw_order: float | None = None, + opacity: float | None = None, + ) -> None: + """ + Logs a ROS OccupancyGrid as a Rerun GridMap. + """ + time = Time.from_msg(grid.header.stamp) + rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns")) + + # Log the coordinate frame ID of the map. + # The local offset of the map frame within the grid is handled by the archetype (see below). + rr.log(entity_path, rr.CoordinateFrame(frame=grid.header.frame_id)) + + # ROS maps start at the bottom-left cell; Rerun image buffers are top-row first. + data = np.asarray(grid.data, dtype=np.int8).reshape((grid.info.height, grid.info.width)) + image_data = np.flipud(data).astype(np.uint8, copy=False) + + rr.log( + entity_path, + rr.GridMap( + data=image_data.tobytes(), + format=rr.components.ImageFormat( + width=grid.info.width, + height=grid.info.height, + color_model="L", + channel_datatype="U8", + ), + cell_size=grid.info.resolution, + translation=[ + grid.info.origin.position.x, + grid.info.origin.position.y, + grid.info.origin.position.z, + ], + quaternion=rr.Quaternion( + xyzw=[ + grid.info.origin.orientation.x, + grid.info.origin.orientation.y, + grid.info.origin.orientation.z, + grid.info.origin.orientation.w, + ] + ), + colormap=colormap, + draw_order=draw_order, + opacity=opacity, + ), + ) + def scan_callback(self, scan: LaserScan) -> None: """ Logs a LaserScan after transforming it to line-segments. diff --git a/examples/python/server_tables/pyproject.toml b/examples/python/server_tables/pyproject.toml index 8ef6bce937ba..a1505aec3fc8 100644 --- a/examples/python/server_tables/pyproject.toml +++ b/examples/python/server_tables/pyproject.toml @@ -2,7 +2,7 @@ name = "server_tables" version = "0.1.1" readme = "README.md" -dependencies = ["rerun-sdk", "datafusion==52.3.0"] +dependencies = ["rerun-sdk", "datafusion==53.0.0"] [project.scripts] server_tables = "server_tables:main" diff --git a/examples/python/state_timeline/README.md b/examples/python/state_timeline/README.md new file mode 100644 index 000000000000..a97aee65807a --- /dev/null +++ b/examples/python/state_timeline/README.md @@ -0,0 +1,64 @@ + + +This example simulates a robot work cell and demonstrates every feature of the state timeline view. + + + +## Used Rerun types + +[`StateChange`](https://www.rerun.io/docs/reference/types/archetypes/state_change), [`StateConfiguration`](https://www.rerun.io/docs/reference/types/archetypes/state_configuration), [`TextDocument`](https://www.rerun.io/docs/reference/types/archetypes/text_document) + +## Logging and visualizing with Rerun + +Discrete states are logged with the [`StateChange`](https://www.rerun.io/docs/reference/types/archetypes/state_change) archetype. +Each logged `StateChange` marks a transition into a new state, and the state timeline view displays these as horizontal colored lanes over time. + +The example covers all features of the view: + +- **State transitions**: each entity gets its own lane, and a state extends until the next transition (`robot/task`). +- **Custom styling**: [`StateConfiguration`](https://www.rerun.io/docs/reference/types/archetypes/state_configuration) maps raw state values to display labels and colors (`robot/task`). +- **Automatic styling**: without a configuration, raw values are used as labels and colors come from a built-in palette (`robot/gripper`). +- **Label fallback**: a `labels` array shorter than `values` falls back to the raw value for the missing entries (`robot/connection`). +- **State resets**: logging an empty string resets the state and leaves a gap in the lane (`robot/connection`). +- **Per-state visibility**: the `visible` array of `StateConfiguration` hides noisy states (`robot/diagnostics`). +- **Columnar logging**: batches of state changes can be sent in one call with `send_columns`; `null` entries reset the state and leave a gap, just like empty strings (`conveyor`). +- **Beyond strings**: any string, integer, float, or boolean component can drive a state lane, including custom components logged with `DynamicArchetype`. The blueprint maps them onto the `StateChange:state` slot of the state visualizer, so integer enums and boolean flags each get their own lane; `StateConfiguration` applies to them too, keyed by the displayed form of the value (`plc`). +- **Blueprint**: state timeline views are scoped with `origin` and filtered with `contents` entity path expressions. + +## Run the code + +To run this example, make sure you have the Rerun repository checked out and the latest SDK installed: + +```bash +pip install --upgrade rerun-sdk # install the latest Rerun SDK +git clone git@github.com:rerun-io/rerun.git # Clone the repository +cd rerun +``` + +Install the necessary libraries specified in the requirements file: + +```bash +pip install -e examples/python/state_timeline +``` + +To experiment with the provided example, simply execute the main Python script: + +```bash +python -m state_timeline +``` + +If you wish to customize it, explore additional features, or save it, use the CLI with the `--help` option for guidance: + +```bash +python -m state_timeline --help +``` diff --git a/examples/python/state_timeline/pyproject.toml b/examples/python/state_timeline/pyproject.toml new file mode 100644 index 000000000000..5ac82cfb0bf9 --- /dev/null +++ b/examples/python/state_timeline/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "state_timeline" +version = "0.1.0" +readme = "README.md" +dependencies = ["rerun-sdk"] + +[project.scripts] +state_timeline = "state_timeline:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/python/state_timeline/state_timeline.py b/examples/python/state_timeline/state_timeline.py new file mode 100755 index 000000000000..3fe49865735d --- /dev/null +++ b/examples/python/state_timeline/state_timeline.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +""" +Demonstrates all features of the state timeline view. + +Run: +```sh +./examples/python/state_timeline/state_timeline.py +``` +""" + +from __future__ import annotations + +import argparse + +import numpy as np +import pyarrow as pa + +import rerun as rr +import rerun.blueprint as rrb +from rerun.blueprint.datatypes import ComponentSourceKind, VisualizerComponentMapping + +DESCRIPTION = """ +# State timeline +This example simulates a robot work cell and demonstrates every feature of the state timeline view: +state changes, custom styling (labels, colors, per-state visibility), state resets, and columnar logging. + +The full source code for this example is available +[on GitHub](https://github.com/rerun-io/rerun/blob/latest/examples/python/state_timeline). +""".strip() + +CYCLE_DURATION_SEC = 8.0 +NUM_CYCLES = 6 +TOTAL_DURATION_SEC = CYCLE_DURATION_SEC * NUM_CYCLES + + +def log_task() -> None: + # A fully styled lane: `StateConfiguration` maps each raw state value to a display label + # and a color. The configuration is time-independent, so it's logged as static. + rr.log( + "robot/task", + rr.StateConfiguration( + values=["idle", "pick", "place", "error"], + labels=["Idle", "Picking", "Placing", "Error"], + # Wrapped as `np.uint32` so that the list isn't mistaken for a single RGB color. + colors=np.array([0x9E9E9EFF, 0x42A5F5FF, 0x66BB6AFF, 0xEF5350FF], dtype=np.uint32), + ), + static=True, + ) + + # A `StateChange` marks a transition into a new state; the state timeline view extends + # each state until the next transition. + for cycle in range(NUM_CYCLES): + t = cycle * CYCLE_DURATION_SEC + + rr.set_time("time", duration=t) + rr.log("robot/task", rr.StateChange(state="idle")) + + rr.set_time("time", duration=t + 2.0) + rr.log("robot/task", rr.StateChange(state="pick")) + + if cycle == 3: + # Something went wrong during this pick. + rr.set_time("time", duration=t + 3.5) + rr.log("robot/task", rr.StateChange(state="error")) + else: + rr.set_time("time", duration=t + 5.0) + rr.log("robot/task", rr.StateChange(state="place")) + + rr.set_time("time", duration=TOTAL_DURATION_SEC) + rr.log("robot/task", rr.StateChange(state="idle")) + + +def log_gripper() -> None: + # This lane has no `StateConfiguration` at all: raw state values are used as labels, and + # colors are assigned automatically from a built-in palette. + rr.set_time("time", duration=0.0) + rr.log("robot/gripper", rr.StateChange(state="open")) + + for cycle in range(NUM_CYCLES): + t = cycle * CYCLE_DURATION_SEC + + rr.set_time("time", duration=t + 3.0) + rr.log("robot/gripper", rr.StateChange(state="closed")) + + rr.set_time("time", duration=t + 6.0) + rr.log("robot/gripper", rr.StateChange(state="open")) + + +def log_connection() -> None: + # `labels` is shorter than `values` here: states without a label fall back to showing + # their raw value ("degraded"). + rr.log( + "robot/connection", + rr.StateConfiguration( + values=["online", "degraded"], + labels=["Online"], + colors=np.array([0x66BB6AFF, 0xFFB300FF], dtype=np.uint32), + ), + static=True, + ) + + rr.set_time("time", duration=0.0) + rr.log("robot/connection", rr.StateChange(state="online")) + + # An empty string resets the state: the state timeline view shows a gap until the next + # state change. + rr.set_time("time", duration=18.0) + rr.log("robot/connection", rr.StateChange(state="")) + + rr.set_time("time", duration=22.0) + rr.log("robot/connection", rr.StateChange(state="online")) + + rr.set_time("time", duration=34.0) + rr.log("robot/connection", rr.StateChange(state="degraded")) + + rr.set_time("time", duration=42.0) + rr.log("robot/connection", rr.StateChange(state="online")) + + +def log_diagnostics() -> None: + # Per-state visibility: "chatter" is a noisy diagnostic state that would clutter the + # timeline; setting its `visible` entry to `False` hides those segments. + rr.log( + "robot/diagnostics", + rr.StateConfiguration( + values=["ok", "chatter", "fault"], + colors=np.array([0x66BB6AFF, 0x9E9E9EFF, 0xEF5350FF], dtype=np.uint32), + visible=[True, False, True], + ), + static=True, + ) + + transitions = [ + (0.0, "ok"), + (10.0, "chatter"), + (11.0, "ok"), + (20.0, "chatter"), + (21.0, "ok"), + (26.0, "fault"), + (29.0, "ok"), + (40.0, "chatter"), + (41.0, "ok"), + ] + for t, state in transitions: + rr.set_time("time", duration=t) + rr.log("robot/diagnostics", rr.StateChange(state=state)) + + +def log_conveyor() -> None: + # State changes can also be logged in one batch using the columnar API. A `null` state + # resets the state, just like an empty string: the conveyor sensor drops out twice, and + # the state timeline view shows a gap until the next state. The states are wrapped in a + # `pyarrow` array, since a plain Python list would stringify `None` entries. + times = np.arange(0.0, TOTAL_DURATION_SEC, 6.0) + states = pa.array(["running", "stopped", None, "jammed", "running", None, "stopped", "running"], type=pa.utf8()) + + rr.send_columns( + "conveyor", + indexes=[rr.TimeColumn("time", duration=times)], + columns=rr.StateChange.columns(state=states), + ) + + +def log_plc() -> None: + # States don't have to be strings logged with `StateChange`: any string, integer, float, + # or boolean component can be shown as a state lane, including custom components logged + # with `DynamicArchetype`. The blueprint maps them onto the `StateChange:state` slot of + # the state visualizer (see `main()`). + times = np.arange(0.0, TOTAL_DURATION_SEC, 4.0) + rr.send_columns( + "plc", + indexes=[rr.TimeColumn("time", duration=times)], + columns=rr.DynamicArchetype.columns( + archetype="plc", + components={ + # An integer enum: 0 = auto, 1 = manual, 2 = maintenance. + "mode": np.array([0, 0, 1, 1, 0, 1, 1, 2, 2, 2, 1, 0], dtype=np.int32), + # A boolean flag; the emergency stop engages while the robot task errors out. + "estop": np.array([False, False, False, False, False, False, True, True, False, False, False, False]), + }, + ), + ) + + # `StateConfiguration` works for non-string states too: values are matched against the + # displayed form of the state, so the integer enum is keyed by "0", "1", "2". + rr.log( + "plc", + rr.StateConfiguration( + values=["0", "1", "2"], + labels=["Auto", "Manual", "Maintenance"], + ), + static=True, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Demonstrates all features of the state timeline view") + rr.script_add_args(parser) + args = parser.parse_args() + + def map_to_state(source_component: str) -> rrb.Visualizer: + # Install a state visualizer that sources its state from a custom component. + return rr.StateChange().visualizer( + mappings=[ + VisualizerComponentMapping( + target="StateChange:state", + source_kind=ComponentSourceKind.SourceComponent, + source_component=source_component, + ), + ], + ) + + blueprint = rrb.Blueprint( + rrb.Horizontal( + rrb.Vertical( + rrb.StateTimelineView( + name="All states", + origin="/", + overrides={ + # The custom `plc` components are not picked up automatically; each + # one gets its own state lane by explicitly mapping it onto the + # `StateChange:state` slot of a state visualizer. + "plc": [ + map_to_state("plc:mode"), + map_to_state("plc:estop"), + ], + }, + ), + # A view can be scoped to a subtree with `origin`, and its contents can be + # further filtered with entity path expressions. + rrb.StateTimelineView( + name="Robot (without diagnostics)", + origin="/robot", + contents=["$origin/**", "- $origin/diagnostics"], + ), + ), + rrb.TextDocumentView(name="Description", origin="/description"), + column_shares=[3, 1], + ), + rrb.SelectionPanel(state="collapsed"), + ) + + rr.script_setup(args, "rerun_example_state_timeline", default_blueprint=blueprint) + + rr.log("description", rr.TextDocument(DESCRIPTION, media_type=rr.MediaType.MARKDOWN), static=True) + + log_task() + log_gripper() + log_connection() + log_diagnostics() + log_conveyor() + log_plc() + + rr.script_teardown(args) + + +if __name__ == "__main__": + main() diff --git a/examples/python/table_blueprints/README.md b/examples/python/table_blueprints/README.md new file mode 100644 index 000000000000..388d343118e0 --- /dev/null +++ b/examples/python/table_blueprints/README.md @@ -0,0 +1,96 @@ + + +## Table blueprints + +Creates tables whose rows link to recording segment URIs. +The viewer can load those recordings on demand and render row previews with a registered `.rbl` table blueprint. + +The example also adds a boolean `marker_flag` column and names it in the table blueprint. +That column is the per-row flag state: the Viewer renders it as a clickable flag on each grid card, updates the visible table immediately when toggled, and upserts the changed boolean value back to the server using the `rerun:is_table_index` column as the row key. +The column is still regular table data, so its saved values are what you get back when you query the table later. + +Blueprints can also be registered on a dataset's **own segment table** instead of on a separate demo table, using `DatasetEntry.register_blueprint(..., segment_table=True)`. +Use `--target` to choose (see [Run the code](#run-the-code)): + +- `tables`: create the demo tables, each with its own table blueprint. +- `dataset`: register a blueprint on the dataset's segment table (no tables created). +- `both`: do both. + +The segment-table blueprint leaves `segment_preview_column` and `flag_column` unset — the viewer auto-picks the column to preview, and segment tables have no demo flag column. +Flagging does **not** yet work on dataset segment tables: segment tables have no write operations yet, so flag changes cannot be persisted back to the server. Flagging therefore only works on the demo tables created with `--target tables`. + + +Table cards and blueprints are experimental. +Enable `Settings > Experimental > Table cards and blueprints` in the viewer. + +## Dataset-specific setup + +This sample contains a small `Dataset-specific customization` section near the top of `table_blueprints.py`. +Please edit these functions before using it with your own data — the defaults are geared towards RRDs from the DROID dataset and assume that segment-table schema, timeline, entity paths, coordinate frame, and card-title column: + +- `extract_dataset_property_columns` — which segment-table columns get copied into the demo tables. +- `setup_preview_views` — all views (plot, 3D, 2D) shared by the table and segment-table blueprints. Any view type can be used for previews! +- `make_dataset_blueprints` — the table blueprints (preview/flag/card-title columns, timeline). +- `make_segment_table_blueprint` — the blueprint registered on the dataset's own segment table (views, timeline). + +## Run the code + +The sample has two run modes. + +### Local server mode + +Without `--url`, the script starts a temporary local Rerun server, serves a directory of `.rrd` files +as a dataset named `local`, writes the `.rbl` blueprint files, and (depending on `--target`) creates +the demo tables and registers their blueprints with +`TableEntry.register_blueprint(...)` and/or registers a blueprint on the dataset's segment table with `DatasetEntry.register_blueprint(..., segment_table=True)`. + +Run without arguments to serve the checked-in sample files from `tests/assets/rrd/sample_5`: + +```bash +pip install -e examples/python/table_blueprints +table_blueprints +``` + +Or pass any dataset directory containing `.rrd` files: + +```bash +table_blueprints /path/to/dataset +``` + +Choose what the blueprints apply to with `--target` (`tables` by default): + +```bash +table_blueprints --target dataset # only the dataset's segment table +table_blueprints --target both # demo tables and the segment table +``` + +Use `--port` if you want the local server to listen on a specific port: + +```bash +table_blueprints /path/to/dataset --port 9876 +``` + +Via pixi/uv: + +```bash +pixi run py-build && pixi run uv run examples/python/table_blueprints/table_blueprints.py /path/to/dataset +``` + +### Remote client mode + +With `--url`, the script connects as a client to an existing Rerun server or catalog and looks up the dataset by name. +The generated `.rbl` files must be visible to that server before registration. +Use `--write-blueprints-only` to write them locally (both the table blueprints and `segment_table.rbl`), upload them yourself, then rerun with `--blueprint-uri-base` pointing at the uploaded directory. +`--target` works the same way in remote mode. + +```bash +table_blueprints --write-blueprints-only --blueprint-dir /tmp/table-blueprints +# Upload /tmp/table-blueprints/*.rbl to a server-visible location, for example s3://my-bucket/table-blueprints/ +table_blueprints --url rerun+https://… --blueprint-dir /tmp/table-blueprints --blueprint-uri-base s3://my-bucket/table-blueprints/ +# …or register on the dataset's segment table instead: +table_blueprints --url rerun+https://… --target dataset --blueprint-dir /tmp/table-blueprints --blueprint-uri-base s3://my-bucket/table-blueprints/ +``` diff --git a/examples/python/table_blueprints/pyproject.toml b/examples/python/table_blueprints/pyproject.toml new file mode 100644 index 000000000000..fac51fedc5f9 --- /dev/null +++ b/examples/python/table_blueprints/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "table_blueprints" +version = "0.1.0" +readme = "README.md" +dependencies = ["pyarrow", "rerun-sdk"] + +[project.scripts] +table_blueprints = "table_blueprints:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/python/table_blueprints/table_blueprints.py b/examples/python/table_blueprints/table_blueprints.py new file mode 100644 index 000000000000..4434d281e230 --- /dev/null +++ b/examples/python/table_blueprints/table_blueprints.py @@ -0,0 +1,453 @@ +""" +Demo for table blueprints & segment previews. + +Table blueprints allow configuring table layouts and use segment previews. + +**TODO(#12745, #12746): This feature is experimental.** Enable it in the +viewer under Settings > Experimental > Table cards and blueprints. + +Each row can reference a recording via a URI column. The viewer loads those recordings +on demand and renders them through the registered blueprint's view definition. + +The demo also includes a boolean `marker_flag` column and points the registered table +blueprint at it. The Viewer uses that column as the per-row flag state: toggling a +card's flag updates the visible table immediately and upserts the new boolean value +back to the server using the `rerun:is_table_index` column as the row key. + +For testing you can use this droid rrd dataset: +https://huggingface.co/datasets/rerun/droid_sample/tree/main + +Usage: + table_blueprints + table_blueprints /path/to/dataset + table_blueprints --target dataset + table_blueprints --target both + table_blueprints --write-blueprints-only --blueprint-dir /tmp/table-blueprints + table_blueprints --url rerun+https://… --blueprint-uri-base s3://bucket/table-blueprints/ + +`--target` selects what the blueprints are applied to: +- `tables` (default): create the demo tables, each with its own table blueprint. +- `dataset`: register a blueprint on the dataset's own segment table (no tables created). +- `both`: do both. + +Without `--url`, this starts a temporary local Rerun server for the given directory of +`.rrd` files. With `--url`, this connects as a client to an existing Rerun server or +catalog and expects `dataset` to be the remote dataset name. +Remote registration requires `--blueprint-uri-base` pointing at a server-visible +location containing the `.rbl` files written by this script. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any, NamedTuple + +import pyarrow as pa + +import rerun as rr +import rerun.blueprint as rrb +from rerun import bindings +from rerun.recording_stream import RecordingStream +from rerun.server import Server + + +def save_table_blueprint( + path: Path, + *views: rrb.View, + segment_preview_column: str | None = None, + flag_column: str | None = None, + grid_view_card_title: str | None = None, + timeline: str | None = None, +) -> None: + """ + Write a table blueprint with one or more views into a `.rbl` file. + + Parameters + ---------- + path: + File path to write the serialized `.rbl` blueprint to. + *views: + One or more view definitions to embed (e.g. `Spatial3DView`, `TimeSeriesView`). + segment_preview_column: + If set, names the column whose values are `rerun://` recording URIs. + The viewer will load those recordings and render inline previews. + flag_column: + If set, names the boolean column used for flag/annotation toggles. + The column must exist in the table schema. + grid_view_card_title: + If set, names the column to use as card titles in grid view. + If unset, the first visible string column is used. + timeline: + If set, configures the time panel to display this timeline. + + """ + blueprint = rrb.Blueprint(*views) + + with RecordingStream._from_native( + bindings.new_blueprint( + application_id="embedded", + make_default=False, + make_thread_default=False, + default_enabled=True, + ), + ) as blueprint_stream: + blueprint_stream.save(str(path)) + blueprint_stream.set_time("blueprint", sequence=0) + blueprint._log_to_stream(blueprint_stream) + + table_blueprint_kwargs = {} + if segment_preview_column is not None: + table_blueprint_kwargs["segment_preview_column"] = segment_preview_column + if flag_column is not None: + table_blueprint_kwargs["flag_column"] = flag_column + if grid_view_card_title is not None: + table_blueprint_kwargs["grid_view_card_title"] = grid_view_card_title + if table_blueprint_kwargs: + blueprint_stream.log( + "/table", + rrb.experimental.TableBlueprint(**table_blueprint_kwargs), + ) + + if timeline is not None: + rrb.TimePanel(timeline=timeline)._log_to_stream(blueprint_stream) + + +# --------------------------------------------------------------------------- +# Dataset-specific customization +# --------------------------------------------------------------------------- + +DEFAULT_LOCAL_DATASET = Path(__file__).resolve().parents[3] / "tests/assets/rrd/sample_5" +MARKER_FLAG_COLUMN = "marker_flag" +SEGMENT_TABLE_BLUEPRINT_NAME = "segment_table" +PropertyColumn = tuple[str, pa.Field, list[Any]] + +# Please edit the functions in this section to match your own dataset. +# The defaults below are geared towards RRDs from the DROID dataset and its schema, +# timelines, entity paths, and coordinate frames; they are intended as a starting point only. + + +def extract_dataset_property_columns(seg_arrow: pa.Table, num_segments: int) -> list[PropertyColumn]: + """ + Pick which segment-table columns should be copied into the demo tables. + + PLEASE EDIT THIS for your dataset. The default implementation looks for + columns named `property:episode:*` and strips that prefix. + """ + episode_prefix = "property:episode:" + props: list[PropertyColumn] = [] + for field in seg_arrow.schema: + if field.name.startswith(episode_prefix): + original_name = field.name + short_name = original_name[len(episode_prefix) :] + values = seg_arrow.column(original_name).to_pylist()[:num_segments] + props.append((short_name, pa.field(short_name, field.type, field.nullable), values)) + + return props + + +class PreviewViews(NamedTuple): + """The views shared by the table and segment-table blueprints.""" + + plot: rrb.TimeSeriesView + spatial_3d: rrb.Spatial3DView + spatial_2d: rrb.Spatial2DView + + +def setup_preview_views() -> PreviewViews: + """ + Build all views used by the demo blueprints. + + PLEASE EDIT THIS for your dataset: view origins, contents, target frame, and excluded paths. + """ + return PreviewViews( + plot=rrb.TimeSeriesView( + origin="/observation/joint_positions", + plot_legend=rrb.PlotLegend(visible=False), + ), + spatial_3d=rrb.Spatial3DView( + contents=[ + "+ /**", + "- /camera/**", + "- /**/collision_0/**", + "- /thumbnail/**", + ], + spatial_information=rrb.SpatialInformation( + target_frame="panda_link0", + ), + background=rrb.Background( + color=[0.1, 0.1, 0.1, 1.0], + ), + ), + spatial_2d=rrb.Spatial2DView( + contents=["+ /camera/wrist/**"], + ), + ) + + +def make_dataset_blueprints(blueprint_dir: Path) -> dict[str, Path]: + """ + Write the table blueprints used by this demo to `blueprint_dir` and return their paths by name. + + These target the demo *tables* created by this script, whose schema has `recording_uri`, + `marker_flag`, and `uuid` columns. For the dataset's own segment table, see + `make_segment_table_blueprint`. + + PLEASE EDIT THIS for your dataset. In particular, update: + - `grid_view_card_title` to a string column that exists in your copied properties. + - `timeline` to the timeline used by your recordings. + """ + common_bp_kwargs = { + "segment_preview_column": "recording_uri", + "flag_column": MARKER_FLAG_COLUMN, + "grid_view_card_title": "uuid", + "timeline": "real_time", + } + + views = setup_preview_views() + + blueprint_dir.mkdir(parents=True, exist_ok=True) + paths = { + name: blueprint_dir / f"{name}.rbl" for name in ("previews_plot", "previews_3d_only", "previews_3d_and_2d") + } + + save_table_blueprint(paths["previews_plot"], views.plot, **common_bp_kwargs) + save_table_blueprint(paths["previews_3d_only"], views.spatial_3d, **common_bp_kwargs) + save_table_blueprint(paths["previews_3d_and_2d"], views.spatial_3d, views.spatial_2d, **common_bp_kwargs) + + return paths + + +def make_segment_table_blueprint(blueprint_dir: Path) -> Path: + """ + Write the blueprint used for the dataset's own segment table and return its path. + + Unlike the table blueprints, this targets the dataset's native segment table, so: + - `segment_preview_column` is left unset, letting the viewer auto-pick the column to preview. + - `flag_column` is left unset (segment tables have no demo flag column). + + PLEASE EDIT THIS for your dataset. By default it uses the combined 3D & 2D views and the + `real_time` timeline; adjust the views (via `setup_preview_views`) and timeline to match your + recordings. + """ + blueprint_dir.mkdir(parents=True, exist_ok=True) + path = blueprint_dir / f"{SEGMENT_TABLE_BLUEPRINT_NAME}.rbl" + + views = setup_preview_views() + save_table_blueprint(path, views.spatial_3d, views.spatial_2d, timeline="real_time") + + return path + + +# --------------------------------------------------------------------------- +# Generic demo plumbing: start a local server, query segments, and create tables. +# --------------------------------------------------------------------------- + + +def query_segment_data( + dataset: rr.catalog.DatasetEntry, +) -> tuple[list[str], list[str], list[PropertyColumn]]: + """ + Query segment table and return (segment_ids, segment_uris, property_columns). + + Returns all entries from the segment table. + """ + seg_df = dataset.segment_table() + seg_arrow = pa.Table.from_batches(seg_df.collect()) + + segment_ids = seg_arrow.column("rerun_segment_id").to_pylist() + n = len(segment_ids) + segment_uris = [dataset.segment_url(sid) for sid in segment_ids] + props = extract_dataset_property_columns(seg_arrow, n) + + return segment_ids, segment_uris, props + + +def create_table( + client: rr.catalog.CatalogClient, + *, + table_name: str, + segment_uris: list[str], + property_columns: list[PropertyColumn], +) -> rr.catalog.TableEntry: + """Create a table with the given segment data.""" + n = len(segment_uris) + + fields: list[pa.Field] = [ + pa.field("id", pa.int64(), metadata={rr.SORBET_IS_TABLE_INDEX: "true"}), + pa.field("recording_uri", pa.utf8()), + ] + data: dict[str, list[Any]] = { + "id": list(range(n)), + "recording_uri": segment_uris, + } + + for short_name, field, values in property_columns: + fields.append(field) + data[short_name] = values + + fields.append(pa.field(MARKER_FLAG_COLUMN, pa.bool_())) + data[MARKER_FLAG_COLUMN] = [False] * n + + schema = pa.schema(fields) + table = client.create_table(table_name, schema) + table.append(**data) + return table + + +def blueprint_uri(name: str, local_path: Path, blueprint_uri_base: str | None) -> str: + """Return the URI to register for a blueprint.""" + if blueprint_uri_base is None: + return local_path.absolute().as_uri() + return blueprint_uri_base.rstrip("/") + f"/{name}.rbl" + + +def create_demo_tables( + client: rr.catalog.CatalogClient, + dataset: rr.catalog.DatasetEntry, + dataset_name: str, + *, + blueprint_dir: Path, + blueprint_uri_base: str | None, +) -> None: + """Create one demo table per table blueprint, populated from the dataset's segment properties.""" + _, segment_uris, props = query_segment_data(dataset) + print(f"Using {len(segment_uris)} segments from dataset '{dataset_name}'") + + blueprint_paths = make_dataset_blueprints(blueprint_dir) + + existing_table_names = set(client.table_names()) + for name in blueprint_paths: + if name in existing_table_names: + client.get_table(name).delete() + print(f" {name}: deleted existing table") + table = create_table( + client, + table_name=name, + segment_uris=segment_uris, + property_columns=props, + ) + uri = blueprint_uri(name, blueprint_paths[name], blueprint_uri_base) + table.register_blueprint(uri) + print(f" {name}: registered table blueprint {uri}") + + +def apply_segment_table_blueprint( + dataset: rr.catalog.DatasetEntry, + *, + blueprint_dir: Path, + blueprint_uri_base: str | None, +) -> None: + """Register the segment-table blueprint on the dataset's own segment table.""" + path = make_segment_table_blueprint(blueprint_dir) + uri = blueprint_uri(SEGMENT_TABLE_BLUEPRINT_NAME, path, blueprint_uri_base) + dataset.register_blueprint(uri, segment_table=True) + print(f" segment table: registered blueprint {uri}") + + +def run_with_client( + client: rr.catalog.CatalogClient, + dataset_name: str, + *, + target: str, + blueprint_dir: Path, + blueprint_uri_base: str | None, +) -> None: + """Create demo tables and/or register a blueprint on the dataset's segment table, per `target`.""" + dataset = client.get_dataset(dataset_name) + + if target in ("tables", "both"): + create_demo_tables( + client, + dataset, + dataset_name, + blueprint_dir=blueprint_dir, + blueprint_uri_base=blueprint_uri_base, + ) + + if target in ("dataset", "both"): + apply_segment_table_blueprint( + dataset, + blueprint_dir=blueprint_dir, + blueprint_uri_base=blueprint_uri_base, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Create table-blueprint demo tables.") + parser.add_argument( + "dataset", + nargs="?", + help=(f"Local dataset directory to serve. Defaults to {DEFAULT_LOCAL_DATASET}."), + ) + + connection_group = parser.add_mutually_exclusive_group() + connection_group.add_argument("--port", type=int, default=None, help="Port for local server mode.") + connection_group.add_argument("--url", help="Remote server/catalog URL for client mode.") + parser.add_argument( + "--blueprint-dir", + type=Path, + default=Path.cwd(), + help="Directory where generated .rbl table blueprints are written.", + ) + parser.add_argument( + "--blueprint-uri-base", + help=( + "Server-visible URI prefix used when registering generated .rbl files. " + "Required with --url unless --write-blueprints-only is used." + ), + ) + parser.add_argument( + "--target", + choices=("tables", "dataset", "both"), + default="both", + help=( + "What to apply blueprints to:\n" + "* 'tables' creates the demo tables\n" + "* 'dataset' registers a blueprint on the dataset's own segment table\n" + "* 'both' (default) does both." + ), + ) + parser.add_argument( + "--write-blueprints-only", + action="store_true", + help="Only write generated .rbl files to --blueprint-dir, then exit.", + ) + + args = parser.parse_args() + + if args.write_blueprints_only: + make_dataset_blueprints(args.blueprint_dir) + make_segment_table_blueprint(args.blueprint_dir) + return + + if args.url is not None: + if args.dataset is None: + parser.error("Provide a remote dataset name when using --url") + if args.blueprint_uri_base is None: + parser.error("Provide --blueprint-uri-base with --url after uploading the generated .rbl files") + client = rr.catalog.CatalogClient(args.url) + run_with_client( + client, + dataset_name=args.dataset, + target=args.target, + blueprint_dir=args.blueprint_dir, + blueprint_uri_base=args.blueprint_uri_base, + ) + else: + local_dataset = args.dataset or str(DEFAULT_LOCAL_DATASET) + with Server(port=args.port, datasets={"local": local_dataset}) as srv: + print(srv.url()) + client = srv.client() + run_with_client( + client, + dataset_name="local", + target=args.target, + blueprint_dir=args.blueprint_dir, + blueprint_uri_base=args.blueprint_uri_base, + ) + input("Press Enter to stop the server…") + + +if __name__ == "__main__": + main() diff --git a/examples/python/table_grid_with_flags/README.md b/examples/python/table_grid_with_flags/README.md new file mode 100644 index 000000000000..0d3e99bbb53f --- /dev/null +++ b/examples/python/table_grid_with_flags/README.md @@ -0,0 +1,33 @@ + + +## Table grid with flags + +Starts a local server with a table containing an index column and a boolean flag column. +The flag column is marked with Arrow metadata so the viewer's card/grid view can toggle flags +and persist them back to the server. + +The flag column remains part of the table data. Its current boolean value controls the flag icon shown on each grid card. Clicking the icon immediately updates the visible table state and sends an upsert back to the server containing the row's table-index value plus the new flag value. The `rerun:is_table_index` column is required so the server knows which row to update. + + +Enable `Settings > Experimental > Table cards and blueprints` in the viewer, then open the +printed URL. + +Flagging works on regular tables, but does **not** yet work on the segment tables of datasets: +segment tables have no write operations yet, so flag changes cannot be persisted back to the server. + +## Run the code + +```bash +pip install -e examples/python/table_grid_with_flags +table_grid_with_flags +``` + +or via pixi/uv: + +```bash +pixi run py-build && pixi run uv run examples/python/table_grid_with_flags/table_grid_with_flags.py +``` diff --git a/examples/python/table_grid_with_flags/pyproject.toml b/examples/python/table_grid_with_flags/pyproject.toml new file mode 100644 index 000000000000..ccb3076628c5 --- /dev/null +++ b/examples/python/table_grid_with_flags/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "table_grid_with_flags" +version = "0.1.0" +readme = "README.md" +dependencies = ["pyarrow", "rerun-sdk"] + +[project.scripts] +table_grid_with_flags = "table_grid_with_flags:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/rerun_py/tests/example_grid_view_flagging.py b/examples/python/table_grid_with_flags/table_grid_with_flags.py similarity index 50% rename from rerun_py/tests/example_grid_view_flagging.py rename to examples/python/table_grid_with_flags/table_grid_with_flags.py index efb9f92784b1..ab316b4c8fae 100644 --- a/rerun_py/tests/example_grid_view_flagging.py +++ b/examples/python/table_grid_with_flags/table_grid_with_flags.py @@ -1,36 +1,46 @@ """ -Experimental grid view with flag toggling. +Experimental table grid with flag toggles. -Demonstrates the grid view card layout and per-row flag annotations on a +Demonstrates the card/grid table layout and per-row flag annotations on a remote table. -**This feature is experimental.** Enable it in the viewer under -Settings > Experimental > Grid view. +**TODO(#12745): This feature is experimental.** Enable it in the viewer under +Settings > Experimental > Table cards and blueprints. The flag column is configured via Arrow field metadata -(`rerun:is_flag_column = "true"`). The table must also have a -`rerun:is_table_index` column so that flag changes can be persisted -back to the server via upsert. +(`rerun:is_flag_column = "true"`). The Viewer treats that boolean column +as the per-row flag state: each value drives the flag icon on the grid card, +and clicking the icon updates the visible table state and upserts the new +boolean value back to the server. The table must also have a +`rerun:is_table_index` column so the upsert can target the row to update. Usage: - pixi run uvpy rerun_py/tests/example_grid_view_flagging.py + table_grid_with_flags # In a separate terminal, open the viewer with the URL printed by the script: - pixi run rerun + rerun """ from __future__ import annotations +import argparse + import pyarrow as pa + +import rerun as rr from rerun.server import Server def main() -> None: + parser = argparse.ArgumentParser(description="Create an experimental table grid with flag toggles.") + parser.add_argument("--port", type=int, default=None, help="Port for the local Rerun server.") + args = parser.parse_args() + schema = pa.schema([ pa.field( "id", pa.int64(), - metadata={"rerun:is_table_index": "true"}, + metadata={rr.SORBET_IS_TABLE_INDEX: "true"}, ), pa.field("name", pa.utf8()), pa.field("category", pa.utf8()), @@ -50,14 +60,13 @@ def main() -> None: "flagged": [False, False, False, False, False], } - port = 1234 - with Server(port=port) as srv: + with Server(port=args.port) as srv: client = srv.client() table = client.create_table("flag_demo", schema) table.append(**data) - url = f"rerun+http://localhost:{port}/entry/{table.id}" - print(f"Open the viewer with:\n pixi run rerun {url}") + url = f"{srv.url()}/entry/{table.id}" + print(f"Open the viewer with:\n rerun {url}") input("Press Enter to stop the server…") diff --git a/examples/python/table_zoo/table_zoo.py b/examples/python/table_zoo/table_zoo.py index 957052092f40..4c65abb1a2d6 100644 --- a/examples/python/table_zoo/table_zoo.py +++ b/examples/python/table_zoo/table_zoo.py @@ -304,8 +304,8 @@ def _write_recordbatch_to_lance(reader: pa.RecordBatchReader, path: Path | str) def _run_viewer_mode(host: str, port: int) -> None: name, batch = _build_record_batch() - addr = f"rerun+http://{host}:{port}/proxy" - client = rr.experimental.ViewerClient(addr=addr) + url = f"rerun+http://{host}:{port}/proxy" + client = rr.experimental.ViewerClient.connect(url=url) client.send_table(name, batch) diff --git a/examples/python/tfrecord_loader/README.md b/examples/python/tfrecord_loader/README.md index c937fc8007b8..4f673ba29d80 100644 --- a/examples/python/tfrecord_loader/README.md +++ b/examples/python/tfrecord_loader/README.md @@ -17,7 +17,7 @@ thumbnail_dimensions = [480, 480] ## Overview -This is an example importer plugin that lets you view a TFRecord of Events (i.e., Tensorboard log files). It uses the [external importer mechanism](https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview?speculative-link#external-importers) to add this capability to the Rerun Viewer without modifying the Viewer itself. +This is an example importer plugin that lets you view a TFRecord of Events (i.e., Tensorboard log files). It uses the [external importer mechanism](https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview#external-importers) to add this capability to the Rerun Viewer without modifying the Viewer itself. This example is written in Python, and uses [TensorFlow](https://www.tensorflow.org/) to read the files. The events are then logged to Rerun. diff --git a/examples/rust/animated_urdf/Cargo.toml b/examples/rust/animated_urdf/Cargo.toml index c90426b57b24..5b2fc4eb7384 100644 --- a/examples/rust/animated_urdf/Cargo.toml +++ b/examples/rust/animated_urdf/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "animated_urdf" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/blueprint/Cargo.toml b/examples/rust/blueprint/Cargo.toml index ef262138919b..6d1835d3a0d2 100644 --- a/examples/rust/blueprint/Cargo.toml +++ b/examples/rust/blueprint/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "blueprint" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/blueprint_stocks/Cargo.toml b/examples/rust/blueprint_stocks/Cargo.toml index cbceabf8b4ad..ad58827c721d 100644 --- a/examples/rust/blueprint_stocks/Cargo.toml +++ b/examples/rust/blueprint_stocks/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "blueprint_stocks" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/clock/Cargo.toml b/examples/rust/clock/Cargo.toml index ec6152c76901..bba73064b89b 100644 --- a/examples/rust/clock/Cargo.toml +++ b/examples/rust/clock/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "clock" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/custom_callback/Cargo.toml b/examples/rust/custom_callback/Cargo.toml index 217653410c06..aabc5b5a209a 100644 --- a/examples/rust/custom_callback/Cargo.toml +++ b/examples/rust/custom_callback/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "custom_callback" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/custom_callback/src/panel.rs b/examples/rust/custom_callback/src/panel.rs index 2cc606294514..d856aeecf361 100644 --- a/examples/rust/custom_callback/src/panel.rs +++ b/examples/rust/custom_callback/src/panel.rs @@ -50,7 +50,7 @@ impl eframe::App for Control { // First add our panel(s): egui::Panel::right("Control Panel") .default_size(400.0) - .show_inside(ui, |ui| { + .show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| { self.ui(ui); }); @@ -58,6 +58,10 @@ impl eframe::App for Control { self.app.ui(ui, frame); } + + fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + self.app.logic(ctx, frame); + } } impl Control { diff --git a/examples/rust/custom_callback/src/viewer.rs b/examples/rust/custom_callback/src/viewer.rs index 8fcd083199cf..71c7a4df6080 100644 --- a/examples/rust/custom_callback/src/viewer.rs +++ b/examples/rust/custom_callback/src/viewer.rs @@ -24,7 +24,7 @@ async fn main() -> Result<(), Box> { // Listen for gRPC connections from Rerun's logging SDKs. // There are other ways of "feeding" the viewer though - all you need is a `re_log_channel::LogReceiver`. - let rx_log = re_grpc_server::spawn_with_recv( + let (rx_log, _grpc_server_handle) = re_grpc_server::spawn_with_recv( "0.0.0.0:9877".parse()?, Default::default(), re_grpc_server::shutdown::never(), diff --git a/examples/rust/custom_importer/Cargo.toml b/examples/rust/custom_importer/Cargo.toml index 4721a966bf25..734dc8c8bdb4 100644 --- a/examples/rust/custom_importer/Cargo.toml +++ b/examples/rust/custom_importer/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "custom_importer" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/custom_store_subscriber/Cargo.toml b/examples/rust/custom_store_subscriber/Cargo.toml index 7313da518695..038272a5d742 100644 --- a/examples/rust/custom_store_subscriber/Cargo.toml +++ b/examples/rust/custom_store_subscriber/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "custom_store_subscriber" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/custom_view/Cargo.toml b/examples/rust/custom_view/Cargo.toml index 2423ce224180..d97772f8cd94 100644 --- a/examples/rust/custom_view/Cargo.toml +++ b/examples/rust/custom_view/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "custom_view" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/custom_view/src/color_coordinate_config.rs b/examples/rust/custom_view/src/color_coordinate_config.rs new file mode 100644 index 000000000000..df28926a965f --- /dev/null +++ b/examples/rust/custom_view/src/color_coordinate_config.rs @@ -0,0 +1,181 @@ +//! Custom blueprint configuration for the color coordinates view. +//! +//! Built-in views get this from `.fbs` + codegen. This example does it manually: +//! define a component, make it [`rerun::Loggable`], group it in an [`rerun::Archetype`], provide +//! reflection, and register an editor UI. + +use rerun::external::egui; +use rerun::external::re_sdk_types::reflection::{ + ArchetypeFieldFlags, ArchetypeFieldReflection, ArchetypeReflection, +}; +use rerun::external::re_sdk_types::{ArchetypeName, ComponentDescriptor}; +use rerun::external::re_viewer_context::MaybeMutRef; + +/// Blueprint properties for the color coordinates view. +pub struct ColorCoordinatesConfiguration; + +impl ColorCoordinatesConfiguration { + pub fn descriptor_mode() -> ComponentDescriptor { + ComponentDescriptor { + archetype: Some(::name()), + component: "ColorCoordinates:mode".into(), + component_type: Some(::name()), + } + } + + /// Minimal reflection metadata for the `mode` field. + pub fn field_mode() -> ArchetypeFieldReflection { + ArchetypeFieldReflection { + name: "mode", + display_name: "Coordinates mode", + component_type: ::name(), + docstring_md: "The color channels to use as 2D coordinates.", + flags: ArchetypeFieldFlags::UI_EDITABLE, + } + } + + /// Reflection metadata for the custom archetype. + /// + /// Register once with [`rerun::external::re_viewer::App::add_archetype_reflection`] to enable + /// `re_view::view_property_ui::`. + pub fn reflection() -> ArchetypeReflection { + ArchetypeReflection { + display_name: ::display_name(), + deprecation_summary: None, + view_types: &[], + scope: Some("blueprint"), + fields: vec![Self::field_mode()], + } + } +} + +impl rerun::Archetype for ColorCoordinatesConfiguration { + fn name() -> ArchetypeName { + "rerun.blueprint.archetypes.ColorCoordinates".into() + } + + fn display_name() -> &'static str { + "Coordinates mode" + } + + fn required_components() -> std::borrow::Cow<'static, [ComponentDescriptor]> { + std::borrow::Cow::Borrowed(&[]) + } + + fn optional_components() -> std::borrow::Cow<'static, [ComponentDescriptor]> { + std::borrow::Cow::Owned(vec![Self::descriptor_mode()]) + } +} + +impl rerun::external::re_sdk_types::ArchetypeReflectionMarker for ColorCoordinatesConfiguration {} + +/// The different modes for displaying color coordinates in the custom view. +/// +/// This blueprint component is manually encoded as a `UInt32` below. +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy, rerun::SizeBytes)] +pub enum ColorCoordinatesMode { + #[default] + Hs, + Hv, + Rg, +} + +impl ColorCoordinatesMode { + pub const ALL: [ColorCoordinatesMode; 3] = [ + ColorCoordinatesMode::Hs, + ColorCoordinatesMode::Hv, + ColorCoordinatesMode::Rg, + ]; + + fn as_u32(self) -> u32 { + match self { + Self::Hs => 0, + Self::Hv => 1, + Self::Rg => 2, + } + } + + fn from_u32(value: u32) -> rerun::DeserializationResult { + match value { + 0 => Ok(Self::Hs), + 1 => Ok(Self::Hv), + 2 => Ok(Self::Rg), + _ => Err(rerun::DeserializationError::ValidationError(format!( + "invalid color coordinates mode: {value}" + ))), + } + } +} + +impl rerun::Loggable for ColorCoordinatesMode { + // Components are stored as Arrow arrays; encode the enum as stable `UInt32` values. + fn arrow_datatype() -> rerun::external::arrow::datatypes::DataType { + ::arrow_datatype() + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> rerun::SerializationResult + where + Self: 'a, + { + ::to_arrow_opt( + data.into_iter() + .map(|mode| mode.map(|mode| rerun::datatypes::UInt32(mode.into().as_u32()))), + ) + } + + fn from_arrow_opt( + data: &dyn rerun::external::arrow::array::Array, + ) -> rerun::DeserializationResult>> { + ::from_arrow_opt(data)? + .into_iter() + .map(|mode| mode.map(|mode| Self::from_u32(mode.0)).transpose()) + .collect() + } +} + +impl rerun::Component for ColorCoordinatesMode { + // Pick a stable fully-qualified component type name. + fn name() -> rerun::ComponentType { + "rerun.blueprint.components.ColorCoordinatesMode".into() + } +} + +impl std::fmt::Display for ColorCoordinatesMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ColorCoordinatesMode::Hs => "Hue/Saturation".fmt(f), + ColorCoordinatesMode::Hv => "Hue/Value".fmt(f), + ColorCoordinatesMode::Rg => "Red/Green".fmt(f), + } + } +} + +/// Single-line editor for `ColorCoordinatesMode`. +/// +/// The registry writes back the value when the returned response is marked as changed. +pub fn edit_view_color_coordinates_mode( + ui: &mut egui::Ui, + value: &mut MaybeMutRef<'_, ColorCoordinatesMode>, +) -> egui::Response { + if let Some(value) = value.as_mut() { + let previous_value = *value; + let mut response = egui::ComboBox::from_id_salt("color_coordinates_mode") + .selected_text(value.to_string()) + .show_ui(ui, |ui| { + for mode in ColorCoordinatesMode::ALL { + ui.selectable_value(value, mode, mode.to_string()); + } + }) + .response; + + if *value != previous_value { + response.mark_changed(); + } + + response + } else { + ui.label(value.to_string()) + } +} diff --git a/examples/rust/custom_view/src/main.rs b/examples/rust/custom_view/src/main.rs index a17a9cf4981b..9319711f261a 100644 --- a/examples/rust/custom_view/src/main.rs +++ b/examples/rust/custom_view/src/main.rs @@ -2,6 +2,7 @@ use rerun::external::{re_crash_handler, re_grpc_server, re_log, re_memory, re_viewer, tokio}; +mod color_coordinate_config; mod points3d_color_view; mod points3d_color_visualizer; @@ -25,7 +26,7 @@ async fn main() -> Result<(), Box> { // Listen for gRPC connections from Rerun's logging SDKs. // There are other ways of "feeding" the viewer though - all you need is a `re_log_channel::LogReceiver`. - let rx = re_grpc_server::spawn_with_recv( + let (rx, _grpc_server_handle) = re_grpc_server::spawn_with_recv( "0.0.0.0:9876".parse()?, Default::default(), re_grpc_server::shutdown::never(), @@ -59,7 +60,22 @@ async fn main() -> Result<(), Box> { ); app.add_log_receiver(rx); - // Register the custom view + // Register reflection + component UI for our hand-written blueprint property. + let color_coordinates_archetype = + ::name( + ); + app.add_archetype_reflection( + color_coordinates_archetype, + color_coordinate_config::ColorCoordinatesConfiguration::reflection(), + ); + app.component_ui_registry_mut() + .add_singleline_edit_or_view::( + |_ctx, ui, value| { + color_coordinate_config::edit_view_color_coordinates_mode(ui, value) + }, + ); + + // Register the custom view class and its visualizer/fallbacks. app.add_view_class::() .unwrap(); diff --git a/examples/rust/custom_view/src/points3d_color_view.rs b/examples/rust/custom_view/src/points3d_color_view.rs index 487662fb3c72..f70af75e222f 100644 --- a/examples/rust/custom_view/src/points3d_color_view.rs +++ b/examples/rust/custom_view/src/points3d_color_view.rs @@ -6,69 +6,24 @@ use rerun::external::re_entity_db::InstancePath; use rerun::external::re_log_types::EntityPath; use rerun::external::re_sdk_types::ViewClassIdentifier; use rerun::external::re_ui::{self, Help}; +use rerun::external::re_view; use rerun::external::re_viewer_context::{ DataResultInteractionAddress, HoverHighlight, IdentifiedViewSystem as _, IndicatedEntities, Item, MissingChunkReporter, PerVisualizerType, RecommendedVisualizers, SelectionHighlight, - SystemExecutionOutput, UiLayout, ViewClass, ViewClassLayoutPriority, ViewClassRegistryError, - ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, ViewStateExt as _, ViewSystemExecutionError, - ViewSystemIdentifier, ViewSystemRegistrator, ViewerContext, VisualizableReason, + SystemExecutionOutput, UiLayout, ViewClass, ViewClassExt as _, ViewClassLayoutPriority, + ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, + ViewSystemExecutionError, ViewSystemIdentifier, ViewSystemRegistrator, ViewerContext, + VisualizableReason, }; +use rerun::external::re_viewport_blueprint::ViewProperty; +use crate::color_coordinate_config::{ColorCoordinatesConfiguration, ColorCoordinatesMode}; use crate::points3d_color_visualizer::{ColorWithInstance, Points3DColorVisualizer}; -/// The different modes for displaying color coordinates in the custom view. -#[derive(Default, Debug, PartialEq, Clone, Copy)] -enum ColorCoordinatesMode { - #[default] - Hs, - Hv, - Rg, -} - -impl ColorCoordinatesMode { - pub const ALL: [ColorCoordinatesMode; 3] = [ - ColorCoordinatesMode::Hs, - ColorCoordinatesMode::Hv, - ColorCoordinatesMode::Rg, - ]; -} - -impl std::fmt::Display for ColorCoordinatesMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ColorCoordinatesMode::Hs => "Hue/Saturation".fmt(f), - ColorCoordinatesMode::Hv => "Hue/Value".fmt(f), - ColorCoordinatesMode::Rg => "Red/Green".fmt(f), - } - } -} - -/// View state for the custom view. -/// -/// This state is preserved between frames, but not across Viewer sessions. -#[derive(Default)] -pub struct ColorCoordinatesViewState { - // TODO(wumpf, jleibs): This should be part of the Blueprint so that it is serialized out. - // but right now there is no way of doing that. - mode: ColorCoordinatesMode, -} - -impl ViewState for ColorCoordinatesViewState { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } -} - #[derive(Default)] pub struct ColorCoordinatesView; impl ViewClass for ColorCoordinatesView { - // State type as described above. - fn identifier() -> ViewClassIdentifier { "ColorCoordinates".into() } @@ -86,16 +41,21 @@ impl ViewClass for ColorCoordinatesView { .markdown("A demo view that shows colors as coordinates on a 2D plane.") } - /// Register all systems (contexts & parts) that the view needs. + /// Register systems and a blueprint fallback for the mode property. fn on_register( &self, system_registry: &mut ViewSystemRegistrator<'_>, ) -> Result<(), ViewClassRegistryError> { + system_registry.register_fallback_provider( + ColorCoordinatesConfiguration::descriptor_mode().component, + |_ctx| ColorCoordinatesMode::default(), + ); + system_registry.register_visualizer::() } fn new_state(&self) -> Box { - Box::::default() + Box::new(()) } fn preferred_tile_aspect_ratio(&self, _state: &dyn ViewState) -> Option { @@ -147,27 +107,18 @@ impl ViewClass for ColorCoordinatesView { /// Additional UI displayed when the view is selected. /// - /// In this sample we show a combo box to select the color coordinates mode. + /// Uses the same generic blueprint-property UI as built-in views. The custom archetype + /// reflection and component editor are registered in `main`. fn selection_ui( &self, - _ctx: &ViewerContext<'_>, + ctx: &ViewerContext<'_>, ui: &mut egui::Ui, state: &mut dyn ViewState, - _space_origin: &EntityPath, - _view_id: ViewId, + space_origin: &EntityPath, + view_id: ViewId, ) -> Result<(), ViewSystemExecutionError> { - let state = state.downcast_mut::()?; - - ui.horizontal(|ui| { - ui.label("Coordinates mode"); - egui::ComboBox::from_id_salt("color_coordinates_mode") - .selected_text(state.mode.to_string()) - .show_ui(ui, |ui| { - for mode in &ColorCoordinatesMode::ALL { - ui.selectable_value(&mut state.mode, *mode, mode.to_string()); - } - }); - }); + let view_ctx = self.view_context(ctx, view_id, state, space_origin); + re_view::view_property_ui::(&view_ctx, ui); Ok(()) } @@ -184,21 +135,26 @@ impl ViewClass for ColorCoordinatesView { query: &ViewQuery<'_>, system_output: SystemExecutionOutput, ) -> Result<(), ViewSystemExecutionError> { - let empty_colors = crate::points3d_color_visualizer::Points3DColorVisualizerOutput::new(); let colors = system_output - .visualizer_data::( + .visualizer_data_or_default::( Points3DColorVisualizer::identifier(), - ) - .unwrap_or(&empty_colors); - let state = state.downcast_mut::()?; + )?; + // Read the same blueprint property that the selection UI edits. + let view_ctx = self.view_context(ctx, query.view_id, state, query.space_origin); + let color_coordinates = + ViewProperty::from_archetype::(&view_ctx); + let mode = color_coordinates.component_or_fallback::( + &view_ctx, + ColorCoordinatesConfiguration::descriptor_mode().component, + )?; egui::Frame::default().show(ui, |ui| { - let color_at = match state.mode { + let color_at = match mode { ColorCoordinatesMode::Hs => |x, y| egui::ecolor::Hsva::new(x, y, 1.0, 1.0).into(), ColorCoordinatesMode::Hv => |x, y| egui::ecolor::Hsva::new(x, 1.0, y, 1.0).into(), ColorCoordinatesMode::Rg => |x, y| egui::ecolor::Rgba::from_rgb(x, y, 0.0).into(), }; - let position_at = match state.mode { + let position_at = match mode { ColorCoordinatesMode::Hs => |c: egui::Color32| { let hsva = egui::ecolor::Hsva::from(c); (hsva.h, hsva.s) @@ -212,7 +168,7 @@ impl ViewClass for ColorCoordinatesView { (rgba.r(), rgba.g()) }, }; - color_space_ui(ui, ctx, colors, query, color_at, position_at); + color_space_ui(ui, ctx, colors.as_ref(), query, color_at, position_at); }); Ok(()) } diff --git a/examples/rust/custom_view/src/points3d_color_visualizer.rs b/examples/rust/custom_view/src/points3d_color_visualizer.rs index 1acfdba98c03..90cb7098fe45 100644 --- a/examples/rust/custom_view/src/points3d_color_visualizer.rs +++ b/examples/rust/custom_view/src/points3d_color_visualizer.rs @@ -12,6 +12,7 @@ use rerun::external::re_viewer_context::{ #[derive(Default)] pub struct Points3DColorVisualizer; +#[derive(Clone)] pub struct ColorWithInstance { pub color: egui::Color32, pub instance: Instance, @@ -71,7 +72,7 @@ impl VisualizerSystem for Points3DColorVisualizer { // Collect all different kinds of colors that are returned from the cache. let mut colors_for_entity = Vec::new(); for ((_time, _row_id), colors_slice) in color_slices_per_time { - for (instance, color) in (0..).zip(colors_slice) { + for (instance, color) in std::iter::zip(0.., colors_slice) { let [r, g, b, _] = rerun::Color::from_u32(*color).to_array(); colors_for_entity.push(ColorWithInstance { #[expect(clippy::disallowed_methods)] // This is not a hard-coded color. diff --git a/examples/rust/custom_visualizer/Cargo.toml b/examples/rust/custom_visualizer/Cargo.toml index b3072cca005d..6649d868d501 100644 --- a/examples/rust/custom_visualizer/Cargo.toml +++ b/examples/rust/custom_visualizer/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "custom_visualizer" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/custom_visualizer/shader/height_field.wgsl b/examples/rust/custom_visualizer/shader/height_field.wgsl index 7cedc1a3ca24..67b2e6539e92 100644 --- a/examples/rust/custom_visualizer/shader/height_field.wgsl +++ b/examples/rust/custom_visualizer/shader/height_field.wgsl @@ -64,8 +64,7 @@ fn vs_main(in: VertexIn) -> VertexOut { // Normalize height to [0,1] and apply the colormap. let height_range = ubo.max_height - ubo.min_height; let t = select((in.height - ubo.min_height) / height_range, 0.5, height_range <= 0.0); - let color_rgb = colormap_linear(ubo.colormap, t); - out.color = vec4f(color_rgb, 1.0); + out.color = colormap_linear(ubo.colormap, t); return out; } diff --git a/examples/rust/custom_visualizer/src/main.rs b/examples/rust/custom_visualizer/src/main.rs index 278bd3def013..b1184b7040df 100644 --- a/examples/rust/custom_visualizer/src/main.rs +++ b/examples/rust/custom_visualizer/src/main.rs @@ -36,7 +36,7 @@ async fn main() -> Result<(), Box> { // Listen for gRPC connections from Rerun's logging SDKs. // There are other ways of "feeding" the viewer though - all you need is a `re_log_channel::LogReceiver`. - let grpc_rx = re_grpc_server::spawn_with_recv( + let (grpc_rx, _grpc_server_handle) = re_grpc_server::spawn_with_recv( "0.0.0.0:9876".parse()?, re_grpc_server::ServerOptions::default(), re_grpc_server::shutdown::never(), diff --git a/examples/rust/dataframe_query/Cargo.toml b/examples/rust/dataframe_query/Cargo.toml index 8a76224b90d6..040a6505d903 100644 --- a/examples/rust/dataframe_query/Cargo.toml +++ b/examples/rust/dataframe_query/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "dataframe_query" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/dataframe_query/src/main.rs b/examples/rust/dataframe_query/src/main.rs index 373a1cc970b2..f636e6956ab1 100644 --- a/examples/rust/dataframe_query/src/main.rs +++ b/examples/rust/dataframe_query/src/main.rs @@ -57,10 +57,11 @@ fn main() -> Result<(), Box> { ..Default::default() }; - let query_handle = engine.query(query.clone()); + let mut query_handle = engine.query(query.clone()); + let schema = query_handle.schema().clone(); let record_batches = query_handle.batch_iter().take(10).collect_vec(); - let batch = arrow::compute::concat_batches(query_handle.schema(), &record_batches)?; + let batch = arrow::compute::concat_batches(&schema, &record_batches)?; println!("{}", format_record_batch(&batch)); } diff --git a/examples/rust/dna/Cargo.toml b/examples/rust/dna/Cargo.toml index 725ea968e7ca..f406ee753e1d 100644 --- a/examples/rust/dna/Cargo.toml +++ b/examples/rust/dna/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "dna" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/dna/src/main.rs b/examples/rust/dna/src/main.rs index 1c59297b72ad..ba67ddc159f2 100644 --- a/examples/rust/dna/src/main.rs +++ b/examples/rust/dna/src/main.rs @@ -32,9 +32,7 @@ fn main() -> Result<(), Box> { .with_radii([0.08]), )?; - let lines: Vec<[glam::Vec3; 2]> = points1 - .iter() - .zip(&points2) + let lines: Vec<[glam::Vec3; 2]> = std::iter::zip(&points1, &points2) .map(|(&p1, &p2)| (p1, p2).into()) .collect_vec(); @@ -53,9 +51,7 @@ fn main() -> Result<(), Box> { rec.set_duration_secs("stable_time", time as f64); let times = offsets.iter().map(|offset| time + offset).collect_vec(); - let beads = lines - .iter() - .zip(×) + let beads = std::iter::zip(&lines, ×) .map(|(&[p1, p2], &time)| bounce_lerp(p1, p2, time)) .collect_vec(); let colors = times diff --git a/examples/rust/extend_viewer_ui/Cargo.toml b/examples/rust/extend_viewer_ui/Cargo.toml index ae7f735ec09f..cabf97198202 100644 --- a/examples/rust/extend_viewer_ui/Cargo.toml +++ b/examples/rust/extend_viewer_ui/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "extend_viewer_ui" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/extend_viewer_ui/src/main.rs b/examples/rust/extend_viewer_ui/src/main.rs index 0ab1eafd067c..70de5065a6f7 100644 --- a/examples/rust/extend_viewer_ui/src/main.rs +++ b/examples/rust/extend_viewer_ui/src/main.rs @@ -25,7 +25,7 @@ async fn main() -> Result<(), Box> { // Listen for gRPC connections from Rerun's logging SDKs. // There are other ways of "feeding" the viewer though - all you need is a `re_log_channel::LogReceiver`. - let rx = re_grpc_server::spawn_with_recv( + let (rx, _grpc_server_handle) = re_grpc_server::spawn_with_recv( "0.0.0.0:9876".parse()?, Default::default(), re_grpc_server::shutdown::never(), @@ -80,13 +80,17 @@ impl eframe::App for MyApp { // First add our panel(s): egui::Panel::right("my_side_panel") .default_size(200.0) - .show_inside(ui, |ui| { + .show(ui, |ui| { self.ui(ui); }); // Now show the Rerun Viewer in the remaining space: self.rerun_app.ui(ui, frame); } + + fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + self.rerun_app.logic(ctx, frame); + } } impl MyApp { @@ -160,10 +164,12 @@ fn component_ui( // just show the last value logged for each component: let query = re_chunk_store::LatestAtQuery::latest(timeline); - let results = entity_db - .storage_engine() - .cache() - .latest_at(&query, entity_path, [component]); + let results = entity_db.storage_engine().cache().latest_at( + re_chunk_store::ChunkTrackingMode::Report, + &query, + entity_path, + [component], + ); if let Some(data) = results.component_batch_raw(component) { egui::ScrollArea::vertical() diff --git a/examples/rust/external_importer/Cargo.toml b/examples/rust/external_importer/Cargo.toml index 6d591af6f16a..646a1ed21b91 100644 --- a/examples/rust/external_importer/Cargo.toml +++ b/examples/rust/external_importer/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "rerun-importer-rust-file" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/external_importer/src/main.rs b/examples/rust/external_importer/src/main.rs index 893c32c7b99b..ee86c41d9b14 100644 --- a/examples/rust/external_importer/src/main.rs +++ b/examples/rust/external_importer/src/main.rs @@ -119,7 +119,7 @@ fn timepoint_from_args(args: &Args) -> anyhow::Result { continue; }; timepoint.insert_cell( - seqline_name, + rerun::TimelineName::try_new(seqline_name)?, rerun::TimeCell::from_sequence(seq.parse::()?), ); } @@ -129,7 +129,7 @@ fn timepoint_from_args(args: &Args) -> anyhow::Result { continue; }; timepoint.insert_cell( - seqline_name, + rerun::TimelineName::try_new(seqline_name)?, rerun::TimeCell::from_duration_nanos(duration_nd.parse::()?), ); } @@ -139,7 +139,7 @@ fn timepoint_from_args(args: &Args) -> anyhow::Result { continue; }; timepoint.insert_cell( - seqline_name, + rerun::TimelineName::try_new(seqline_name)?, rerun::TimeCell::from_timestamp_nanos_since_epoch(timestamp_nd.parse::()?), ); } diff --git a/examples/rust/graph_lattice/Cargo.toml b/examples/rust/graph_lattice/Cargo.toml index e60ada51b8b6..069b5ca5da08 100644 --- a/examples/rust/graph_lattice/Cargo.toml +++ b/examples/rust/graph_lattice/Cargo.toml @@ -1,14 +1,13 @@ [package] name = "graph_lattice" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false [dependencies] -rerun = { path = "../../../crates/top/rerun", features = ["clap"] } +rerun = { path = "../../../crates/top/rerun" } anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } itertools.workspace = true diff --git a/examples/rust/graph_lattice/README.md b/examples/rust/graph_lattice/README.md index eaddac3f256d..65fc14dd9b12 100644 --- a/examples/rust/graph_lattice/README.md +++ b/examples/rust/graph_lattice/README.md @@ -1,6 +1,7 @@ diff --git a/examples/rust/graph_lattice/src/main.rs b/examples/rust/graph_lattice/src/main.rs index d1ecf50d71c1..eab05ec2b800 100644 --- a/examples/rust/graph_lattice/src/main.rs +++ b/examples/rust/graph_lattice/src/main.rs @@ -1,30 +1,12 @@ //! Shows how to draw a graph with various node properties. -//! -//! Usage: -//! ``` -//! cargo run -p graph_lattice -- --connect -//! ``` use itertools::Itertools as _; -use rerun::external::re_log; use rerun::{Color, GraphEdges, GraphNodes}; -#[derive(Debug, clap::Parser)] -#[clap(author, version, about)] -pub struct Args { - #[command(flatten)] - rerun: rerun::clap::RerunArgs, -} - const NUM_NODES: usize = 10; fn main() -> anyhow::Result<()> { - re_log::setup_logging(); - - use clap::Parser as _; - let args = Args::parse(); - - let (rec, _serve_guard) = args.rerun.init("rerun_example_graph_lattice")?; + let rec = rerun::RecordingStreamBuilder::new("rerun_example_graph_lattice").spawn()?; let coordinates = (0..NUM_NODES).cartesian_product(0..NUM_NODES); diff --git a/examples/rust/incremental_logging/Cargo.toml b/examples/rust/incremental_logging/Cargo.toml index 09a013523b97..a6d7f7a2a59d 100644 --- a/examples/rust/incremental_logging/Cargo.toml +++ b/examples/rust/incremental_logging/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "incremental_logging" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/lenses/Cargo.toml b/examples/rust/lenses/Cargo.toml index af0d8ffb22c7..c825589fe52b 100644 --- a/examples/rust/lenses/Cargo.toml +++ b/examples/rust/lenses/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "lenses" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/lenses/src/main.rs b/examples/rust/lenses/src/main.rs index a765521f4be7..1ecfa943f6e1 100644 --- a/examples/rust/lenses/src/main.rs +++ b/examples/rust/lenses/src/main.rs @@ -5,7 +5,7 @@ use arrow::array::{ }; use arrow::datatypes::{DataType, Field}; use rerun::external::re_log; -use rerun::lenses::{Lens, Lenses, LensesSink, OutputMode, Selector, op}; +use rerun::lenses::{CastTo, Lens, Lenses, LensesSink, OutputMode, Selector}; use rerun::sink::GrpcSink; use rerun::{ ComponentDescriptor, DynamicArchetype, RecordingStream, Scalars, SerializedComponentColumn, @@ -15,38 +15,37 @@ use rerun::{ fn main() -> anyhow::Result<()> { re_log::setup_logging(); - let instruction = Lens::for_input_column("example:Instruction:text") - .output_columns(|out| { - out.component(TextDocument::descriptor_text(), Selector::parse(".")?) - })? - .build(); - - let destructure = Lens::for_input_column("example:Nested:payload") - .output_columns_at("nested/a", |out| { - out.component( - Scalars::descriptor_scalars(), - Selector::parse(".a")?.pipe(op::cast(DataType::Float64)), - ) - })? - .output_columns_at("nested/b", |out| { - out.component(Scalars::descriptor_scalars(), Selector::parse(".b")?) - })? - .build(); - - let time = Lens::for_input_column("my_timestamp") - .output_columns(|out| { - out.time( - "my_timeline", - rerun::time::TimeType::Sequence, - Selector::parse(".")?, - )? - .component(ComponentDescriptor::partial("value"), Selector::parse(".")?) - })? - .build(); + let instruction = Lens::derive("example:Instruction:text") + .to_component(TextDocument::descriptor_text(), Selector::parse(".")?) + .build()?; + + let destructure_a = Lens::derive("example:Nested:payload") + .output_entity("nested/a") + .to_component_with_cast( + Scalars::descriptor_scalars(), + Selector::parse(".a")?, + CastTo::Auto, + ) + .build()?; + + let destructure_b = Lens::derive("example:Nested:payload") + .output_entity("nested/b") + .to_component(Scalars::descriptor_scalars(), Selector::parse(".b")?) + .build()?; + + let time = Lens::derive("my_timestamp") + .to_timeline( + "my_timeline", + rerun::time::TimeType::Sequence, + Selector::parse(".")?, + ) + .to_component(ComponentDescriptor::partial("value"), Selector::parse(".")?) + .build()?; let lenses = Lenses::new(OutputMode::DropUnmatched) .add_lens(instruction) - .add_lens(destructure) + .add_lens(destructure_a) + .add_lens(destructure_b) .add_lens(time); let lenses_sink = LensesSink::new(GrpcSink::default(), lenses); diff --git a/examples/rust/log_file/Cargo.toml b/examples/rust/log_file/Cargo.toml index 1133ca575c64..affbf97a3258 100644 --- a/examples/rust/log_file/Cargo.toml +++ b/examples/rust/log_file/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "log_file" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/log_file/README.md b/examples/rust/log_file/README.md index 0248a3649a29..8258f51efbc7 100644 --- a/examples/rust/log_file/README.md +++ b/examples/rust/log_file/README.md @@ -2,7 +2,7 @@ title = "Log file example" --> -Demonstrates how to log any file from the SDK using the [`Importer`](https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview?speculative-link) machinery. +Demonstrates how to log any file from the SDK using the [`Importer`](https://www.rerun.io/docs/concepts/logging-and-ingestion/importers/overview) machinery. Usage: ```bash diff --git a/examples/rust/log_file/src/main.rs b/examples/rust/log_file/src/main.rs index e36aa04ae0a9..b48980a38af5 100644 --- a/examples/rust/log_file/src/main.rs +++ b/examples/rust/log_file/src/main.rs @@ -1,6 +1,6 @@ //! Demonstrates how to log any file from the SDK using the `Importer` machinery. //! -//! See for more information. +//! See for more information. //! //! Usage: //! ``` diff --git a/examples/rust/minimal/Cargo.toml b/examples/rust/minimal/Cargo.toml index 1275f641ad9a..b94665878af3 100644 --- a/examples/rust/minimal/Cargo.toml +++ b/examples/rust/minimal/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "minimal" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/minimal_options/Cargo.toml b/examples/rust/minimal_options/Cargo.toml index f3a73e9fe3a7..2f413edc404d 100644 --- a/examples/rust/minimal_options/Cargo.toml +++ b/examples/rust/minimal_options/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "minimal_options" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/minimal_serve/Cargo.toml b/examples/rust/minimal_serve/Cargo.toml index da2c16ddfd03..58db022f328f 100644 --- a/examples/rust/minimal_serve/Cargo.toml +++ b/examples/rust/minimal_serve/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "minimal_serve" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/objectron/Cargo.toml b/examples/rust/objectron/Cargo.toml index b8c9774dabfa..25e5adeb88e6 100644 --- a/examples/rust/objectron/Cargo.toml +++ b/examples/rust/objectron/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "objectron" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/objectron/src/objectron.rs b/examples/rust/objectron/src/objectron.rs index 5b0bbafc76cf..5ae2f70696bf 100644 --- a/examples/rust/objectron/src/objectron.rs +++ b/examples/rust/objectron/src/objectron.rs @@ -992,8 +992,8 @@ pub mod ar_mesh_geometry { } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Face { - /// / Indices of vertices defining the face from correspondent array of parent - /// / message. A typical face is triangular. + /// Indices of vertices defining the face from correspondent array of parent + /// message. A typical face is triangular. #[prost(int32, repeated, tag = "1")] pub vertex_indices: ::prost::alloc::vec::Vec, } diff --git a/examples/rust/raw_mesh/Cargo.toml b/examples/rust/raw_mesh/Cargo.toml index 100386c516dc..3630669a13e7 100644 --- a/examples/rust/raw_mesh/Cargo.toml +++ b/examples/rust/raw_mesh/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "raw_mesh" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/shared_recording/Cargo.toml b/examples/rust/shared_recording/Cargo.toml index 0362cc3321dc..194bad6de218 100644 --- a/examples/rust/shared_recording/Cargo.toml +++ b/examples/rust/shared_recording/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "shared_recording" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/spawn_viewer/Cargo.toml b/examples/rust/spawn_viewer/Cargo.toml index d7bb6131a2e8..e498444fabe4 100644 --- a/examples/rust/spawn_viewer/Cargo.toml +++ b/examples/rust/spawn_viewer/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "spawn_viewer" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/status_example/Cargo.toml b/examples/rust/state_timeline/Cargo.toml similarity index 50% rename from examples/rust/status_example/Cargo.toml rename to examples/rust/state_timeline/Cargo.toml index b40aaeb6d638..7e42d5ddad72 100644 --- a/examples/rust/status_example/Cargo.toml +++ b/examples/rust/state_timeline/Cargo.toml @@ -1,14 +1,12 @@ [package] -name = "status_example" -version = "0.32.0-alpha.1" +name = "state_timeline_example" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false [dependencies] -rerun = { path = "../../../crates/top/rerun", default-features = false, features = [ - "native_viewer", - "sdk", - "server", -] } +anyhow.workspace = true +arrow.workspace = true +rerun = { path = "../../../crates/top/rerun", default-features = false, features = ["sdk"] } diff --git a/examples/rust/state_timeline/src/main.rs b/examples/rust/state_timeline/src/main.rs new file mode 100644 index 000000000000..ece8d6936738 --- /dev/null +++ b/examples/rust/state_timeline/src/main.rs @@ -0,0 +1,151 @@ +//! Logs test state data for the state timeline view. + +use std::sync::Arc; + +/// Build a `StateChange` whose state array can contain nulls. +/// +/// A null entry resets that instance's state, showing a gap in its lane. +fn multi_state(states: &[Option<&str>]) -> rerun::StateChange { + rerun::StateChange::new().with_state_opt(states.iter().copied()) +} + +fn main() -> anyhow::Result<()> { + let rec = rerun::RecordingStreamBuilder::new("rerun_example_state_timeline").spawn()?; + + // An example of a static annotation context. An edge case for the state timeline view. + rec.log_static( + "/", + &rerun::AnnotationContext::new([ + (1, "person", rerun::Rgba32::from_rgb(220, 20, 60)), + (2, "bicycle", rerun::Rgba32::from_rgb(119, 11, 32)), + (3, "car", rerun::Rgba32::from_rgb(0, 0, 142)), + (4, "motorcycle", rerun::Rgba32::from_rgb(0, 0, 230)), + (5, "airplane", rerun::Rgba32::from_rgb(106, 0, 228)), + ]), + )?; + + // Base timestamp: 2025-04-01 12:00:00 UTC + let base_ts: f64 = 1_743_508_800.0; + let step_secs: f64 = 5.0; + + let states: Vec<(i64, &str, &str)> = vec![ + (0, "state/robot_mode", "1"), + (10, "state/robot_mode", "2"), + (25, "state/robot_mode", "3"), + (40, "state/robot_mode", "1"), + (0, "state/power", "On"), + (20, "state/power", "Low"), + (35, "state/power", "Critical"), + (45, "state/power", "On"), + (0, "state/connection", "Connected"), + (15, "state/connection", "Disconnected"), + (30, "state/connection", "Connected"), + ]; + + for (tick, entity, label) in &states { + rec.set_time_sequence("tick", *tick); + rec.set_timestamp_secs_since_epoch("timestamp", base_ts + *tick as f64 * step_secs); + rec.log(*entity, &rerun::StateChange::single(*label))?; + } + + // Multi-instance state: a gamepad's buttons logged as one state array, in the spirit of + // ROS `sensor_msgs/Joy`. Each instance gets its own lane, grouped under a single label. + // Every row is a full assignment of the array: `None` resets its instance (gap in that + // lane), and the shorter row at tick 28 resets the omitted third button the same way. + #[rustfmt::skip] + let button_states: Vec<(i64, Vec>)> = vec![ + (0, vec![Some("Released"), Some("Released"), Some("Released")]), + (5, vec![Some("Pressed"), Some("Released"), Some("Released")]), + (12, vec![Some("Pressed"), Some("Pressed"), Some("Released")]), + (18, vec![Some("Released"), None, Some("Pressed")]), + (28, vec![Some("Released"), Some("Released")]), + (38, vec![Some("Pressed"), Some("Pressed"), Some("Pressed")]), + (46, vec![Some("Pressed"), Some("Released"), Some("Released")]), + ]; + for (tick, states) in &button_states { + rec.set_time_sequence("tick", *tick); + rec.set_timestamp_secs_since_epoch("timestamp", base_ts + *tick as f64 * step_secs); + rec.log("state/gamepad_buttons", &multi_state(states))?; + } + + // One shared configuration styles every instance lane of the group. + rec.log_static( + "state/gamepad_buttons", + &rerun::StateConfiguration::new() + .with_values(["Pressed", "Released"]) + .with_colors([ + rerun::Rgba32::from_rgb(239, 83, 80), + rerun::Rgba32::from_rgb(76, 175, 80), + ]), + )?; + + // Log an alternative string component on robot_mode via DynamicArchetype. + // This allows switching the state source in the source selector dropdown. + let alt_states: Vec<(i64, &str)> = + vec![(0, "IDLE"), (10, "MOVING"), (25, "WORK"), (40, "NOPE")]; + for (tick, state) in &alt_states { + rec.set_time_sequence("tick", *tick); + rec.set_timestamp_secs_since_epoch("timestamp", base_ts + 2.0 * *tick as f64 * step_secs); + rec.log( + "state/robot_mode", + &rerun::DynamicArchetype::new("sensor_data").with_component_from_data( + "state", + Arc::new(arrow::array::StringArray::from(vec![*state])), + ), + )?; + } + + // Log a boolean signal as an alternative state source — the user can remap it onto the + // state slot via the source selector. Exercises the polymorphic state cast (Bool + // passthrough) and the simplified true/false editor in the selection panel. + let bool_states: Vec<(i64, bool)> = vec![ + (0, true), + (8, false), + (15, false), + (22, false), + (30, true), + (38, false), + (45, true), + ]; + for (tick, state) in &bool_states { + rec.set_time_sequence("tick", *tick); + rec.set_timestamp_secs_since_epoch("timestamp", base_ts + *tick as f64 * step_secs); + rec.log( + "state/heartbeat", + &rerun::DynamicArchetype::new("heartbeat_signal").with_component_from_data( + "alive", + Arc::new(arrow::array::BooleanArray::from(vec![*state])), + ), + )?; + } + + // Log scalar data on the same timelines so a time series view can be added. + for tick in 0..50 { + let t = tick as f64; + rec.set_time_sequence("tick", tick); + rec.set_timestamp_secs_since_epoch("timestamp", base_ts + t * step_secs); + rec.log("scalar/sine", &rerun::Scalars::new([f64::sin(t * 0.3)]))?; + } + + // Bad data: changes component type from string to boolean. + rec.set_time_sequence("tick", 1); + rec.log( + "foo", + &rerun::DynamicArchetype::new("bar").with_component_from_data( + "state", + Arc::new(arrow::array::StringArray::from(vec!["ponies"])), + ), + )?; + rec.set_time_sequence("tick", 2); + rec.log( + "foo", + &rerun::DynamicArchetype::new("bar").with_component_from_data( + "state", + Arc::new(arrow::array::BooleanArray::from(vec![true])), + ), + )?; + + let _ = rec.flush_blocking(); + + Ok(()) +} diff --git a/examples/rust/status_example/src/main.rs b/examples/rust/status_example/src/main.rs deleted file mode 100644 index 128a86bbd56a..000000000000 --- a/examples/rust/status_example/src/main.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Example app that opens a Rerun Viewer with the Status view showing test state data. - -use rerun::external::{re_crash_handler, re_grpc_server, re_log, re_viewer, tokio}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let main_thread_token = rerun::MainThreadToken::i_promise_i_am_on_the_main_thread(); - - re_log::setup_logging(); - re_crash_handler::install_crash_handlers(re_viewer::build_info()); - - // Listen for gRPC connections. - let rx = re_grpc_server::spawn_with_recv( - "0.0.0.0:9876".parse()?, - Default::default(), - re_grpc_server::shutdown::never(), - ); - - let startup_options = re_viewer::StartupOptions::default(); - let app_env = re_viewer::AppEnvironment::Custom("Status view example".to_owned()); - - // Log some status data via SDK so the Status view has something to show. - log_status_data()?; - - re_viewer::run_native_app( - main_thread_token, - Box::new(move |cc| { - let mut app = re_viewer::App::new( - main_thread_token, - re_viewer::build_info(), - app_env, - startup_options, - cc, - None, - re_viewer::AsyncRuntimeHandle::from_current_tokio_runtime_or_wasmbindgen().expect( - "Could not get a runtime handle from the current Tokio runtime or Wasm bindgen.", - ), - ); - app.add_log_receiver(rx); - Ok(Box::new(app)) - }), - None, - )?; - - Ok(()) -} - -fn log_status_data() -> Result<(), Box> { - let rec = rerun::RecordingStreamBuilder::new("rerun_example_status") - .default_enabled(true) - .connect_grpc() - .map_err(|err| format!("Failed to connect: {err}"))?; - - // Base timestamp: 2025-04-01 12:00:00 UTC - let base_ts: f64 = 1_743_508_800.0; - let step_secs: f64 = 5.0; - - let states: Vec<(i64, &str, &str)> = vec![ - (0, "state/robot_mode", "Idle"), - (10, "state/robot_mode", "Moving"), - (25, "state/robot_mode", "Working"), - (40, "state/robot_mode", "Idle"), - (0, "state/power", "On"), - (20, "state/power", "Low"), - (35, "state/power", "Critical"), - (45, "state/power", "On"), - (0, "state/connection", "Connected"), - (15, "state/connection", "Disconnected"), - (30, "state/connection", "Connected"), - ]; - - for (tick, entity, label) in &states { - rec.set_time_sequence("tick", *tick); - rec.set_timestamp_secs_since_epoch("timestamp", base_ts + *tick as f64 * step_secs); - rec.log(*entity, &rerun::Status::new().with_status(*label))?; - } - - // Log scalar data on the same timelines so a time series view can be added. - for tick in 0..50 { - let t = tick as f64; - rec.set_time_sequence("tick", tick); - rec.set_timestamp_secs_since_epoch("timestamp", base_ts + t * step_secs); - rec.log("scalar/sine", &rerun::Scalars::new([f64::sin(t * 0.3)]))?; - } - - let _ = rec.flush_blocking(); - - Ok(()) -} diff --git a/examples/rust/stdio/Cargo.toml b/examples/rust/stdio/Cargo.toml index 19d6c941d079..58bfc957feb8 100644 --- a/examples/rust/stdio/Cargo.toml +++ b/examples/rust/stdio/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "stdio" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false [dependencies] +itertools.workspace = true rerun = { path = "../../../crates/top/rerun" } diff --git a/examples/rust/stdio/src/main.rs b/examples/rust/stdio/src/main.rs index 887a208f1b16..b419705c5027 100644 --- a/examples/rust/stdio/src/main.rs +++ b/examples/rust/stdio/src/main.rs @@ -6,13 +6,13 @@ //! echo 'hello from stdin!' | cargo run | rerun - //! ``` +use itertools::Itertools as _; + fn main() -> Result<(), Box> { let rec = rerun::RecordingStreamBuilder::new("rerun_example_stdio").stdout()?; - let input = std::io::stdin() - .lines() - .collect::, _>>()? - .join("\n"); + let lines: Vec = std::io::stdin().lines().try_collect()?; + let input = lines.join("\n"); rec.log("stdin", &rerun::TextDocument::new(input))?; diff --git a/examples/rust/template/Cargo.toml b/examples/rust/template/Cargo.toml index e29b68094940..8720a18f88a7 100644 --- a/examples/rust/template/Cargo.toml +++ b/examples/rust/template/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "template" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/viewer_callbacks/Cargo.toml b/examples/rust/viewer_callbacks/Cargo.toml index 246cdcbb6762..da9253632ead 100644 --- a/examples/rust/viewer_callbacks/Cargo.toml +++ b/examples/rust/viewer_callbacks/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "viewer_callbacks" -version = "0.32.0-alpha.1" +version = "0.35.0" edition = "2024" -rust-version = "1.92" +rust-version = "1.95" license = "MIT OR Apache-2.0" publish = false diff --git a/examples/rust/viewer_callbacks/src/main.rs b/examples/rust/viewer_callbacks/src/main.rs index 16d190991b56..cfa5b718dd9a 100644 --- a/examples/rust/viewer_callbacks/src/main.rs +++ b/examples/rust/viewer_callbacks/src/main.rs @@ -27,7 +27,7 @@ async fn main() -> Result<(), Box> { // Listen for gRPC connections from Rerun's logging SDKs. // There are other ways of "feeding" the viewer though - all you need is a `re_log_channel::LogReceiver`. - let rx = re_grpc_server::spawn_with_recv( + let (rx, _grpc_server_handle) = re_grpc_server::spawn_with_recv( "0.0.0.0:9876".parse()?, Default::default(), re_grpc_server::shutdown::never(), @@ -120,7 +120,7 @@ impl eframe::App for MyApp { // First add our panel(s): egui::Panel::right("my_side_panel") .default_size(200.0) - .show_inside(ui, |ui| { + .show(ui, |ui| { self.ui(ui); }); diff --git a/lychee.toml b/lychee.toml index 464d6877d3d8..ceaed45aff72 100644 --- a/lychee.toml +++ b/lychee.toml @@ -53,7 +53,6 @@ exclude_path = [ ".pixi", "build", "docs/python/", - "landing", "rerun_cpp/_deps", "rerun_cpp/docs/html", "rerun_cpp/docs/xml", @@ -76,6 +75,7 @@ exclude_path = [ "crates/store/re_uri/src/redap_uri.rs", # Same as above. "crates/store/re_grpc_server/src/lib.rs", # Contains CORS tests with fake origin URLs "crates/utils/re_analytics/src/event.rs", # Contains test with malformed urls + "crates/utils/re_perf_telemetry/src/telemetry.rs", # Contains `parse_rerun_endpoint` docs/tests with fake `rerun://host…` URLs "crates/viewer/re_viewer/src/reflection/mod.rs", # Checker struggles how links from examples are escaped here. They are all checked elsewhere, so not an issue. "docs/snippets/INDEX.md", # The snippet index is guaranteed should be correct by design. "scripts/lint.py", # Contains url-matching regexes that aren't actual urls @@ -90,6 +90,7 @@ exclude = [ # Strings with replacements. '/__VIEWER_VERSION__/', # Replacement variable __VIEWER_VERSION__. '/\$', # Replacement variable $. + '\$$', # URL ending in `$`: stripped template variable (e.g. `${{ inputs.X }}`). '/GIT_HASH/', # Replacement variable GIT_HASH. '\{\}', # Ignore links with string interpolation. '\$relpath\^', # Relative paths as used by rerun_cpp's doc header. @@ -101,7 +102,7 @@ exclude = [ # Local links that require further setup. '/examples', # Relative link to our examples gallery. - 'http://0.0.0.0:51234', + 'http://0.0.0.0', 'http://127.0.0.1', 'http://localhost', 're_viewer.js', # Build artifact that html is linking to. @@ -130,6 +131,7 @@ exclude = [ 'file://somehost/file/path.rrd', 'http://foo.*', 'http://x/', + 'https://x/', 'https://foo.*', 'https://link.to', 'https://rerun.rs', @@ -146,11 +148,14 @@ exclude = [ 'http(s)?://example.com/.*', 'file:///uri1.rrd', 'file:///uri2.rrd', + 'file:///recordings/data.rrd', # Example URL in the url-pill tests. # Link fragments and data links in examples. 'https://raw.githubusercontent.com/googlefonts/noto-emoji/', # URL fragment. 'https://static.rerun.io/rgbd_dataset', # Base data link for rgbd dataset. 'https://storage.googleapis.com/', # Storage API entrypoint, not a link. + 'https://ref\.rerun\.io/prose/?$', # GCS bucket prefix, no directory index. + 'https://build\.rerun\.io/mirror/mozilla/sccache/?$', # GCS bucket prefix, no directory index. # Not accessible from CI. '.github/workflows/.*.yml', # GitHub action workflows cause issues on CI. @@ -158,6 +163,7 @@ exclude = [ 'https://claude.site/artifacts/*', # Giving a 500, but only from CI 'https://fifteen-thirtyeight.rerun.io/script.js', # Gives 403 forbidden on CI. 'https://github.com/user-attachments/assets/.*', # Gives Not Found on CI, but works locally. + 'https://git.sr.ht/*', # Bot protection returns 418 I'm a teapot on CI. 'https://lib.rs/*', # Gives 403 forbidden on CI. 'https://math.stackexchange.com/*', # Gives 403 forbidden on CI. 'https://pixabay.com/photos/brother-sister-girl-family-boy-977170/', # Gives 403 forbidden on CI. diff --git a/pixi.lock b/pixi.lock index 04c88a2ed5c7..f871ce927bdc 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1,130 +1,128 @@ -version: 6 +version: 7 +platforms: +- name: p1 + subdir: linux-64 + virtual-packages: + - __glibc=2.28 + - __unix=0=0 + - __linux=4.18 + - __archspec=0=x86_64 +- name: p2 + subdir: linux-aarch64 + virtual-packages: + - __glibc=2.28 + - __unix=0=0 + - __linux=4.18 + - __archspec=0=aarch64 +- name: p3 + subdir: osx-arm64 + virtual-packages: + - __osx=11.0 + - __unix=0=0 + - __archspec=0=m1 +- name: p4 + subdir: osx-64 + virtual-packages: + - __osx=11.0 + - __unix=0=0 + - __archspec=0=x86_64 +- name: win-64 + virtual-packages: + - __win=10.0 + - __archspec=0=x86_64 environments: - cpp: + coverage: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + p1: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.9.5-py311h459d7ec_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.14-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16.1-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binaryen-117-h59595ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.44-h4852527_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.44-h4852527_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.46.1-default_hfdba357_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h66f275b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/buf-1.57.0-ha8f183a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.6.0-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/buf-1.66.0-ha8f183a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py311h5b438cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cargo-llvm-cov-0.8.7-hdab8a38_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cargo-nextest-0.9.140-hb17b654_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cargo-zigbuild-0.20.1-hb17b654_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.0-py311h03d9500_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16-16.0.6-default_hddf928d_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16.0.6-default_hfa515fb_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16-16.0.6-default_hddf928d_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16.0.6-default_hddf928d_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-16.0.6-default_hddf928d_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-3.27.6-hcfe8598_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.6.0-h00ab1b0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/doxygen-1.9.7-h661eb56_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fd-find-10.3.0-hdab8a38_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fd-find-10.4.2-hdab8a38_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-7.1.1-gpl_ha0aeed6_910.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/flatbuffers-25.2.10-hb7832b1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.0-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/flatbuffers-25.12.19-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.1-h27c8c51_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.7.0-py311h52bc045_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.4.0-h236703b_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.4.0-h26ba24d_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.4.0-h6b7512a_10.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.0-h2b0a6b4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-tools-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gh-2.79.0-h76a2195_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.8.0-py311h52bc045_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.7-h2b0a6b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gh-2.96.0-hfc2019e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-12.4.0-h236703b_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.4.0-h3ff227c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.4.0-h8489865_10.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.4.5-h15599e2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.1-h15599e2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.8.2-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.10.0-hb700be7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-media-driver-25.3.4-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.26.1-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.29.0-hb700be7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.25.1-h3f43e3d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.4-h96ad9f0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.71-h39aace5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libattr-2.5.2-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.75-h39aace5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp16-16.0.6-default_hddf928d_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp18.1-18.1.8-default_h99862b1_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-21.1.0-default_h746c552_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.0-ha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.0-h73754d4_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.4.0-h1762d19_102.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-lib-1.11.1-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.55-h3f2d84a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.5.0-he200343_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-lib-1.12.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.88.2-h0d30a3d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.61-h54a6638_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.12.1-default_h3d81e11_1000.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm16-16.0.6-ha7bfdaf_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.8-default_hddf928d_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm21-21.1.0-hecd9e04_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2025.2.0-hb617929_1.conda @@ -140,27 +138,26 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2025.2.0-hecca717_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2025.2.0-h0767aad_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hecca717_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.5.2-hd0c01bc_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.6.1-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-hfb7daa7_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.4.0-ha732cd4_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc7d488a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.4.0-h1762d19_102.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-256.9-h2774228_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.4-h9a4d06a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-257.7-h4e0b6ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-h9d88235_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev-257.4-hbe16f8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.4-hbe16f8c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.6.2-h9c3ff4c_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/liburing-2.9-h84d6215_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.22.0-h4f16b4b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.24.1-he1eb515_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h54a6638_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpl-2.15.0-h54a6638_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda @@ -168,271 +165,263 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.9-h04c0eec_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lychee-0.23.0-he64ecbb_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py311h2dc5d0c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py311h3778330_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/meilisearch-1.5.1-he8a937b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.6.3-py311h2dc5d0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py311h3778330_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.14.1-py311h9ecbd09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/nasm-2.16.03-h4bc722e_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.17.1-heeeca48_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-h5888daf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-hc22cd8d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-24.13.0-h36edbcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.4-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-h65dd3cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/patchelf-0.17.2-h58526e2_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/prettier-3.6.2-h4c22ac6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.3.1-py311h2dc5d0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pkgconf-3.0.3-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prettier-3.8.1-h7e4c9f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.5.2-py311h3778330_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.31.1-py311h425ed32_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.3-py311haee01d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py311haee01d2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.13-h9e4cc4f_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-h9a8bead_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.15-h7508c33_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.27.1-py311h902ca64_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.6.3-py311h1baac5b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.7-h7805a7d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.54-h3f2d84a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.14-he3e324a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-3.1.2-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/taplo-0.9.1-h1ff36dd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2022.2.0-hb60516a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2022.3.0-h8d10470_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.31-h4e94fc0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/typos-1.45.1-hb17b654_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.45-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/typos-1.48.0-hb17b654_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wasm-pack-0.15.0-hb17b654_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hd6090a7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/x264-1!164.3095-h166bdaf_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-h280c20c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.1-hbcc6ac9_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.1-hbcc6ac9_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.3-ha02ee65_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.3-ha02ee65_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.3-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.1-py311h2dc5d0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.24.5-py311h3778330_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zig-0.13.0-h97ab28e_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d8/09/0822403f40932a165a4f1df289d41653683019e4fd7a86b63ed20e9b6177/backports_zstd-1.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/26/f8/a81170a816679fca9ccd907b801992acfc03c33f952440421c921af2cc57/cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-hcc6f6b0_119.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.49-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/f8/a81170a816679fca9ccd907b801992acfc03c33f952440421c921af2cc57/cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/25/a9e37dd035027565fa0b7e367da50e88a6ab26e7fd413269aa118e25258b/backports_zstd-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/fb/bc7f639aed026bca6e7beb1e33f6951e16b7d315594e7635a4f7d21d63f4/py_spy-0.4.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - - pypi: ./rerun_pixi_env - linux-aarch64: - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f9/34/dd7d3c763a00b7b965e25a5eab0acd1a345dbaf0f45fffe595278873a1c0/py_spy-0.4.2-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p2: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.9.5-py311hcd402e7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.14-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.16.1-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aom-3.9.1-hcccb83c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/attr-2.5.1-h4e544f5_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/attr-2.5.2-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binaryen-117-h2f0025b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils-2.44-hf1166c9_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.44-h4c662bb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_linux-aarch64-2.44-hf1166c9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.46.1-default_h5f4c503_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py311h14a79a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.5-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-compiler-1.6.0-h31becfc_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.8-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.0.0-py311h3324b35_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cargo-llvm-cov-0.8.7-h1ebd7d5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cargo-nextest-0.9.140-h069e38c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cargo-zigbuild-0.20.1-h069e38c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.1.0-py311h460c349_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16-16.0.6-default_hf07bfb7_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16.0.6-default_h3935787_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16-16.0.6-default_hf07bfb7_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16.0.6-default_hf07bfb7_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-tools-16.0.6-default_hf07bfb7_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cmake-3.27.6-hef020d8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cxx-compiler-1.6.0-h2a328a1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dav1d-1.2.1-h31becfc_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-heda779d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-h70963c4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/doxygen-1.9.7-h7b6a552_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fd-find-10.3.0-h1ebd7d5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fd-find-10.4.2-h1ebd7d5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ffmpeg-7.1.1-gpl_h8d881e6_910.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/flatbuffers-25.2.10-ha90f286_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.15.0-h8dda3cd_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.0-h8af1aa0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/flatbuffers-25.12.19-h7ac5ae9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.18.1-hba86a56_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.3-h8af1aa0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.7.0-py311h91c1192_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc-12.4.0-h7e62973_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_impl_linux-aarch64-12.4.0-h628656a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_linux-aarch64-12.4.0-heb3b579_10.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.0-h90308e0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-tools-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gh-2.79.0-h94b2740_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.8.0-py311h91c1192_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.7-h90308e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gh-2.96.0-h22914b5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gmp-6.3.0-h0a1ffab_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx-12.4.0-h7e62973_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_impl_linux-aarch64-12.4.0-h0bf7a72_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_linux-aarch64-12.4.0-h3f57e68_10.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-11.4.5-he4899c9_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.15-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-11.5.1-he4899c9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-h2fb54aa_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lame-3.100-h4e544f5_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.44-h5e2c951_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-hfdc4d58_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.46.1-default_h1979696_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.1.0-h52b7260_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libabseil-20250512.1-cxx17_h201e9ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-0.25.1-h5e0f5ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-devel-0.25.1-h5e0f5ae_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libass-0.17.4-hcfe818d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.71-h51d75a7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libattr-2.5.2-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.75-h51d75a7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang-cpp16-16.0.6-default_hf07bfb7_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang-cpp18.1-18.1.8-default_he95a3c9_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang13-21.1.0-default_h94a09a5_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.14.1-h6702fde_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.24-he377734_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.21.0-hc57f145_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.127-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.1-hfae3067_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.6-he21f813_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libflac-1.4.3-h2f0025b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.0-h8af1aa0_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.0-hdae7a39_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.1.0-he277a41_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-12.4.0-h7b3af7c_102.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.1.0-he9431aa_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcrypt-lib-1.11.1-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-devel-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.0-h7cdfd2c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.1.0-he277a41_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgpg-error-1.55-h5ad3122_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.8.1-hfae3067_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libflac-1.5.0-he9c94f4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.3-h8af1aa0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.3-hdae7a39_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcrypt-lib-1.12.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.88.2-h96a7f82_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgpg-error-1.61-h7ac5ae9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libhwloc-2.12.1-default_h6f258fa_1000.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.0-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.2.0-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm16-16.0.6-h2edbd07_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm18-18.1.8-default_hbd976d5_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm21-21.1.0-h2b567e5_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.1-h86ecc28_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-devel-5.8.1-h86ecc28_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.67.0-ha888d0e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-devel-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.68.1-hd3077d7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libogg-1.3.5-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-2025.2.0-hcd21e76_1.conda @@ -446,522 +435,319 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-pytorch-frontend-2025.2.0-hfae3067_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-frontend-2025.2.0-h38473e3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-lite-frontend-2025.2.0-hfae3067_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopus-1.5.2-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.18-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.50-h1abf092_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-6.31.1-h2cf3c76_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopus-1.6.1-h80f16a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.19-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.58-h1abf092_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-6.31.1-h61c7711_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.58.4-h3ac5bce_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsanitizer-12.4.0-h469570c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsndfile-1.2.2-h79657aa_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.50.4-h022381a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsndfile-1.2.2-h30591a0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.3-h022381a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-h18c354c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.1.0-h3f4de04_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-aarch64-12.4.0-h7b3af7c_102.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.1.0-hf1166c9_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsystemd0-256.9-hd54d049_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.0-h7a57436_6.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev1-257.4-h1187dce_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.2.0-hdbbeba8_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsystemd0-257.7-h2bb824b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.2-hdb009f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev-257.4-h7b9e449_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev1-257.4-h7b9e449_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libunwind-1.6.2-h01db608_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liburing-2.9-h17cf362_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libusb-1.0.29-h06eaf92_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.1-h3e4203c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.51.0-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42.2-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.52.1-h80f16a2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvorbis-1.3.7-h7ac5ae9_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvpx-1.14.1-h0a1ffab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.11.0-h95ca766_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.8-he58860d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.9-he58860d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lychee-0.23.0-hb434046_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.9.4-hd600fc2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.2-py311ha09ea12_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.10.0-h5ad3122_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py311h2dad8b0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mpg123-1.32.9-h65af167_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.6.3-py311h58d527c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.7.1-py311h164a683_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mypy-1.14.1-py311ha879c10_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nasm-2.16.03-h68df207_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.11.1-hdd96247_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-24.4.1-hc854191_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openh264-2.6.0-h0564a2a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.0-h8e36d6e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-22.21.1-h43d1aef_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openh264-2.6.0-h663e864_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.3-h546c87b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.46-h15761aa_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/prettier-3.6.2-h70496c1_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.3.1-py311h58d527c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.47-hf841c20_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pkgconf-3.0.3-h80f16a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/prettier-3.8.1-h1e5041c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.5.2-py311h164a683_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/protobuf-6.31.1-py311he3e547a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.1.3-py311h51cfe5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.2.2-py311h51cfe5d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pugixml-1.15-h6ef32b0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pulseaudio-client-17.0-h729494f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.11.13-h1683364_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py311h164a683_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pulseaudio-client-17.0-h77cf2aa_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.11.15-h53314ec_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py311h164a683_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rhash-1.4.6-h86ecc28_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.27.1-py311hc91c717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-2026.6.3-py311h3b69377_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruff-0.15.7-h9f438e6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl2-2.32.54-h5ad3122_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl3-3.2.14-h7e2c5d6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.2.2-he774c54_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.2.2-he774c54_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/svt-av1-3.1.2-hfae3067_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/taplo-0.9.1-hb8f9562_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tbb-2022.2.0-h8f856e4_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5688188_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tbb-2022.3.0-h0eac15c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5cf4473_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ty-0.0.31-h47ce4e6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/typos-1.45.1-h069e38c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.24.0-h698ed42_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/typos-1.48.0-h069e38c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wasm-pack-0.15.0-h069e38c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.26.0-h4f8a99f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x264-1!164.3095-h4e544f5_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x265-3.5-hdd96247_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.45-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.48-h80f16a2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.12-hca56bd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.13-h63a1b12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.6-h57736b2_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.1-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.7-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.8.1-h2dbfc1b_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-gpl-tools-5.8.1-h2dbfc1b_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-tools-5.8.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.8.3-hd704e39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-gpl-tools-5.8.3-hd704e39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-tools-5.8.3-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.20.1-py311h58d527c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.24.5-py311h164a683_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zig-0.13.0-h49d127f_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.25.0-py311h51cfe5d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6f/3e/2667c0ddb53ddf28667e330bf9fe92e8e17705a481c9b698e283120565f7/backports_zstd-1.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/8f/6c52b1f9d650863e8f67edbe062c04f1c8455579eaace1593d8fe469319a/cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/df/79/9ed50bb0a9de63ed023aa2db8b6265b04a7760d98c61eb54def6a5fddb68/py_spy-0.4.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - - pypi: ./rerun_pixi_env - osx-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.9.5-py311he705e18_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/binaryen-117-h73e2aa4_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py311h7e844b6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.5-hf13058a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.6.0-h282daa2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h950ec3b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cctools-1010.6-h40f6528_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cctools_osx-64-1010.6-heaa7f0c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py311h8ebb5ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-default_h510d6ca_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-tools-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx-16.0.6-default_h1b9e3cd_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-16.0.6-hb91bd55_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cmake-3.27.6-hf40c264_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/compiler-rt-16.0.6-ha38d28d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-64-16.0.6-ha38d28d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cxx-compiler-1.6.0-h7728843_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h27bd348_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/doxygen-1.9.7-hd7636e7_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/fd-find-10.3.0-hb440939_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-7.1.1-gpl_hf226373_110.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/flatbuffers-25.2.10-h2cf7b43_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.15.0-h37eeddb_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.0-h694c41f_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.7.0-py311h7a2b322_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.0-h07555a4_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/gh-2.79.0-hfb6d0b5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.14-h21dd04a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-11.4.5-h0ffbb26_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lame-3.100-hb7f2c08_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/ld64-951.9-ha02d983_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ld64_osx-64-951.9-h3516399_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hcca01a6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.4-h87c4fc2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang13-21.1.0-default_h7f9524c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.14.1-h5dec5d8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.1-h3d58e20_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-devel-16.0.6-h8f8a49f_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.24-hcc1b750_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.1-h21dd04a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.0-h694c41f_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.0-h6912278_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.86.0-h7cafd41_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.12.1-default_h8c32e24_1000.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.0-h6e16a3a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm21-21.1.0-h9b4ebcc_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2025.2.0-h346e020_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2025.2.0-heda8b29_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2025.2.0-heda8b29_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2025.2.0-hd57c75b_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2025.2.0-h346e020_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2025.2.0-hd57c75b_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2025.2.0-ha4fb624_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2025.2.0-ha4fb624_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2025.2.0-hbc7d668_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2025.2.0-hd87add6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hbc7d668_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.5.2-he3325bb_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.50-h84aeda2_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h03562ea_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.58.4-h21a6cfa_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.50.4-h39a8b3b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.0-h59ddb5d_6.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libvorbis-1.3.7-ha059160_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.14.1-hf036a51_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.8-he1bc88e_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.0-hf4e0ed4_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-tools-16.0.6-hbedff68_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lychee-0.23.0-h651e3a3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py311ha3cf9ac_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-15.2.0-h55c397f_119.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.6.3-py311h1cc1194_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.14.1-py311h4d7f069_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/nasm-2.16.03-hfdf4475_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ninja-1.11.1-hb8565cd_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-24.4.1-h2e7699b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-h4883158_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-h6ef8af8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.46-ha3e7e28_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-ha059160_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/prettier-3.6.2-h07b0e94_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.3.1-py311ha3cf9ac_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.31.1-py311h1c9791f_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.1.3-py311h62e9434_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.13-h9ccd52b_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py311he13f9b5_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/rhash-1.4.6-h6e16a3a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.27.1-py311hd3d88a1_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.7-h16586dd_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.54-h92383a6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.2.22-hc0b302d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/sigtool-0.1.3-h88f4db0_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h25c286d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-3.1.2-h21dd04a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tapi-1300.6.5-h390ca13_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/taplo-0.9.1-h236d3af_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2022.2.0-hc025b3e_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ty-0.0.31-h479939e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/typos-1.45.1-h19f9e61_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/x264-1!164.3095-h775f41a_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.1-h357f2ed_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.1-h357f2ed_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.20.1-py311ha3cf9ac_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py311h62e9434_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h8210216_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ac/28/ed31a0e35feb4538a996348362051b52912d50f00d25c2d388eccef9242c/backports_zstd-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/52/1b/49ebc2b59e9126f1f378ae910e98704d54a3f48b78e2d6d6c8cfe6fbe06f/cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/23/3eb4c23c684ebd667674ce1d076ae855e0621d1d9bd5e052aa3f7982f757/py_spy-0.4.2-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/14/e3/3a32500d845bdd94f6a2b4ed6244982f42ec2bc64602ea8fcfe900678ae7/py_spy-0.4.1-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/8f/6c52b1f9d650863e8f67edbe062c04f1c8455579eaace1593d8fe469319a/cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/cb/af58363b0dd0b497282ecef1fa99789b03cc1885a01a41394cad42ceeff6/backports_zstd-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - - pypi: ./rerun_pixi_env - osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.9.5-py311h05b510d_0.conda + - pypi: https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p3: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.9.5-py311h05b510d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aom-3.9.1-h7bae524_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/binaryen-117-hebf3989_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py311hdc60ec4_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/buf-1.57.0-h75b854d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-compiler-1.6.0-h6aa9301_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/buf-1.66.0-h75b854d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.8-h84a0fba_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools-1010.6-h4faf515_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1010.6-h4f2c9d0_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py311hcfc1310_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cargo-llvm-cov-0.8.7-h748bcf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cargo-nextest-0.9.140-h6fdd925_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.1.0-py311h833bfeb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16-16.0.6-default_h3c2e7ce_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16.0.6-default_h3e759af_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16-16.0.6-default_h3c2e7ce_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16.0.6-default_h3c2e7ce_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-16.0.6-default_h3c2e7ce_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-16.0.6-hc421ffc_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_osx-arm64-16.0.6-h54d7cd3_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-16.0.6-default_hc1b5c72_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-16.0.6-hcd7bac0_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_osx-arm64-16.0.6-h54d7cd3_19.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-3.27.6-h1c59155_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-16.0.6-h3808999_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-16.0.6-h3808999_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cxx-compiler-1.6.0-h2ffa867_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dbus-1.16.2-hda038a8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dbus-1.16.2-h3ff7a7c_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/doxygen-1.9.7-h0e2417a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fd-find-10.3.0-h0ca00b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fd-find-10.4.2-h748bcf4_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ffmpeg-7.1.1-gpl_h93d53e2_110.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/flatbuffers-25.2.10-h3144c11_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.0-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/flatbuffers-25.12.19-h784d473_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.18.1-h2b252f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.3-hce30654_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.7.0-py311h8740443_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.0-h7542897_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gh-2.79.0-h4e0460a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.8.0-py311hf75086c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.7-h4e57454_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gh-2.96.0-hf76c51c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gmp-6.3.0-h7bae524_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-11.4.5-hf4e55d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.15-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-12.2.0-haf38c7b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lame-3.100-h1a8c8d9_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64-951.9-h634c8be_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-951.9-h0605c9f_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libass-0.17.4-hcbd7ca7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp16-16.0.6-default_h3c2e7ce_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.0-default_h6e8f826_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.1-hf598326_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-16.0.6-h86353a2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.8-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.0-hce30654_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.0-h6da58f4_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.1.0-hb74de2c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.0-h1bb475b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.3-hdfa99f5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.2-ha08bb59_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwloc-2.12.1-default_h88f92a7_1000.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm16-16.0.6-hc4b4ae8_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.0-h846d351_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libogg-1.3.5-h48c0fde_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-2025.2.0-h56e7ac4_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-arm-cpu-plugin-2025.2.0-h56e7ac4_1.conda @@ -974,480 +760,681 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-pytorch-frontend-2025.2.0-hec049ff_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-frontend-2025.2.0-hee62d61_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-lite-frontend-2025.2.0-hec049ff_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopus-1.5.2-h48c0fde_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h658db43_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopus-1.6.1-h1a92334_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h29102cf_5.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.58.4-h266df6f_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.0-h025e3ab_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.2-h282da08_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libusb-1.0.29-hbc156a2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.52.1-h1a92334_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvorbis-1.3.7-h81086ad_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvpx-1.14.1-h7bae524_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvulkan-loader-1.4.341.0-h3feff0a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.8-h4a9ca0c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.0-hbb9b287_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.9-h4a9ca0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.8-hc7d1edf_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-16.0.6-hc4b4ae8_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lychee-0.23.0-h17e24d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py311h4921393_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py311hc290fe0_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/meilisearch-1.5.1-h5ef7bb8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.6.3-py311h30e7462_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.7.1-py311ha275503_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.14.1-py311h917b07b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nasm-2.16.03-h99b78c6_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ninja-1.11.1-hffc8910_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-24.4.1-hab9d20b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openh264-2.6.0-hb5b2745_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-24.13.0-h3a0f24a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openh264-2.6.0-hdf0efb5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prettier-3.6.2-h9907cc9_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.3.1-py311h4921393_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-h30297fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h784d473_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prettier-3.8.1-h9907cc9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.5.2-py311hc290fe0_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.31.1-py311h93f9908_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.3-py311h5bb9006_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py311he363849_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pugixml-1.15-hd3d436d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.13-hc22306f_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311ha9b3269_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h0c9c016_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311hc290fe0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rhash-1.4.6-h5505292_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.27.1-py311h1c3fc1a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-2026.6.3-py311haff49d3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.7-hc5c3a1d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl2-2.32.54-ha1acc90_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl3-3.2.22-he22eeb8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-0.1.3-h44b9a77_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hd121638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl2-2.32.56-h784d473_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl3-3.4.12-h6fa9c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/svt-av1-3.1.2-h12ba402_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1300.6.5-h03f4b80_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/taplo-0.9.1-h16c8c8b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tbb-2022.2.0-h5b2e6d4_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tbb-2022.3.0-h66ce52b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hd3d0363_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ty-0.0.31-hdfcc030_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/typos-1.45.1-h6fdd925_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/typos-1.48.0-h6fdd925_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wasm-pack-0.15.0-h6fdd925_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x264-1!164.3095-h57fd34a_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x265-3.5-hbc6ce65_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.1-h9a6d368_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.1-h9a6d368_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.3-hd0f0c4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.3-hd0f0c4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.3-h8088a28_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.20.1-py311h4921393_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.24.5-py311hc290fe0_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py311h5bb9006_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/00/0d/3db362169d80442adda9dd563c4f0bb10091c8c1c9a158037f4ecd53988e/backports_zstd-1.3.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/75/7a/2ea7dd2202638cf1053aaa8fbbaddded0b78c78832b3d03cafa0416a6c84/cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/7a/2ea7dd2202638cf1053aaa8fbbaddded0b78c78832b3d03cafa0416a6c84/cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/14/e3/3a32500d845bdd94f6a2b4ed6244982f42ec2bc64602ea8fcfe900678ae7/py_spy-0.4.1-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/48/768edf21fe33bae8d874470b1be136681d4d32eb820a32e1c98262ebe39b/backports_zstd-1.6.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p4: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.9.5-py311he705e18_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/binaryen-117-h73e2aa4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py311h7e844b6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.8-ha1e9b39_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h950ec3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cargo-llvm-cov-0.8.7-h009cd8f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cargo-nextest-0.9.140-h19f9e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.1.0-py311hc34a7ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-default_h510d6ca_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-tools-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cmake-3.27.6-hf40c264_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h6e7f9a9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/doxygen-1.9.7-hd7636e7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fd-find-10.4.2-h009cd8f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-7.1.1-gpl_hf226373_110.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/flatbuffers-25.12.19-h06076ce_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.18.1-h7a4440b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.3-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.8.0-py311ha09d3ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.7-hae309b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gh-2.96.0-h5839d16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.15-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-12.2.0-hc5d3ef4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h3ddfcb2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lame-3.100-hb7f2c08_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.4-h87c4fc2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang13-21.1.0-default_h7f9524c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.21.0-h8f0b9e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.8-h19cb2f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.3-h58fbd8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.2-hf28f236_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.12.1-default_h8c32e24_1000.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.2.0-ha1e9b39_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm21-21.1.0-h9b4ebcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2025.2.0-h346e020_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2025.2.0-heda8b29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2025.2.0-heda8b29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2025.2.0-hd57c75b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2025.2.0-h346e020_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2025.2.0-hd57c75b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2025.2.0-ha4fb624_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2025.2.0-ha4fb624_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2025.2.0-hbc7d668_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2025.2.0-hd87add6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hbc7d668_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.6.1-hc6ced15_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h774df25_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.58.4-h21a6cfa_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h77d7759_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.2-h95d6d7f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.52.1-ha3d0635_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvorbis-1.3.7-ha059160_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.14.1-hf036a51_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvulkan-loader-1.4.341.0-ha6bc089_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.9-he1bc88e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lychee-0.23.0-h651e3a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py311ha8ae342_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.7.1-py311h42ed68f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.14.1-py311h4d7f069_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nasm-2.16.03-hfdf4475_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ninja-1.11.1-hb8565cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-24.12.0-hb2861ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-hd629203_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-h6ef8af8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.47-h13923f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-h2fb4741_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/prettier-3.8.1-h07b0e94_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.5.2-py311ha8ae342_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.31.1-py311h1c9791f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py311ha332486_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.15-ha9537fe_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py311h53ebfaf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rhash-1.4.6-h6e16a3a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-2026.6.3-py311hcd3406c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.7-h16586dd_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.56-h2fb4741_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.4.12-hf9078ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-3.1.2-h21dd04a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/taplo-0.9.1-h236d3af_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2022.3.0-hf0c99ee_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hb794df6_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ty-0.0.31-h479939e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/typos-1.48.0-h19f9e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/wasm-pack-0.15.0-h19f9e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/x264-1!164.3095-h775f41a_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.3-h6a5a847_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.3-h6a5a847_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.24.5-py311ha8ae342_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py311h62e9434_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/1b/49ebc2b59e9126f1f378ae910e98704d54a3f48b78e2d6d6c8cfe6fbe06f/cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/90/428dd82228b1b6d62d5a1bf312c29e6c125af6a182fcfd82768ca179dcc7/backports_zstd-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.9.5-py311ha68e1ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.9.5-py311ha68e1ae_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/binaryen-117-h63175ca_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py311hc5da9e4_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/buf-1.57.0-hd02998f_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-h4c7d964_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py311h3485c13_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/buf-1.66.0-hd02998f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cargo-llvm-cov-0.8.7-h77a83cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cargo-nextest-0.9.140-h18a1a76_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.1.0-py311h3485c13_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/clang-16-16.0.6-default_h7df9e1c_15.conda - conda: https://conda.anaconda.org/conda-forge/win-64/clang-16.0.6-default_h5a21124_15.conda - conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-16.0.6-default_h7df9e1c_15.conda - conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-16.0.6-default_h7df9e1c_15.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-3.27.6-hf0feee3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.13-py311hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/dav1d-1.2.1-hcfcfb64_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/doxygen-1.9.7-h849606c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/fd-find-10.3.0-h77a83cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fd-find-10.4.2-h77a83cd_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-7.1.1-gpl_h70aa942_910.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/flatbuffers-25.2.10-hc130f0a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/flatbuffers-25.12.19-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.18.1-hd47e2ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.7.0-py311hdf60d3a_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.0-h1f5b9c4_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/gh-2.79.0-h36e2d1d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-11.4.5-h5f2951f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.8.0-py311hdf60d3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.7-h1f5b9c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gh-2.96.0-h11686cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glib-2.88.2-h395db07_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.88.2-h74ecf4c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.15-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-14.2.1-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.3-h637d24d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lame-3.100-hcfcfb64_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250814.1-cxx17_habfad5f_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-21.1.1-default_ha2db4b5_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.0-hdbac1cb_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.0-h5f26cbf_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20260107.1-cxx17_h0eb2380_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.8-default_ha2db4b5_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h51a1c48_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.2-h7ce1215_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-14.2.1-h03b5201_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-devel-14.2.1-h03b5201_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libllvm16-16.0.6-h2a44499_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.5.2-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.32.1-h514701f_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.6.1-h6a83c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.33.5-h637c107_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpsl-0.22.0-h25e0afd_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/librsvg-2.58.4-h5ce5fed_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.3-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.0-h550210a_6.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.2-h8f73337_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libusb-1.0.29-h1839187_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libuv-1.51.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libuv-1.52.1-h6a83c73_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h5112557_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.8-h741aa76_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.341.0-h477610d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.9-h741aa76_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-tools-16.0.6-h2a44499_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lychee-0.23.0-hb3eb754_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-core-5.3.0-7.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py311h5082efb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py311h3f79411_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.6.3-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py311h3f79411_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.14.1-py311he736701_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/nasm-2.16.03-hfd05255_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ninja-1.11.1-h91493d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-24.4.1-he453025_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-hb17fa0b_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/prettier-3.6.2-hc21fffc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.3.1-py311h5082efb_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/protobuf-6.32.1-py311heca59f8_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.3-py311hf893f09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.13-h3f84c4b_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.13-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py311hefeebc8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.27.1-py311hf51aa87_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-24.18.0-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-h1eab103_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h13911b6_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.47-hd2b5f0e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/prettier-3.9.3-hc21fffc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.5.2-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/protobuf-6.33.5-py311heca59f8_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py311hf893f09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.15-h0159041_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-312-py311hefeebc8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-2026.6.3-py311h7337c20_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.7-h02f8532_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/sdl2-2.32.54-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.2.22-h5112557_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sdl2-2.32.56-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.4.12-h5112557_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-3.1.2-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/taplo-0.9.1-h7f3b576_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h967ab96_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ty-0.0.31-hc21aad4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/typos-1.45.1-h18a1a76_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/typos-1.48.0-h18a1a76_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_31.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.37.32822-h0123c8e_17.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.51.36231-h84cd919_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/wasm-pack-0.15.0-h18a1a76_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/x264-1!164.3095-h8ffe710_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/x265-3.5-h2d74725_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.1-h208afaa_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.3-hb6c8415_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.3-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.20.1-py311h5082efb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.24.5-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py311hf893f09_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/1a/379061e2abf8c3150ad51c1baab9ac723e01cf7538860a6a74c48f8b73ee/backports_zstd-1.3.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/eb/f52b165db2abd662cda0a76efb7579a291fed1a7979cf41146cdc19e0d7a/cryptography-38.0.4-cp36-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/f2/f22c19b4cdde429805ff5ac8dd77a95569a7c4cb8991741b2ff0d538f220/backports_zstd-1.6.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/ed/1409cdb557e558a6c98003ab12fdd4284699e158c167c187cb0f124eea4c/py_spy-0.4.2-py2.py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e1/da/fcc9a9fcd4ca946ff402cff20348e838b051d69f50f5d1f5dca4cd3c5eb8/py_spy-0.4.1-py2.py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/eb/f52b165db2abd662cda0a76efb7579a291fed1a7979cf41146cdc19e0d7a/cryptography-38.0.4-cp36-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - - pypi: ./rerun_pixi_env - default: + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + cpp: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + p1: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.9.5-py311h459d7ec_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.14-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16.1-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binaryen-117-h59595ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.46.1-default_h4852527_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.46.1-default_hfdba357_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.46.1-default_h4852527_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h66f275b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/buf-1.57.0-ha8f183a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/buf-1.66.0-ha8f183a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.6.0-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py311h5b438cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cargo-zigbuild-0.20.1-hb17b654_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.0-py311h03d9500_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16-16.0.6-default_hddf928d_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16.0.6-default_hfa515fb_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16-16.0.6-default_hddf928d_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16.0.6-default_hddf928d_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-16.0.6-default_hddf928d_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-3.27.6-hcfe8598_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.6.0-h00ab1b0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/doxygen-1.9.7-h661eb56_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fd-find-10.3.0-hdab8a38_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fd-find-10.4.2-hdab8a38_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-7.1.1-gpl_ha0aeed6_910.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/flatbuffers-25.2.10-hb7832b1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.0-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/flatbuffers-25.12.19-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.1-h27c8c51_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.7.0-py311h52bc045_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.0-h2b0a6b4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-tools-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gh-2.79.0-h76a2195_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.8.0-py311h52bc045_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.4.0-h236703b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.4.0-h26ba24d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.4.0-h6b7512a_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.7-h2b0a6b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gh-2.96.0-hfc2019e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.4.5-h15599e2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-12.4.0-h236703b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.4.0-h3ff227c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.4.0-h8489865_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.1-h15599e2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.8.2-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.10.0-hb700be7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-media-driver-25.3.4-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.26.1-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.29.0-hb700be7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.25.1-h3f43e3d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.4-h96ad9f0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.71-h39aace5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libattr-2.5.2-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.75-h39aace5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp16-16.0.6-default_hddf928d_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp18.1-18.1.8-default_h99862b1_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-21.1.0-default_h746c552_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.0-ha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.0-h73754d4_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_105.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-lib-1.11.1-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.55-h3f2d84a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.5.0-he200343_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-lib-1.12.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.88.2-h0d30a3d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.61-h54a6638_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.12.1-default_h3d81e11_1000.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm16-16.0.6-ha7bfdaf_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.8-default_hddf928d_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm21-21.1.0-hecd9e04_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2025.2.0-hb617929_1.conda @@ -1463,25 +1450,27 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2025.2.0-hecca717_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2025.2.0-h0767aad_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hecca717_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.5.2-hd0c01bc_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.6.1-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-hfb7daa7_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.4.0-ha732cd4_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc7d488a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-256.9-h2774228_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.4-h9a4d06a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-257.7-h4e0b6ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-h9d88235_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev-257.4-hbe16f8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.4-hbe16f8c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.6.2-h9c3ff4c_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/liburing-2.9-h84d6215_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.22.0-h4f16b4b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.24.1-he1eb515_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h54a6638_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpl-2.15.0-h54a6638_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda @@ -1489,258 +1478,275 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.9-h04c0eec_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lychee-0.23.0-he64ecbb_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py311h2dc5d0c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py311h3778330_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/meilisearch-1.5.1-he8a937b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.6.3-py311h2dc5d0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py311h3778330_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.14.1-py311h9ecbd09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/nasm-2.16.03-h4bc722e_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.17.1-heeeca48_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-h5888daf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-hc22cd8d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-24.13.0-h36edbcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.4-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-h65dd3cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/patchelf-0.17.2-h58526e2_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/prettier-3.6.2-h4c22ac6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.3.1-py311h2dc5d0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pkgconf-3.0.3-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prettier-3.8.1-h7e4c9f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.5.2-py311h3778330_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.31.1-py311h425ed32_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.3-py311haee01d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py311haee01d2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.13-h9e4cc4f_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-h9a8bead_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.15-h7508c33_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.27.1-py311h902ca64_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.6.3-py311h1baac5b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.7-h7805a7d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.54-h3f2d84a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.14-he3e324a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-3.1.2-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/taplo-0.9.1-h1ff36dd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2022.2.0-hb60516a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2022.3.0-h8d10470_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.31-h4e94fc0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/typos-1.45.1-hb17b654_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.45-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/typos-1.48.0-hb17b654_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wasm-pack-0.15.0-hb17b654_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hd6090a7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/x264-1!164.3095-h166bdaf_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-h280c20c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.1-hbcc6ac9_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.1-hbcc6ac9_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.3-ha02ee65_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.3-ha02ee65_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.3-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.1-py311h2dc5d0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.24.5-py311h3778330_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zig-0.13.0-h97ab28e_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d8/09/0822403f40932a165a4f1df289d41653683019e4fd7a86b63ed20e9b6177/backports_zstd-1.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/26/f8/a81170a816679fca9ccd907b801992acfc03c33f952440421c921af2cc57/cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.4.0-h1762d19_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.4.0-h1762d19_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.49-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/f8/a81170a816679fca9ccd907b801992acfc03c33f952440421c921af2cc57/cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/25/a9e37dd035027565fa0b7e367da50e88a6ab26e7fd413269aa118e25258b/backports_zstd-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/fb/bc7f639aed026bca6e7beb1e33f6951e16b7d315594e7635a4f7d21d63f4/py_spy-0.4.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - - pypi: ./rerun_pixi_env - linux-aarch64: - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/34/dd7d3c763a00b7b965e25a5eab0acd1a345dbaf0f45fffe595278873a1c0/py_spy-0.4.2-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p2: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.9.5-py311hcd402e7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.14-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.16.1-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aom-3.9.1-hcccb83c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/attr-2.5.1-h4e544f5_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/attr-2.5.2-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binaryen-117-h2f0025b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.44-h4c662bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils-2.46.1-default_hf1166c9_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.46.1-default_h5f4c503_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_linux-aarch64-2.46.1-default_hf1166c9_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py311h14a79a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.5-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.8-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-compiler-1.6.0-h31becfc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.0.0-py311h3324b35_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cargo-zigbuild-0.20.1-h069e38c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.1.0-py311h460c349_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16-16.0.6-default_hf07bfb7_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16.0.6-default_h3935787_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16-16.0.6-default_hf07bfb7_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16.0.6-default_hf07bfb7_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-tools-16.0.6-default_hf07bfb7_15.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cmake-3.27.6-hef020d8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cxx-compiler-1.6.0-h2a328a1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dav1d-1.2.1-h31becfc_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-heda779d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-h70963c4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/doxygen-1.9.7-h7b6a552_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fd-find-10.3.0-h1ebd7d5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fd-find-10.4.2-h1ebd7d5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ffmpeg-7.1.1-gpl_h8d881e6_910.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/flatbuffers-25.2.10-ha90f286_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.15.0-h8dda3cd_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.0-h8af1aa0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/flatbuffers-25.12.19-h7ac5ae9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.18.1-hba86a56_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.3-h8af1aa0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.7.0-py311h91c1192_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.0-h90308e0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-tools-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gh-2.79.0-h94b2740_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.8.0-py311h91c1192_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc-12.4.0-h7e62973_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_impl_linux-aarch64-12.4.0-h628656a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_linux-aarch64-12.4.0-heb3b579_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.7-h90308e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gh-2.96.0-h22914b5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gmp-6.3.0-h0a1ffab_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-11.4.5-he4899c9_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.15-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx-12.4.0-h7e62973_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_impl_linux-aarch64-12.4.0-h0bf7a72_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_linux-aarch64-12.4.0-h3f57e68_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-11.5.1-he4899c9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-h2fb54aa_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lame-3.100-h4e544f5_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.44-h5e2c951_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-hfdc4d58_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.46.1-default_h1979696_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.1.0-h52b7260_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libabseil-20250512.1-cxx17_h201e9ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-0.25.1-h5e0f5ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-devel-0.25.1-h5e0f5ae_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libass-0.17.4-hcfe818d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.71-h51d75a7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libattr-2.5.2-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.75-h51d75a7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang-cpp16-16.0.6-default_hf07bfb7_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang-cpp18.1-18.1.8-default_he95a3c9_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang13-21.1.0-default_h94a09a5_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.14.1-h6702fde_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.24-he377734_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.21.0-hc57f145_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.127-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.1-hfae3067_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.6-he21f813_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libflac-1.4.3-h2f0025b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.0-h8af1aa0_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.0-hdae7a39_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.1.0-he277a41_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-15.1.0-hd0aa34e_105.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.1.0-he9431aa_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcrypt-lib-1.11.1-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-devel-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.0-h7cdfd2c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.1.0-he277a41_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgpg-error-1.55-h5ad3122_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.8.1-hfae3067_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libflac-1.5.0-he9c94f4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.3-h8af1aa0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.3-hdae7a39_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcrypt-lib-1.12.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.88.2-h96a7f82_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgpg-error-1.61-h7ac5ae9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libhwloc-2.12.1-default_h6f258fa_1000.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.0-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.2.0-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm16-16.0.6-h2edbd07_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm18-18.1.8-default_hbd976d5_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm21-21.1.0-h2b567e5_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.1-h86ecc28_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-devel-5.8.1-h86ecc28_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.67.0-ha888d0e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-devel-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.68.1-hd3077d7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libogg-1.3.5-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-2025.2.0-hcd21e76_1.conda @@ -1754,482 +1760,336 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-pytorch-frontend-2025.2.0-hfae3067_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-frontend-2025.2.0-h38473e3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-lite-frontend-2025.2.0-hfae3067_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopus-1.5.2-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.18-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.50-h1abf092_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-6.31.1-h2cf3c76_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopus-1.6.1-h80f16a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.19-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.58-h1abf092_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-6.31.1-h61c7711_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.58.4-h3ac5bce_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsndfile-1.2.2-h79657aa_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.50.4-h022381a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsanitizer-12.4.0-h469570c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsndfile-1.2.2-h30591a0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.3-h022381a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-h18c354c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.1.0-h3f4de04_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.1.0-hf1166c9_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsystemd0-256.9-hd54d049_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.0-h7a57436_6.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev1-257.4-h1187dce_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.2.0-hdbbeba8_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsystemd0-257.7-h2bb824b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.2-hdb009f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev-257.4-h7b9e449_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev1-257.4-h7b9e449_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libunwind-1.6.2-h01db608_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liburing-2.9-h17cf362_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libusb-1.0.29-h06eaf92_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.1-h3e4203c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.51.0-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42.2-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.52.1-h80f16a2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvorbis-1.3.7-h7ac5ae9_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvpx-1.14.1-h0a1ffab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.11.0-h95ca766_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.8-he58860d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.9-he58860d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lychee-0.23.0-hb434046_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.9.4-hd600fc2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.2-py311ha09ea12_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.10.0-h5ad3122_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py311h2dad8b0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mpg123-1.32.9-h65af167_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.6.3-py311h58d527c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.7.1-py311h164a683_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mypy-1.14.1-py311ha879c10_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nasm-2.16.03-h68df207_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.11.1-hdd96247_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-24.4.1-hc854191_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openh264-2.6.0-h0564a2a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.0-h8e36d6e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-22.21.1-h43d1aef_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openh264-2.6.0-h663e864_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.3-h546c87b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.46-h15761aa_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/prettier-3.6.2-h70496c1_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.3.1-py311h58d527c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.47-hf841c20_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pkgconf-3.0.3-h80f16a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/prettier-3.8.1-h1e5041c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.5.2-py311h164a683_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/protobuf-6.31.1-py311he3e547a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.1.3-py311h51cfe5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.2.2-py311h51cfe5d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pugixml-1.15-h6ef32b0_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pulseaudio-client-17.0-h729494f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.11.13-h1683364_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py311h164a683_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pulseaudio-client-17.0-h77cf2aa_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.11.15-h53314ec_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py311h164a683_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rhash-1.4.6-h86ecc28_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.27.1-py311hc91c717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-2026.6.3-py311h3b69377_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruff-0.15.7-h9f438e6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl2-2.32.54-h5ad3122_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl3-3.2.14-h7e2c5d6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.2.2-he774c54_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.2.2-he774c54_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/svt-av1-3.1.2-hfae3067_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/taplo-0.9.1-hb8f9562_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tbb-2022.2.0-h8f856e4_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5688188_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tbb-2022.3.0-h0eac15c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5cf4473_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ty-0.0.31-h47ce4e6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/typos-1.45.1-h069e38c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.24.0-h698ed42_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/typos-1.48.0-h069e38c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wasm-pack-0.15.0-h069e38c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.26.0-h4f8a99f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x264-1!164.3095-h4e544f5_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x265-3.5-hdd96247_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.45-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.48-h80f16a2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.12-hca56bd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.13-h63a1b12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.6-h57736b2_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.1-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.7-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.8.1-h2dbfc1b_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-gpl-tools-5.8.1-h2dbfc1b_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-tools-5.8.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.8.3-hd704e39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-gpl-tools-5.8.3-hd704e39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-tools-5.8.3-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.20.1-py311h58d527c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.24.5-py311h164a683_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zig-0.13.0-h49d127f_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.25.0-py311h51cfe5d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6f/3e/2667c0ddb53ddf28667e330bf9fe92e8e17705a481c9b698e283120565f7/backports_zstd-1.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a2/8f/6c52b1f9d650863e8f67edbe062c04f1c8455579eaace1593d8fe469319a/cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/df/79/9ed50bb0a9de63ed023aa2db8b6265b04a7760d98c61eb54def6a5fddb68/py_spy-0.4.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - - pypi: ./rerun_pixi_env - osx-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.9.5-py311he705e18_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/binaryen-117-h73e2aa4_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py311h7e844b6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.5-hf13058a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h950ec3b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py311h8ebb5ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-default_h510d6ca_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-tools-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cmake-3.27.6-hf40c264_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h27bd348_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/doxygen-1.9.7-hd7636e7_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/fd-find-10.3.0-hb440939_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-7.1.1-gpl_hf226373_110.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/flatbuffers-25.2.10-h2cf7b43_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.15.0-h37eeddb_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.0-h694c41f_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.7.0-py311h7a2b322_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.0-h07555a4_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/gh-2.79.0-hfb6d0b5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.14-h21dd04a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-11.4.5-h0ffbb26_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lame-3.100-hb7f2c08_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hcca01a6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.4-h87c4fc2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h4651f56_15.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang13-21.1.0-default_h7f9524c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.14.1-h5dec5d8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.1-h3d58e20_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.24-hcc1b750_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.1-h21dd04a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.0-h694c41f_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.0-h6912278_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.86.0-h7cafd41_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.12.1-default_h8c32e24_1000.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.0-h6e16a3a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm21-21.1.0-h9b4ebcc_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2025.2.0-h346e020_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2025.2.0-heda8b29_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2025.2.0-heda8b29_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2025.2.0-hd57c75b_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2025.2.0-h346e020_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2025.2.0-hd57c75b_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2025.2.0-ha4fb624_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2025.2.0-ha4fb624_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2025.2.0-hbc7d668_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2025.2.0-hd87add6_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hbc7d668_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.5.2-he3325bb_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.50-h84aeda2_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h03562ea_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.58.4-h21a6cfa_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.50.4-h39a8b3b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.0-h59ddb5d_6.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libvorbis-1.3.7-ha059160_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.14.1-hf036a51_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.8-he1bc88e_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/lychee-0.23.0-h651e3a3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py311ha3cf9ac_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-12.4.0-h7b3af7c_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-aarch64-12.4.0-h7b3af7c_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.6.3-py311h1cc1194_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.14.1-py311h4d7f069_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/nasm-2.16.03-hfdf4475_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ninja-1.11.1-hb8565cd_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-24.4.1-h2e7699b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-h4883158_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-h6ef8af8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.46-ha3e7e28_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-ha059160_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/prettier-3.6.2-h07b0e94_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.3.1-py311ha3cf9ac_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.31.1-py311h1c9791f_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.1.3-py311h62e9434_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.13-h9ccd52b_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py311he13f9b5_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/rhash-1.4.6-h6e16a3a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.27.1-py311hd3d88a1_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.7-h16586dd_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.54-h92383a6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.2.22-hc0b302d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h25c286d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-3.1.2-h21dd04a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/taplo-0.9.1-h236d3af_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2022.2.0-hc025b3e_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/ty-0.0.31-h479939e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/typos-1.45.1-h19f9e61_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/x264-1!164.3095-h775f41a_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.1-h357f2ed_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.1-h357f2ed_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.1-hd471939_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.20.1-py311ha3cf9ac_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py311h62e9434_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h8210216_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ac/28/ed31a0e35feb4538a996348362051b52912d50f00d25c2d388eccef9242c/backports_zstd-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/52/1b/49ebc2b59e9126f1f378ae910e98704d54a3f48b78e2d6d6c8cfe6fbe06f/cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/23/3eb4c23c684ebd667674ce1d076ae855e0621d1d9bd5e052aa3f7982f757/py_spy-0.4.2-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/14/e3/3a32500d845bdd94f6a2b4ed6244982f42ec2bc64602ea8fcfe900678ae7/py_spy-0.4.1-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/8f/6c52b1f9d650863e8f67edbe062c04f1c8455579eaace1593d8fe469319a/cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/cb/af58363b0dd0b497282ecef1fa99789b03cc1885a01a41394cad42ceeff6/backports_zstd-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - - pypi: ./rerun_pixi_env - osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.9.5-py311h05b510d_0.conda + - pypi: https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p3: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-16.0.6-h3808999_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.9.5-py311h05b510d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aom-3.9.1-h7bae524_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/binaryen-117-hebf3989_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py311hdc60ec4_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/buf-1.57.0-h75b854d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/buf-1.66.0-h75b854d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.8-h84a0fba_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-compiler-1.6.0-h6aa9301_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py311hcfc1310_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools-1010.6-h4faf515_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1010.6-h4f2c9d0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.1.0-py311h833bfeb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16-16.0.6-default_h3c2e7ce_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16.0.6-default_h3e759af_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16-16.0.6-default_h3c2e7ce_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16.0.6-default_h3c2e7ce_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-16.0.6-default_h3c2e7ce_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-16.0.6-hc421ffc_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_osx-arm64-16.0.6-h54d7cd3_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-16.0.6-default_hc1b5c72_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-16.0.6-hcd7bac0_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_osx-arm64-16.0.6-h54d7cd3_19.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-3.27.6-h1c59155_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-16.0.6-h3808999_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cxx-compiler-1.6.0-h2ffa867_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dbus-1.16.2-hda038a8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dbus-1.16.2-h3ff7a7c_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/doxygen-1.9.7-h0e2417a_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fd-find-10.3.0-h0ca00b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fd-find-10.4.2-h748bcf4_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ffmpeg-7.1.1-gpl_h93d53e2_110.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/flatbuffers-25.2.10-h3144c11_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.0-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/flatbuffers-25.12.19-h784d473_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.18.1-h2b252f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.3-hce30654_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.7.0-py311h8740443_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.0-h7542897_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gh-2.79.0-h4e0460a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.8.0-py311hf75086c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.7-h4e57454_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gh-2.96.0-hf76c51c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gmp-6.3.0-h7bae524_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-11.4.5-hf4e55d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.15-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-12.2.0-haf38c7b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lame-3.100-h1a8c8d9_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64-951.9-h634c8be_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-951.9-h0605c9f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libass-0.17.4-hcbd7ca7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp16-16.0.6-default_h3c2e7ce_15.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.0-default_h6e8f826_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.1-hf598326_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.8-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-16.0.6-h86353a2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.0-hce30654_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.0-h6da58f4_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.1.0-hb74de2c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.0-h1bb475b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.3-hdfa99f5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.2-ha08bb59_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwloc-2.12.1-default_h88f92a7_1000.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm16-16.0.6-hc4b4ae8_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.0-h846d351_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libogg-1.3.5-h48c0fde_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-2025.2.0-h56e7ac4_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-arm-cpu-plugin-2025.2.0-h56e7ac4_1.conda @@ -2242,668 +2102,1986 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-pytorch-frontend-2025.2.0-hec049ff_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-frontend-2025.2.0-hee62d61_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-lite-frontend-2025.2.0-hec049ff_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopus-1.5.2-h48c0fde_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h658db43_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopus-1.6.1-h1a92334_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h29102cf_5.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.58.4-h266df6f_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsigtool-0.1.3-h98dc951_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.0-h025e3ab_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.2-h282da08_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libusb-1.0.29-hbc156a2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.52.1-h1a92334_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvorbis-1.3.7-h81086ad_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvpx-1.14.1-h7bae524_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvulkan-loader-1.4.341.0-h3feff0a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.8-h4a9ca0c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.0-hbb9b287_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.9-h4a9ca0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.8-hc7d1edf_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-16.0.6-hc4b4ae8_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lychee-0.23.0-h17e24d4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py311h4921393_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py311hc290fe0_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/meilisearch-1.5.1-h5ef7bb8_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.6.3-py311h30e7462_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.7.1-py311ha275503_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.14.1-py311h917b07b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nasm-2.16.03-h99b78c6_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ninja-1.11.1-hffc8910_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-24.4.1-hab9d20b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openh264-2.6.0-hb5b2745_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-24.13.0-h3a0f24a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openh264-2.6.0-hdf0efb5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prettier-3.6.2-h9907cc9_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.3.1-py311h4921393_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-h30297fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h784d473_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prettier-3.8.1-h9907cc9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.5.2-py311hc290fe0_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.31.1-py311h93f9908_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.3-py311h5bb9006_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py311he363849_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pugixml-1.15-hd3d436d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.13-hc22306f_0_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311ha9b3269_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h0c9c016_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311hc290fe0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rhash-1.4.6-h5505292_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.27.1-py311h1c3fc1a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-2026.6.3-py311haff49d3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.7-hc5c3a1d_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl2-2.32.54-ha1acc90_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl3-3.2.22-he22eeb8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hd121638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl2-2.32.56-h784d473_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl3-3.4.12-h6fa9c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-0.1.3-h98dc951_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-codesign-0.1.3-h98dc951_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/svt-av1-3.1.2-h12ba402_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1300.6.5-h03f4b80_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/taplo-0.9.1-h16c8c8b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tbb-2022.2.0-h5b2e6d4_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tbb-2022.3.0-h66ce52b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hd3d0363_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ty-0.0.31-hdfcc030_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/typos-1.45.1-h6fdd925_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/typos-1.48.0-h6fdd925_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wasm-pack-0.15.0-h6fdd925_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x264-1!164.3095-h57fd34a_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x265-3.5-hbc6ce65_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.1-h9a6d368_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.1-h9a6d368_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.3-hd0f0c4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.3-hd0f0c4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.3-h8088a28_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.20.1-py311h4921393_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.24.5-py311hc290fe0_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py311h5bb9006_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/00/0d/3db362169d80442adda9dd563c4f0bb10091c8c1c9a158037f4ecd53988e/backports_zstd-1.3.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/75/7a/2ea7dd2202638cf1053aaa8fbbaddded0b78c78832b3d03cafa0416a6c84/cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/7a/2ea7dd2202638cf1053aaa8fbbaddded0b78c78832b3d03cafa0416a6c84/cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/14/e3/3a32500d845bdd94f6a2b4ed6244982f42ec2bc64602ea8fcfe900678ae7/py_spy-0.4.1-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl - - pypi: ./rerun_pixi_env - win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.9.5-py311ha68e1ae_0.conda + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/48/768edf21fe33bae8d874470b1be136681d4d32eb820a32e1c98262ebe39b/backports_zstd-1.6.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p4: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/binaryen-117-h63175ca_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py311hc5da9e4_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/buf-1.57.0-hd02998f_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-h4c7d964_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py311h3485c13_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/clang-16-16.0.6-default_h7df9e1c_15.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/clang-16.0.6-default_h5a21124_15.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-16.0.6-default_h7df9e1c_15.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-16.0.6-default_h7df9e1c_15.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-3.27.6-hf0feee3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.13-py311hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/dav1d-1.2.1-hcfcfb64_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/doxygen-1.9.7-h849606c_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/fd-find-10.3.0-h77a83cd_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-7.1.1-gpl_h70aa942_910.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/flatbuffers-25.2.10-hc130f0a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-64-16.0.6-ha38d28d_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.7.0-py311hdf60d3a_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.0-h1f5b9c4_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/gh-2.79.0-h36e2d1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-11.4.5-h5f2951f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/lame-3.100-hcfcfb64_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250814.1-cxx17_habfad5f_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-21.1.1-default_ha2db4b5_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.0-hdbac1cb_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.0-h5f26cbf_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.5.2-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.32.1-h514701f_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/librsvg-2.58.4-h5ce5fed_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.0-h550210a_6.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libusb-1.0.29-h1839187_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libuv-1.51.0-hfd05255_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h5112557_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.8-h741aa76_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/lychee-0.23.0-hb3eb754_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-core-5.3.0-7.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py311h5082efb_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.6.3-py311h3f79411_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.14.1-py311he736701_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nasm-2.16.03-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ninja-1.11.1-h91493d7_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-24.4.1-he453025_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-hb17fa0b_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/prettier-3.6.2-hc21fffc_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.3.1-py311h5082efb_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/protobuf-6.32.1-py311heca59f8_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.3-py311hf893f09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.13-h3f84c4b_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.13-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py311hefeebc8_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.27.1-py311hf51aa87_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.7-h02f8532_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/sdl2-2.32.54-he0c23c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.2.22-h5112557_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-3.1.2-hac47afa_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/taplo-0.9.1-h7f3b576_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ty-0.0.31-hc21aad4_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/typos-1.45.1-h18a1a76_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_31.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/x264-1!164.3095-h8ffe710_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/x265-3.5-h2d74725_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.1-h208afaa_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.1-h2466b09_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.20.1-py311h5082efb_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py311hf893f09_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - - pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/1a/379061e2abf8c3150ad51c1baab9ac723e01cf7538860a6a74c48f8b73ee/backports_zstd-1.3.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/eb/f52b165db2abd662cda0a76efb7579a291fed1a7979cf41146cdc19e0d7a/cryptography-38.0.4-cp36-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl + - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.9.5-py311he705e18_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/binaryen-117-h73e2aa4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py311h7e844b6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.8-ha1e9b39_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.6.0-h282daa2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h950ec3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cctools-1010.6-h40f6528_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cctools_osx-64-1010.6-heaa7f0c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.1.0-py311hc34a7ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-default_h510d6ca_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-tools-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx-16.0.6-default_h1b9e3cd_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-16.0.6-hb91bd55_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cmake-3.27.6-hf40c264_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/compiler-rt-16.0.6-ha38d28d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cxx-compiler-1.6.0-h7728843_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h6e7f9a9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/doxygen-1.9.7-hd7636e7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fd-find-10.4.2-h009cd8f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-7.1.1-gpl_hf226373_110.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/flatbuffers-25.12.19-h06076ce_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.18.1-h7a4440b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.3-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.8.0-py311ha09d3ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.7-hae309b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gh-2.96.0-h5839d16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.15-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-12.2.0-hc5d3ef4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h3ddfcb2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lame-3.100-hb7f2c08_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/ld64-951.9-ha02d983_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ld64_osx-64-951.9-h3516399_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.4-h87c4fc2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang13-21.1.0-default_h7f9524c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.21.0-h8f0b9e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.8-h19cb2f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-devel-16.0.6-h8f8a49f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.3-h58fbd8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.2-hf28f236_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.12.1-default_h8c32e24_1000.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.2.0-ha1e9b39_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm21-21.1.0-h9b4ebcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2025.2.0-h346e020_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2025.2.0-heda8b29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2025.2.0-heda8b29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2025.2.0-hd57c75b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2025.2.0-h346e020_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2025.2.0-hd57c75b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2025.2.0-ha4fb624_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2025.2.0-ha4fb624_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2025.2.0-hbc7d668_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2025.2.0-hd87add6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hbc7d668_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.6.1-hc6ced15_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h774df25_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.58.4-h21a6cfa_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsigtool-0.1.3-hc0f2934_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h77d7759_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.2-h95d6d7f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.52.1-ha3d0635_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvorbis-1.3.7-ha059160_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.14.1-hf036a51_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvulkan-loader-1.4.341.0-ha6bc089_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.9-he1bc88e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.8-h0d3cbff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-tools-16.0.6-hbedff68_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lychee-0.23.0-h651e3a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py311ha8ae342_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.7.1-py311h42ed68f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.14.1-py311h4d7f069_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nasm-2.16.03-hfdf4475_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ninja-1.11.1-hb8565cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-24.12.0-hb2861ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-hd629203_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-h6ef8af8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.47-h13923f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-h2fb4741_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/prettier-3.8.1-h07b0e94_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.5.2-py311ha8ae342_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.31.1-py311h1c9791f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py311ha332486_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.15-ha9537fe_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py311h53ebfaf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rhash-1.4.6-h6e16a3a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-2026.6.3-py311hcd3406c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.7-h16586dd_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.56-h2fb4741_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.4.12-hf9078ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sigtool-0.1.3-hc0f2934_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sigtool-codesign-0.1.3-hc0f2934_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-3.1.2-h21dd04a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tapi-1300.6.5-h390ca13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/taplo-0.9.1-h236d3af_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2022.3.0-hf0c99ee_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hb794df6_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ty-0.0.31-h479939e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/typos-1.48.0-h19f9e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/wasm-pack-0.15.0-h19f9e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/x264-1!164.3095-h775f41a_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.3-h6a5a847_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.3-h6a5a847_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.24.5-py311ha8ae342_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py311h62e9434_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/1b/49ebc2b59e9126f1f378ae910e98704d54a3f48b78e2d6d6c8cfe6fbe06f/cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/90/428dd82228b1b6d62d5a1bf312c29e6c125af6a182fcfd82768ca179dcc7/backports_zstd-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.9.5-py311ha68e1ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/binaryen-117-h63175ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py311hc5da9e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/buf-1.66.0-hd02998f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.1.0-py311h3485c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/clang-16-16.0.6-default_h7df9e1c_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/clang-16.0.6-default_h5a21124_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-16.0.6-default_h7df9e1c_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-16.0.6-default_h7df9e1c_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-3.27.6-hf0feee3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/dav1d-1.2.1-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/doxygen-1.9.7-h849606c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fd-find-10.4.2-h77a83cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-7.1.1-gpl_h70aa942_910.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/flatbuffers-25.12.19-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.18.1-hd47e2ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.8.0-py311hdf60d3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.7-h1f5b9c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gh-2.96.0-h11686cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glib-2.88.2-h395db07_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.88.2-h74ecf4c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.15-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-14.2.1-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.3-h637d24d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lame-3.100-hcfcfb64_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20260107.1-cxx17_h0eb2380_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.8-default_ha2db4b5_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h51a1c48_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.2-h7ce1215_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-14.2.1-h03b5201_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-devel-14.2.1-h03b5201_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libllvm16-16.0.6-h2a44499_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.6.1-h6a83c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.33.5-h637c107_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpsl-0.22.0-h25e0afd_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/librsvg-2.58.4-h5ce5fed_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.3-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.2-h8f73337_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libusb-1.0.29-h1839187_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libuv-1.52.1-h6a83c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h5112557_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.341.0-h477610d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.9-h741aa76_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-tools-16.0.6-h2a44499_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lychee-0.23.0-hb3eb754_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-core-5.3.0-7.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py311h3f79411_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.14.1-py311he736701_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nasm-2.16.03-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ninja-1.11.1-h91493d7_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-24.18.0-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-h1eab103_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h13911b6_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.47-hd2b5f0e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/prettier-3.9.3-hc21fffc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.5.2-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/protobuf-6.33.5-py311heca59f8_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py311hf893f09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.15-h0159041_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-312-py311hefeebc8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-2026.6.3-py311h7337c20_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.7-h02f8532_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sdl2-2.32.56-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.4.12-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-3.1.2-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/taplo-0.9.1-h7f3b576_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h967ab96_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ty-0.0.31-hc21aad4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/typos-1.48.0-h18a1a76_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.51.36231-h84cd919_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.37.32822-h0123c8e_17.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/wasm-pack-0.15.0-h18a1a76_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/x264-1!164.3095-h8ffe710_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/x265-3.5-h2d74725_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.3-hb6c8415_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.24.5-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py311hf893f09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e1/da/fcc9a9fcd4ca946ff402cff20348e838b051d69f50f5d1f5dca4cd3c5eb8/py_spy-0.4.1-py2.py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/f2/f22c19b4cdde429805ff5ac8dd77a95569a7c4cb8991741b2ff0d538f220/backports_zstd-1.6.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/ed/1409cdb557e558a6c98003ab12fdd4284699e158c167c187cb0f124eea4c/py_spy-0.4.2-py2.py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/eb/f52b165db2abd662cda0a76efb7579a291fed1a7979cf41146cdc19e0d7a/cryptography-38.0.4-cp36-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + p1: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.9.5-py311h459d7ec_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16.1-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binaryen-117-h59595ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.46.1-default_hfdba357_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h66f275b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/buf-1.66.0-ha8f183a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cargo-zigbuild-0.20.1-hb17b654_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.0-py311h03d9500_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16-16.0.6-default_hddf928d_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16.0.6-default_hfa515fb_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16-16.0.6-default_hddf928d_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16.0.6-default_hddf928d_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-16.0.6-default_hddf928d_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-3.27.6-hcfe8598_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/doxygen-1.9.7-h661eb56_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fd-find-10.4.2-hdab8a38_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-7.1.1-gpl_ha0aeed6_910.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/flatbuffers-25.12.19-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.1-h27c8c51_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.8.0-py311h52bc045_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.7-h2b0a6b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gh-2.96.0-hfc2019e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.1-h15599e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.10.0-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/intel-media-driver-25.3.4-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.29.0-hb700be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.4-h96ad9f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libattr-2.5.2-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.75-h39aace5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp16-16.0.6-default_hddf928d_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp18.1-18.1.8-default_h99862b1_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-21.1.0-default_h746c552_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.5.0-he200343_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-lib-1.12.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.88.2-h0d30a3d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.61-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.12.1-default_h3d81e11_1000.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm16-16.0.6-ha7bfdaf_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.8-default_hddf928d_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm21-21.1.0-hecd9e04_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2025.2.0-hb617929_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-batch-plugin-2025.2.0-hed573e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-plugin-2025.2.0-hed573e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-hetero-plugin-2025.2.0-hd41364c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-cpu-plugin-2025.2.0-hb617929_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-gpu-plugin-2025.2.0-hb617929_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-npu-plugin-2025.2.0-hb617929_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-ir-frontend-2025.2.0-hd41364c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-onnx-frontend-2025.2.0-h1862bb8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-paddle-frontend-2025.2.0-h1862bb8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2025.2.0-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2025.2.0-h0767aad_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.6.1-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-hfb7daa7_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc7d488a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-257.7-h4e0b6ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-h9d88235_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev-257.4-hbe16f8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.4-hbe16f8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.6.2-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/liburing-2.9-h84d6215_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.24.1-he1eb515_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h54a6638_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpl-2.15.0-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.9-h04c0eec_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lychee-0.23.0-he64ecbb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py311h3778330_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/meilisearch-1.5.1-he8a937b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py311h3778330_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.14.1-py311h9ecbd09_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nasm-2.16.03-h4bc722e_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-24.13.0-h36edbcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.4-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-h65dd3cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/patchelf-0.17.2-h58526e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pkgconf-3.0.3-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prettier-3.8.1-h7e4c9f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.5.2-py311h3778330_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.31.1-py311h425ed32_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py311haee01d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-h9a8bead_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.15-h7508c33_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.6.3-py311h1baac5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.7-h7805a7d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.54-h3f2d84a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.14-he3e324a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-3.1.2-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/taplo-0.9.1-h1ff36dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2022.3.0-h8d10470_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.31-h4e94fc0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/typos-1.48.0-hb17b654_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wasm-pack-0.15.0-hb17b654_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hd6090a7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/x264-1!164.3095-h166bdaf_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.3-ha02ee65_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.3-ha02ee65_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.24.5-py311h3778330_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zig-0.13.0-h97ab28e_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-hcc6f6b0_119.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.49-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 - pypi: ./rerun_pixi_env -packages: -- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 - md5: d7c89558ba9fa0495403155b64376d81 - license: None - purls: [] - size: 2562 - timestamp: 1578324546067 -- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 - md5: 73aaf86a425cc6e73fcf236a5a46396d - depends: - - _libgcc_mutex 0.1 conda_forge - - libgomp >=7.5.0 - constrains: - - openmp_impl 9999 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 23621 - timestamp: 1650670423406 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: 3702bef2f0a4d38bd8288bbe54aace623602a1343c2cfbefd3fa188e015bebf0 - md5: 6168d71addc746e8f2b8d57dfd2edcea - depends: - - libgomp >=7.5.0 - constrains: - - openmp_impl 9999 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 23712 - timestamp: 1650670790230 -- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 - md5: aaa2a381ccc56eac91d63b6c1240312f - depends: - - cpython - - python-gil - license: MIT - license_family: MIT - purls: [] - size: 8191 - timestamp: 1744137672556 -- conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.9.5-py311h459d7ec_0.conda - sha256: 2eb99d920ef0dcd608e195bb852a64634ecf13f74680796959f1b9d9a9650a7b - md5: 0175d2636cc41dc019b51462c13ce225 - depends: - - aiosignal >=1.1.2 - - attrs >=17.3.0 - - frozenlist >=1.1.1 - - libgcc-ng >=12 - - multidict >=4.5,<7.0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - yarl >=1.0,<2.0 - license: MIT AND Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/aiohttp?source=hash-mapping - size: 810945 - timestamp: 1713965013081 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.9.5-py311hcd402e7_0.conda - sha256: a385a27e4510a55d7094eca5a09cd11d3c1c35a91925e51acef47c85636cc440 - md5: e717043d9f39fb3a3a6dff8d085e5a4d - depends: - - aiosignal >=1.1.2 - - attrs >=17.3.0 - - frozenlist >=1.1.1 - - libgcc-ng >=12 - - multidict >=4.5,<7.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - - yarl >=1.0,<2.0 - license: MIT AND Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/aiohttp?source=hash-mapping - size: 805564 - timestamp: 1713965086056 -- conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.9.5-py311he705e18_0.conda - sha256: 6e1c28d255830f350ccc135db4932153a978956d480e7bcd26c1663e19db4f9d - md5: a955769e6187495614f719668695e28f - depends: - - aiosignal >=1.1.2 - - attrs >=17.3.0 - - frozenlist >=1.1.1 - - multidict >=4.5,<7.0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - yarl >=1.0,<2.0 - license: MIT AND Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/aiohttp?source=hash-mapping - size: 779497 - timestamp: 1713965157234 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.9.5-py311h05b510d_0.conda - sha256: 63ee70099b66bfa62751d1eb82831438426e3cfc9671a0b836dd9b9d94c92bd6 - md5: 69eee7117ab7f3ef9eb59a600a9079a3 + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/f8/a81170a816679fca9ccd907b801992acfc03c33f952440421c921af2cc57/cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/25/a9e37dd035027565fa0b7e367da50e88a6ab26e7fd413269aa118e25258b/backports_zstd-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f9/34/dd7d3c763a00b7b965e25a5eab0acd1a345dbaf0f45fffe595278873a1c0/py_spy-0.4.2-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p2: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.9.5-py311hcd402e7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.16.1-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aom-3.9.1-hcccb83c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/attr-2.5.2-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binaryen-117-h2f0025b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.46.1-default_h5f4c503_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py311h14a79a7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.8-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cargo-zigbuild-0.20.1-h069e38c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.1.0-py311h460c349_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16-16.0.6-default_hf07bfb7_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16.0.6-default_h3935787_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16-16.0.6-default_hf07bfb7_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16.0.6-default_hf07bfb7_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-tools-16.0.6-default_hf07bfb7_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cmake-3.27.6-hef020d8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dav1d-1.2.1-h31becfc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-h70963c4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/doxygen-1.9.7-h7b6a552_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fd-find-10.4.2-h1ebd7d5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ffmpeg-7.1.1-gpl_h8d881e6_910.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/flatbuffers-25.12.19-h7ac5ae9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.18.1-hba86a56_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.3-h8af1aa0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.8.0-py311h91c1192_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.7-h90308e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gh-2.96.0-h22914b5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gmp-6.3.0-h0a1ffab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.15-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-11.5.1-he4899c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-h2fb54aa_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lame-3.100-h4e544f5_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.46.1-default_h1979696_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.1.0-h52b7260_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libabseil-20250512.1-cxx17_h201e9ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libass-0.17.4-hcfe818d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libattr-2.5.2-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.75-h51d75a7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang-cpp16-16.0.6-default_hf07bfb7_15.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang-cpp18.1-18.1.8-default_he95a3c9_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang13-21.1.0-default_h94a09a5_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.21.0-hc57f145_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.127-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.8.1-hfae3067_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libflac-1.5.0-he9c94f4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.3-h8af1aa0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.3-hdae7a39_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcrypt-lib-1.12.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.88.2-h96a7f82_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgpg-error-1.61-h7ac5ae9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libhwloc-2.12.1-default_h6f258fa_1000.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.2.0-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm16-16.0.6-h2edbd07_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm18-18.1.8-default_hbd976d5_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm21-21.1.0-h2b567e5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-devel-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.68.1-hd3077d7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libogg-1.3.5-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-2025.2.0-hcd21e76_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-arm-cpu-plugin-2025.2.0-hcd21e76_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-auto-batch-plugin-2025.2.0-h3890994_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-auto-plugin-2025.2.0-h3890994_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-hetero-plugin-2025.2.0-he07c6df_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-ir-frontend-2025.2.0-he07c6df_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-onnx-frontend-2025.2.0-h07d5dce_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-paddle-frontend-2025.2.0-h07d5dce_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-pytorch-frontend-2025.2.0-hfae3067_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-frontend-2025.2.0-h38473e3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-lite-frontend-2025.2.0-hfae3067_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopus-1.6.1-h80f16a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.19-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.58-h1abf092_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-6.31.1-h61c7711_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.58.4-h3ac5bce_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsndfile-1.2.2-h30591a0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.3-h022381a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-h18c354c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.2.0-hdbbeba8_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsystemd0-257.7-h2bb824b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.2-hdb009f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev-257.4-h7b9e449_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev1-257.4-h7b9e449_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libunwind-1.6.2-h01db608_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liburing-2.9-h17cf362_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libusb-1.0.29-h06eaf92_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42.2-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.52.1-h80f16a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvorbis-1.3.7-h7ac5ae9_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvpx-1.14.1-h0a1ffab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.11.0-h95ca766_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.9-he58860d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lychee-0.23.0-hb434046_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.10.0-h5ad3122_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py311h2dad8b0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mpg123-1.32.9-h65af167_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.7.1-py311h164a683_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mypy-1.14.1-py311ha879c10_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nasm-2.16.03-h68df207_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.11.1-hdd96247_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-22.21.1-h43d1aef_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openh264-2.6.0-h663e864_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.3-h546c87b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.47-hf841c20_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pkgconf-3.0.3-h80f16a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/prettier-3.8.1-h1e5041c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.5.2-py311h164a683_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/protobuf-6.31.1-py311he3e547a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.2.2-py311h51cfe5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pugixml-1.15-h6ef32b0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pulseaudio-client-17.0-h77cf2aa_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.11.15-h53314ec_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py311h164a683_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rhash-1.4.6-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-2026.6.3-py311h3b69377_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruff-0.15.7-h9f438e6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl2-2.32.54-h5ad3122_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl3-3.2.14-h7e2c5d6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.2.2-he774c54_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/svt-av1-3.1.2-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/taplo-0.9.1-hb8f9562_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tbb-2022.3.0-h0eac15c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5cf4473_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ty-0.0.31-h47ce4e6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/typos-1.48.0-h069e38c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wasm-pack-0.15.0-h069e38c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.26.0-h4f8a99f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x264-1!164.3095-h4e544f5_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x265-3.5-hdd96247_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.48-h80f16a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.13-h63a1b12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.7-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.8.3-hd704e39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-gpl-tools-5.8.3-hd704e39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-tools-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.24.5-py311h164a683_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zig-0.13.0-h49d127f_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.25.0-py311h51cfe5d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-15.2.0-h55c397f_119.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/23/3eb4c23c684ebd667674ce1d076ae855e0621d1d9bd5e052aa3f7982f757/py_spy-0.4.2-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/8f/6c52b1f9d650863e8f67edbe062c04f1c8455579eaace1593d8fe469319a/cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/cb/af58363b0dd0b497282ecef1fa99789b03cc1885a01a41394cad42ceeff6/backports_zstd-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p3: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.9.5-py311h05b510d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aom-3.9.1-h7bae524_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/binaryen-117-hebf3989_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py311hdc60ec4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/buf-1.66.0-h75b854d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.8-h84a0fba_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.1.0-py311h833bfeb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16-16.0.6-default_h3c2e7ce_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16.0.6-default_h3e759af_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16-16.0.6-default_h3c2e7ce_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16.0.6-default_h3c2e7ce_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-16.0.6-default_h3c2e7ce_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-3.27.6-h1c59155_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/dbus-1.16.2-h3ff7a7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/doxygen-1.9.7-h0e2417a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fd-find-10.4.2-h748bcf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ffmpeg-7.1.1-gpl_h93d53e2_110.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/flatbuffers-25.12.19-h784d473_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.18.1-h2b252f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.3-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.8.0-py311hf75086c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.7-h4e57454_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gh-2.96.0-hf76c51c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gmp-6.3.0-h7bae524_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.15-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-12.2.0-haf38c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lame-3.100-h1a8c8d9_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libass-0.17.4-hcbd7ca7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp16-16.0.6-default_h3c2e7ce_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.0-default_h6e8f826_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.8-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.3-hdfa99f5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.2-ha08bb59_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwloc-2.12.1-default_h88f92a7_1000.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm16-16.0.6-hc4b4ae8_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.0-h846d351_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libogg-1.3.5-h48c0fde_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-2025.2.0-h56e7ac4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-arm-cpu-plugin-2025.2.0-h56e7ac4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-auto-batch-plugin-2025.2.0-he81eb65_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-auto-plugin-2025.2.0-he81eb65_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-hetero-plugin-2025.2.0-h273c05f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-ir-frontend-2025.2.0-h273c05f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-onnx-frontend-2025.2.0-h6386500_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-paddle-frontend-2025.2.0-h6386500_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-pytorch-frontend-2025.2.0-hec049ff_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-frontend-2025.2.0-hee62d61_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-lite-frontend-2025.2.0-hec049ff_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopus-1.6.1-h1a92334_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h29102cf_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.58.4-h266df6f_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.2-h282da08_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libusb-1.0.29-hbc156a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.52.1-h1a92334_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvorbis-1.3.7-h81086ad_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvpx-1.14.1-h7bae524_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvulkan-loader-1.4.341.0-h3feff0a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.9-h4a9ca0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.8-hc7d1edf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-16.0.6-hc4b4ae8_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lychee-0.23.0-h17e24d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py311hc290fe0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/meilisearch-1.5.1-h5ef7bb8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.7.1-py311ha275503_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.14.1-py311h917b07b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nasm-2.16.03-h99b78c6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ninja-1.11.1-hffc8910_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-24.13.0-h3a0f24a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openh264-2.6.0-hdf0efb5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-h30297fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h784d473_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prettier-3.8.1-h9907cc9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.5.2-py311hc290fe0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.31.1-py311h93f9908_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py311he363849_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pugixml-1.15-hd3d436d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h0c9c016_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311hc290fe0_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rhash-1.4.6-h5505292_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-2026.6.3-py311haff49d3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.7-hc5c3a1d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl2-2.32.56-h784d473_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl3-3.4.12-h6fa9c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/svt-av1-3.1.2-h12ba402_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/taplo-0.9.1-h16c8c8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tbb-2022.3.0-h66ce52b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hd3d0363_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ty-0.0.31-hdfcc030_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/typos-1.48.0-h6fdd925_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wasm-pack-0.15.0-h6fdd925_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x264-1!164.3095-h57fd34a_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x265-3.5-hbc6ce65_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.3-hd0f0c4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.3-hd0f0c4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.24.5-py311hc290fe0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py311h5bb9006_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/7a/2ea7dd2202638cf1053aaa8fbbaddded0b78c78832b3d03cafa0416a6c84/cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/48/768edf21fe33bae8d874470b1be136681d4d32eb820a32e1c98262ebe39b/backports_zstd-1.6.0-cp311-cp311-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + p4: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.9.5-py311he705e18_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/binaryen-117-h73e2aa4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py311h7e844b6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.8-ha1e9b39_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h950ec3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.1.0-py311hc34a7ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-default_h510d6ca_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/clang-tools-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cmake-3.27.6-hf40c264_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h6e7f9a9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/doxygen-1.9.7-hd7636e7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fd-find-10.4.2-h009cd8f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-7.1.1-gpl_hf226373_110.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/flatbuffers-25.12.19-h06076ce_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.18.1-h7a4440b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.3-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.8.0-py311ha09d3ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.7-hae309b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gh-2.96.0-h5839d16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.15-hcc62823_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-12.2.0-hc5d3ef4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h3ddfcb2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lame-3.100-hb7f2c08_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.4-h87c4fc2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h4651f56_15.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libclang13-21.1.0-default_h7f9524c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.21.0-h8f0b9e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.8-h19cb2f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.3-h58fbd8d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.2-hf28f236_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.12.1-default_h8c32e24_1000.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.2.0-ha1e9b39_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm21-21.1.0-h9b4ebcc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2025.2.0-h346e020_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2025.2.0-heda8b29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2025.2.0-heda8b29_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2025.2.0-hd57c75b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2025.2.0-h346e020_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2025.2.0-hd57c75b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2025.2.0-ha4fb624_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2025.2.0-ha4fb624_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2025.2.0-hbc7d668_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2025.2.0-hd87add6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hbc7d668_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.6.1-hc6ced15_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h774df25_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.58.4-h21a6cfa_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h77d7759_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.2-h95d6d7f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.52.1-ha3d0635_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvorbis-1.3.7-ha059160_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.14.1-hf036a51_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libvulkan-loader-1.4.341.0-ha6bc089_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.9-he1bc88e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lychee-0.23.0-h651e3a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py311ha8ae342_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.7.1-py311h42ed68f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.14.1-py311h4d7f069_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nasm-2.16.03-hfdf4475_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ninja-1.11.1-hb8565cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-24.12.0-hb2861ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-hd629203_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-h6ef8af8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.47-h13923f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-h2fb4741_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/prettier-3.8.1-h07b0e94_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.5.2-py311ha8ae342_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.31.1-py311h1c9791f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py311ha332486_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.15-ha9537fe_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py311h53ebfaf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rhash-1.4.6-h6e16a3a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-2026.6.3-py311hcd3406c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.7-h16586dd_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.56-h2fb4741_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.4.12-hf9078ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-3.1.2-h21dd04a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/taplo-0.9.1-h236d3af_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2022.3.0-hf0c99ee_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hb794df6_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ty-0.0.31-h479939e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/typos-1.48.0-h19f9e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/wasm-pack-0.15.0-h19f9e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/x264-1!164.3095-h775f41a_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.3-h6a5a847_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.3-h6a5a847_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.24.5-py311ha8ae342_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py311h62e9434_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/52/1b/49ebc2b59e9126f1f378ae910e98704d54a3f48b78e2d6d6c8cfe6fbe06f/cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/90/428dd82228b1b6d62d5a1bf312c29e6c125af6a182fcfd82768ca179dcc7/backports_zstd-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.9.5-py311ha68e1ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/binaryen-117-h63175ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py311hc5da9e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/buf-1.66.0-hd02998f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.1.0-py311h3485c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/clang-16-16.0.6-default_h7df9e1c_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/clang-16.0.6-default_h5a21124_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-16.0.6-default_h7df9e1c_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-16.0.6-default_h7df9e1c_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cmake-3.27.6-hf0feee3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/dav1d-1.2.1-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/doxygen-1.9.7-h849606c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fd-find-10.4.2-h77a83cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-7.1.1-gpl_h70aa942_910.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/flatbuffers-25.12.19-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.18.1-hd47e2ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.8.0-py311hdf60d3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.7-h1f5b9c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gh-2.96.0-h11686cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glib-2.88.2-h395db07_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.88.2-h74ecf4c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.15-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-14.2.1-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.3-h637d24d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lame-3.100-hcfcfb64_1003.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20260107.1-cxx17_h0eb2380_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.8-default_ha2db4b5_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h51a1c48_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.2-h7ce1215_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-14.2.1-h03b5201_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-devel-14.2.1-h03b5201_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libllvm16-16.0.6-h2a44499_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.6.1-h6a83c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.33.5-h637c107_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpsl-0.22.0-h25e0afd_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/librsvg-2.58.4-h5ce5fed_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.3-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.2-h8f73337_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libusb-1.0.29-h1839187_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libuv-1.52.1-h6a83c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h5112557_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.341.0-h477610d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.9-h741aa76_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-tools-16.0.6-h2a44499_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lychee-0.23.0-hb3eb754_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-core-5.3.0-7.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py311h3f79411_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.14.1-py311he736701_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nasm-2.16.03-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ninja-1.11.1-h91493d7_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-24.18.0-h80d1838_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-h1eab103_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h13911b6_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.47-hd2b5f0e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/prettier-3.9.3-hc21fffc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.5.2-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/protobuf-6.33.5-py311heca59f8_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py311hf893f09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.15-h0159041_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-312-py311hefeebc8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-2026.6.3-py311h7337c20_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.7-h02f8532_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sdl2-2.32.56-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.4.12-h5112557_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-3.1.2-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/taplo-0.9.1-h7f3b576_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h967ab96_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ty-0.0.31-hc21aad4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/typos-1.48.0-h18a1a76_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.51.36231-h84cd919_39.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/wasm-pack-0.15.0-h18a1a76_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/x264-1!164.3095-h8ffe710_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/x265-3.5-h2d74725_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.3-hb6c8415_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.24.5-py311h3f79411_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py311hf893f09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: ./rerun_pixi_env + - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/f2/f22c19b4cdde429805ff5ac8dd77a95569a7c4cb8991741b2ff0d538f220/backports_zstd-1.6.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/ed/1409cdb557e558a6c98003ab12fdd4284699e158c167c187cb0f124eea4c/py_spy-0.4.2-py2.py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/eb/f52b165db2abd662cda0a76efb7579a291fed1a7979cf41146cdc19e0d7a/cryptography-38.0.4-cp36-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de depends: - - aiosignal >=1.1.2 - - attrs >=17.3.0 - - frozenlist >=1.1.1 - - multidict >=4.5,<7.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - - yarl >=1.0,<2.0 - license: MIT AND Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/aiohttp?source=hash-mapping - size: 782527 - timestamp: 1713965372169 -- conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.9.5-py311ha68e1ae_0.conda - sha256: 03e161ef1e710089630276964921bb6de9c9852d0b04a59e3fe528c608327767 - md5: 9c350d73bdc0e3c68fd1d20afa9466a1 + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + strong: + - _openmp_mutex >=4.5 + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.9.5-py311h459d7ec_0.conda + sha256: 2eb99d920ef0dcd608e195bb852a64634ecf13f74680796959f1b9d9a9650a7b + md5: 0175d2636cc41dc019b51462c13ce225 depends: - aiosignal >=1.1.2 - attrs >=17.3.0 - frozenlist >=1.1.1 + - libgcc-ng >=12 - multidict >=4.5,<7.0 - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - yarl >=1.0,<2.0 - license: MIT AND Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/aiohttp?source=hash-mapping - size: 769123 - timestamp: 1713965512225 -- conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - sha256: 8dc149a6828d19bf104ea96382a9d04dae185d4a03cc6beb1bc7b84c428e3ca2 - md5: 421a865222cd0c9d83ff08bc78bf3a61 - depends: - - frozenlist >=1.1.0 - - python >=3.9 - - typing_extensions >=4.2 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/aiosignal?source=hash-mapping - size: 13688 - timestamp: 1751626573984 -- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.14-hb9d3cd8_0.conda - sha256: b9214bc17e89bf2b691fad50d952b7f029f6148f4ac4fe7c60c08f093efdf745 - md5: 76df83c2a9035c54df5d04ff81bcc02d - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-or-later - license_family: GPL - purls: [] - size: 566531 - timestamp: 1744668655747 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.14-h86ecc28_0.conda - sha256: 0aa836f6dd9132f243436898ed8024f408910f65220bafbfc95f71ab829bb395 - md5: a696b24c1b473ecc4774bcb5a6ac6337 - depends: - - libgcc >=13 - license: LGPL-2.1-or-later - license_family: GPL - purls: [] - size: 595290 - timestamp: 1744668754404 -- pypi: https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl - name: anyio - version: 4.12.1 - sha256: d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c - requires_dist: - - exceptiongroup>=1.0.2 ; python_full_version < '3.11' - - idna>=2.8 - - typing-extensions>=4.5 ; python_full_version < '3.13' - - trio>=0.32.0 ; python_full_version >= '3.10' and extra == 'trio' - - trio>=0.31.0 ; python_full_version < '3.10' and extra == 'trio' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda - sha256: b08ef033817b5f9f76ce62dfcac7694e7b6b4006420372de22494503decac855 - md5: 346722a0be40f6edc53f12640d301338 - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 2706396 - timestamp: 1718551242397 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aom-3.9.1-hcccb83c_0.conda - sha256: ac438ce5d3d3673a9188b535fc7cda413b479f0d52536aeeac1bd82faa656ea0 - md5: cc744ac4efe5bcaa8cca51ff5b850df0 - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 3250813 - timestamp: 1718551360260 -- conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda - sha256: 3032f2f55d6eceb10d53217c2a7f43e1eac83603d91e21ce502e8179e63a75f5 - md5: 3f17bc32cb7fcb2b4bf3d8d37f656eb8 - depends: - - __osx >=10.13 - - libcxx >=16 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 2749186 - timestamp: 1718551450314 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aom-3.9.1-h7bae524_0.conda - sha256: ec238f18ce8140485645252351a0eca9ef4f7a1c568a420f240a585229bc12ef - md5: 7adba36492a1bb22d98ffffe4f6fc6de + - python_abi 3.11.* *_cp311 + - yarl >=1.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/aiohttp?source=hash-mapping + run_exports: {} + size: 810945 + timestamp: 1713965013081 +- conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16.1-hb03c661_0.conda + sha256: cf93ca0f1f107e95a35969a4622684e08fcb8cf37f8cf4a1e9e424828386c921 + md5: 8904e09bda369377b3dd07e2ac828c5d depends: - - __osx >=11.0 - - libcxx >=16 - license: BSD-2-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 2235747 - timestamp: 1718551382432 -- conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda - sha256: 0524d0c0b61dacd0c22ac7a8067f977b1d52380210933b04141f5099c5b6fec7 - md5: 3d7c14285d3eb3239a76ff79063f27a5 + run_exports: + weak: + - alsa-lib >=1.2.16.1,<1.3.0a0 + size: 592377 + timestamp: 1781521980743 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda + sha256: b08ef033817b5f9f76ce62dfcac7694e7b6b4006420372de22494503decac855 + md5: 346722a0be40f6edc53f12640d301338 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - libgcc-ng >=12 + - libstdcxx-ng >=12 license: BSD-2-Clause license_family: BSD purls: [] - size: 1958151 - timestamp: 1718551737234 -- conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda - sha256: a9c114cbfeda42a226e2db1809a538929d2f118ef855372293bd188f71711c48 - md5: 791365c5f65975051e4e017b5da3abf5 + run_exports: + weak: + - aom >=3.9.1,<3.10.0a0 + size: 2706396 + timestamp: 1718551242397 +- conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-hb03c661_1.conda + sha256: 78c516af87437f52d883193cf167378f592ad445294c69f7c69f56059087c40d + md5: 9bb149f49de3f322fca007283eaa2725 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: GPL-2.0-or-later - license_family: GPL - purls: [] - size: 68072 - timestamp: 1756738968573 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/attr-2.5.1-h4e544f5_1.tar.bz2 - sha256: 2c793b48e835a8fac93f1664c706442972a0206963bf8ca202e83f7f4d29a7d7 - md5: 1ef6c06fec1b6f5ee99ffe2152e53568 - depends: - - libgcc-ng >=12 + - libattr 2.5.2 hb03c661_1 + - libgcc >=14 license: GPL-2.0-or-later license_family: GPL purls: [] - size: 74992 - timestamp: 1660065534958 -- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - sha256: 99c53ffbcb5dc58084faf18587b215f9ac8ced36bbfb55fa807c00967e419019 - md5: a10d11958cadc13fdb43df75f8b1903f - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/attrs?source=hash-mapping - size: 57181 - timestamp: 1741918625732 -- pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - name: backports-tarfile - version: 1.2.0 - sha256: 77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 - requires_dist: - - sphinx>=3.5 ; extra == 'docs' - - jaraco-packaging>=9.3 ; extra == 'docs' - - rst-linker>=1.9 ; extra == 'docs' - - furo ; extra == 'docs' - - sphinx-lint ; extra == 'docs' - - pytest>=6,!=8.1.* ; extra == 'testing' - - pytest-checkdocs>=2.4 ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-enabler>=2.2 ; extra == 'testing' - - jaraco-test ; extra == 'testing' - - pytest!=8.0.* ; extra == 'testing' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/00/0d/3db362169d80442adda9dd563c4f0bb10091c8c1c9a158037f4ecd53988e/backports_zstd-1.3.0-cp311-cp311-macosx_11_0_arm64.whl - name: backports-zstd - version: 1.3.0 - sha256: b0e71e83e46154a9d3ced6d4de9a2fea8207ee1e4832aeecf364dc125eda305c - requires_python: '>=3.9,<3.14' -- pypi: https://files.pythonhosted.org/packages/28/1a/379061e2abf8c3150ad51c1baab9ac723e01cf7538860a6a74c48f8b73ee/backports_zstd-1.3.0-cp311-cp311-win_amd64.whl - name: backports-zstd - version: 1.3.0 - sha256: d8aac2e7cdcc8f310c16f98a0062b48d0a081dbb82862794f4f4f5bdafde30a4 - requires_python: '>=3.9,<3.14' -- pypi: https://files.pythonhosted.org/packages/6f/3e/2667c0ddb53ddf28667e330bf9fe92e8e17705a481c9b698e283120565f7/backports_zstd-1.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - name: backports-zstd - version: 1.3.0 - sha256: 1df583adc0ae84a8d13d7139f42eade6d90182b1dd3e0d28f7df3c564b9fd55d - requires_python: '>=3.9,<3.14' -- pypi: https://files.pythonhosted.org/packages/ac/28/ed31a0e35feb4538a996348362051b52912d50f00d25c2d388eccef9242c/backports_zstd-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl - name: backports-zstd - version: 1.3.0 - sha256: 249f90b39d3741c48620021a968b35f268ca70e35f555abeea9ff95a451f35f9 - requires_python: '>=3.9,<3.14' -- pypi: https://files.pythonhosted.org/packages/d8/09/0822403f40932a165a4f1df289d41653683019e4fd7a86b63ed20e9b6177/backports_zstd-1.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: backports-zstd - version: 1.3.0 - sha256: 5eed0a09a163f3a8125a857cb031be87ed052e4a47bc75085ed7fca786e9bb5b - requires_python: '>=3.9,<3.14' + run_exports: + weak: + - libattr >=2.5.2,<2.6.0a0 + size: 31386 + timestamp: 1773595914754 - conda: https://conda.anaconda.org/conda-forge/linux-64/binaryen-117-h59595ed_0.conda sha256: f6d7f876c514d2d138fd8b06e485b042598cf3dcda40a8a346252bb7e1adf8d7 md5: 58aea5eaef8cb663104654734d432ba3 @@ -2913,113 +4091,44 @@ packages: license: Apache-2.0 license_family: APACHE purls: [] + run_exports: {} size: 5783056 timestamp: 1709092512197 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binaryen-117-h2f0025b_0.conda - sha256: 3820ab878d1a20792271a37440da1d304b36e26effff6f302592d5098cefa496 - md5: 69f34782ba69df988531f13d6bcc4385 - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 5372762 - timestamp: 1710444374732 -- conda: https://conda.anaconda.org/conda-forge/osx-64/binaryen-117-h73e2aa4_0.conda - sha256: f1dae7bbbdae9ee2f4b3479b51578fc67e77d54c5c235a5e5c7c1c58b2fff13e - md5: 029b1d804ba237f99163740225d53abc - depends: - - libcxx >=16 - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 3797571 - timestamp: 1709093347983 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/binaryen-117-hebf3989_0.conda - sha256: 9f4696ff6bf7a43261e549c1142dc24f45905fff68a6c0a1ebbdd0a84acd9056 - md5: 26d849f5539e7e20d8b7465a3616a622 - depends: - - libcxx >=16 - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 3466426 - timestamp: 1709092708128 -- conda: https://conda.anaconda.org/conda-forge/win-64/binaryen-117-h63175ca_0.conda - sha256: 2cc0e433360f7c4a5ce8e2b5f8960cfba8675b6b3232830da7e6f8403c6b4186 - md5: b0028cf00bb7d8f3fd8075de8165b1a8 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 40046563 - timestamp: 1709093094826 -- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.44-h4852527_1.conda - sha256: 3feccd1dd61bc18e41548d015e65f731400aa3ffe65802bc22ad772052d5326c - md5: 0fab3ce18775aba71131028a04c20dfe - depends: - - binutils_impl_linux-64 >=2.44,<2.45.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 34998 - timestamp: 1752032786202 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils-2.44-hf1166c9_1.conda - sha256: 6d779687e9b2c4e14e79881b9f900cd5c091f3e63e497d0aa6166e837f386126 - md5: 8a61cad75a4364056d7632e0b520562a +- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.46.1-default_h4852527_102.conda + sha256: 659c367ef49df7741749a2ad240b007d7df51b4f210505a78543deafb001e44c + md5: e8452fe381cac5fff20563a07722dfa5 depends: - - binutils_impl_linux-aarch64 >=2.44,<2.45.0a0 + - binutils_impl_linux-64 >=2.46.1,<2.46.2.0a0 license: GPL-3.0-only license_family: GPL purls: [] - size: 34983 - timestamp: 1752032881809 -- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.44-h4bf12b8_1.conda - sha256: 8556847f91a85c31ef65b05b7e9182a52775616d5d4e550dfb48cdee5fd35687 - md5: e45cfedc8ca5630e02c106ea36d2c5c6 + run_exports: {} + size: 35399 + timestamp: 1784214547142 +- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.46.1-default_hfdba357_102.conda + sha256: fb7bf36984a37ce7e4714d1d1da0bd0e3bfc679520f5cdc184afc676fd4b5da2 + md5: a0c5e0b7f58c8ceeb08e5bc41251d5a2 depends: - - ld_impl_linux-64 2.44 h1423503_1 + - ld_impl_linux-64 2.46.1 default_hbd61a6d_102 - sysroot_linux-64 + - zstd >=1.5.7,<1.6.0a0 license: GPL-3.0-only license_family: GPL purls: [] - size: 3781716 - timestamp: 1752032761608 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.44-h4c662bb_1.conda - sha256: 9a5ec0fa37e285afa0be9e12cb08bf2f20a25a7465e79fab5c64d91986b36883 - md5: bf817b2e2523697c4084ae109c5184ae - depends: - - ld_impl_linux-aarch64 2.44 h5e2c951_1 - - sysroot_linux-aarch64 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 3823090 - timestamp: 1752032859155 -- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.44-h4852527_1.conda - sha256: fbd94448d099a8c5fe7d9ec8c67171ab6e2f4221f453fe327de9b5aaf507f992 - md5: 38e0be090e3af56e44a9cac46101f6cd - depends: - - binutils_impl_linux-64 2.44 h4bf12b8_1 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 36046 - timestamp: 1752032788780 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_linux-aarch64-2.44-hf1166c9_1.conda - sha256: 8cfbbbfe780285722773bb74a68a2a82fd8b672858e3ba00d98f1f2292d64930 - md5: da245a6f768008f3181d7528a91230cd + run_exports: {} + size: 3713752 + timestamp: 1784214522814 +- conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.46.1-default_h4852527_102.conda + sha256: 08d7238663fc408ba2ab60b02fa3d06a7ca9d872962e03e90c7e0fdecb7ed1d0 + md5: 32fd07abe84eb14f17c7f5cc6fa8df82 depends: - - binutils_impl_linux-aarch64 2.44 h4c662bb_1 + - binutils_impl_linux-64 2.46.1 default_hfdba357_102 license: GPL-3.0-only license_family: GPL purls: [] - size: 36129 - timestamp: 1752032884469 + run_exports: {} + size: 36337 + timestamp: 1784214551894 - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py311h66f275b_1.conda sha256: c36eb061d9ead85f97644cfb740d485dba9b8823357f35c17851078e95e975c1 md5: 86daecb8e4ed1042d5dc6efbe0152590 @@ -3035,3836 +4144,3933 @@ packages: license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping + run_exports: {} size: 367573 timestamp: 1764017405384 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py311h14a79a7_1.conda - sha256: bf73f124e8dd683c5f414b9bea077246fcdec3f6c530bd83234b5eb329b52423 - md5: 292e7c014bfab5c77a2ff9c92728bb50 +- conda: https://conda.anaconda.org/conda-forge/linux-64/buf-1.66.0-ha8f183a_0.conda + sha256: 7c6ee35d348ab57625507cb136b87a224730d93d9aa86d3387d29532a243418f + md5: 7e57a26476ad5025547e7eaa1f292baa + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 61180590 + timestamp: 1771990000002 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hb03c661_0.conda + sha256: dee93a82d045f9aa8277829aa465224afa3c7a6ac18388de66099b2be7f705e7 + md5: 6130ad6705adc993b5d8482b7f66e01f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - c-ares >=1.34.8,<2.0a0 + size: 210929 + timestamp: 1784091008551 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.6.0-hd590300_0.conda + sha256: d741ff93d5f71a83a9be0f592682f31ca2d468c37177f18a8d1a2469bb821c05 + md5: ea6c792f792bdd7ae6e7e2dee32f0a48 + depends: + - binutils + - gcc + - gcc_linux-64 12.* + license: BSD + purls: [] + run_exports: {} + size: 6184 + timestamp: 1689097480051 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + sha256: 3bd6a391ad60e471de76c0e9db34986c4b5058587fbf2efa5a7f54645e28c2c7 + md5: 09262e66b19567aff4f592fb53b28760 + depends: + - __glibc >=2.17,<3.0.a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libstdcxx >=13 + - libxcb >=1.17.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + - xorg-libice >=1.1.2,<2.0a0 + - xorg-libsm >=1.2.5,<2.0a0 + - xorg-libx11 >=1.8.11,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.1-only or MPL-1.1 + purls: [] + run_exports: + weak: + - cairo >=1.18.4,<2.0a0 + size: 978114 + timestamp: 1741554591855 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cargo-llvm-cov-0.8.7-hdab8a38_0.conda + sha256: 53c56b26634f024bb0f5959a74a246f1d6636e55153822c36d200f262e8d6a2c + md5: 111b31aa6e7d28827526d4e4ce904710 + depends: + - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libstdcxx >=14 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 constrains: - - libbrotlicommon 1.2.0 he30d5cf_1 + - __glibc >=2.17 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: {} + size: 1324967 + timestamp: 1778642097940 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cargo-nextest-0.9.140-hb17b654_0.conda + sha256: fe0a6e9c936f65518d1c5d10c9a7ef46c2208e2bccbb90c8e454aac6f1617210 + md5: ed8a6f270fbb4ea25513ce67171ee81b + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + constrains: + - __glibc >=2.17 license: MIT license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 373346 - timestamp: 1764017600174 -- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py311h7e844b6_1.conda - sha256: 292026d98fd60bb25852792e2fd6ee97be35515057cfe258416ea6e1998e3564 - md5: ae49e04114f7f1673920fdbf326a047f + purls: [] + run_exports: {} + size: 7118275 + timestamp: 1783308093079 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cargo-zigbuild-0.20.1-hb17b654_1.conda + sha256: e9166bb4bea22e41547617c5c49b820229512089a89f7f120bffc4b2d007dc51 + md5: e9964ee918f0aa7b460bcc71a5c7c24a depends: - - __osx >=10.13 - - libcxx >=19 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 + - zig >=0.9.0 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 constrains: - - libbrotlicommon 1.2.0 h8616949_1 + - __glibc >=2.17 license: MIT license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 389997 - timestamp: 1764017848151 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py311hdc60ec4_1.conda - sha256: 617545ec0e97d35ed2ff7852f2581a20c0dda80b366d0c42a43706687f971ba8 - md5: 150cbf381febcf0a5e470a8d066e1bc0 + purls: [] + run_exports: {} + size: 1168384 + timestamp: 1753433525364 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.0-py311h03d9500_0.conda + sha256: 7c775ed3b4fae5ab0fbd6c7863538b85f7f66a715bb5d29badb2e91c58007cad + md5: 95f915485a5ce932a680b50926079064 depends: - - __osx >=11.0 - - libcxx >=19 + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - constrains: - - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT purls: - - pkg:pypi/brotli?source=hash-mapping - size: 359588 - timestamp: 1764018467340 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py311hc5da9e4_1.conda - sha256: 1803c838946d79ef6485ae8c7dafc93e28722c5999b059a34118ef758387a4c9 - md5: b0c459f98ac5ea504a9d9df6242f7ee1 + - pkg:pypi/cffi?source=hash-mapping + run_exports: {} + size: 309500 + timestamp: 1783424169890 +- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16-16.0.6-default_hddf928d_15.conda + sha256: aa96d079366b0c456c5f4f045451eee4aaa862b03cc1c28b01dd784e0ddef47a + md5: 61d63a0f0954f5b043930ec4fcf40e3f + depends: + - __glibc >=2.17,<3.0.a0 + - libclang-cpp16 16.0.6 default_hddf928d_15 + - libgcc >=14 + - libllvm16 >=16.0.6,<16.1.0a0 + - libstdcxx >=14 + constrains: + - clangdev 16.0.6 + - clang-tools 16.0.6 + - clangxx 16.0.6 + - llvm-tools 16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: {} + size: 778881 + timestamp: 1756166860477 +- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16.0.6-default_hfa515fb_15.conda + sha256: b303447a1f3d40386ca79d34a9383b2fe522f1e8358087bf7ca699647ac844b4 + md5: c3357d588e7330cebbe34b0fba0f09c0 depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - binutils_impl_linux-64 + - clang-16 16.0.6 default_hddf928d_15 + - libgcc-devel_linux-64 + - sysroot_linux-64 constrains: - - libbrotlicommon 1.2.0 hfd05255_1 - license: MIT - license_family: MIT - purls: - - pkg:pypi/brotli?source=hash-mapping - size: 335333 - timestamp: 1764018370925 -- conda: https://conda.anaconda.org/conda-forge/linux-64/buf-1.57.0-ha8f183a_0.conda - sha256: 0caf3bb93f1e0240701a8920bfcbe870188279358183291be5a8c63d0e5ccec6 - md5: 781f9fb31077acdb1bb1002634437b1f - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 53752836 - timestamp: 1756313661485 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/buf-1.57.0-h75b854d_0.conda - sha256: 7c46797b02b95d97876a718c08b6e4aaf4b18e85c7881c3fa1d90d4960031592 - md5: 37b8e638384c8f4665d575be4ac1f23d - license: Apache-2.0 - license_family: APACHE + - clang-tools 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 49692831 - timestamp: 1756313727722 -- conda: https://conda.anaconda.org/conda-forge/win-64/buf-1.57.0-hd02998f_0.conda - sha256: f55c5905e58090446c547bd51e1c39379b6217e28b6b6ced1719ff81f5461274 - md5: bfaaf99b539d7e819861eea5feb77d85 - license: Apache-2.0 - license_family: APACHE + run_exports: {} + size: 91663 + timestamp: 1756166910935 +- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16-16.0.6-default_hddf928d_15.conda + sha256: c298b3982508413eea55027ddbfb97ef81e83f79a90904a2f4e8f158c9000446 + md5: 410d6d9619792bb965e00753e7e51bd5 + depends: + - __glibc >=2.17,<3.0.a0 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libgcc >=14 + - libllvm16 >=16.0.6,<16.1.0a0 + - libstdcxx >=14 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 52900944 - timestamp: 1756313762692 -- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 - md5: 51a19bba1b8ebfb60df25cde030b7ebc + run_exports: {} + size: 132097 + timestamp: 1756167055330 +- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16.0.6-default_hddf928d_15.conda + sha256: 18572fc7752aad18c1f63afe22b33b3caa19c12ec04618716ec86faae68d16c3 + md5: 343da6ed76363ed69872e9fba4258f32 depends: - __glibc >=2.17,<3.0.a0 + - clang-format-16 16.0.6 default_hddf928d_15 + - libclang-cpp16 >=16.0.6,<16.1.0a0 - libgcc >=14 - license: bzip2-1.0.6 - license_family: BSD + - libllvm16 >=16.0.6,<16.1.0a0 + - libstdcxx >=14 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 260341 - timestamp: 1757437258798 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda - sha256: d2a296aa0b5f38ed9c264def6cf775c0ccb0f110ae156fcde322f3eccebf2e01 - md5: 2921ac0b541bf37c69e66bd6d9a43bca + run_exports: {} + size: 91745 + timestamp: 1756167099993 +- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-16.0.6-default_hddf928d_15.conda + sha256: 699fb4d288d693c55f7eaed5e3ae8363383fb1f95a99b6dfbb6f759efc3097a4 + md5: 5195e7353fc2e1a8038d6550c7738b57 depends: + - __glibc >=2.17,<3.0.a0 + - clang-format 16.0.6 default_hddf928d_15 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libclang13 >=16.0.6 - libgcc >=14 - license: bzip2-1.0.6 - license_family: BSD + - libllvm16 >=16.0.6,<16.1.0a0 + - libstdcxx >=14 + - libxml2 >=2.13.8,<2.14.0a0 + constrains: + - clangdev 16.0.6 + - clang 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 192536 - timestamp: 1757437302703 -- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_8.conda - sha256: 8f50b58efb29c710f3cecf2027a8d7325ba769ab10c746eff75cea3ac050b10c - md5: 97c4b3bd8a90722104798175a1bdddbf + run_exports: {} + size: 27294116 + timestamp: 1756167142932 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-3.27.6-hcfe8598_0.conda + sha256: 64e08c246195d6956f7a04fa7d96a53de696b26b1dae8b08cfe716950f696e12 + md5: 4c0101485c452ea86f846523c4fae698 depends: - - __osx >=10.13 - license: bzip2-1.0.6 + - bzip2 >=1.0.8,<2.0a0 + - libcurl >=8.3.0,<9.0a0 + - libexpat >=2.5.0,<3.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + - libuv >=1.46.0,<2.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - ncurses >=6.4,<7.0a0 + - rhash >=1.4.4,<2.0a0 + - xz >=5.2.6,<6.0a0 + - zstd >=1.5.5,<1.6.0a0 + license: BSD-3-Clause license_family: BSD purls: [] - size: 132607 - timestamp: 1757437730085 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 - md5: 58fd217444c2a5701a44244faf518206 + run_exports: {} + size: 18494905 + timestamp: 1695269729661 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.6.0-h00ab1b0_0.conda + sha256: 472b6b7f967df1db634c67d71c6b31cd186d18b5d0548196c2e426833ff17d99 + md5: 364c6ae36c4e36fcbd4d273cf4db78af depends: - - __osx >=11.0 - license: bzip2-1.0.6 - license_family: BSD + - c-compiler 1.6.0 hd590300_0 + - gxx + - gxx_linux-64 12.* + license: BSD purls: [] - size: 125061 - timestamp: 1757437486465 -- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - sha256: d882712855624641f48aa9dc3f5feea2ed6b4e6004585d3616386a18186fe692 - md5: 1077e9333c41ff0be8edd1a5ec0ddace + run_exports: {} + size: 6179 + timestamp: 1689097484095 +- conda: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda + sha256: 22053a5842ca8ee1cf8e1a817138cdb5e647eb2c46979f84153f6ad7bde73020 + md5: 418c6ca5929a611cbd69204907a83995 depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: bzip2-1.0.6 + - libgcc-ng >=12 + license: BSD-2-Clause license_family: BSD purls: [] - size: 55977 - timestamp: 1757437738856 -- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda - sha256: f8003bef369f57396593ccd03d08a8e21966157269426f71e943f96e4b579aeb - md5: f7f0d6cc2dc986d42ac2689ec88192be + run_exports: + weak: + - dav1d >=1.2.1,<1.2.2.0a0 + size: 760229 + timestamp: 1685695754230 +- conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h24cb091_1.conda + sha256: 8bb557af1b2b7983cf56292336a1a1853f26555d9c6cecf1e5b2b96838c9da87 + md5: ce96f2f470d39bd96ce03945af92e280 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 206884 - timestamp: 1744127994291 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.5-h86ecc28_0.conda - sha256: ccae98c665d86723993d4cb0b456bd23804af5b0645052c09a31c9634eebc8df - md5: 5deaa903d46d62a1f8077ad359c3062e - depends: - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 215950 - timestamp: 1744127972012 -- conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.5-hf13058a_0.conda - sha256: b37f5dacfe1c59e0a207c1d65489b760dff9ddb97b8df7126ceda01692ba6e97 - md5: eafe5d9f1a8c514afe41e6e833f66dfd + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - libglib >=2.86.2,<3.0a0 + - libexpat >=2.7.3,<3.0a0 + license: AFL-2.1 OR GPL-2.0-or-later + purls: [] + run_exports: + weak: + - dbus >=1.16.2,<2.0a0 + size: 447649 + timestamp: 1764536047944 +- conda: https://conda.anaconda.org/conda-forge/linux-64/doxygen-1.9.7-h661eb56_1.conda + sha256: 41334db7aaea41ca7e5968f598c52dbe714a4f5019d482ebc16f0e1d7ba1992d + md5: cc4690294cdd88059b42428f68ab9def depends: - - __osx >=10.13 - license: MIT - license_family: MIT + - libgcc-ng >=12 + - libiconv >=1.17,<2.0a0 + - libstdcxx-ng >=12 + license: GPL-2.0-only + license_family: GPL purls: [] - size: 184824 - timestamp: 1744128064511 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda - sha256: b4bb55d0806e41ffef94d0e3f3c97531f322b3cb0ca1f7cdf8e47f62538b7a2b - md5: f8cd1beb98240c7edb1a95883360ccfa + run_exports: {} + size: 6179024 + timestamp: 1687332729384 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fd-find-10.4.2-hdab8a38_0.conda + sha256: 2bdbb8a0e3682d68c5055c505e3fdd0aa1e4a0eb1d87908c4b1516fad2fee53b + md5: c3ffe1a848578c3035a10a1b9bb85cce depends: - - __osx >=11.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - __glibc >=2.17 license: MIT license_family: MIT purls: [] - size: 179696 - timestamp: 1744128058734 -- conda: https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.6.0-hd590300_0.conda - sha256: d741ff93d5f71a83a9be0f592682f31ca2d468c37177f18a8d1a2469bb821c05 - md5: ea6c792f792bdd7ae6e7e2dee32f0a48 - depends: - - binutils - - gcc - - gcc_linux-64 12.* - license: BSD - purls: [] - size: 6184 - timestamp: 1689097480051 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-compiler-1.6.0-h31becfc_0.conda - sha256: 36bc9d1673939980e7692ccce27e677dd4477d4c727ea173ec4210605b73927d - md5: b98866e63b17433ea5921a826c93cb97 - depends: - - binutils - - gcc - - gcc_linux-aarch64 12.* - license: BSD - purls: [] - size: 6213 - timestamp: 1689097449087 -- conda: https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.6.0-h282daa2_0.conda - sha256: c52dcdd9b5fc9fd9a7eb028b7d4bb9f11f4ba3a7361e904d2b28bc12053bac23 - md5: 2b801fd417843897458f4f8e132e05bb + run_exports: {} + size: 1197834 + timestamp: 1773352748932 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-7.1.1-gpl_ha0aeed6_910.conda + sha256: cb2453b75759813beb3ca1af8cc134b7b5ae3580a43745964f61d921ad3f591a + md5: 983afde30790eeb90054f0838fabaff2 depends: - - cctools >=949.0.1 - - clang_osx-64 16.* - - ld64 >=530 - - llvm-openmp - license: BSD + - __glibc >=2.17,<3.0.a0 + - alsa-lib >=1.2.14,<1.3.0a0 + - aom >=3.9.1,<3.10.0a0 + - bzip2 >=1.0.8,<2.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - gmp >=6.3.0,<7.0a0 + - harfbuzz >=11.4.5 + - lame >=3.100,<3.101.0a0 + - libass >=0.17.4,<0.17.5.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libopenvino >=2025.2.0,<2025.2.1.0a0 + - libopenvino-auto-batch-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-auto-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-hetero-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-intel-cpu-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-intel-gpu-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-intel-npu-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 + - libopus >=1.5.2,<2.0a0 + - librsvg >=2.58.4,<3.0a0 + - libstdcxx >=14 + - libva >=2.22.0,<3.0a0 + - libvorbis >=1.3.7,<1.4.0a0 + - libvpl >=2.15.0,<2.16.0a0 + - libvpx >=1.14.1,<1.15.0a0 + - libxcb >=1.17.0,<2.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - openh264 >=2.6.0,<2.6.1.0a0 + - openssl >=3.5.2,<4.0a0 + - pulseaudio-client >=17.0,<17.1.0a0 + - sdl2 >=2.32.54,<3.0a0 + - svt-av1 >=3.1.2,<3.1.3.0a0 + - x264 >=1!164.3095,<1!165 + - x265 >=3.5,<3.6.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 + constrains: + - __cuda >=12.8 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 6375 - timestamp: 1701504699534 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-compiler-1.6.0-h6aa9301_0.conda - sha256: c7d7c09724e7c324ecd3ad2dee4f016149b93f9bd8ee67661cafb20993f5b8a9 - md5: 0b204833d66694f214a5b3d7d2b87700 + run_exports: + weak: + - ffmpeg >=7.1.1,<8.0a0 + size: 10543003 + timestamp: 1757215060681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/flatbuffers-25.12.19-h54a6638_0.conda + sha256: 8e7e28382d6daffe7ff34edb5f9c115d8943f6b5132624048243b9b27ea0a537 + md5: 0d08f16839ff30d3852aae4bfeb0d44a depends: - - cctools >=949.0.1 - - clang_osx-arm64 16.* - - ld64 >=530 - - llvm-openmp - license: BSD - purls: [] - size: 6380 - timestamp: 1701504712958 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-h4c7d964_0.conda - sha256: 3b82f62baad3fd33827b01b0426e8203a2786c8f452f633740868296bcbe8485 - md5: c9e0c0f82f6e63323827db462b40ede8 + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - flatbuffers >=25.12.19,<25.12.20.0a0 + size: 1841035 + timestamp: 1766388876335 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.1-h27c8c51_0.conda + sha256: 2e50bdcebdf70a865b81f2456bbc586386451ec601c60f2b6cd22b8c40a2d384 + md5: e0e050cfa9fa85fe39632ab11cb7f3e0 depends: - - __win - license: ISC + - __glibc >=2.17,<3.0.a0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libgcc >=14 + - libuuid >=2.42.1,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + size: 281880 + timestamp: 1780450077431 +- conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda + sha256: c934c385889c7836f034039b43b05ccfa98f53c900db03d8411189892ced090b + md5: 8462b5322567212beeb025f3519fb3e2 + depends: + - libfreetype 2.14.3 ha770c72_0 + - libfreetype6 2.14.3 h73754d4_0 + license: GPL-2.0-only OR FTL purls: [] - size: 154489 - timestamp: 1754210967212 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda - sha256: 837b795a2bb39b75694ba910c13c15fa4998d4bb2a622c214a6a5174b2ae53d1 - md5: 74784ee3d225fc3dca89edb635b4e5cc + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + size: 173839 + timestamp: 1774298173462 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda + sha256: 858283ff33d4c033f4971bf440cebff217d5552a5222ba994c49be990dacd40d + md5: f9f81ea472684d75b9dd8d0b328cf655 depends: - - __unix - license: ISC + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-or-later purls: [] - size: 154402 - timestamp: 1754210968730 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda - sha256: 3bd6a391ad60e471de76c0e9db34986c4b5058587fbf2efa5a7f54645e28c2c7 - md5: 09262e66b19567aff4f592fb53b28760 + run_exports: + weak: + - fribidi >=1.0.16,<2.0a0 + size: 61244 + timestamp: 1757438574066 +- conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.8.0-py311h52bc045_0.conda + sha256: 9537f677fb492bf2bc4290e7fc2eafab6675c5ab0a6fb628d74b6a496d4a93e5 + md5: 6f0bb7a70fe713df47cabcc72bfbcd8e depends: - __glibc >=2.17,<3.0.a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - freetype >=2.12.1,<3.0a0 - - icu >=75.1,<76.0a0 - - libexpat >=2.6.4,<3.0a0 - - libgcc >=13 - - libglib >=2.82.2,<3.0a0 - - libpng >=1.6.47,<1.7.0a0 - - libstdcxx >=13 - - libxcb >=1.17.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - pixman >=0.44.2,<1.0a0 - - xorg-libice >=1.1.2,<2.0a0 - - xorg-libsm >=1.2.5,<2.0a0 - - xorg-libx11 >=1.8.11,<2.0a0 - - xorg-libxext >=1.3.6,<2.0a0 - - xorg-libxrender >=0.9.12,<0.10.0a0 - license: LGPL-2.1-only or MPL-1.1 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} + size: 54087 + timestamp: 1779999786304 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.4.0-h236703b_2.conda + sha256: ebe2dabb0a6f0ef05039d3a26b9c6b0aa050d7e791c6ab77ee91653b2098cdc3 + md5: ec54d965fd9d276c256ae3cf1d3aface + depends: + - gcc_impl_linux-64 12.4.0.* + license: BSD-3-Clause + license_family: BSD purls: [] - size: 978114 - timestamp: 1741554591855 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda - sha256: 37cfff940d2d02259afdab75eb2dbac42cf830adadee78d3733d160a1de2cc66 - md5: cd55953a67ec727db5dc32b167201aa6 + run_exports: {} + size: 55424 + timestamp: 1740240489245 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.4.0-h26ba24d_2.conda + sha256: 635cd3d70ca6f4c3ad3f4b5837b5badb058f2416392592bd5914aa805f0bc28e + md5: f091c5ea6c862ab1796c82465a7c2364 depends: - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - freetype >=2.12.1,<3.0a0 - - icu >=75.1,<76.0a0 - - libexpat >=2.6.4,<3.0a0 - - libgcc >=13 - - libglib >=2.82.2,<3.0a0 - - libpng >=1.6.47,<1.7.0a0 - - libstdcxx >=13 - - libxcb >=1.17.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - pixman >=0.44.2,<1.0a0 - - xorg-libice >=1.1.2,<2.0a0 - - xorg-libsm >=1.2.5,<2.0a0 - - xorg-libx11 >=1.8.11,<2.0a0 - - xorg-libxext >=1.3.6,<2.0a0 - - xorg-libxrender >=0.9.12,<0.10.0a0 - license: LGPL-2.1-only or MPL-1.1 + - binutils_impl_linux-64 >=2.40 + - libgcc >=12.4.0 + - libgcc-devel_linux-64 12.4.0 h1762d19_102 + - libgomp >=12.4.0 + - libsanitizer 12.4.0 ha732cd4_2 + - libstdcxx >=12.4.0 + - sysroot_linux-64 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 966667 - timestamp: 1741554768968 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h950ec3b_0.conda - sha256: d4297c3a9bcff9add3c5a46c6e793b88567354828bcfdb6fc9f6b1ab34aa4913 - md5: 32403b4ef529a2018e4d8c4f2a719f16 + run_exports: {} + size: 60389645 + timestamp: 1740240375167 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.4.0-h6b7512a_10.conda + sha256: 004d2ed6a3fc79452dec4c6cac556d0b26cf2457d33c4ace95beed4e6e832b55 + md5: 18432a261dca2bb05b45e60adee37d77 depends: - - __osx >=10.13 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - freetype >=2.12.1,<3.0a0 - - icu >=75.1,<76.0a0 - - libcxx >=18 - - libexpat >=2.6.4,<3.0a0 - - libglib >=2.82.2,<3.0a0 - - libpng >=1.6.47,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - - pixman >=0.44.2,<1.0a0 - license: LGPL-2.1-only or MPL-1.1 + - binutils_linux-64 + - gcc_impl_linux-64 12.4.0.* + - sysroot_linux-64 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 893252 - timestamp: 1741554808521 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda - sha256: 00439d69bdd94eaf51656fdf479e0c853278439d22ae151cabf40eb17399d95f - md5: 38f6df8bc8c668417b904369a01ba2e2 + run_exports: + strong: + - libgcc >=12 + size: 32617 + timestamp: 1745040673228 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.7-h2b0a6b4_0.conda + sha256: 1c22e37f9d7e06e9e0582ee5a55c2ddd19ea75f71f44eb13b56f504ef5c37aa5 + md5: 5d355db3e937086e22cf4cb5fe19787c depends: - - __osx >=11.0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - freetype >=2.12.1,<3.0a0 - - icu >=75.1,<76.0a0 - - libcxx >=18 - - libexpat >=2.6.4,<3.0a0 - - libglib >=2.82.2,<3.0a0 - - libpng >=1.6.47,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - - pixman >=0.44.2,<1.0a0 - license: LGPL-2.1-only or MPL-1.1 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libglib >=2.88.2,<3.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 896173 - timestamp: 1741554795915 -- conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda - sha256: b9f577bddb033dba4533e851853924bfe7b7c1623d0697df382eef177308a917 - md5: 20e32ced54300292aff690a69c5e7b97 + run_exports: + weak: + - gdk-pixbuf >=2.44.7,<3.0a0 + size: 581631 + timestamp: 1782591374199 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gh-2.96.0-hfc2019e_0.conda + sha256: 1039356041a7d0fa8b0fa8357f690a24f2ee538ba0928d0893349595e3368722 + md5: 80742ccfb74b96d201384a3c754da689 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 13429920 + timestamp: 1783039122532 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda + sha256: 309cf4f04fec0c31b6771a5809a1909b4b3154a2208f52351e1ada006f4c750c + md5: c94a5994ef49749880a8139cf9afcbe1 depends: - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - freetype >=2.12.1,<3.0a0 - - icu >=75.1,<76.0a0 - - libexpat >=2.6.4,<3.0a0 - - libglib >=2.82.2,<3.0a0 - - libpng >=1.6.47,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - - pixman >=0.44.2,<1.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: LGPL-2.1-only or MPL-1.1 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: GPL-2.0-or-later OR LGPL-3.0-or-later purls: [] - size: 1524254 - timestamp: 1741555212198 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cctools-1010.6-h40f6528_1.conda - sha256: 3e6ab1eb84f55df432af6b1893067c0dfa86e312c04d91824b199c125cf729e1 - md5: 7e7eb6bef28acef1112673443a8d692b + run_exports: + weak: + - gmp >=6.3.0,<7.0a0 + size: 460055 + timestamp: 1718980856608 +- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-hecca717_0.conda + sha256: 885fa7d1d7e2ad9ed0a700ee0d81ceb49de278253082d517959b22d6336eecce + md5: cf09e9fc938518e91d0706572cadf17a depends: - - cctools_osx-64 1010.6 heaa7f0c_1 - - ld64 951.9 ha02d983_1 - - libllvm16 >=16.0.6,<16.1.0a0 - license: APSL-2.0 - license_family: Other + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: LGPL-2.0-or-later + license_family: LGPL purls: [] - size: 21588 - timestamp: 1726771695380 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools-1010.6-h4faf515_1.conda - sha256: e0a69226e1f70b79f41c471c86fd0c450cc4fd5ec3343cd7689eb1c016babc70 - md5: d200afcb0b601ad89c79212b9a124347 + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 + size: 100054 + timestamp: 1780454302233 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-12.4.0-h236703b_2.conda + sha256: 6c3ea9877dc6babf064bafacd9e67280072b676864c26e90cbfec52eaa32a60e + md5: 5735863174438abb776bd1fefccec00a depends: - - cctools_osx-arm64 1010.6 h4f2c9d0_1 - - ld64 951.9 h634c8be_1 - - libllvm16 >=16.0.6,<16.1.0a0 - license: APSL-2.0 - license_family: Other + - gcc 12.4.0.* + - gxx_impl_linux-64 12.4.0.* + license: BSD-3-Clause + license_family: BSD purls: [] - size: 21621 - timestamp: 1726771337947 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cctools_osx-64-1010.6-heaa7f0c_1.conda - sha256: 2769f7bde9888d100a9997da14aabef345a8ee0850fe2c90e2ca2306e7fe79bd - md5: eaedf7d6a7b93b35381f7a0b4663922a + run_exports: {} + size: 54818 + timestamp: 1740240626426 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.4.0-h3ff227c_2.conda + sha256: 548987d77c5d6d648c1166e9a1eb810032f25fb1d61692a0a5a072db126e5f3f + md5: 5f8ae076e514514aeeb0eb52dac2d55d depends: - - __osx >=10.13 - - ld64_osx-64 >=951.9,<951.10.0a0 - - libcxx - - libllvm16 >=16.0.6,<16.1.0a0 - - libzlib >=1.3.1,<2.0a0 - - llvm-tools 16.0.* - - sigtool - constrains: - - ld64 951.9.* - - cctools 1010.6.* - - clang 16.0.* - license: APSL-2.0 - license_family: Other + - gcc_impl_linux-64 12.4.0 h26ba24d_2 + - libstdcxx-devel_linux-64 12.4.0 h1762d19_102 + - sysroot_linux-64 + - tzdata + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 1099432 - timestamp: 1726771664399 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1010.6-h4f2c9d0_1.conda - sha256: 3585a1d44fae9fd6839734e25ddde9dfb1dbb99c6974deb7bdbc6470b54af76d - md5: 3cf0dad98fcf3cec8cf6372ba2954724 + run_exports: {} + size: 12720023 + timestamp: 1740240582818 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.4.0-h8489865_10.conda + sha256: 6ea7b3957ace8960347069f032851a66755b785a5e34cd845c1b6b1e649b686e + md5: f01962bad75d6d68802a1eb56bb70478 depends: - - __osx >=11.0 - - ld64_osx-arm64 >=951.9,<951.10.0a0 - - libcxx - - libllvm16 >=16.0.6,<16.1.0a0 - - libzlib >=1.3.1,<2.0a0 - - llvm-tools 16.0.* - - sigtool - constrains: - - ld64 951.9.* - - cctools 1010.6.* - - clang 16.0.* - license: APSL-2.0 - license_family: Other + - binutils_linux-64 + - gcc_linux-64 12.4.0 h6b7512a_10 + - gxx_impl_linux-64 12.4.0.* + - sysroot_linux-64 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1091944 - timestamp: 1726771303834 -- pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - name: certifi - version: 2026.1.4 - sha256: 9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py311h5b438cf_0.conda - sha256: 4986d5b3ce60af4e320448a1a2231cb5dd5e3705537e28a7b58951a24bd69893 - md5: 6cb6c4d57d12dfa0ecdd19dbe758ffc9 + run_exports: + strong: + - libstdcxx >=12 + - libgcc >=12 + size: 30953 + timestamp: 1745040691868 +- conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.1-h15599e2_0.conda + sha256: 3bf149eab76768ed10f95eba015ca996cd6be7dc666996a004c4a8340a57cd60 + md5: b90a6ec73cc7d630981f78d4c7ca8fed depends: - __glibc >=2.17,<3.0.a0 - - libffi >=3.4.6,<3.5.0a0 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 - libgcc >=14 - - pycparser - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 + - libglib >=2.86.0,<3.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 license: MIT license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 304057 - timestamp: 1758716282627 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.0.0-py311h3324b35_0.conda - sha256: 39515e0613053a1239dd838bf1b28faac97633839cb0aa3c1ab1c07f3b79e947 - md5: 606a6f422c769fd5180ceca519200ee9 + purls: [] + run_exports: + weak: + - harfbuzz >=11.5.1 + size: 2427482 + timestamp: 1758640288422 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e + md5: 8b189310083baabfb622af68fd9d3ae3 depends: - - libffi >=3.4.6,<3.5.0a0 - - libgcc >=14 - - pycparser - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 license: MIT license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 322247 - timestamp: 1758717481152 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.0.0-py311h8ebb5ae_0.conda - sha256: 2f83931e589c53ce9cdd85d96686c3ec431077f567c85e6710e6b05c9099b202 - md5: c79d9a886f7089352482fbb9b7f079a9 + purls: [] + run_exports: + weak: + - icu >=75.1,<76.0a0 + size: 12129203 + timestamp: 1720853576813 +- conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.10.0-hb700be7_0.conda + sha256: bc231d69eb6663db0e09738fb916c5e5507147cf1ac60f364f964004e0b29bab + md5: 10909406c1b0e4b57f9f4f0eb0999af8 depends: - - __osx >=10.13 - - libffi >=3.4.6,<3.5.0a0 - - pycparser - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 license: MIT license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 295961 - timestamp: 1758716718225 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py311hcfc1310_0.conda - sha256: 207802a43cca5e81e1c267daabbb9b393d8c766f23883b3a2cb099d34eb51345 - md5: 419d91ef5b062ce19b3a513dcd566df8 + purls: [] + run_exports: + weak: + - intel-gmmlib >=22.10.0,<23.0a0 + size: 1013714 + timestamp: 1774422680665 +- conda: https://conda.anaconda.org/conda-forge/linux-64/intel-media-driver-25.3.4-hecca717_0.conda + sha256: 286679d4c175e8db2d047be766d1629f1ea5828bff9fe7e6aac2e6f0fad2b427 + md5: 7ae2034a0e2e24eb07468f1a50cdf0bb depends: - - __osx >=11.0 - - libffi >=3.4.6,<3.5.0a0 - - pycparser - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 + - __glibc >=2.17,<3.0.a0 + - intel-gmmlib >=22.8.1,<23.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libva >=2.22.0,<3.0a0 license: MIT license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 294021 - timestamp: 1758716481369 -- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py311h3485c13_1.conda - sha256: c9caca6098e3d92b1a269159b759d757518f2c477fbbb5949cb9fee28807c1f1 - md5: f02335db0282d5077df5bc84684f7ff9 + purls: [] + run_exports: + weak: + - intel-media-driver >=25.3.4,<25.4.0a0 + size: 8424610 + timestamp: 1757591682198 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 depends: - - pycparser - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - keyutils >=1.6.3,<2.0a0 + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + sha256: 9b07046870772f28740e3f6149f09ff222843733087a33c5540b169c6289652d + md5: 54157a1c8c0bb70f62dd0b17fba7e7f2 + depends: + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.7,<4.0a0 license: MIT license_family: MIT - purls: - - pkg:pypi/cffi?source=hash-mapping - size: 297941 - timestamp: 1761203850323 -- pypi: https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl - name: charset-normalizer - version: 3.4.4 - sha256: 5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: charset-normalizer - version: 3.4.4 - sha256: 840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - name: charset-normalizer - version: 3.4.4 - sha256: 5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl - name: charset-normalizer - version: 3.4.4 - sha256: 6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16.0.6-default_hfa515fb_15.conda - sha256: b303447a1f3d40386ca79d34a9383b2fe522f1e8358087bf7ca699647ac844b4 - md5: c3357d588e7330cebbe34b0fba0f09c0 + purls: [] + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1388990 + timestamp: 1781859420533 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2 + sha256: aad2a703b9d7b038c0f745b853c6bb5f122988fe1a7a096e0e606d9cbec4eaab + md5: a8832b479f93521a9e7b5b743803be51 depends: - - binutils_impl_linux-64 - - clang-16 16.0.6 default_hddf928d_15 - - libgcc-devel_linux-64 - - sysroot_linux-64 - constrains: - - clang-tools 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc-ng >=12 + license: LGPL-2.0-only + license_family: LGPL purls: [] - size: 91663 - timestamp: 1756166910935 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16.0.6-default_h3935787_15.conda - sha256: 8bc5e189a65f25c3492604b1d096306d7d4abdaf49d760d992cb6dfba5208963 - md5: cc3c7361a42241d132da775e34628510 + run_exports: + weak: + - lame >=3.100,<3.101.0a0 + size: 508258 + timestamp: 1664996250081 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + sha256: 27d83f1188cd19bcb7754a078b3fa7f4cfb8527f8eb2fde54dd01fc529d1adec + md5: 449500f2c089da11c40f5c21312e3e07 depends: - - binutils_impl_linux-aarch64 - - clang-16 16.0.6 default_hf07bfb7_15 - - libgcc-devel_linux-aarch64 - - sysroot_linux-aarch64 + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 constrains: - - clang-tools 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - binutils_impl_linux-64 2.46.1 + license: GPL-3.0-only + license_family: GPL purls: [] - size: 91765 - timestamp: 1756169905137 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-default_h510d6ca_15.conda - sha256: 377762f985606a4a5104cf7810acf0e0371bca30eef66b99cdfad92dd359f72a - md5: 6a57e5f291f44a7b1360372db400d672 + run_exports: {} + size: 745303 + timestamp: 1784214507189 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda + sha256: f84cb54782f7e9cea95e810ea8fef186e0652d0fa73d3009914fa2c1262594e1 + md5: a752488c68f2e7c456bcbd8f16eec275 depends: - - clang-16 16.0.6 default_h4651f56_15 - constrains: - - clang-tools 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* - license: Apache-2.0 WITH LLVM-exception + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 license_family: Apache purls: [] - size: 92070 - timestamp: 1756167200107 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16.0.6-default_h3e759af_15.conda - sha256: 13772739cdacadffdc7b3b97dd2a5b4c1ea8526f6e48b2bb5c28d4be0de0200c - md5: 0d4af3afc0ec97952578b965da7fee34 + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 + size: 261513 + timestamp: 1773113328888 +- conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.29.0-hb700be7_0.conda + sha256: d87cfc5eaa08eefff97d891ecb49faa958fcfc32a425767796269c4100d4e516 + md5: f3c3bc77c96af553f761af0e78bc8d9d depends: - - clang-16 16.0.6 default_h3c2e7ce_15 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 875773 + timestamp: 1780142086148 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda + sha256: dcd1429a1782864c452057a6c5bc1860f2b637dc20a2b7e6eacd57395bbceff8 + md5: 83b160d4da3e1e847bf044997621ed63 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 constrains: - - clang-tools 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* - license: Apache-2.0 WITH LLVM-exception + - libabseil-static =20250512.1=cxx17* + - abseil-cpp =20250512.1 + license: Apache-2.0 license_family: Apache purls: [] - size: 92190 - timestamp: 1756166136780 -- conda: https://conda.anaconda.org/conda-forge/win-64/clang-16.0.6-default_h5a21124_15.conda - sha256: b6ce3d51658f230f93058bc1a6ac03fb410b876a9ee5c1ca9cff3886654f4352 - md5: 40fc30ac75e2efce770e636bc42c928a + run_exports: + weak: + - libabseil >=20250512.1,<20250513.0a0 + - libabseil =*=cxx17* + size: 1310612 + timestamp: 1750194198254 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.4-h96ad9f0_0.conda + sha256: 035eb8b54e03e72e42ef707420f9979c7427776ea99e0f1e3c969f92eb573f19 + md5: d3be7b2870bf7aff45b12ea53165babd depends: - - clang-16 16.0.6 default_h7df9e1c_15 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 - libzlib >=1.3.1,<2.0a0 - - ucrt - - vc14_runtime - - zstd >=1.5.7,<1.6.0a0 - constrains: - - clang-tools 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - fribidi >=1.0.10,<2.0a0 + - libiconv >=1.18,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - harfbuzz >=11.0.1 + license: ISC purls: [] - size: 90373728 - timestamp: 1756194582485 -- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-16-16.0.6-default_hddf928d_15.conda - sha256: aa96d079366b0c456c5f4f045451eee4aaa862b03cc1c28b01dd784e0ddef47a - md5: 61d63a0f0954f5b043930ec4fcf40e3f + run_exports: + weak: + - libass >=0.17.4,<0.17.5.0a0 + size: 152179 + timestamp: 1749328931930 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libattr-2.5.2-hb03c661_1.conda + sha256: 0cef37eb013dc7091f17161c357afbdef9a9bc79ef6462508face6db3f37db77 + md5: 7e7f0a692eb62b95d3010563e7f963b6 depends: - __glibc >=2.17,<3.0.a0 - - libclang-cpp16 16.0.6 default_hddf928d_15 - libgcc >=14 - - libllvm16 >=16.0.6,<16.1.0a0 - - libstdcxx >=14 - constrains: - - clangdev 16.0.6 - - clang-tools 16.0.6 - - clangxx 16.0.6 - - llvm-tools 16.0.6 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 778881 - timestamp: 1756166860477 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16-16.0.6-default_hf07bfb7_15.conda - sha256: 94710355171eeb54c62306bd7858c04a5b429382e972f254110bac822e1cc388 - md5: e8d44b518e1d877e99b85a2404ea7b6c + run_exports: + weak: + - libattr >=2.5.2,<2.6.0a0 + size: 53316 + timestamp: 1773595896163 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e + md5: 72c8fd1af66bd67bf580645b426513ed + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + size: 79965 + timestamp: 1764017188531 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b + md5: 366b40a69f0ad6072561c1d09301c886 + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 34632 + timestamp: 1764017199083 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d + md5: 4ffbb341c8b616aa2494b6afb26a0c5f + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 + size: 298378 + timestamp: 1764017210931 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.75-h39aace5_0.conda + sha256: 9c84448305e7c9cc44ccec7757cf5afcb5a021f4579aa750a1fa6ea398783950 + md5: c44c16d6976d2aebbd65894d7741e67e + depends: + - __glibc >=2.17,<3.0.a0 + - attr >=2.5.1,<2.6.0a0 + - libgcc >=13 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libcap >=2.75,<2.76.0a0 + size: 120375 + timestamp: 1741176638215 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp16-16.0.6-default_hddf928d_15.conda + sha256: 218ea23f992734c3cb40bca39266768240f8f099a23c5d69305692f3485f1bea + md5: ebf034fe29aad0a581668bcbf8ca4431 depends: - - libclang-cpp16 16.0.6 default_hf07bfb7_15 + - __glibc >=2.17,<3.0.a0 - libgcc >=14 - libllvm16 >=16.0.6,<16.1.0a0 - libstdcxx >=14 - constrains: - - clang-tools 16.0.6 - - llvm-tools 16.0.6 - - clangdev 16.0.6 - - clangxx 16.0.6 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 780063 - timestamp: 1756169863023 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h4651f56_15.conda - sha256: 53fefb4b47993b9cfc1b7d2f85fe66b8a62e312658eaa23f054b2d5e31bb529e - md5: 696148e51e076dea8f4d39b60280f933 + run_exports: + weak: + - libclang-cpp16 >=16.0.6,<16.1.0a0 + size: 18328968 + timestamp: 1756166766219 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp18.1-18.1.8-default_h99862b1_18.conda + sha256: c54c902679012a22ed1727d5a5b87b0e46727d1e2b201e3a79c19188fbb5b829 + md5: 6c396d954c81b50021d2df4b567acc93 depends: - - __osx >=10.13 - - libclang-cpp16 16.0.6 default_h4651f56_15 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - constrains: - - clang-tools 16.0.6 - - clangxx 16.0.6 - - clangdev 16.0.6 - - llvm-tools 16.0.6 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libllvm18 >=18.1.8,<18.2.0a0 + - libstdcxx >=14 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 762659 - timestamp: 1756166994264 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16-16.0.6-default_h3c2e7ce_15.conda - sha256: 7412ca9b68eefe9ae8f509a4badac9e8a70f5d06024285604aad36fae9710317 - md5: 19739ec9eae7382a7be37881a95f30e2 + run_exports: + weak: + - libclang-cpp18.1 >=18.1.8,<18.2.0a0 + size: 19595525 + timestamp: 1773511447721 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-21.1.0-default_h746c552_1.conda + sha256: e6c0123b888d6abf03c66c52ed89f9de1798dde930c5fd558774f26e994afbc6 + md5: 327c78a8ce710782425a89df851392f7 depends: - - __osx >=11.0 - - libclang-cpp16 16.0.6 default_h3c2e7ce_15 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - constrains: - - clang-tools 16.0.6 - - clangxx 16.0.6 - - llvm-tools 16.0.6 - - clangdev 16.0.6 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libllvm21 >=21.1.0,<21.2.0a0 + - libstdcxx >=14 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 761355 - timestamp: 1756166017332 -- conda: https://conda.anaconda.org/conda-forge/win-64/clang-16-16.0.6-default_h7df9e1c_15.conda - sha256: 4fd4b39552367bcc94476810a32013f7495b473851dfcac0089cc499d467943f - md5: 3eeb79ed453b1c3b87b0dc60ac092763 + run_exports: + weak: + - libclang13 >=21.1.0 + size: 12358102 + timestamp: 1757383373129 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda + sha256: 7d243f45a48f62cb8e3b871dd336137fe0fb90094a191756fb553508837f6338 + md5: 2c1e55d695b11525c760b486ac0be517 depends: - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.22.2,<1.23.0a0 + - libgcc >=14 + - libnghttp2 >=1.68.1,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 - zstd >=1.5.7,<1.6.0a0 - constrains: - - clangxx 16.0.6 - - clang-tools 16.0.6 - - llvm-tools 16.0.6 - - clangdev 16.0.6 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + license: curl + license_family: MIT purls: [] - size: 30820305 - timestamp: 1756194440748 -- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16.0.6-default_hddf928d_15.conda - sha256: 18572fc7752aad18c1f63afe22b33b3caa19c12ec04618716ec86faae68d16c3 - md5: 343da6ed76363ed69872e9fba4258f32 + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 478914 + timestamp: 1782802520033 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda + sha256: aa8e8c4be9a2e81610ddf574e05b64ee131fab5e0e3693210c9d6d2fba32c680 + md5: 6c77a605a7a689d17d4819c0f8ac9a00 depends: - __glibc >=2.17,<3.0.a0 - - clang-format-16 16.0.6 default_hddf928d_15 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - libgcc >=14 - - libllvm16 >=16.0.6,<16.1.0a0 - - libstdcxx >=14 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + license: MIT + license_family: MIT purls: [] - size: 91745 - timestamp: 1756167099993 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16.0.6-default_hf07bfb7_15.conda - sha256: 73e4acdbdfbc75e01ee83ba8456dcfdc70132b76b0bb2918b3b15159fb50b27f - md5: 4719e45a61adee841186fe0c71e2acd0 + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 + size: 73490 + timestamp: 1761979956660 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.127-hb03c661_0.conda + sha256: 7d3187c11b7ae66c5595a8afd5a7ce352a490527fdf6614cab129bc7f2c16ba3 + md5: d8d16b9b32a3c5df7e5b3350e2cbe058 depends: - - clang-format-16 16.0.6 default_hf07bfb7_15 - - libclang-cpp16 >=16.0.6,<16.1.0a0 + - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libllvm16 >=16.0.6,<16.1.0a0 - - libstdcxx >=14 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libpciaccess >=0.19,<0.20.0a0 + license: MIT + license_family: MIT purls: [] - size: 91926 - timestamp: 1756170098419 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16.0.6-default_h4651f56_15.conda - sha256: a6d474ca999b1a0fabc34c9e92a68a6c4ef2eaf6c4a86a0881c6c4b14c22884f - md5: 4f39be6579f9ec9afad0b15fec159fc0 + run_exports: + weak: + - libdrm >=2.4.127,<2.5.0a0 + size: 311505 + timestamp: 1778975798004 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b depends: - - __osx >=10.13 - - clang-format-16 16.0.6 default_h4651f56_15 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 92323 - timestamp: 1756167820703 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16.0.6-default_h3c2e7ce_15.conda - sha256: 211a4772f2912ac1ea68eaa8cebb411dec46efdc5ef4d899dba316e53aebcb7f - md5: da984913ad26e91c30015e58ec58d902 + run_exports: + weak: + - libedit >=3.1.20250104,<3.2.0a0 + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_3.conda + sha256: 9a25ea93e8272785405a21d30f84e620befb1d545f6dfaae18f06103b5df0443 + md5: 75e9f795be506c96dd43cb09c7c8d557 depends: - - __osx >=11.0 - - clang-format-16 16.0.6 default_h3c2e7ce_15 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - __glibc >=2.17,<3.0.a0 + - libglvnd 1.7.0 ha4b6fd6_3 + license: LicenseRef-libglvnd purls: [] - size: 92531 - timestamp: 1756166546529 -- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-16.0.6-default_h7df9e1c_15.conda - sha256: 958dabce7477e2ed377e415bc2aca29e43f5003faf9efee62373b656a444e4cb - md5: c68cbb230d69b2343c9e96878643eeb6 + run_exports: {} + size: 46500 + timestamp: 1779728188901 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 + md5: 172bf1cd1ff8629f2b1179945ed45055 depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 1184074 - timestamp: 1756195538812 -- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-format-16-16.0.6-default_hddf928d_15.conda - sha256: c298b3982508413eea55027ddbfb97ef81e83f79a90904a2f4e8f158c9000446 - md5: 410d6d9619792bb965e00753e7e51bd5 + run_exports: + weak: + - libev >=4.33,<4.34.0a0 + size: 112766 + timestamp: 1702146165126 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + sha256: 16feffd9ddbbe5b718515d38ee376c685ba95491cd901244e24671d20b952a77 + md5: b24d3c612f71e7aa74158d92106318b2 depends: - __glibc >=2.17,<3.0.a0 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - libgcc >=14 - - libllvm16 >=16.0.6,<16.1.0a0 - - libstdcxx >=14 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT purls: [] - size: 132097 - timestamp: 1756167055330 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16-16.0.6-default_hf07bfb7_15.conda - sha256: 791e0d76e5cfe0b67ceb92a8479463767b724a4bcc2fb3b47da8b8292a6b7e5e - md5: 4c46a862eacaaf54b3f51b3c384e352c + run_exports: {} + size: 77856 + timestamp: 1781203599810 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb depends: - - libclang-cpp16 >=16.0.6,<16.1.0a0 + - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libllvm16 >=16.0.6,<16.1.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libffi >=3.5.2,<3.6.0a0 + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.5.0-he200343_1.conda + sha256: e755e234236bdda3d265ae82e5b0581d259a9279e3e5b31d745dc43251ad64fb + md5: 47595b9d53054907a00d95e4d47af1d6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - libogg >=1.3.5,<1.4.0a0 - libstdcxx >=14 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + license: BSD-3-Clause + license_family: BSD purls: [] - size: 132889 - timestamp: 1756170067259 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16-16.0.6-default_h4651f56_15.conda - sha256: 4881cded9d4551050a04184f380b4bb50d75e4c7868136b4d9f7d96481e8affb - md5: 79ce8c25f88855de6a6acc2c8a31430b + run_exports: + weak: + - libflac >=1.5.0,<1.6.0a0 + size: 424563 + timestamp: 1764526740626 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda + sha256: 38f014a7129e644636e46064ecd6b1945e729c2140e21d75bb476af39e692db2 + md5: e289f3d17880e44b633ba911d57a321b depends: - - __osx >=10.13 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL purls: [] - size: 128881 - timestamp: 1756167683330 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16-16.0.6-default_h3c2e7ce_15.conda - sha256: b7049fd6cfab9306522c4e9399cd74692a6897f0d11a50a63ad92f15b40db2f6 - md5: af53806ee8d5023c799d6186fd1442f6 + run_exports: {} + size: 8049 + timestamp: 1774298163029 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda + sha256: 16f020f96da79db1863fcdd8f2b8f4f7d52f177dd4c58601e38e9182e91adf1d + md5: fb16b4b69e3f1dcfe79d80db8fd0c55d depends: - - __osx >=11.0 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libpng >=1.6.55,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL purls: [] - size: 127337 - timestamp: 1756166443161 -- conda: https://conda.anaconda.org/conda-forge/linux-64/clang-tools-16.0.6-default_hddf928d_15.conda - sha256: 699fb4d288d693c55f7eaed5e3ae8363383fb1f95a99b6dfbb6f759efc3097a4 - md5: 5195e7353fc2e1a8038d6550c7738b57 + run_exports: {} + size: 384575 + timestamp: 1774298162622 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + sha256: 8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46 + md5: 57736f29cc2b0ec0b6c2952d3f101b6a + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_19 + - libgomp 15.2.0 he0feb66_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: {} + size: 1041084 + timestamp: 1778269013026 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + sha256: 9dcf54adfaa5e861123c2da4f2f0451a685464ea7e5a41ad91cf67b31d658d98 + md5: 331ee9b72b9dff570d56b1302c5ab37d + depends: + - libgcc 15.2.0 he0feb66_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: + strong: + - libgcc + size: 27694 + timestamp: 1778269016987 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-lib-1.12.2-hb03c661_0.conda + sha256: 9779af09721826ebd0217f507e03dfa09df899483d8f129116ae38b374c6788b + md5: 02d1c2751ad5cf488e53d69884e60313 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgpg-error >=1.61,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - libgcrypt-lib >=1.12.2,<2.0a0 + size: 610146 + timestamp: 1779976247301 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_3.conda + sha256: ec353b3076ed8e357ed961d0e9ff6997491cade0e603de5bd18a2e301ac78ebd + md5: f25206d7322c0e9648e8b83694d143ab + depends: + - __glibc >=2.17,<3.0.a0 + - libglvnd 1.7.0 ha4b6fd6_3 + - libglx 1.7.0 ha4b6fd6_3 + license: LicenseRef-libglvnd + purls: [] + run_exports: {} + size: 133469 + timestamp: 1779728207669 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.88.2-h0d30a3d_0.conda + sha256: 4bee10e62796f01e4fa2b5849135b1cc061337fe9cf5eb9bd79e9664922ae0e4 + md5: 889febc66cd9e4190f80ef9718fa239b + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libffi >=3.5.2,<3.6.0a0 + - pcre2 >=10.47,<10.48.0a0 + constrains: + - glib >2.66 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 4754220 + timestamp: 1782463895250 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_3.conda + sha256: e019ebe4e3f5cdf23e2f5e58ddf7ade27988c53820115b17b98f218ebcc87748 + md5: eb83f3f8cecc3e9bff9e250817fc69b6 + depends: + - __glibc >=2.17,<3.0.a0 + license: LicenseRef-libglvnd + purls: [] + run_exports: {} + size: 133586 + timestamp: 1779728183422 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_3.conda + sha256: 2f74713c9ca408ea84e88a30a9028153e7b553e8bb42e06139eac9a753c27da9 + md5: ec3c4350aa0261bf7f87b8ca15c8e80e + depends: + - __glibc >=2.17,<3.0.a0 + - libglvnd 1.7.0 ha4b6fd6_3 + - xorg-libx11 >=1.8.13,<2.0a0 + license: LicenseRef-libglvnd + purls: [] + run_exports: {} + size: 76586 + timestamp: 1779728199059 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + sha256: 5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b + md5: faac990cb7aedc7f3a2224f2c9b0c26c depends: - __glibc >=2.17,<3.0.a0 - - clang-format 16.0.6 default_hddf928d_15 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - - libclang13 >=16.0.6 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: + strong: + - _openmp_mutex >=4.5 + size: 603817 + timestamp: 1778268942614 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.61-h54a6638_0.conda + sha256: 22c789e7186380e0c728d6ab4064e5b026aab112a10ecb8b7a6eb270adfcd7d8 + md5: f980b2ea1d227356414a2fb47be406de + depends: - libgcc >=14 - - libllvm16 >=16.0.6,<16.1.0a0 + - __glibc >=2.17,<3.0.a0 - libstdcxx >=14 - - libxml2 >=2.13.8,<2.14.0a0 - constrains: - - clangdev 16.0.6 - - clang 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + license: LGPL-2.1-only purls: [] - size: 27294116 - timestamp: 1756167142932 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-tools-16.0.6-default_hf07bfb7_15.conda - sha256: a5761f23a044f8a5429fa7a17cd0240934fbd3e241fa2526682d4934ac36e23e - md5: 536fb2e63e6e5211067127498ceb8e7a + run_exports: + weak: + - libgpg-error >=1.61,<2.0a0 + size: 318054 + timestamp: 1778157441447 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.12.1-default_h3d81e11_1000.conda + sha256: eecaf76fdfc085d8fed4583b533c10cb7f4a6304be56031c43a107e01a56b7e2 + md5: d821210ab60be56dd27b5525ed18366d depends: - - clang-format 16.0.6 default_hf07bfb7_15 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - - libclang13 >=16.0.6 + - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libllvm16 >=16.0.6,<16.1.0a0 - libstdcxx >=14 - libxml2 >=2.13.8,<2.14.0a0 - constrains: - - clangdev 16.0.6 - - clang 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + license: BSD-3-Clause + license_family: BSD purls: [] - size: 27244600 - timestamp: 1756170139674 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-tools-16.0.6-default_h4651f56_15.conda - sha256: 324e12f7d311d584d28098e048bf3610c7a12eea96ddc0d55616025c214d0fd8 - md5: 006fd8afef42dd85907aca61970d553a + run_exports: + weak: + - libhwloc >=2.12.1,<2.12.2.0a0 + size: 2450422 + timestamp: 1752761850672 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f + md5: 915f5995e94f60e9a4826e0b0920ee88 depends: - - __osx >=10.13 - - clang-format 16.0.6 default_h4651f56_15 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - - libclang13 >=16.0.6 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - - libxml2 >=2.13.8,<2.14.0a0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-only + purls: [] + run_exports: + weak: + - libiconv >=1.18,<2.0a0 + size: 790176 + timestamp: 1754908768807 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_0.conda + sha256: 716332fd31b3808da7a4235a05212a9e9da05e854864c3def2dea0b5d2bf7610 + md5: 466badda5536d85ddc63ee9404f29735 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 constrains: - - clangdev 16.0.6 - - clang 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 652868 + timestamp: 1783731886811 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm16-16.0.6-ha7bfdaf_4.conda + sha256: 421fed3a23f5657c2f6ab672b253ae3fce6039c109be6484bd9ce6a16e90bc2b + md5: 5cf4080515925080bff5ac96d82a3bfa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - libxml2 >=2.13.5,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.6,<1.6.0a0 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 17932186 - timestamp: 1756168027313 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-16.0.6-default_h3c2e7ce_15.conda - sha256: 791b96da9f4c361831c250f7d40f171ce0f41cb9d6d8891860c8f3f418b4177d - md5: 8ff3b05785dbd769ae3328d3199fef9d + run_exports: + weak: + - libllvm16 >=16.0.6,<16.1.0a0 + size: 35234903 + timestamp: 1739806428307 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm18-18.1.8-default_hddf928d_9.conda + sha256: f961140d65db57e7c228b21617c483611a9eb08dcea4bab4d1efd5fda6584c23 + md5: 3c0e77d748a924416324457a5b2b86f0 depends: - - __osx >=11.0 - - clang-format 16.0.6 default_h3c2e7ce_15 - - libclang-cpp16 >=16.0.6,<16.1.0a0 - - libclang13 >=16.0.6 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 - libxml2 >=2.13.8,<2.14.0a0 - constrains: - - clangdev 16.0.6 - - clang 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 16980094 - timestamp: 1756166687551 -- conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-16.0.6-default_h7df9e1c_15.conda - sha256: 54f20f43fb6719d01508fc43595d547e696cef7635fda1b4203911c26ab9bbac - md5: b7e6316f5f7d8ecba9d209d70bc83ffe + run_exports: + weak: + - libllvm18 >=18.1.8,<18.2.0a0 + size: 39176118 + timestamp: 1756470449936 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm21-21.1.0-hecd9e04_0.conda + sha256: d190f1bf322149321890908a534441ca2213a9a96c59819da6cabf2c5b474115 + md5: 9ad637a7ac380c442be142dfb0b1b955 depends: - - clang-format 16.0.6 default_h7df9e1c_15 - - libclang13 >=16.0.6 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 - libxml2 >=2.13.8,<2.14.0a0 - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - zstd >=1.5.7,<1.6.0a0 - constrains: - - clangdev 16.0.6 - - clang 16.0.6.* - - llvm 16.0.6.* - - llvm-tools 16.0.6.* - - llvmdev 16.0.6.* license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 226347785 - timestamp: 1756195760295 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_19.conda - sha256: 7c8146bb69ddf42af2e30d83ad357985732052eccfbaf279d433349e0c1324de - md5: 64155ef139280e8c181dad866dea2980 - depends: - - cctools_osx-64 - - clang 16.0.6.* - - compiler-rt 16.0.6.* - - ld64_osx-64 - - llvm-tools 16.0.6.* - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 17589 - timestamp: 1723069343993 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-16.0.6-hc421ffc_19.conda - sha256: e131b316c772b9ecd57f47e221b0b460d817650ee29de3a6d017ba17f834e3a3 - md5: 44d46e1690d60e9dfdf9ab9fc8a344f6 - depends: - - cctools_osx-arm64 - - clang 16.0.6.* - - compiler-rt 16.0.6.* - - ld64_osx-arm64 - - llvm-tools 16.0.6.* - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 17659 - timestamp: 1723069383236 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_19.conda - sha256: d38be1dc9476fdc60dfbd428df0fb3e284ee9101e7eeaa1764b54b11bab54105 - md5: 760ecbc6f4b6cecbe440b0080626286f - depends: - - clang_impl_osx-64 16.0.6 h8787910_19 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 20580 - timestamp: 1723069348997 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_osx-arm64-16.0.6-h54d7cd3_19.conda - sha256: 1be2d2b837267e9cc61c1cb5e0ce780047ceb87063005144c1332a82a5996fb3 - md5: 1a9ab8ce6143c14e425059e61a4fb737 - depends: - - clang_impl_osx-arm64 16.0.6 hc421ffc_19 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 20589 - timestamp: 1723069388608 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx-16.0.6-default_h1b9e3cd_15.conda - sha256: 945d52e908b9a52b3a290eedcf7a7865f80334f2cc1dacc7a2809f5189388086 - md5: 75da7c70527c5330f3a88ea8138d0303 + run_exports: + weak: + - libllvm21 >=21.1.0,<21.2.0a0 + size: 44363060 + timestamp: 1756291822911 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d + md5: b88d90cad08e6bc8ad540cb310a761fb depends: - - clang 16.0.6 default_h510d6ca_15 - - libcxx-devel 16.0.6.* + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 constrains: - - libcxx-devel 16.0.6 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - xz 5.8.3.* + license: 0BSD purls: [] - size: 92193 - timestamp: 1756167234819 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-16.0.6-default_hc1b5c72_15.conda - sha256: da0634e0c5f0d117169bf2d65d696277a07cd1ec18cd04bd98c90baded3541d0 - md5: f9cfd9b8b33f762dd456cc770fa5b29f + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 113478 + timestamp: 1775825492909 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.3-hb03c661_0.conda + sha256: 7858f6a173206bc8a5bdc8e75690483bb66c0dcc3809ac1cb43c561a4723623a + md5: 55c20edec8e90c4703787acaade60808 depends: - - clang 16.0.6 default_h3e759af_15 - - libcxx-devel 16.0.6.* - constrains: - - libcxx-devel 16.0.6 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - liblzma 5.8.3 hb03c661_0 + license: 0BSD purls: [] - size: 92329 - timestamp: 1756166158732 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_19.conda - sha256: c99c773d76a93066f1e78d368f934cd904b4f39a3939bf1d5a5cf26e3b812dbc - md5: 9ffa16e2bd7eb5b8b1a0d19185710cd3 + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 491429 + timestamp: 1775825511214 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f + md5: 2a45e7f8af083626f009645a6481f12d depends: - - clang_osx-64 16.0.6 hb91bd55_19 - - clangxx 16.0.6.* - - libcxx >=16 - - libllvm16 >=16.0.6,<16.1.0a0 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.6,<2.0a0 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT purls: [] - size: 17642 - timestamp: 1723069387016 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-16.0.6-hcd7bac0_19.conda - sha256: 6847b38f815e43a01e7cfe78fc9d2d7ab90c749bce1301322707ccbad4f2d7a2 - md5: 263f7e2b3196bea030602830381cc84e + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 + size: 663344 + timestamp: 1773854035739 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 depends: - - clang_osx-arm64 16.0.6 h54d7cd3_19 - - clangxx 16.0.6.* - - libcxx >=16 - - libllvm16 >=16.0.6,<16.1.0a0 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL purls: [] - size: 17740 - timestamp: 1723069417515 -- conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-16.0.6-hb91bd55_19.conda - sha256: 8c2cf371561f8de565aa721520d34e14ff9cf9b7e3a868879ec2f99760c433cc - md5: 81d40fad4c14cc7a893f2e274647c7a4 + run_exports: + weak: + - libnsl >=2.0.1,<2.1.0a0 + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda + sha256: ffb066ddf2e76953f92e06677021c73c85536098f1c21fcd15360dbc859e22e4 + md5: 68e52064ed3897463c0e958ab5c8f91b depends: - - clang_osx-64 16.0.6 hb91bd55_19 - - clangxx_impl_osx-64 16.0.6 h6d92fbe_19 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 19289 - timestamp: 1723069392162 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_osx-arm64-16.0.6-h54d7cd3_19.conda - sha256: 6e4344d0bc29fc76e6c6c8aa463536ea0615ffe60512c883b8ae26d73ac4804d - md5: 26ffc845adddf183c15dd4285e97fc66 + run_exports: + weak: + - libogg >=1.3.5,<1.4.0a0 + size: 218500 + timestamp: 1745825989535 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2025.2.0-hb617929_1.conda + sha256: 235e7d474c90ad9d8955401b8a91dbe373aa1dc65db3c8232a5e22e4eaf41976 + md5: 1da20cc4ff32dc74424dec68ec087dba depends: - - clang_osx-arm64 16.0.6 h54d7cd3_19 - - clangxx_impl_osx-arm64 16.0.6 hcd7bac0_19 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 19366 - timestamp: 1723069423746 -- pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl - name: click - version: 8.3.1 - sha256: 981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 - requires_dist: - - colorama ; sys_platform == 'win32' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/cmake-3.27.6-hcfe8598_0.conda - sha256: 64e08c246195d6956f7a04fa7d96a53de696b26b1dae8b08cfe716950f696e12 - md5: 4c0101485c452ea86f846523c4fae698 + run_exports: + weak: + - libopenvino >=2025.2.0,<2025.2.1.0a0 + size: 6244771 + timestamp: 1753211097492 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-batch-plugin-2025.2.0-hed573e4_1.conda + sha256: 193f760e828b0dd5168dd1d28580d4bf429c5f14a4eee5e0c02ff4c6d4cf8093 + md5: 94f9d17be1d658213b66b22f63cc6578 depends: - - bzip2 >=1.0.8,<2.0a0 - - libcurl >=8.3.0,<9.0a0 - - libexpat >=2.5.0,<3.0a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - libuv >=1.46.0,<2.0a0 - - libzlib >=1.2.13,<2.0.0a0 - - ncurses >=6.4,<7.0a0 - - rhash >=1.4.4,<2.0a0 - - xz >=5.2.6,<6.0a0 - - zstd >=1.5.5,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 + - tbb >=2021.13.0 purls: [] - size: 18494905 - timestamp: 1695269729661 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cmake-3.27.6-hef020d8_0.conda - sha256: 099e3d6deac7fc29251552f87b59ee7299582caf291a20de71107327a4aded57 - md5: e20b2e0185007a671ebbb72f4353d70b + run_exports: {} + size: 114760 + timestamp: 1753211116381 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-plugin-2025.2.0-hed573e4_1.conda + sha256: a6f9f996e64e6d2f295f017a833eda7018ff58b6894503272d72f0002dfd6f33 + md5: 071b3a82342715a411f216d379ab6205 depends: - - bzip2 >=1.0.8,<2.0a0 - - libcurl >=8.3.0,<9.0a0 - - libexpat >=2.5.0,<3.0a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - libuv >=1.46.0,<2.0a0 - - libzlib >=1.2.13,<2.0.0a0 - - ncurses >=6.4,<7.0a0 - - rhash >=1.4.4,<2.0a0 - - xz >=5.2.6,<6.0a0 - - zstd >=1.5.5,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 + - tbb >=2021.13.0 purls: [] - size: 17776308 - timestamp: 1695269663260 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cmake-3.27.6-hf40c264_0.conda - sha256: 9216698f88b82e99db950f8c372038931c54ea3e0b0b05e2a3ce03ec4b405df7 - md5: 771da6a52aaf0f9d84114d0ed0d0299f + run_exports: {} + size: 250500 + timestamp: 1753211127339 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-hetero-plugin-2025.2.0-hd41364c_1.conda + sha256: f43f9049338ef9735b6815bac3f483d1e3adddecbfdeb13be365bc3f601fe156 + md5: 77c0c7028a8110076d40314dc7b1fa98 depends: - - bzip2 >=1.0.8,<2.0a0 - - libcurl >=8.3.0,<9.0a0 - - libcxx >=15.0.7 - - libexpat >=2.5.0,<3.0a0 - - libuv >=1.46.0,<2.0a0 - - libzlib >=1.2.13,<2.0.0a0 - - ncurses >=6.4,<7.0a0 - - rhash >=1.4.4,<2.0a0 - - xz >=5.2.6,<6.0a0 - - zstd >=1.5.5,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 purls: [] - size: 16525734 - timestamp: 1695270838345 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-3.27.6-h1c59155_0.conda - sha256: 31be31e358e6f6f8818d8f9c9086da4404f8c6fc89d71d55887bed11ce6d463e - md5: 3c0dd04401438fec44cd113247ba2852 + run_exports: {} + size: 194815 + timestamp: 1753211138624 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-cpu-plugin-2025.2.0-hb617929_1.conda + sha256: a4a1cd320fa010a45d01f438dc3431b7a60271ee19188a901f884399fe744268 + md5: e4cc6db5bdc8b554c06bf569de57f85f depends: - - bzip2 >=1.0.8,<2.0a0 - - libcurl >=8.3.0,<9.0a0 - - libcxx >=15.0.7 - - libexpat >=2.5.0,<3.0a0 - - libuv >=1.46.0,<2.0a0 - - libzlib >=1.2.13,<2.0.0a0 - - ncurses >=6.4,<7.0a0 - - rhash >=1.4.4,<2.0a0 - - xz >=5.2.6,<6.0a0 - - zstd >=1.5.5,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 16007289 - timestamp: 1695270816826 -- conda: https://conda.anaconda.org/conda-forge/win-64/cmake-3.27.6-hf0feee3_0.conda - sha256: 12b94bce6d7c76ff408f8ea240c7d78987b0bc3cb4f632f381c4b0efd30ebfe0 - md5: 4dc81f3bf26f0949fedd4e31cecea1d1 + run_exports: {} + size: 12377488 + timestamp: 1753211149903 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-gpu-plugin-2025.2.0-hb617929_1.conda + sha256: 03ebf700586775144ca5913f401393a386b9a1d7a7cfcba4494830063ca5eb92 + md5: b846fe6c158ca417e246122172d68d3a depends: - - bzip2 >=1.0.8,<2.0a0 - - libcurl >=8.3.0,<9.0a0 - - libexpat >=2.5.0,<3.0a0 - - libuv >=1.44.2,<2.0a0 - - libzlib >=1.2.13,<2.0.0a0 - - ucrt >=10.0.20348.0 - - vc14_runtime >=14.29.30139 - - xz >=5.2.6,<6.0a0 - - zstd >=1.5.5,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 + - ocl-icd >=2.3.3,<3.0a0 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 13777396 - timestamp: 1695270971791 -- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 - md5: 962b9857ee8e7018c22f2776ffa0b2d7 + run_exports: {} + size: 10815480 + timestamp: 1753211182626 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-npu-plugin-2025.2.0-hb617929_1.conda + sha256: b6dbc342293d6ce0c7b37c9f29f734b3e1856cff9405a02fb33cedd1b36528e6 + md5: 86fd4c25f6accaf646c86adf0f1382d3 depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/colorama?source=hash-mapping - size: 27011 - timestamp: 1733218222191 -- conda: https://conda.anaconda.org/conda-forge/osx-64/compiler-rt-16.0.6-ha38d28d_2.conda - sha256: de0e2c94d9a04f60ec9aedde863d6c1fad3f261bdb63ec8adc70e2d9ecdb07bb - md5: 3b9e8c5c63b8e86234f499490acd85c2 + - __glibc >=2.17,<3.0.a0 + - level-zero >=1.23.1,<2.0a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 + purls: [] + run_exports: {} + size: 1261488 + timestamp: 1753211212823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-ir-frontend-2025.2.0-hd41364c_1.conda + sha256: 334733396d4c9a9b2b2d7d7d850e8ee8deca1f9becd0368d106010076ceb20ca + md5: 75e595d9f2019a60f6dcb500266da615 depends: - - clang 16.0.6.* - - clangxx 16.0.6.* - - compiler-rt_osx-64 16.0.6.* - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 purls: [] - size: 94198 - timestamp: 1701467261175 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-16.0.6-h3808999_2.conda - sha256: 67f6883f37ea720f97d016c3384962d86ec8853e5f4b0065aa77e335ca80193e - md5: 517f18b3260bb7a508d1f54a96e6285b + run_exports: + weak: + - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 + size: 204890 + timestamp: 1753211224567 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-onnx-frontend-2025.2.0-h1862bb8_1.conda + sha256: 3937b028e7192ed3805581ac0ea171725843056c8544537754fad45a1791e864 + md5: 68f5ad9d8e3979362bb9dfc9388980aa depends: - - clang 16.0.6.* - - clangxx 16.0.6.* - - compiler-rt_osx-arm64 16.0.6.* - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 + purls: [] + run_exports: + weak: + - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 + size: 1724503 + timestamp: 1753211235981 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-paddle-frontend-2025.2.0-h1862bb8_1.conda + sha256: c7ac3d4187323ab37ef62ec0896a41c8ca7da426c7f587494c72fe74852269e5 + md5: a032d03468dee9fb5b8eaf635b4571c2 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 + purls: [] + run_exports: + weak: + - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 + size: 744746 + timestamp: 1753211248776 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2025.2.0-hecca717_1.conda + sha256: 2d4a680a16509b8dd06ccd7a236655e46cc7c242bb5b6e88b83a834b891658db + md5: cd40cf2d10a3279654c9769f3bc8caf5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 purls: [] - size: 93724 - timestamp: 1701467327657 -- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-64-16.0.6-ha38d28d_2.conda - sha256: 75270bd8e306967f6e1a8c17d14f2dfe76602a5c162088f3ea98034fe3d71e0c - md5: 7a46507edc35c6c8818db0adaf8d787f + run_exports: + weak: + - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 + size: 1243134 + timestamp: 1753211260154 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2025.2.0-h0767aad_1.conda + sha256: 311ec1118448a28e76f0359c4393c7f7f5e64761c48ac7b169bf928a391eae77 + md5: f71c6b4e342b560cc40687063ef62c50 depends: - - clang 16.0.6.* - - clangxx 16.0.6.* - constrains: - - compiler-rt 16.0.6 - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 + - snappy >=1.2.2,<1.3.0a0 purls: [] - size: 9895261 - timestamp: 1701467223753 -- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-16.0.6-h3808999_2.conda - sha256: 61f1a10e6e8ec147f17c5e36cf1c2fe77ac6d1907b05443fa319fd59be20fa33 - md5: 8c7d77d888e1a218cccd9e82b1458ec6 + run_exports: + weak: + - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 + size: 1325059 + timestamp: 1753211272484 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hecca717_1.conda + sha256: 581f4951e645e820c4a6ffe40fb0174b56d6e31fb1fefd2d64913fea01f8f69e + md5: fd9dacd7101f80ff1110ea6b76adb95d depends: - - clang 16.0.6.* - - clangxx 16.0.6.* - constrains: - - compiler-rt 16.0.6 - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libopenvino 2025.2.0 hb617929_1 + - libstdcxx >=14 purls: [] - size: 9829914 - timestamp: 1701467293179 -- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.13-py311hd8ed1ab_0.conda - noarch: generic - sha256: ab70477f5cfb60961ba27d84a4c933a24705ac4b1736d8f3da14858e95bbfa7a - md5: 4666fd336f6d48d866a58490684704cd + run_exports: + weak: + - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 + size: 497047 + timestamp: 1753211285617 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.6.1-h280c20c_0.conda + sha256: f1061a26213b9653bbb8372bfa3f291787ca091a9a3060a10df4d5297aad74fd + md5: 2446ac1fe030c2aa6141386c1f5a6aed depends: - - python >=3.11,<3.12.0a0 - - python_abi * *_cp311 - license: Python-2.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 47495 - timestamp: 1749048148121 -- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.14-py311hd8ed1ab_2.conda - noarch: generic - sha256: c871fe68dcc6b79b322e4fcf9f2b131162094c32a68d48c87aa8582995948a01 - md5: 43ed151bed1a0eb7181d305fed7cf051 + run_exports: + weak: + - libopus >=1.6.1,<2.0a0 + size: 324993 + timestamp: 1768497114401 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_0.conda + sha256: f41721636a7c2e51bc2c642e1127955ab9c81145470714fdaac44d4d09e4af41 + md5: 33082e13b4769b48cfeb648e15bfe3fc depends: - - python >=3.11,<3.12.0a0 - - python_abi * *_cp311 - license: Python-2.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT purls: [] - size: 47257 - timestamp: 1761172995774 -- pypi: https://files.pythonhosted.org/packages/26/f8/a81170a816679fca9ccd907b801992acfc03c33f952440421c921af2cc57/cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl - name: cryptography - version: 38.0.4 - sha256: ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c - requires_dist: - - cffi>=1.12 - - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - - pyenchant>=1.6.11 ; extra == 'docstest' - - twine>=1.12.0 ; extra == 'docstest' - - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' - - black ; extra == 'pep8test' - - flake8 ; extra == 'pep8test' - - flake8-import-order ; extra == 'pep8test' - - pep8-naming ; extra == 'pep8test' - - setuptools-rust>=0.11.4 ; extra == 'sdist' - - bcrypt>=3.1.5 ; extra == 'ssh' - - pytest>=6.2.0 ; extra == 'test' - - pytest-benchmark ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-subtests ; extra == 'test' - - pytest-xdist ; extra == 'test' - - pretend ; extra == 'test' - - iso8601 ; extra == 'test' - - pytz ; extra == 'test' - - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/52/1b/49ebc2b59e9126f1f378ae910e98704d54a3f48b78e2d6d6c8cfe6fbe06f/cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl - name: cryptography - version: 38.0.4 - sha256: 1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb - requires_dist: - - cffi>=1.12 - - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - - pyenchant>=1.6.11 ; extra == 'docstest' - - twine>=1.12.0 ; extra == 'docstest' - - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' - - black ; extra == 'pep8test' - - flake8 ; extra == 'pep8test' - - flake8-import-order ; extra == 'pep8test' - - pep8-naming ; extra == 'pep8test' - - setuptools-rust>=0.11.4 ; extra == 'sdist' - - bcrypt>=3.1.5 ; extra == 'ssh' - - pytest>=6.2.0 ; extra == 'test' - - pytest-benchmark ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-subtests ; extra == 'test' - - pytest-xdist ; extra == 'test' - - pretend ; extra == 'test' - - iso8601 ; extra == 'test' - - pytz ; extra == 'test' - - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/75/7a/2ea7dd2202638cf1053aaa8fbbaddded0b78c78832b3d03cafa0416a6c84/cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl - name: cryptography - version: 38.0.4 - sha256: 2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70 - requires_dist: - - cffi>=1.12 - - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - - pyenchant>=1.6.11 ; extra == 'docstest' - - twine>=1.12.0 ; extra == 'docstest' - - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' - - black ; extra == 'pep8test' - - flake8 ; extra == 'pep8test' - - flake8-import-order ; extra == 'pep8test' - - pep8-naming ; extra == 'pep8test' - - setuptools-rust>=0.11.4 ; extra == 'sdist' - - bcrypt>=3.1.5 ; extra == 'ssh' - - pytest>=6.2.0 ; extra == 'test' - - pytest-benchmark ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-subtests ; extra == 'test' - - pytest-xdist ; extra == 'test' - - pretend ; extra == 'test' - - iso8601 ; extra == 'test' - - pytz ; extra == 'test' - - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/a2/8f/6c52b1f9d650863e8f67edbe062c04f1c8455579eaace1593d8fe469319a/cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl - name: cryptography - version: 38.0.4 - sha256: bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b - requires_dist: - - cffi>=1.12 - - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - - pyenchant>=1.6.11 ; extra == 'docstest' - - twine>=1.12.0 ; extra == 'docstest' - - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' - - black ; extra == 'pep8test' - - flake8 ; extra == 'pep8test' - - flake8-import-order ; extra == 'pep8test' - - pep8-naming ; extra == 'pep8test' - - setuptools-rust>=0.11.4 ; extra == 'sdist' - - bcrypt>=3.1.5 ; extra == 'ssh' - - pytest>=6.2.0 ; extra == 'test' - - pytest-benchmark ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-subtests ; extra == 'test' - - pytest-xdist ; extra == 'test' - - pretend ; extra == 'test' - - iso8601 ; extra == 'test' - - pytz ; extra == 'test' - - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' - requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/c0/eb/f52b165db2abd662cda0a76efb7579a291fed1a7979cf41146cdc19e0d7a/cryptography-38.0.4-cp36-abi3-win_amd64.whl - name: cryptography - version: 38.0.4 - sha256: 8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d - requires_dist: - - cffi>=1.12 - - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - - pyenchant>=1.6.11 ; extra == 'docstest' - - twine>=1.12.0 ; extra == 'docstest' - - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' - - black ; extra == 'pep8test' - - flake8 ; extra == 'pep8test' - - flake8-import-order ; extra == 'pep8test' - - pep8-naming ; extra == 'pep8test' - - setuptools-rust>=0.11.4 ; extra == 'sdist' - - bcrypt>=3.1.5 ; extra == 'ssh' - - pytest>=6.2.0 ; extra == 'test' - - pytest-benchmark ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-subtests ; extra == 'test' - - pytest-xdist ; extra == 'test' - - pretend ; extra == 'test' - - iso8601 ; extra == 'test' - - pytz ; extra == 'test' - - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' - requires_python: '>=3.6' -- conda: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.6.0-h00ab1b0_0.conda - sha256: 472b6b7f967df1db634c67d71c6b31cd186d18b5d0548196c2e426833ff17d99 - md5: 364c6ae36c4e36fcbd4d273cf4db78af + run_exports: + weak: + - libpciaccess >=0.19,<0.20.0a0 + size: 29147 + timestamp: 1773533027610 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda + sha256: 377cfe037f3eeb3b1bf3ad333f724a64d32f315ee1958581fc671891d63d3f89 + md5: eba48a68a1a2b9d3c0d9511548db85db depends: - - c-compiler 1.6.0 hd590300_0 - - gxx - - gxx_linux-64 12.* - license: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: zlib-acknowledgement purls: [] - size: 6179 - timestamp: 1689097484095 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cxx-compiler-1.6.0-h2a328a1_0.conda - sha256: aebe297f355fb3a5101eb11a5233d94c3445d2f1bbf4c0d7e3ff88b98d399694 - md5: 3847c922cacfe5a3d7ee663ffde014a4 + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 + size: 317729 + timestamp: 1776315175087 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-hfb7daa7_5.conda + sha256: b04322e2128684d4043e256f56b74528b0a0a296ba4a81299056ec04655a0580 + md5: da31d891434e50d7e7be8adc5832269b depends: - - c-compiler 1.6.0 h31becfc_0 - - gxx - - gxx_linux-aarch64 12.* - license: BSD + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 6220 - timestamp: 1689097451413 -- conda: https://conda.anaconda.org/conda-forge/osx-64/cxx-compiler-1.6.0-h7728843_0.conda - sha256: 3d609b7cf397b1d9f8627dedd0abd95a9daffa919d9593b56096a4e6e4a8597e - md5: 52efcad0d146779100e46c973cc1cb56 + run_exports: + weak: + - libprotobuf >=6.31.1,<6.31.2.0a0 + size: 4204474 + timestamp: 1780003940664 +- conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + sha256: a45ef03e6e700cc6ac6c375e27904531cf8ade27eb3857e080537ff283fb0507 + md5: d27665b20bc4d074b86e628b3ba5ab8b depends: - - c-compiler 1.6.0 h282daa2_0 - - clangxx_osx-64 16.* - license: BSD + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - freetype >=2.13.3,<3.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - harfbuzz >=11.0.0,<12.0a0 + - libgcc >=13 + - libglib >=2.84.0,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libxml2 >=2.13.7,<2.14.0a0 + - pango >=1.56.3,<2.0a0 + constrains: + - __glibc >=2.17 + license: LGPL-2.1-or-later purls: [] - size: 6415 - timestamp: 1701504710176 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cxx-compiler-1.6.0-h2ffa867_0.conda - sha256: c3a4ee7382e548f1e98ca1a348c941094b8d5f38c84d3258c00f9e493c591344 - md5: b3bf27600fda1f6770fd28c45805d689 + run_exports: + weak: + - librsvg >=2.58.4,<3.0a0 + size: 6543651 + timestamp: 1743368725313 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.4.0-ha732cd4_2.conda + sha256: d9a23eee55fc2a901e67565c328c37e7c2336ca805d985ad4a67b7837fb4e40a + md5: e729f335fee31fd68429187c9e0f97c2 depends: - - c-compiler 1.6.0 h6aa9301_0 - - clangxx_osx-arm64 16.* - license: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=12.4.0 + - libstdcxx >=12.4.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 6399 - timestamp: 1701504753445 -- conda: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda - sha256: 22053a5842ca8ee1cf8e1a817138cdb5e647eb2c46979f84153f6ad7bde73020 - md5: 418c6ca5929a611cbd69204907a83995 + run_exports: + weak: + - libsanitizer 12.4.0 + size: 3955974 + timestamp: 1740240321338 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc7d488a_2.conda + sha256: 57cb5f92110324c04498b96563211a1bca6a74b2918b1e8df578bfed03cc32e4 + md5: 067590f061c9f6ea7e61e3b2112ed6b3 depends: - - libgcc-ng >=12 - license: BSD-2-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - lame >=3.100,<3.101.0a0 + - libflac >=1.5.0,<1.6.0a0 + - libgcc >=14 + - libogg >=1.3.5,<1.4.0a0 + - libopus >=1.5.2,<2.0a0 + - libstdcxx >=14 + - libvorbis >=1.3.7,<1.4.0a0 + - mpg123 >=1.32.9,<1.33.0a0 + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 760229 - timestamp: 1685695754230 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dav1d-1.2.1-h31becfc_0.conda - sha256: 33fe66d025cf5bac7745196d1a3dd7a437abcf2dbce66043e9745218169f7e17 - md5: 6e5a87182d66b2d1328a96b61ca43a62 + run_exports: + weak: + - libsndfile >=1.2.2,<1.3.0a0 + size: 355619 + timestamp: 1765181778282 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + sha256: 365376f4815e5e80def2b3462a2419708b7c292da0da85278386c2618621fff4 + md5: 4aed8e657e9ff156bdbe849b4df44389 depends: - - libgcc-ng >=12 - license: BSD-2-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing purls: [] - size: 347363 - timestamp: 1685696690003 -- conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda - sha256: ec71a835866b42e946cd2039a5f7a6458851a21890d315476f5e66790ac11c96 - md5: 9d88733c715300a39f8ca2e936b7808d - license: BSD-2-Clause + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 962119 + timestamp: 1782519076616 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661 + md5: eecce068c7e4eddeb169591baac20ac4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause license_family: BSD purls: [] - size: 668439 - timestamp: 1685696184631 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda - sha256: 93e077b880a85baec8227e8c72199220c7f87849ad32d02c14fb3807368260b8 - md5: 5a74cdee497e6b65173e10d94582fae6 - license: BSD-2-Clause - license_family: BSD + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 + size: 304790 + timestamp: 1745608545575 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + sha256: dff1058c76ec6b8759e41cefa2508162d00e4a5e6721aa68ec3fd10094e702dc + md5: 5794b3bdc38177caf969dabd3af08549 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_19 + constrains: + - libstdcxx-ng ==15.2.0=*_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 316394 - timestamp: 1685695959391 -- conda: https://conda.anaconda.org/conda-forge/win-64/dav1d-1.2.1-hcfcfb64_0.conda - sha256: 2aa2083c9c186da7d6f975ccfbef654ed54fff27f4bc321dbcd12cee932ec2c4 - md5: ed2c27bda330e3f0ab41577cf8b9b585 + run_exports: {} + size: 5852044 + timestamp: 1778269036376 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_19.conda + sha256: 0672b6b6e1791c92e8eccad58081a99d614fcf82bca5841f9dfa3c3e658f83b9 + md5: e5ce228e579726c07255dbf90dc62101 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-2-Clause - license_family: BSD + - libstdcxx 15.2.0 h934c35e_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 618643 - timestamp: 1685696352968 -- conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda - sha256: 3b988146a50e165f0fa4e839545c679af88e4782ec284cc7b6d07dd226d6a068 - md5: 679616eb5ad4e521c83da4650860aba7 + run_exports: + strong: + - libstdcxx + size: 27776 + timestamp: 1778269074600 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-257.7-h4e0b6ca_0.conda + sha256: e26b22c0ae40fb6ad4356104d5fa4ec33fe8dd8a10e6aef36a9ab0c6a6f47275 + md5: 1e12c8aa74fa4c3166a9bdc135bc4abf depends: - - libstdcxx >=13 - - libgcc >=13 - __glibc >=2.17,<3.0.a0 + - libcap >=2.75,<2.76.0a0 - libgcc >=13 - - libexpat >=2.7.0,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - - libglib >=2.84.2,<3.0a0 - license: GPL-2.0-or-later - license_family: GPL + - libgcrypt-lib >=1.11.1,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: LGPL-2.1-or-later purls: [] - size: 437860 - timestamp: 1747855126005 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-heda779d_0.conda - sha256: 5c9166bbbe1ea7d0685a1549aad4ea887b1eb3a07e752389f86b185ef8eac99a - md5: 9203b74bb1f3fa0d6f308094b3b44c1e + run_exports: {} + size: 487969 + timestamp: 1750949895969 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-h9d88235_0.conda + sha256: b31346e1c01ab40a170e91147092ee8fd92b1dee3c66ee47ef025571c879b159 + md5: c1fcb4a88bc15a9f77ad8d27d7af1df9 depends: - - libgcc >=13 - - libstdcxx >=13 - - libgcc >=13 - - libexpat >=2.7.0,<3.0a0 - - libglib >=2.84.2,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - license: GPL-2.0-or-later - license_family: GPL + - __glibc >=2.17,<3.0.a0 + - lerc >=4.1.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libgcc >=14 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND purls: [] - size: 469781 - timestamp: 1747855172617 -- conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h27bd348_0.conda - sha256: 1106cf25c1b64e58f599e0bce9dd0b77b744146d324539fe715596f179dc37b7 - md5: ed5f537f1cefb3a15bcce7cb02d3c149 + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 452337 + timestamp: 1783084902636 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libudev-257.4-hbe16f8c_1.conda + sha256: e5abe70f378c7bac8c495e7a6179b65d852685a4a3926863fb70884756cbc650 + md5: 08871e4aded260b21673d7d1a2e1e08b depends: - - libcxx >=18 - - __osx >=10.13 - - libzlib >=1.3.1,<2.0a0 - - libexpat >=2.7.0,<3.0a0 - - libglib >=2.84.2,<3.0a0 - license: GPL-2.0-or-later - license_family: GPL + - libudev1 257.4 hbe16f8c_1 + license: LGPL-2.1-or-later purls: [] - size: 398137 - timestamp: 1747855120103 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/dbus-1.16.2-hda038a8_0.conda - sha256: 2ef01ab52dedb477cb7291994ad556279b37c8ad457521e75c47cad20248ea30 - md5: 80c663e4f6b0fd8d6723ff7d68f09429 + run_exports: + weak: + - libudev1 >=257.4 + size: 20263 + timestamp: 1741629480959 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.4-hbe16f8c_1.conda + sha256: 56e55a7e7380a980b418c282cb0240b3ac55ab9308800823ff031a9529e2f013 + md5: d6716795cd81476ac2f5465f1b1cde75 depends: - - __osx >=11.0 - - libcxx >=18 - - libzlib >=1.3.1,<2.0a0 - - libglib >=2.84.2,<3.0a0 - - libexpat >=2.7.0,<3.0a0 - license: GPL-2.0-or-later - license_family: GPL + - __glibc >=2.17,<3.0.a0 + - libcap >=2.75,<2.76.0a0 + - libgcc >=13 + license: LGPL-2.1-or-later purls: [] - size: 384376 - timestamp: 1747855177419 -- pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl - name: deprecated - version: 1.3.1 - sha256: 597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f - requires_dist: - - wrapt>=1.10,<3 - - inspect2 ; python_full_version < '3' - - tox ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - bump2version<1 ; extra == 'dev' - - setuptools ; python_full_version >= '3.12' and extra == 'dev' - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' -- pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - name: distlib - version: 0.4.0 - sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 -- conda: https://conda.anaconda.org/conda-forge/linux-64/doxygen-1.9.7-h661eb56_1.conda - sha256: 41334db7aaea41ca7e5968f598c52dbe714a4f5019d482ebc16f0e1d7ba1992d - md5: cc4690294cdd88059b42428f68ab9def + run_exports: {} + size: 144039 + timestamp: 1741629479455 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.6.2-h9c3ff4c_0.tar.bz2 + sha256: f2ac872920833960e514ce9efd8f7c08ce66dd870738d73839d1bce1ac497de6 + md5: a730b2badd586580c5752cc73842e068 depends: - - libgcc-ng >=12 - - libiconv >=1.17,<2.0a0 - - libstdcxx-ng >=12 - license: GPL-2.0-only - license_family: GPL + - libgcc-ng >=9.4.0 + - libstdcxx-ng >=9.4.0 + license: MIT + license_family: MIT purls: [] - size: 6179024 - timestamp: 1687332729384 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/doxygen-1.9.7-h7b6a552_1.conda - sha256: cb4e2a628da54bf13d2decd9bbe982c611c216eb82b5ab826da59397492babd8 - md5: f619530bed063f8498eb2e15de71cf32 + run_exports: + weak: + - libunwind >=1.6.2,<1.7.0a0 + size: 75491 + timestamp: 1638450786937 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liburing-2.9-h84d6215_0.conda + sha256: bfa34a5a929d792dfcfbbe2d9ee21bd870d73d646512e21c871dab0b80194468 + md5: ecd409e7bfcf4ee73f74d7a2cc91a4c3 depends: - - libgcc-ng >=12 - - libiconv >=1.17,<2.0a0 - - libstdcxx-ng >=12 - license: GPL-2.0-only - license_family: GPL + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: MIT + license_family: MIT purls: [] - size: 5785379 - timestamp: 1687332318274 -- conda: https://conda.anaconda.org/conda-forge/osx-64/doxygen-1.9.7-hd7636e7_1.conda - sha256: b3a43f399a710dbfff7f0380d43db3c7155ae128af5f14a0a23ac51a48209123 - md5: 00ada1ebe41c7febae72032969017b09 + run_exports: + weak: + - liburing >=2.9,<2.10.0a0 + size: 121336 + timestamp: 1738604403935 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda + sha256: 89c84f5b26028a9d0f5c4014330703e7dff73ba0c98f90103e9cef6b43a5323c + md5: d17e3fb595a9f24fa9e149239a33475d depends: - - libcxx >=15.0.7 - - libiconv >=1.17,<2.0a0 - license: GPL-2.0-only - license_family: GPL + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libudev1 >=257.4 + license: LGPL-2.1-or-later purls: [] - size: 5344962 - timestamp: 1687332955991 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/doxygen-1.9.7-h0e2417a_1.conda - sha256: 4bfaf6721b163301135c2db1268b40a099f51e2a42fdec60262137c72e20b9eb - md5: 02c4969f0c780d47e3f95b43f18a8ad7 + run_exports: + weak: + - libusb >=1.0.29,<2.0a0 + size: 89551 + timestamp: 1748856210075 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + sha256: 9b1bdce27a7e31f7d241aeecff67a1f3101d52a2b1e33ccc2cdf2613072bf81f + md5: 01bb81d12c957de066ea7362007df642 depends: - - libcxx >=15.0.7 - - libiconv >=1.17,<2.0a0 - license: GPL-2.0-only - license_family: GPL + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 5103390 - timestamp: 1687332854077 -- conda: https://conda.anaconda.org/conda-forge/win-64/doxygen-1.9.7-h849606c_1.conda - sha256: b78b504b6c61a7a6252be49f2838c4788332332616fdd427f81adddc650b2520 - md5: 7c9a71d497a45a053fa85eeef616f936 + run_exports: + weak: + - libuuid >=2.42.2,<3.0a0 + size: 40017 + timestamp: 1781625522462 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_0.conda + sha256: e28e4519223f78b3163599ca89c3f2d80bfb53e907e7fc74e806e60d1efa578b + md5: 4e33d49bf4fc853855a3b00643aa5484 depends: - - libiconv >=1.17,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: GPL-2.0-only - license_family: GPL + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT purls: [] - size: 4861033 - timestamp: 1687333355663 -- conda: https://conda.anaconda.org/conda-forge/linux-64/fd-find-10.3.0-hdab8a38_0.conda - sha256: 55d3011ca72e1d97acc651b2af5d4d4d785988a8cfa9026205e9cf11f2d4ee67 - md5: 1b8aaa7bb23496abb0e23369db7fb5b7 + run_exports: + weak: + - libuv >=1.52.1,<2.0a0 + size: 419935 + timestamp: 1779396012261 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.24.1-he1eb515_0.conda + sha256: 16a76abbb4fd1de4516ac4a3d06cbf1f561bc8049ca72b04dcac395eee74d017 + md5: eb1b7f8bfdea40eef150c4a1d37df09e depends: - __glibc >=2.17,<3.0.a0 + - libdrm >=2.4.127,<2.5.0a0 + - libegl >=1.7.0,<2.0a0 - libgcc >=14 - constrains: - - __glibc >=2.17 + - libgl >=1.7.0,<2.0a0 + - libglx >=1.7.0,<2.0a0 + - libxcb >=1.17.0,<2.0a0 + - wayland >=1.25.0,<2.0a0 + - wayland-protocols + - xorg-libx11 >=1.8.13,<2.0a0 + - xorg-libxext >=1.3.7,<2.0a0 + - xorg-libxfixes >=6.0.2,<7.0a0 license: MIT license_family: MIT purls: [] - size: 1209421 - timestamp: 1757336717570 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fd-find-10.3.0-h1ebd7d5_0.conda - sha256: ef3af2c2e5e8c7646edbb1f261aaa1e4e9c3c1d66c71634e24913a3ed05a0dd8 - md5: d0c2b9916fe5497616c920589b23b8cc + run_exports: + weak: + - libva >=2.24.1,<3.0a0 + size: 222717 + timestamp: 1783519315031 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h54a6638_2.conda + sha256: ca494c99c7e5ecc1b4cd2f72b5584cef3d4ce631d23511184411abcbb90a21a5 + md5: b4ecbefe517ed0157c37f8182768271c depends: + - libogg - libgcc >=14 - constrains: - - __glibc >=2.17 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - libogg >=1.3.5,<1.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libvorbis >=1.3.7,<1.4.0a0 + size: 285894 + timestamp: 1753879378005 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libvpl-2.15.0-h54a6638_1.conda + sha256: bf0010d93f5b154c59bd9d3cc32168698c1d24f2904729f4693917cce5b27a9f + md5: a41a299c157cc6d0eff05e5fc298cc45 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - intel-media-driver >=25.3.3,<25.4.0a0 + - libva >=2.22.0,<3.0a0 license: MIT license_family: MIT purls: [] - size: 1122159 - timestamp: 1757336712645 -- conda: https://conda.anaconda.org/conda-forge/osx-64/fd-find-10.3.0-hb440939_0.conda - sha256: 816f5945ebe66b1ffedd24989922e60f6a94868958e3b25de2bdffb0e945fb8a - md5: f9c39bfe215fcf09d2173d55c4213915 + run_exports: + weak: + - libvpl >=2.15.0,<2.16.0a0 + size: 287944 + timestamp: 1757278954789 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda + sha256: e7d2daf409c807be48310fcc8924e481b62988143f582eb3a58c5523a6763b13 + md5: cde393f461e0c169d9ffb2fc70f81c33 depends: - - __osx >=10.13 - constrains: - - __osx >=10.13 - license: MIT - license_family: MIT + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1132595 - timestamp: 1757336899061 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fd-find-10.3.0-h0ca00b2_0.conda - sha256: 87360775e2416402e00f386855d0a6d68e9e94db9016f00fc0ebf99e5c71f92a - md5: 7e2ef0657717cee5e385cd5ab26e0365 + run_exports: + weak: + - libvpx >=1.14.1,<1.15.0a0 + size: 1022466 + timestamp: 1717859935011 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b + md5: aea31d2e5b1091feca96fcfe945c3cf9 depends: - - __osx >=11.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 constrains: - - __osx >=11.0 - license: MIT - license_family: MIT + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1050638 - timestamp: 1757337263602 -- conda: https://conda.anaconda.org/conda-forge/win-64/fd-find-10.3.0-h77a83cd_0.conda - sha256: 5c5165853630b8473f0963c1d3018e439e4f90f85443c5d0d00e6ec45457774a - md5: 01b79e8a4cf41a31737ffbbda6a25aab + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 + size: 429011 + timestamp: 1752159441324 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa + md5: 92ed62436b625154323d40d5f2f11dd7 depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp license: MIT license_family: MIT purls: [] - size: 1196708 - timestamp: 1757337405047 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-7.1.1-gpl_ha0aeed6_910.conda - sha256: cb2453b75759813beb3ca1af8cc134b7b5ae3580a43745964f61d921ad3f591a - md5: 983afde30790eeb90054f0838fabaff2 + run_exports: + weak: + - libxcb >=1.17.0,<2.0a0 + size: 395888 + timestamp: 1727278577118 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc depends: - - __glibc >=2.17,<3.0.a0 - - alsa-lib >=1.2.14,<1.3.0a0 - - aom >=3.9.1,<3.10.0a0 - - bzip2 >=1.0.8,<2.0a0 - - dav1d >=1.2.1,<1.2.2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - gmp >=6.3.0,<7.0a0 - - harfbuzz >=11.4.5 - - lame >=3.100,<3.101.0a0 - - libass >=0.17.4,<0.17.5.0a0 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libgcc >=14 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libopenvino >=2025.2.0,<2025.2.1.0a0 - - libopenvino-auto-batch-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-auto-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-hetero-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-intel-cpu-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-intel-gpu-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-intel-npu-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 - - libopus >=1.5.2,<2.0a0 - - librsvg >=2.58.4,<3.0a0 - - libstdcxx >=14 - - libva >=2.22.0,<3.0a0 - - libvorbis >=1.3.7,<1.4.0a0 - - libvpl >=2.15.0,<2.16.0a0 - - libvpx >=1.14.1,<1.15.0a0 - - libxcb >=1.17.0,<2.0a0 - - libxml2 >=2.13.8,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - openh264 >=2.6.0,<2.6.1.0a0 - - openssl >=3.5.2,<4.0a0 - - pulseaudio-client >=17.0,<17.1.0a0 - - sdl2 >=2.32.54,<3.0a0 - - svt-av1 >=3.1.2,<3.1.3.0a0 - - x264 >=1!164.3095,<1!165 - - x265 >=3.5,<3.6.0a0 - - xorg-libx11 >=1.8.12,<2.0a0 - constrains: - - __cuda >=12.8 - license: GPL-2.0-or-later - license_family: GPL + - libgcc-ng >=12 + license: LGPL-2.1-or-later purls: [] - size: 10543003 - timestamp: 1757215060681 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ffmpeg-7.1.1-gpl_h8d881e6_910.conda - sha256: 3b9e3373977e49add71c770b386ddeabbb5f6c43ab4837790f5c9011a5ad050d - md5: a375807e930c22669ae4250745a5c71a + run_exports: + weak: + - libxcrypt >=4.4.36 + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda + sha256: 23f47e86cc1386e7f815fa9662ccedae151471862e971ea511c5c886aa723a54 + md5: 74e91c36d0eef3557915c68b6c2bef96 depends: - - alsa-lib >=1.2.14,<1.3.0a0 - - aom >=3.9.1,<3.10.0a0 - - bzip2 >=1.0.8,<2.0a0 - - dav1d >=1.2.1,<1.2.2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - gmp >=6.3.0,<7.0a0 - - harfbuzz >=11.4.5 - - lame >=3.100,<3.101.0a0 - - libass >=0.17.4,<0.17.5.0a0 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 + - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libopenvino >=2025.2.0,<2025.2.1.0a0 - - libopenvino-arm-cpu-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-auto-batch-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-auto-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-hetero-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 - - libopus >=1.5.2,<2.0a0 - - librsvg >=2.58.4,<3.0a0 - libstdcxx >=14 - - libvorbis >=1.3.7,<1.4.0a0 - - libvpx >=1.14.1,<1.15.0a0 - libxcb >=1.17.0,<2.0a0 - libxml2 >=2.13.8,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - openh264 >=2.6.0,<2.6.1.0a0 - - openssl >=3.5.2,<4.0a0 - - pulseaudio-client >=17.0,<17.1.0a0 - - sdl2 >=2.32.54,<3.0a0 - - svt-av1 >=3.1.2,<3.1.3.0a0 - - x264 >=1!164.3095,<1!165 - - x265 >=3.5,<3.6.0a0 - - xorg-libx11 >=1.8.12,<2.0a0 - constrains: - - __cuda >=12.8 - license: GPL-2.0-or-later - license_family: GPL - purls: [] - size: 10195859 - timestamp: 1757215115776 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-7.1.1-gpl_hf226373_110.conda - sha256: 167c459251ecd586be917042df0432e6c90c115f881231af962f5c35fd35c8f7 - md5: b63b503d159f1eb6c9d98587c65c59b3 - depends: - - __osx >=10.13 - - aom >=3.9.1,<3.10.0a0 - - bzip2 >=1.0.8,<2.0a0 - - dav1d >=1.2.1,<1.2.2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - gmp >=6.3.0,<7.0a0 - - harfbuzz >=11.4.5 - - lame >=3.100,<3.101.0a0 - - libass >=0.17.4,<0.17.5.0a0 - - libcxx >=19 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libopenvino >=2025.2.0,<2025.2.1.0a0 - - libopenvino-auto-batch-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-auto-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-hetero-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-intel-cpu-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 - - libopus >=1.5.2,<2.0a0 - - librsvg >=2.58.4,<3.0a0 - - libvorbis >=1.3.7,<1.4.0a0 - - libvpx >=1.14.1,<1.15.0a0 - - libxml2 >=2.13.8,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - openh264 >=2.6.0,<2.6.1.0a0 - - openssl >=3.5.2,<4.0a0 - - sdl2 >=2.32.54,<3.0a0 - - svt-av1 >=3.1.2,<3.1.3.0a0 - - x264 >=1!164.3095,<1!165 - - x265 >=3.5,<3.6.0a0 - license: GPL-2.0-or-later - license_family: GPL - purls: [] - size: 10215471 - timestamp: 1757215303226 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ffmpeg-7.1.1-gpl_h93d53e2_110.conda - sha256: 68eae35a62e36844aaa4c246c65977403e7a46415a1c4785577154fedba5ec63 - md5: 8adffbcfe629e5817d3921718fd42e7d - depends: - - __osx >=11.0 - - aom >=3.9.1,<3.10.0a0 - - bzip2 >=1.0.8,<2.0a0 - - dav1d >=1.2.1,<1.2.2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - gmp >=6.3.0,<7.0a0 - - harfbuzz >=11.4.5 - - lame >=3.100,<3.101.0a0 - - libass >=0.17.4,<0.17.5.0a0 - - libcxx >=19 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libopenvino >=2025.2.0,<2025.2.1.0a0 - - libopenvino-arm-cpu-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-auto-batch-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-auto-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-hetero-plugin >=2025.2.0,<2025.2.1.0a0 - - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 - - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 - - libopus >=1.5.2,<2.0a0 - - librsvg >=2.58.4,<3.0a0 - - libvorbis >=1.3.7,<1.4.0a0 - - libvpx >=1.14.1,<1.15.0a0 - - libxml2 >=2.13.8,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - openh264 >=2.6.0,<2.6.1.0a0 - - openssl >=3.5.2,<4.0a0 - - sdl2 >=2.32.54,<3.0a0 - - svt-av1 >=3.1.2,<3.1.3.0a0 - - x264 >=1!164.3095,<1!165 - - x265 >=3.5,<3.6.0a0 - license: GPL-2.0-or-later - license_family: GPL + - xkeyboard-config + - xorg-libxau >=1.0.12,<2.0a0 + license: MIT/X11 Derivative + license_family: MIT purls: [] - size: 9159034 - timestamp: 1757215368356 -- conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-7.1.1-gpl_h70aa942_910.conda - sha256: 49d38240ff7bfde5c53d6ae20c98ee65b82b1d0d8e1dcb5e2515de839b8678f3 - md5: 35d77007b30682debfbf97ad6cebbbda + run_exports: + weak: + - libxkbcommon >=1.11.0,<2.0a0 + size: 791328 + timestamp: 1754703902365 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.9-h04c0eec_0.conda + sha256: 5d12e993894cb8e9f209e2e6bef9c90fa2b7a339a1f2ab133014b71db81f5d88 + md5: 35eeb0a2add53b1e50218ed230fa6a02 depends: - - aom >=3.9.1,<3.10.0a0 - - bzip2 >=1.0.8,<2.0a0 - - dav1d >=1.2.1,<1.2.2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - harfbuzz >=11.4.5 - - lame >=3.100,<3.101.0a0 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 + - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 + - libgcc >=14 - libiconv >=1.18,<2.0a0 - liblzma >=5.8.1,<6.0a0 - - libopus >=1.5.2,<2.0a0 - - librsvg >=2.58.4,<3.0a0 - - libvorbis >=1.3.7,<1.4.0a0 - - libxml2 >=2.13.8,<2.14.0a0 - libzlib >=1.3.1,<2.0a0 - - openh264 >=2.6.0,<2.6.1.0a0 - - openssl >=3.5.2,<4.0a0 - - sdl2 >=2.32.54,<3.0a0 - - svt-av1 >=3.1.2,<3.1.3.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - x264 >=1!164.3095,<1!165 - - x265 >=3.5,<3.6.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libxml2 >=2.13.9,<2.14.0a0 + size: 697033 + timestamp: 1761766011241 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec + depends: + - __glibc >=2.17,<3.0.a0 constrains: - - __cuda >=12.8 - license: GPL-2.0-or-later - license_family: GPL + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other purls: [] - size: 10027541 - timestamp: 1757216486092 -- pypi: https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl - name: filelock - version: 3.20.2 - sha256: fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8 - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/flatbuffers-25.2.10-hb7832b1_0.conda - sha256: 0e58114d0e16bc89b94ef9068558e304d2eccae5dbaa55b955274ea60da81dfd - md5: 279ba9719d1afc81538d8260f31e42a0 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 63629 + timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lychee-0.23.0-he64ecbb_0.conda + sha256: 0b1bc4b4a8fde5bf474f5b63c64fe356b3f47034f1d485fedd630b2d17de8fb5 + md5: d89182800d61d9e4922f4f338cd28362 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - license: Apache-2.0 - license_family: APACHE + - libgcc >=14 + - openssl >=3.5.5,<4.0a0 + constrains: + - __glibc >=2.17 + license: Apache-2.0 OR MIT purls: [] - size: 1539958 - timestamp: 1747130572350 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/flatbuffers-25.2.10-ha90f286_0.conda - sha256: 0d802dd9a8b804521a25ee21423a674d73d5ac6cecc2faae4264b5286f9d2deb - md5: 2093f2029d159ec0dc522f42990c0bd2 + run_exports: {} + size: 5582609 + timestamp: 1771270353931 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + sha256: 47326f811392a5fd3055f0f773036c392d26fdb32e4d8e7a8197eed951489346 + md5: 9de5350a85c4a20c685259b889aa6393 depends: + - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 - license: Apache-2.0 - license_family: APACHE + license: BSD-2-Clause + license_family: BSD purls: [] - size: 1380724 - timestamp: 1747130553663 -- conda: https://conda.anaconda.org/conda-forge/osx-64/flatbuffers-25.2.10-h2cf7b43_0.conda - sha256: eb6be3a3db53cb53f9300f08cfd6579549787e6ec45007d589f4629fec1b9a42 - md5: 109d4025e003f228844a06f246503177 + run_exports: + weak: + - lz4-c >=1.10.0,<1.11.0a0 + size: 167055 + timestamp: 1733741040117 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py311h3778330_1.conda + sha256: 710e207b2e91308a34bcfe547c60ad86c1fa294827266ba18548c1fe1a9d8333 + md5: f9efdf9b0f3d0cc309d56af6edf2a6b0 depends: - - __osx >=10.13 - - libcxx >=18 - license: Apache-2.0 - license_family: APACHE + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + run_exports: {} + size: 26756 + timestamp: 1772445078834 +- conda: https://conda.anaconda.org/conda-forge/linux-64/meilisearch-1.5.1-he8a937b_0.conda + sha256: 233f9c2e3c83e2b27a7915cd21433c7f2566971470ec8f2f416cf298b9b73d97 + md5: d648052889e66626c93825ce8ee1d6f2 + depends: + - libgcc-ng >=12 + license: MIT + license_family: MIT purls: [] - size: 1337567 - timestamp: 1747130405020 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/flatbuffers-25.2.10-h3144c11_0.conda - sha256: d339e7b15c6a927b6ecdb27513d001ab037e3d4bb146fa498e330cbec0cdf9fe - md5: 87c66c4a31165b25b9f56da755197a64 + run_exports: {} + size: 83512382 + timestamp: 1702682895721 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda + sha256: 39c4700fb3fbe403a77d8cc27352fa72ba744db487559d5d44bf8411bb4ea200 + md5: c7f302fd11eeb0987a6a5e1f3aed6a21 depends: - - __osx >=11.0 - - libcxx >=18 - license: Apache-2.0 - license_family: APACHE + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: LGPL-2.1-only + license_family: LGPL purls: [] - size: 1286290 - timestamp: 1747130536643 -- conda: https://conda.anaconda.org/conda-forge/win-64/flatbuffers-25.2.10-hc130f0a_0.conda - sha256: 8c26cca2271d99e8b723847c3a3a7e7de3f5f1908dbd1d2413e6b0b154b97d47 - md5: 29353e2ac55f6192b1a5bb0244021128 + run_exports: + weak: + - mpg123 >=1.32.9,<1.33.0a0 + size: 491140 + timestamp: 1730581373280 +- conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py311h3778330_0.conda + sha256: 9f3d7b8d3543f667a2a918e4ac401d98fde65c874e08eb201a41ac735f8d9797 + md5: 657ac3fca589a3da15a287868a146524 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 license: Apache-2.0 license_family: APACHE - purls: [] - size: 1753609 - timestamp: 1747130826577 -- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b - md5: 0c96522c6bdaed4b1566d11387caaf45 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 397370 - timestamp: 1566932522327 -- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c - md5: 34893075a5c9e55cdafac56607368fc6 - license: OFL-1.1 - license_family: Other - purls: [] - size: 96530 - timestamp: 1620479909603 -- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 - md5: 4d59c254e01d9cde7957100457e2d5fb - license: OFL-1.1 - license_family: Other - purls: [] - size: 700814 - timestamp: 1620479612257 -- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 - md5: 49023d73832ef61042f6a237cb2687e7 - license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 - license_family: Other - purls: [] - size: 1620504 - timestamp: 1727511233259 -- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda - sha256: 7093aa19d6df5ccb6ca50329ef8510c6acb6b0d8001191909397368b65b02113 - md5: 8f5b0b297b59e1ac160ad4beec99dbee + purls: + - pkg:pypi/multidict?source=hash-mapping + run_exports: {} + size: 100649 + timestamp: 1771610839808 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.14.1-py311h9ecbd09_0.conda + sha256: 583282ca209e9dc9f91e28bb4d47bbf31456c2d437a4b4bdc3b1684b916b6264 + md5: 2bf2e229fee8e7649a7567dc61156437 depends: - __glibc >=2.17,<3.0.a0 - - freetype >=2.12.1,<3.0a0 - - libexpat >=2.6.3,<3.0a0 - libgcc >=13 - - libuuid >=2.38.1,<3.0a0 - - libzlib >=1.3.1,<2.0a0 + - mypy_extensions >=1.0.0 + - psutil >=4.0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - typing_extensions >=4.1.0 license: MIT license_family: MIT - purls: [] - size: 265599 - timestamp: 1730283881107 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.15.0-h8dda3cd_1.conda - sha256: fe023bb8917c8a3138af86ef537b70c8c5d60c44f93946a87d1e8bb1a6634b55 - md5: 112b71b6af28b47c624bcbeefeea685b + purls: + - pkg:pypi/mypy?source=hash-mapping + run_exports: {} + size: 18730461 + timestamp: 1735601000085 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nasm-2.16.03-h4bc722e_1.conda + sha256: d01bfa655ad08d33dc5830a5166c7b664143df24fab59d41df15f076c58000b6 + md5: 35f8ab79609d5bc56d6d040f12dacf3a depends: - - freetype >=2.12.1,<3.0a0 - - libexpat >=2.6.3,<3.0a0 - - libgcc >=13 - - libuuid >=2.38.1,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 277832 - timestamp: 1730284967179 -- conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.15.0-h37eeddb_1.conda - sha256: 61a9aa1d2dd115ffc1ab372966dc8b1ac7b69870e6b1744641da276b31ea5c0b - md5: 84ccec5ee37eb03dd352db0a3f89ada3 + run_exports: {} + size: 1221519 + timestamp: 1721652638250 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 + md5: fc21868a1a5aacc937e7a18747acb8a5 depends: - - __osx >=10.13 - - freetype >=2.12.1,<3.0a0 - - libexpat >=2.6.3,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: X11 AND BSD-3-Clause purls: [] - size: 232313 - timestamp: 1730283983397 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda - sha256: f79d3d816fafbd6a2b0f75ebc3251a30d3294b08af9bb747194121f5efa364bc - md5: 7b29f48742cea5d1ccb5edd839cb5621 + run_exports: + weak: + - ncurses >=6.6,<7.0a0 + size: 918956 + timestamp: 1777422145199 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda + sha256: b555247ac8859b4ff311e3d708a0640f1bfe9fae7125c485b444072474a84c41 + md5: 73a4953a2d9c115bdc10ff30a52f675f depends: - - __osx >=11.0 - - freetype >=2.12.1,<3.0a0 - - libexpat >=2.6.3,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: Apache-2.0 + license_family: Apache purls: [] - size: 234227 - timestamp: 1730284037572 -- conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda - sha256: ed122fc858fb95768ca9ca77e73c8d9ddc21d4b2e13aaab5281e27593e840691 - md5: 9bb0026a2131b09404c59c4290c697cd + run_exports: {} + size: 2251263 + timestamp: 1676837602636 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-24.13.0-h36edbcc_0.conda + sha256: fb00d762a607cc76104b1d8ab76d631d48afde72e5e4ec81a1536be3a4cb19c8 + md5: 09e6207319af4800a2b7e352b39ec151 depends: - - freetype >=2.12.1,<3.0a0 - - libexpat >=2.6.3,<3.0a0 - - libiconv >=1.17,<2.0a0 + - libgcc >=14 + - __glibc >=2.28,<3.0.a0 + - libstdcxx >=14 + - libsqlite >=3.51.2,<4.0a0 - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - c-ares >=1.34.6,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - openssl >=3.5.5,<4.0a0 + - icu >=75.1,<76.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libuv >=1.51.0,<2.0a0 + - libnghttp2 >=1.67.0,<2.0a0 license: MIT license_family: MIT purls: [] - size: 192355 - timestamp: 1730284147944 -- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 - md5: fee5683a3f04bd15cbd8318b096a27ab - depends: - - fonts-conda-forge - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 3667 - timestamp: 1566974674465 -- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - sha256: 53f23a3319466053818540bcdf2091f253cbdbab1e0e9ae7b9e509dcaa2a5e38 - md5: f766549260d6815b0c52253f1fb1bb29 + run_exports: + weak: + - nodejs >=24.13.0,<25.0a0 + size: 17546184 + timestamp: 1770653299633 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.4-hb03c661_1.conda + sha256: 75f3bf733523a338f73d6c276c4a26634877cd970edb558f2769d9fa52b100a9 + md5: c2871ba95727fd1382c05db66048b64c depends: - - font-ttf-dejavu-sans-mono - - font-ttf-inconsolata - - font-ttf-source-code-pro - - font-ttf-ubuntu - license: BSD-3-Clause + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - opencl-headers >=2025.6.13 + license: BSD-2-Clause license_family: BSD purls: [] - size: 4102 - timestamp: 1566932280397 -- conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.0-ha770c72_1.conda - sha256: 57cc2f8ec88529c41afd494f853c1e439abb3a658387c92fc65aab85d2fa821e - md5: 01d8409cffb4cb37b5007f5c46ffa55b + run_exports: + weak: + - ocl-icd >=2.3.4,<3.0a0 + size: 109598 + timestamp: 1780362789611 +- conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-hecca717_0.conda + sha256: 8de2f0cd8a659b01abf86e7fbb8cea4f28ada62fd288429a2bbc040db1b98dd0 + md5: c930c8052d780caa41216af7de472226 depends: - - libfreetype 2.14.0 ha770c72_1 - - libfreetype6 2.14.0 h73754d4_1 - license: GPL-2.0-only OR FTL + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 173443 - timestamp: 1757461581149 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.0-h8af1aa0_1.conda - sha256: 3ba1831b852cb833f4901384dbbee02ab710174e6dbbe641f53993b554d5177d - md5: 61a80e18987f75b75a2fa58bc555c759 + run_exports: {} + size: 55754 + timestamp: 1773844383536 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-h65dd3cf_1.conda + sha256: 5317c5c23762f3fe1c8510565a2bb94c645e1470ff73b386315656404f7eb58a + md5: 69894a95220a17a66272daa701c387bc depends: - - libfreetype 2.14.0 h8af1aa0_1 - - libfreetype6 2.14.0 hdae7a39_1 - license: GPL-2.0-only OR FTL + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 172893 - timestamp: 1757517670259 -- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.0-h694c41f_1.conda - sha256: 57349f4844b3fc38c290e103f589b1ec529950b5aa66080f77da990c7e06bc46 - md5: 5ed7e552da1e055959dfeb862810911e + run_exports: + weak: + - openh264 >=2.6.0,<2.6.1.0a0 + size: 726478 + timestamp: 1782685945856 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + sha256: d48f5c22b9897c01e4dff3680f1f57ceb02711ab9c62f74339b080419dfad34b + md5: 79dd2074b5cd5c5c6b2930514a11e22d depends: - - libfreetype 2.14.0 h694c41f_1 - - libfreetype6 2.14.0 h6912278_1 - license: GPL-2.0-only OR FTL + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache purls: [] - size: 173793 - timestamp: 1757462072986 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.0-hce30654_1.conda - sha256: 119dd87c87362f7b80e4c74e3ae041ff995534fd6875a69ebd6ddfc8b4c51e32 - md5: 59ab8692a6f5c0188bb0876dd95acd96 + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 3159683 + timestamp: 1781069855778 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + sha256: 3613774ad27e48503a3a6a9d72017087ea70f1426f6e5541dbdb59a3b626eaaf + md5: 79f71230c069a287efe3a8614069ddf1 depends: - - libfreetype 2.14.0 hce30654_1 - - libfreetype6 2.14.0 h6da58f4_1 - license: GPL-2.0-only OR FTL + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libgcc >=13 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later purls: [] - size: 173800 - timestamp: 1757461911571 -- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.0-h57928b3_1.conda - sha256: 51f15d020ab0d6cae05f9403a30a6b04d1fa23993b595765eb98f993fb7bbe2e - md5: 73dff2f5c34b42abf41fc9ba084d0019 + run_exports: + weak: + - pango >=1.56.4,<2.0a0 + size: 455420 + timestamp: 1751292466873 +- conda: https://conda.anaconda.org/conda-forge/linux-64/patchelf-0.17.2-h58526e2_0.conda + sha256: eb355ac225be2f698e19dba4dcab7cb0748225677a9799e9cc8e4cadc3cb738f + md5: ba76a6a448819560b5f8b08a9c74f415 depends: - - libfreetype 2.14.0 h57928b3_1 - - libfreetype6 2.14.0 hdbac1cb_1 - license: GPL-2.0-only OR FTL + - libgcc-ng >=7.5.0 + - libstdcxx-ng >=7.5.0 + license: GPL-3.0-or-later + license_family: GPL purls: [] - size: 184608 - timestamp: 1757518017222 -- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda - sha256: 858283ff33d4c033f4971bf440cebff217d5552a5222ba994c49be990dacd40d - md5: f9f81ea472684d75b9dd8d0b328cf655 + run_exports: {} + size: 94048 + timestamp: 1673473024463 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda + sha256: 5e6f7d161356fefd981948bea5139c5aa0436767751a6930cb1ca801ebb113ff + md5: 7a3bff861a6583f1889021facefc08b1 depends: - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 - libgcc >=14 - license: LGPL-2.1-or-later + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 61244 - timestamp: 1757438574066 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda - sha256: 1bfcd715bcb49a0b22d5d1899a22c6ff884b06f8e141eb746f3949752469a422 - md5: f3ac54914f7d3e1d68cb8d891765e5f9 + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 + size: 1222481 + timestamp: 1763655398280 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_2.conda + sha256: 9e5b5be056820ade8b09ef73cf9f4bea037eb9887145d0297ac99e0add88878d + md5: 7cd77fef4da3e1ca9484394616cb71f1 depends: + - libstdcxx >=14 - libgcc >=14 - license: LGPL-2.1-or-later - purls: [] - size: 62909 - timestamp: 1757438620177 -- conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda - sha256: 53dd0a6c561cf31038633aaa0d52be05da1f24e86947f06c4e324606c72c7413 - md5: 4422491d30462506b9f2d554ab55e33d - depends: - - __osx >=10.13 - license: LGPL-2.1-or-later + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT purls: [] - size: 60923 - timestamp: 1757438791418 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda - sha256: d856dc6744ecfba78c5f7df3378f03a75c911aadac803fa2b41a583667b4b600 - md5: 04bdce8d93a4ed181d1d726163c2d447 + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 376220 + timestamp: 1784286827180 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pkgconf-3.0.3-h280c20c_0.conda + sha256: adf88a7b3416bacd43fa22c8464b19528e8292ba9b58cb00ac3a24bbd8e7479f + md5: d45809c4c618263ed32673bb4e3dc57e depends: - - __osx >=11.0 - license: LGPL-2.1-or-later + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT purls: [] - size: 59391 - timestamp: 1757438897523 -- conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda - sha256: 15011071ee56c216ffe276c8d734427f1f893f275ef733f728d13f610ed89e6e - md5: c27bd87e70f970010c1c6db104b88b18 + run_exports: + weak: + - pkgconf >=3.0.3,<4.0a0 + size: 137018 + timestamp: 1784088493420 +- conda: https://conda.anaconda.org/conda-forge/linux-64/prettier-3.8.1-h7e4c9f4_0.conda + sha256: 11889a9e414f7c35dc0de59ce195b9b73e65881d5d3bbe7e2e14a59fe47d702e + md5: 206c0500b835e80dfee8e3ad2e5776ce depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: LGPL-2.1-or-later + - nodejs + - __glibc >=2.17,<3.0.a0 + - nodejs >=24.12.0,<25.0a0 + license: MIT + license_family: MIT purls: [] - size: 64394 - timestamp: 1757438741305 -- conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.7.0-py311h52bc045_0.conda - sha256: cc7ec26db5d61078057da6e24e23abdd973414a065311fe0547a7620dd98e6b8 - md5: d9be554be03e3f2012655012314167d6 + run_exports: {} + size: 1104787 + timestamp: 1769199236423 +- conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.5.2-py311h3778330_0.conda + sha256: 4141ca7e55b09c4c24677112eef554a2ae220b26a3a25e30eb50e0984905b87c + md5: a7465a61562f01c2efd02d6af7b21ee7 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libstdcxx >=14 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/frozenlist?source=hash-mapping - size: 55258 - timestamp: 1752167340913 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.7.0-py311h91c1192_0.conda - sha256: 1e022a44bf00c99eda4ab2c997950f8ac72ffc1e177efb9013be0e1c6876de1d - md5: 283efb3474356970eaf5d479c02afaf1 + - pkg:pypi/propcache?source=compressed-mapping + run_exports: {} + size: 51401 + timestamp: 1780037772959 +- conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.31.1-py311h425ed32_2.conda + sha256: f5216cb89239542d39b9dfc9a757157f8c779e88a769c165e275da035b38cd02 + md5: 28ef5e67a2544510913d04a4a6dd9e12 depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 - libgcc >=14 - libstdcxx >=14 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/frozenlist?source=hash-mapping - size: 55559 - timestamp: 1752167410138 -- conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.7.0-py311h7a2b322_0.conda - sha256: ba999aa4f91a53d1104cf5aa78e318be3323936e5446a26ad1c5f59c85098b10 - md5: ad0e6d1df18292f15eab2dee54518d5c - depends: - - __osx >=10.13 - - libcxx >=19 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/frozenlist?source=hash-mapping - size: 50739 - timestamp: 1752167403997 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.7.0-py311h8740443_0.conda - sha256: b0b21e436d52d15cd29996ddbaa9eff04151b57330e35f436aab6ba303601ae8 - md5: e15cfa88d7671c12a25a574b63f63d9d - depends: - - __osx >=11.0 - - libcxx >=19 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/frozenlist?source=hash-mapping - size: 51115 - timestamp: 1752167450180 -- conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.7.0-py311hdf60d3a_0.conda - sha256: 1d26194d4c6b3c54caf06cebb37ba9f82f2e4a24f6152d9fa9af61b0b0e42509 - md5: ddb0b81f564d1a876c4c1964649d1127 - depends: + - libzlib >=1.3.1,<2.0a0 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/frozenlist?source=hash-mapping - size: 49827 - timestamp: 1752167413069 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.4.0-h236703b_2.conda - sha256: ebe2dabb0a6f0ef05039d3a26b9c6b0aa050d7e791c6ab77ee91653b2098cdc3 - md5: ec54d965fd9d276c256ae3cf1d3aface - depends: - - gcc_impl_linux-64 12.4.0.* - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 55424 - timestamp: 1740240489245 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc-12.4.0-h7e62973_2.conda - sha256: 62b7d45f5e8042890d7d6cacfdabaa0f2e5c9b8fe0f9b12d4f81fc078b66b347 - md5: e605824a02a81b3e3256636524c229d5 - depends: - - gcc_impl_linux-aarch64 12.4.0.* + constrains: + - libprotobuf 6.31.1 license: BSD-3-Clause license_family: BSD - purls: [] - size: 55373 - timestamp: 1740240463826 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.4.0-h26ba24d_2.conda - sha256: 635cd3d70ca6f4c3ad3f4b5837b5badb058f2416392592bd5914aa805f0bc28e - md5: f091c5ea6c862ab1796c82465a7c2364 - depends: - - binutils_impl_linux-64 >=2.40 - - libgcc >=12.4.0 - - libgcc-devel_linux-64 12.4.0 h1762d19_102 - - libgomp >=12.4.0 - - libsanitizer 12.4.0 ha732cd4_2 - - libstdcxx >=12.4.0 - - sysroot_linux-64 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 60389645 - timestamp: 1740240375167 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_impl_linux-aarch64-12.4.0-h628656a_2.conda - sha256: d5434b7ece8e6c3d65a65b67f2c5e8f3c2379f8677a7b2aed214b63082fb9b88 - md5: 2f7cb25395310fa69c251dea18769124 - depends: - - binutils_impl_linux-aarch64 >=2.40 - - libgcc >=12.4.0 - - libgcc-devel_linux-aarch64 12.4.0 h7b3af7c_102 - - libgomp >=12.4.0 - - libsanitizer 12.4.0 h469570c_2 - - libstdcxx >=12.4.0 - - sysroot_linux-aarch64 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 58914699 - timestamp: 1740240285252 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.4.0-h6b7512a_10.conda - sha256: 004d2ed6a3fc79452dec4c6cac556d0b26cf2457d33c4ace95beed4e6e832b55 - md5: 18432a261dca2bb05b45e60adee37d77 + purls: + - pkg:pypi/protobuf?source=hash-mapping + run_exports: {} + size: 486563 + timestamp: 1760393355981 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py311haee01d2_0.conda + sha256: 8d9325af538a8f56013e42bbb91a4dc6935aece34476e20bafacf6007b571e86 + md5: 2ed8f6fe8b51d8e19f7621941f7bb95f depends: - - binutils_linux-64 - - gcc_impl_linux-64 12.4.0.* - - sysroot_linux-64 + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.11.* *_cp311 license: BSD-3-Clause license_family: BSD - purls: [] - size: 32617 - timestamp: 1745040673228 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_linux-aarch64-12.4.0-heb3b579_10.conda - sha256: 1ff4bb3d09d84c42fb1f338c2f76f2ab4ea989e8469583c47ce4b1843a522523 - md5: aa8fc7586ec58fcc44e4b9f4895181fe + purls: + - pkg:pypi/psutil?source=hash-mapping + run_exports: {} + size: 231786 + timestamp: 1769678156460 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 + md5: b3c17d95b5a10c6e64a21fa17573e70e depends: - - binutils_linux-aarch64 - - gcc_impl_linux-aarch64 12.4.0.* - - sysroot_linux-aarch64 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT purls: [] - size: 32648 - timestamp: 1745040658439 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.0-h2b0a6b4_0.conda - sha256: 96f8f8056f135ab395ad86e6fc9878f24eddc2f15f708d5a5400d33a80af5a9a - md5: 2ebf437e1c9df5de32b86b3ac223d620 + run_exports: {} + size: 8252 + timestamp: 1726802366959 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda + sha256: 23c98a5000356e173568dc5c5770b53393879f946f3ace716bbdefac2a8b23d2 + md5: b11a4c6bf6f6f44e5e143f759ffa2087 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libglib >=2.86.0,<3.0a0 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libpng >=1.6.50,<1.7.0a0 - - libtiff >=4.7.0,<4.8.0a0 - license: LGPL-2.1-or-later - license_family: LGPL + - libgcc >=13 + - libstdcxx >=13 + license: MIT + license_family: MIT purls: [] - size: 580990 - timestamp: 1757428259101 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.0-h90308e0_0.conda - sha256: 9b6c25f862991ccb351f374ab29559b1fcc3c5cc1ab5e909b7153e07771d4be9 - md5: 185d59289352628b634449417325a8bd + run_exports: + weak: + - pugixml >=1.15,<1.16.0a0 + size: 118488 + timestamp: 1736601364156 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-h9a8bead_2.conda + sha256: 8a6729861c9813a756b0438c30bd271722fb3f239ded3afc3bf1cb03327a640e + md5: b6f21b1c925ee2f3f7fc37798c5988db depends: + - __glibc >=2.17,<3.0.a0 + - dbus >=1.16.2,<2.0a0 - libgcc >=14 - libglib >=2.86.0,<3.0a0 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libpng >=1.6.50,<1.7.0a0 - - libtiff >=4.7.0,<4.8.0a0 - license: LGPL-2.1-or-later - license_family: LGPL - purls: [] - size: 588866 - timestamp: 1757430057604 -- conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.0-h07555a4_0.conda - sha256: a7c5e6b1f3b25c65a168cb98092ab9756be0f492448213d12070c1d8191b9f0f - md5: 20374cd12eb2a5e55ee0a0a141eaa9f9 - depends: - - __osx >=10.13 - - libglib >=2.86.0,<3.0a0 - - libintl >=0.25.1,<1.0a0 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libpng >=1.6.50,<1.7.0a0 - - libtiff >=4.7.0,<4.8.0a0 - license: LGPL-2.1-or-later - license_family: LGPL - purls: [] - size: 549326 - timestamp: 1757428794308 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.0-h7542897_0.conda - sha256: 793be95fc20812fe67ff732519a26b55110c63268c36a1e841a3996de9c56bcd - md5: c33602d85700e22825832d8c0dd81c4a - depends: - - __osx >=11.0 - - libglib >=2.86.0,<3.0a0 - - libintl >=0.25.1,<1.0a0 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libpng >=1.6.50,<1.7.0a0 - - libtiff >=4.7.0,<4.8.0a0 - license: LGPL-2.1-or-later - license_family: LGPL - purls: [] - size: 543408 - timestamp: 1757429032638 -- conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.0-h1f5b9c4_0.conda - sha256: d38368ef87d768e6d27c8fd80431a34e37c105559659393a0377bcab9c07ca87 - md5: 3a78aa6974df3f835384726b459ac337 - depends: - - libglib >=2.86.0,<3.0a0 - - libintl >=0.22.5,<1.0a0 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libpng >=1.6.50,<1.7.0a0 - - libtiff >=4.7.0,<4.8.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - libiconv >=1.18,<2.0a0 + - libsndfile >=1.2.2,<1.3.0a0 + - libsystemd0 >=257.7 + - libxcb >=1.17.0,<2.0a0 + constrains: + - pulseaudio 17.0 *_2 license: LGPL-2.1-or-later license_family: LGPL purls: [] - size: 572200 - timestamp: 1757428729595 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.25.1-h3f43e3d_1.conda - sha256: cbfa8c80771d1842c2687f6016c5e200b52d4ca8f2cc119f6377f64f899ba4ff - md5: c42356557d7f2e37676e121515417e3b + run_exports: + weak: + - pulseaudio-client >=17.0,<17.1.0a0 + size: 761857 + timestamp: 1757472971364 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.15-h7508c33_1_cpython.conda + build_number: 1 + sha256: e830c8c69605674a997ee280d79c0f05ff5c1ed80ce3743678b2f663f410dfb9 + md5: fa29f621acaa9c0db5fd2c0ffc65312c depends: - __glibc >=2.17,<3.0.a0 - - gettext-tools 0.25.1 h3f43e3d_1 - - libasprintf 0.25.1 h3f43e3d_1 - - libasprintf-devel 0.25.1 h3f43e3d_1 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 - libgcc >=14 - - libgettextpo 0.25.1 h3f43e3d_1 - - libgettextpo-devel 0.25.1 h3f43e3d_1 - - libiconv >=1.18,<2.0a0 - - libstdcxx >=14 - license: LGPL-2.1-or-later AND GPL-3.0-or-later + - liblzma >=5.8.3,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libuuid >=2.42.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 purls: [] - size: 541357 - timestamp: 1753343006214 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-0.25.1-h5ad3122_0.conda - sha256: 510e7eba15e6ba71cd5a2ae403128d56b3bb990878c8110f3abc652f823b4af8 - md5: 1e99d353785a5302bce1a5a86d249b2b + run_exports: + weak: + - python_abi 3.11.* *_cp311 + noarch: + - python + size: 30907259 + timestamp: 1781149782225 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_1.conda + sha256: c9a6cd2c290d7c3d2b30ea34a0ccda30f770e8ddb2937871f2c404faf60d0050 + md5: a24add9a3bababee946f3bc1c829acfe depends: - - gettext-tools 0.25.1 h5ad3122_0 - - libasprintf 0.25.1 h5e0f5ae_0 - - libasprintf-devel 0.25.1 h5e0f5ae_0 - - libgcc >=13 - - libgettextpo 0.25.1 h5ad3122_0 - - libgettextpo-devel 0.25.1 h5ad3122_0 - - libstdcxx >=13 - license: LGPL-2.1-or-later AND GPL-3.0-or-later - purls: [] - size: 534760 - timestamp: 1751557634743 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-tools-0.25.1-h3f43e3d_1.conda - sha256: c792729288bdd94f21f25f80802d4c66957b4e00a57f7cb20513f07aadfaff06 - md5: a59c05d22bdcbb4e984bf0c021a2a02f + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + run_exports: {} + size: 206190 + timestamp: 1770223702917 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libiconv >=1.18,<2.0a0 - license: GPL-3.0-or-later + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only license_family: GPL purls: [] - size: 3644103 - timestamp: 1753342966311 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-tools-0.25.1-h5ad3122_0.conda - sha256: 7b03cc531c9c2d567eb81dffe9f5688c83fbcdfa4882eec3a2045ec43218806f - md5: 4215d91c0eaae5274a36a3f211898c91 + run_exports: + weak: + - readline >=8.3,<9.0a0 + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda + sha256: d5c73079c1dd2c2a313c3bfd81c73dbd066b7eb08d213778c8bff520091ae894 + md5: c1c9b02933fdb2cfb791d936c20e887e depends: + - __glibc >=2.17,<3.0.a0 - libgcc >=13 - license: GPL-3.0-or-later - license_family: GPL + license: MIT + license_family: MIT purls: [] - size: 3999301 - timestamp: 1751557600737 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gh-2.79.0-h76a2195_0.conda - sha256: 1538e838d01ce9f0fa59d48c38da9a9b7e2e81874f2eca787226fa2832f097fb - md5: 50d5937a4cb0a8d6069a5c5a582cead7 + run_exports: + weak: + - rhash >=1.4.6,<2.0a0 + size: 193775 + timestamp: 1748644872902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.6.3-py311h1baac5b_0.conda + sha256: ec844a6df82774ff6790e60ddf6137cac54ee5d41fe2226f0da2bad0ab84e1ae + md5: 0311b3d56cad517ab1a80fd4f9332e3f depends: + - python + - libgcc >=14 - __glibc >=2.17,<3.0.a0 - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 29783390 - timestamp: 1757448972192 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gh-2.79.0-h94b2740_0.conda - sha256: a055b5e9c7d21ef37c1e6bdece3fceff47eb83385a59e4dae4282cb2612b5cc2 - md5: 7057812ef0f3bb15ff488b41bcf7f7af - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 27462312 - timestamp: 1757452381981 -- conda: https://conda.anaconda.org/conda-forge/osx-64/gh-2.79.0-hfb6d0b5_0.conda - sha256: 5a57627d2883d77c83bb5780ce5eacb71853c476ca400200f055dec6446ccce7 - md5: b4105fa1e82ca8fee6f3a211c387bf1c - depends: - - __osx >=10.13 + - python_abi 3.11.* *_cp311 constrains: - - __osx>=10.12 - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 30233997 - timestamp: 1757449356394 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gh-2.79.0-h4e0460a_0.conda - sha256: 843b74ac7151883ed92b7ed7fb9a09974b657174c791cb60278dc04d925fdd39 - md5: 2a64d507d6e69619abf2ceecff0531ae - depends: - - __osx >=11.0 - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 28560952 - timestamp: 1757449619970 -- conda: https://conda.anaconda.org/conda-forge/win-64/gh-2.79.0-h36e2d1d_0.conda - sha256: 7b9bbdc7b0c7e88cd2154b60b1336cd3fccc0d04d3fd3a9a6541393da21b173c - md5: 210831f95301cdecfd8dbae8c2982024 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: Apache-2.0 - license_family: APACHE - purls: [] - size: 28961169 - timestamp: 1757449405981 -- pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - name: ghp-import - version: 2.1.0 - sha256: 8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 - requires_dist: - - python-dateutil>=2.8.1 - - twine ; extra == 'dev' - - markdown ; extra == 'dev' - - flake8 ; extra == 'dev' - - wheel ; extra == 'dev' -- conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - sha256: dbbec21a369872c8ebe23cb9a3b9d63638479ee30face165aa0fccc96e93eec3 - md5: 7c14f3706e099f8fcd47af2d494616cc - depends: - - python >=3.9 - - smmap >=3.0.1,<6 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/gitdb?source=hash-mapping - size: 53136 - timestamp: 1735887290843 -- conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda - sha256: 44d49cf04aa46769e2d8a3b2cb12c94ea5ca572f459a29c8545f68cbe277f65d - md5: 1c7086a72e284675506c76b05acbe8b6 - depends: - - python >=3.10 + - __glibc >=2.17 license: MIT license_family: MIT purls: - - pkg:pypi/gitignore-parser?source=hash-mapping - size: 12133 - timestamp: 1756163102170 -- conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.45-pyhff2d567_0.conda - sha256: 12df2c971e98f30f2a9bec8aa96ea23092717ace109d16815eeb4c095f181aa2 - md5: b91d463ea8be13bcbe644ae8bc99c39f + - pkg:pypi/rpds-py?source=compressed-mapping + run_exports: {} + size: 299898 + timestamp: 1782831307838 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.7-h7805a7d_1.conda + noarch: python + sha256: 2985cfff61368323db477c2a0d7f100a57f6cb34aafec51ae96b6fc409d9090f + md5: f5678c1a929d9efe3c2397675ae90a3c depends: - - gitdb >=4.0.1,<5 - - python >=3.9 - - typing_extensions >=3.10.0.2 - license: BSD-3-Clause - license_family: BSD + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT purls: - - pkg:pypi/gitpython?source=hash-mapping - size: 157875 - timestamp: 1753444241693 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda - sha256: 309cf4f04fec0c31b6771a5809a1909b4b3154a2208f52351e1ada006f4c750c - md5: c94a5994ef49749880a8139cf9afcbe1 - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: GPL-2.0-or-later OR LGPL-3.0-or-later - purls: [] - size: 460055 - timestamp: 1718980856608 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gmp-6.3.0-h0a1ffab_2.conda - sha256: a5e341cbf797c65d2477b27d99091393edbaa5178c7d69b7463bb105b0488e69 - md5: 7cbfb3a8bb1b78a7f5518654ac6725ad - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: GPL-2.0-or-later OR LGPL-3.0-or-later - purls: [] - size: 417323 - timestamp: 1718980707330 -- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda - sha256: 75aa5e7a875afdcf4903b7dc98577672a3dc17b528ac217b915f9528f93c85fc - md5: 427101d13f19c4974552a4e5b072eef1 - depends: - - __osx >=10.13 - - libcxx >=16 - license: GPL-2.0-or-later OR LGPL-3.0-or-later - purls: [] - size: 428919 - timestamp: 1718981041839 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gmp-6.3.0-h7bae524_2.conda - sha256: 76e222e072d61c840f64a44e0580c2503562b009090f55aa45053bf1ccb385dd - md5: eed7278dfbab727b56f2c0b64330814b - depends: - - __osx >=11.0 - - libcxx >=16 - license: GPL-2.0-or-later OR LGPL-3.0-or-later - purls: [] - size: 365188 - timestamp: 1718981343258 -- pypi: https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl - name: google-api-core - version: 2.28.1 - sha256: 4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c - requires_dist: - - googleapis-common-protos>=1.56.2,<2.0.0 - - protobuf>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0 - - proto-plus>=1.22.3,<2.0.0 - - proto-plus>=1.25.0,<2.0.0 ; python_full_version >= '3.13' - - google-auth>=2.14.1,<3.0.0 - - requests>=2.18.0,<3.0.0 - - importlib-metadata>=1.4 ; python_full_version < '3.8' - - google-auth[aiohttp]>=2.35.0,<3.0.0 ; extra == 'async-rest' - - grpcio>=1.33.2,<2.0.0 ; extra == 'grpc' - - grpcio>=1.49.1,<2.0.0 ; python_full_version >= '3.11' and extra == 'grpc' - - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' - - grpcio-status>=1.33.2,<2.0.0 ; extra == 'grpc' - - grpcio-status>=1.49.1,<2.0.0 ; python_full_version >= '3.11' and extra == 'grpc' - - grpcio-status>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' - - grpcio-gcp>=0.2.2,<1.0.0 ; extra == 'grpcgcp' - - grpcio-gcp>=0.2.2,<1.0.0 ; extra == 'grpcio-gcp' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl - name: google-auth - version: 2.47.0 - sha256: c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498 - requires_dist: - - pyasn1-modules>=0.2.1 - - rsa>=3.1.4,<5 - - cryptography>=38.0.3 ; extra == 'cryptography' - - aiohttp>=3.6.2,<4.0.0 ; extra == 'aiohttp' - - requests>=2.20.0,<3.0.0 ; extra == 'aiohttp' - - cryptography ; extra == 'enterprise-cert' - - pyopenssl ; extra == 'enterprise-cert' - - pyopenssl>=20.0.0 ; extra == 'pyopenssl' - - cryptography>=38.0.3 ; extra == 'pyopenssl' - - pyjwt>=2.0 ; extra == 'pyjwt' - - cryptography>=38.0.3 ; extra == 'pyjwt' - - pyu2f>=0.1.5 ; extra == 'reauth' - - requests>=2.20.0,<3.0.0 ; extra == 'requests' - - grpcio ; extra == 'testing' - - flask ; extra == 'testing' - - freezegun ; extra == 'testing' - - oauth2client ; extra == 'testing' - - pyjwt>=2.0 ; extra == 'testing' - - cryptography>=38.0.3 ; extra == 'testing' - - pytest ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-localserver ; extra == 'testing' - - pyopenssl>=20.0.0 ; extra == 'testing' - - cryptography>=38.0.3 ; extra == 'testing' - - pyu2f>=0.1.5 ; extra == 'testing' - - responses ; extra == 'testing' - - urllib3 ; extra == 'testing' - - packaging ; extra == 'testing' - - aiohttp>=3.6.2,<4.0.0 ; extra == 'testing' - - requests>=2.20.0,<3.0.0 ; extra == 'testing' - - aioresponses ; extra == 'testing' - - pytest-asyncio ; extra == 'testing' - - pyopenssl<24.3.0 ; extra == 'testing' - - aiohttp<3.10.0 ; extra == 'testing' - - urllib3 ; extra == 'urllib3' - - packaging ; extra == 'urllib3' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl - name: google-cloud-core - version: 2.5.0 - sha256: 67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc - requires_dist: - - google-api-core>=1.31.6,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0 - - google-auth>=1.25.0,<3.0.0 - - importlib-metadata>1.0.0 ; python_full_version < '3.8' - - grpcio>=1.38.0,<2.0.0 ; python_full_version < '3.14' and extra == 'grpc' - - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' - - grpcio-status>=1.38.0,<2.0.0 ; extra == 'grpc' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl - name: google-cloud-storage - version: 2.9.0 - sha256: 83a90447f23d5edd045e0037982c270302e3aeb45fc1288d2c2ca713d27bad94 - requires_dist: - - google-auth>=1.25.0,<3.0.dev0 - - google-api-core>=1.31.5,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0.dev0 - - google-cloud-core>=2.3.0,<3.0.dev0 - - google-resumable-media>=2.3.2 - - requests>=2.18.0,<3.0.0.dev0 - - protobuf<5.0.0.dev0 ; extra == 'protobuf' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz - name: google-crc32c - version: 1.8.0 - sha256: a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - name: google-crc32c - version: 1.8.0 - sha256: 17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl - name: google-crc32c - version: 1.8.0 - sha256: 71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - name: google-crc32c - version: 1.8.0 - sha256: 19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl - name: google-resumable-media - version: 2.8.0 - sha256: dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582 - requires_dist: - - google-crc32c>=1.0.0,<2.0.0 - - requests>=2.18.0,<3.0.0 ; extra == 'requests' - - aiohttp>=3.6.2,<4.0.0 ; extra == 'aiohttp' - - google-auth>=1.22.0,<2.0.0 ; extra == 'aiohttp' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl - name: googleapis-common-protos - version: 1.72.0 - sha256: 4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038 - requires_dist: - - protobuf>=3.20.2,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0 - - grpcio>=1.44.0,<2.0.0 ; extra == 'grpc' - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda - sha256: 25ba37da5c39697a77fce2c9a15e48cf0a84f1464ad2aafbe53d8357a9f6cc8c - md5: 2cd94587f3a401ae05e03a6caf09539d + - pkg:pypi/ruff?source=hash-mapping + run_exports: {} + size: 9220190 + timestamp: 1774012576023 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.54-h3f2d84a_0.conda + sha256: 7cd82ca1d1989de6ac28e72ba0bfaae1c055278f931b0c7ef51bb1abba3ddd2f + md5: 91f8537d64c4d52cbbb2910e8bd61bd2 depends: + - libgcc >=13 - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - license: LGPL-2.0-or-later - license_family: LGPL - purls: [] - size: 99596 - timestamp: 1755102025473 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda - sha256: c9b1781fe329e0b77c5addd741e58600f50bef39321cae75eba72f2f381374b7 - md5: 4aa540e9541cc9d6581ab23ff2043f13 - depends: - - libgcc >=14 - - libstdcxx >=14 - license: LGPL-2.0-or-later - license_family: LGPL - purls: [] - size: 102400 - timestamp: 1755102000043 -- conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.14-h21dd04a_2.conda - sha256: c356eb7a42775bd2bae243d9987436cd1a442be214b1580251bb7fdc136d804b - md5: ba63822087afc37e01bf44edcc2479f3 - depends: - - __osx >=10.13 - - libcxx >=19 - license: LGPL-2.0-or-later - license_family: LGPL - purls: [] - size: 85465 - timestamp: 1755102182985 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda - sha256: c507ae9989dbea7024aa6feaebb16cbf271faac67ac3f0342ef1ab747c20475d - md5: 0fc46fee39e88bbcf5835f71a9d9a209 - depends: - - __osx >=11.0 - - libcxx >=19 - license: LGPL-2.0-or-later - license_family: LGPL + - libstdcxx >=13 + - libgcc >=13 + - sdl3 >=3.2.10,<4.0a0 + - libgl >=1.7.0,<2.0a0 + - libegl >=1.7.0,<2.0a0 + license: Zlib purls: [] - size: 81202 - timestamp: 1755102333712 -- conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda - sha256: 5f1714b07252f885a62521b625898326ade6ca25fbc20727cfe9a88f68a54bfd - md5: b785694dd3ec77a011ccf0c24725382b + run_exports: + weak: + - sdl2 >=2.32.54,<3.0a0 + size: 587053 + timestamp: 1745799881584 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.14-he3e324a_0.conda + sha256: b55edbcbcbfc7cff671ef15b6a663b91cb2ca59ab285c283d02f29c51de59e9e + md5: a750ab1e94750185033ea96eadfc925d depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: LGPL-2.0-or-later - license_family: LGPL + - libstdcxx >=13 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libgl >=1.7.0,<2.0a0 + - dbus >=1.13.6,<2.0a0 + - libxkbcommon >=1.9.2,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - pulseaudio-client >=17.0,<17.1.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 + - libudev1 >=257.4 + - libunwind >=1.6.2,<1.7.0a0 + - wayland >=1.23.1,<2.0a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - libusb >=1.0.28,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - libdrm >=2.4.124,<2.5.0a0 + - xorg-libxscrnsaver >=1.2.4,<2.0a0 + - liburing >=2.9,<2.10.0a0 + - libegl >=1.7.0,<2.0a0 + license: Zlib purls: [] - size: 96336 - timestamp: 1755102441729 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx-12.4.0-h236703b_2.conda - sha256: 6c3ea9877dc6babf064bafacd9e67280072b676864c26e90cbfec52eaa32a60e - md5: 5735863174438abb776bd1fefccec00a + run_exports: + weak: + - sdl3 >=3.2.14,<4.0a0 + size: 1939690 + timestamp: 1747327532502 +- conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + sha256: 48f3f6a76c34b2cfe80de9ce7f2283ecb55d5ed47367ba91e8bb8104e12b8f11 + md5: 98b6c9dc80eb87b2519b97bcf7e578dd depends: - - gcc 12.4.0.* - - gxx_impl_linux-64 12.4.0.* + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 license: BSD-3-Clause license_family: BSD purls: [] - size: 54818 - timestamp: 1740240626426 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx-12.4.0-h7e62973_2.conda - sha256: f54f7ec55907e31bde2681256d7135215c4ee3f7dcf4d6aebbaebf17ef66efcb - md5: 37d28c3a8d6a9408b8c9b043e74500fa + run_exports: + weak: + - snappy >=1.2.2,<1.3.0a0 + size: 45829 + timestamp: 1762948049098 +- conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-3.1.2-hecca717_0.conda + sha256: 34e2e9c505cd25dba0a9311eb332381b15147cf599d972322a7c197aedfc8ce2 + md5: 9859766c658e78fec9afa4a54891d920 depends: - - gcc 12.4.0.* - - gxx_impl_linux-aarch64 12.4.0.* - license: BSD-3-Clause + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: BSD-2-Clause license_family: BSD purls: [] - size: 54875 - timestamp: 1740240579366 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.4.0-h3ff227c_2.conda - sha256: 548987d77c5d6d648c1166e9a1eb810032f25fb1d61692a0a5a072db126e5f3f - md5: 5f8ae076e514514aeeb0eb52dac2d55d - depends: - - gcc_impl_linux-64 12.4.0 h26ba24d_2 - - libstdcxx-devel_linux-64 12.4.0 h1762d19_102 - - sysroot_linux-64 - - tzdata - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 12720023 - timestamp: 1740240582818 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_impl_linux-aarch64-12.4.0-h0bf7a72_2.conda - sha256: 07edf2303b2816b8d23191c15f40bda6824f4b3f4ba4892d8c27afd0c923e069 - md5: aeaa0618193ad8aa23457cd15eabfd61 + run_exports: + weak: + - svt-av1 >=3.1.2,<3.1.3.0a0 + size: 2741200 + timestamp: 1756086702093 +- conda: https://conda.anaconda.org/conda-forge/linux-64/taplo-0.9.1-h1ff36dd_0.conda + sha256: 82b3528f63ae71e0158fdbf8b66e66f619cb70584c471f3d89a2ee6fd44ef20b + md5: 29207c9b716932300221e5acd0b310f7 depends: - - gcc_impl_linux-aarch64 12.4.0 h628656a_2 - - libstdcxx-devel_linux-aarch64 12.4.0 h7b3af7c_102 - - sysroot_linux-aarch64 - - tzdata - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libgcc-ng >=12 + - openssl >=3.2.1,<4.0a0 + license: MIT + license_family: MIT purls: [] - size: 11915546 - timestamp: 1740240545209 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.4.0-h8489865_10.conda - sha256: 6ea7b3957ace8960347069f032851a66755b785a5e34cd845c1b6b1e649b686e - md5: f01962bad75d6d68802a1eb56bb70478 + run_exports: {} + size: 3877123 + timestamp: 1710792099600 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2022.3.0-h8d10470_1.conda + sha256: 2e3238234ae094d5a5f7c559410ea8875351b6bac0d9d0e576bf64b732b8029e + md5: e3259be3341da4bc06c5b7a78c8bf1bd depends: - - binutils_linux-64 - - gcc_linux-64 12.4.0 h6b7512a_10 - - gxx_impl_linux-64 12.4.0.* - - sysroot_linux-64 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libhwloc >=2.12.1,<2.12.2.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 30953 - timestamp: 1745040691868 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_linux-aarch64-12.4.0-h3f57e68_10.conda - sha256: 19edef472580cef8c145ccb307dd71ed2b7c18ac86e43aafce356047ce0f8352 - md5: ba65e3da87da43ba05bed772c89d084d + run_exports: {} + size: 181262 + timestamp: 1762509955687 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda + build_number: 103 + sha256: 43624eab22f5f29df7d6ffe914cf442f28fd559b55b290906255492826e636e8 + md5: 48a1049e710857572fc2a832aa394d9f depends: - - binutils_linux-aarch64 - - gcc_linux-aarch64 12.4.0 heb3b579_10 - - gxx_impl_linux-aarch64 12.4.0.* - - sysroot_linux-aarch64 - license: BSD-3-Clause - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + constrains: + - xorg-libx11 >=1.8.13,<2.0a0 + license: TCL purls: [] - size: 30955 - timestamp: 1745040677759 -- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - name: h11 - version: 0.16.0 - sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 - md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3550916 + timestamp: 1784229071544 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.31-h4e94fc0_0.conda + noarch: python + sha256: 8af5eb756191050f9516b1db6600303628ddb674b8629ed06dd79065f5dc3046 + md5: 8664e2153bea060af6d021e91a4c057b depends: - - python >=3.10 - - hyperframe >=6.1,<7 - - hpack >=4.1,<5 - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - _python_abi3_support 1.* + - cpython >=3.10 + constrains: + - __glibc >=2.17 license: MIT license_family: MIT purls: - - pkg:pypi/h2?source=compressed-mapping - size: 95967 - timestamp: 1756364871835 -- conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.4.5-h15599e2_0.conda - sha256: 9d0d74858e8f8b76f6d3bf11a7390e6eb18eb743dd6e5fd7c4e9822634556f6d - md5: 1276ae4aa3832a449fcb4253c30da4bc + - pkg:pypi/ty?source=hash-mapping + run_exports: {} + size: 9529492 + timestamp: 1776273647001 +- conda: https://conda.anaconda.org/conda-forge/linux-64/typos-1.48.0-hb17b654_0.conda + sha256: b8cd6270cf71afa1ea5abb21c5ea65d9f4f0ec2614d3af3d0d2185011a1ff3b4 + md5: a66c11d121cd29ee901edb4eb50d1075 depends: - - __glibc >=2.17,<3.0.a0 - - cairo >=1.18.4,<2.0a0 - - graphite2 >=1.3.14,<2.0a0 - - icu >=75.1,<76.0a0 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - libgcc >=14 - - libglib >=2.84.3,<3.0a0 - - libstdcxx >=14 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + - __glibc >=2.17,<3.0.a0 + constrains: + - __glibc >=2.17 + license: MIT OR Apache-2.0 purls: [] - size: 2402438 - timestamp: 1756738217200 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-11.4.5-he4899c9_0.conda - sha256: 7d4eb1084ee222dc97739140bab304aeb4aa1b7f62ff7339f4e3c7e83f61010a - md5: f88ad660d20e7f4eb1c6dcda42ac8965 + run_exports: {} + size: 3366192 + timestamp: 1782859608728 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wasm-pack-0.15.0-hb17b654_0.conda + sha256: f8aed69714662b4da06ee0364d695792ae8d9397191ab441a8a6432dd61f5251 + md5: e528a0758c2dce2dfc8ea4fd0bcbb643 depends: - - cairo >=1.18.4,<2.0a0 - - graphite2 >=1.3.14,<2.0a0 - - icu >=75.1,<76.0a0 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 + - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libglib >=2.84.3,<3.0a0 - - libstdcxx >=14 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + constrains: + - __glibc >=2.17 + license: MIT OR Apache-2.0 purls: [] - size: 2096389 - timestamp: 1756742145636 -- conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-11.4.5-h0ffbb26_0.conda - sha256: 2b5e8af8a457af825360b0aef0b9641a675ea9b0e0945d1e469d8a0f3e1ddc06 - md5: 6dfe87116a746f3c2e93eec0df8386ec + run_exports: {} + size: 2198657 + timestamp: 1780752575057 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hd6090a7_0.conda + sha256: 6b9e182021ef3a64ab3bf788ebdab6de6775035612237c639ccafc941639eb13 + md5: b34c5559f45d8996e3bc0b6250a6cc84 depends: - - __osx >=10.13 - - cairo >=1.18.4,<2.0a0 - - graphite2 >=1.3.14,<2.0a0 - - icu >=75.1,<76.0a0 - - libcxx >=19 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libglib >=2.84.3,<3.0a0 - - libzlib >=1.3.1,<2.0a0 + - __glibc >=2.17,<3.0.a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - libstdcxx >=14 license: MIT license_family: MIT purls: [] - size: 1593280 - timestamp: 1756738433915 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-11.4.5-hf4e55d4_0.conda - sha256: 8106c2941f842dad81444bbc7f68b08b65c63adb5d0ba399d7180926a51f8829 - md5: 0938e21caccd8fd5b30527396f8aaa82 + run_exports: + weak: + - wayland >=1.26.0,<2.0a0 + size: 340543 + timestamp: 1784249169392 +- conda: https://conda.anaconda.org/conda-forge/linux-64/x264-1!164.3095-h166bdaf_2.tar.bz2 + sha256: 175315eb3d6ea1f64a6ce470be00fa2ee59980108f246d3072ab8b977cb048a5 + md5: 6c99772d483f566d59e25037fea2c4b1 depends: - - __osx >=11.0 - - cairo >=1.18.4,<2.0a0 - - graphite2 >=1.3.14,<2.0a0 - - icu >=75.1,<76.0a0 - - libcxx >=19 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libglib >=2.84.3,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + - libgcc-ng >=12 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 1551301 - timestamp: 1756738697245 -- conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-11.4.5-h5f2951f_0.conda - sha256: e1aaf8cf922cb7c7dabc12ddcad16c218b926c5e43d845288a4a8a0910df1b18 - md5: e9f9b4c46f6bc9b51adf57909b4d4652 + run_exports: + weak: + - x264 >=1!164.3095,<1!165 + size: 897548 + timestamp: 1660323080555 +- conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 + sha256: 76c7405bcf2af639971150f342550484efac18219c0203c5ee2e38b8956fe2a0 + md5: e7f6ed84d4623d52ee581325c1587a6b depends: - - cairo >=1.18.4,<2.0a0 - - graphite2 >=1.3.14,<2.0a0 - - icu >=75.1,<76.0a0 - - libexpat >=2.7.1,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libglib >=2.84.3,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - purls: [] - size: 1134542 - timestamp: 1756738659278 -- pypi: https://files.pythonhosted.org/packages/d4/7c/bbed5611b1cd7b0b42b2dadb0721d9ccfa4fa9d03abc05e0f57c85a319c6/hatch-1.16.2-py3-none-any.whl - name: hatch - version: 1.16.2 - sha256: 827eaf9813c63119f172b85975c5c27110a2306b07e5304c9d38527b0239052a - requires_dist: - - backports-zstd>=1.0.0 ; python_full_version < '3.14' - - click>=8.0.6 - - hatchling>=1.27.0 - - httpx>=0.22.0 - - hyperlink>=21.0.0 - - keyring>=23.5.0 - - packaging>=24.2 - - pexpect~=4.8 - - platformdirs>=2.5.0 - - pyproject-hooks - - rich>=11.2.0 - - shellingham>=1.4.0 - - tomli-w>=1.0 - - tomlkit>=0.11.1 - - userpath~=1.7 - - uv>=0.5.23 - - virtualenv>=20.26.6 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - name: hatchling - version: 1.28.0 - sha256: dc48722b68b3f4bbfa3ff618ca07cdea6750e7d03481289ffa8be1521d18a961 - requires_dist: - - packaging>=24.2 - - pathspec>=0.10.1 - - pluggy>=1.0.0 - - tomli>=1.2.2 ; python_full_version < '3.11' - - trove-classifiers - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba - md5: 0a802cb9888dd14eeefc611f05c40b6e + - libgcc-ng >=10.3.0 + - libstdcxx-ng >=10.3.0 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + run_exports: + weak: + - x265 >=3.5,<3.6.0a0 + size: 3357188 + timestamp: 1646609687141 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-h280c20c_0.conda + sha256: 3b04afd5d1a65d2d27ac2d49a63b01ab8bcd875776779ec63e337370ed38afdc + md5: b233b41be0bf210989d57160ed39b394 depends: - - python >=3.9 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - xorg-libx11 >=1.8.13,<2.0a0 license: MIT license_family: MIT - purls: - - pkg:pypi/hpack?source=hash-mapping - size: 30731 - timestamp: 1737618390337 -- pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - name: httpcore - version: 1.0.9 - sha256: 2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 - requires_dist: - - certifi - - h11>=0.16 - - anyio>=4.0,<5.0 ; extra == 'asyncio' - - h2>=3,<5 ; extra == 'http2' - - socksio==1.* ; extra == 'socks' - - trio>=0.22.0,<1.0 ; extra == 'trio' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl - name: httpx - version: 0.28.1 - sha256: d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - requires_dist: - - anyio - - certifi - - httpcore==1.* - - idna - - brotli ; platform_python_implementation == 'CPython' and extra == 'brotli' - - brotlicffi ; platform_python_implementation != 'CPython' and extra == 'brotli' - - click==8.* ; extra == 'cli' - - pygments==2.* ; extra == 'cli' - - rich>=10,<14 ; extra == 'cli' - - h2>=3,<5 ; extra == 'http2' - - socksio==1.* ; extra == 'socks' - - zstandard>=0.18.0 ; extra == 'zstd' - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 - md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + purls: [] + run_exports: {} + size: 441670 + timestamp: 1782027360439 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b + md5: fb901ff28063514abb6046c9ec2c4a45 depends: - - python >=3.9 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 license: MIT license_family: MIT - purls: - - pkg:pypi/hyperframe?source=hash-mapping - size: 17397 - timestamp: 1737618427549 -- pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl - name: hyperlink - version: 21.0.0 - sha256: e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4 - requires_dist: - - idna>=2.5 - - typing ; python_full_version < '3.5' - requires_python: '>=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' -- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e - md5: 8b189310083baabfb622af68fd9d3ae3 + purls: [] + run_exports: + weak: + - xorg-libice >=1.1.2,<2.0a0 + size: 58628 + timestamp: 1734227592886 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + sha256: 277841c43a39f738927145930ff963c5ce4c4dacf66637a3d95d802a64173250 + md5: 1c74ff8c35dcadf952a16f752ca5aa49 depends: - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - xorg-libice >=1.1.2,<2.0a0 license: MIT license_family: MIT purls: [] - size: 12129203 - timestamp: 1720853576813 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda - sha256: 813298f2e54ef087dbfc9cc2e56e08ded41de65cff34c639cc8ba4e27e4540c9 - md5: 268203e8b983fddb6412b36f2024e75c + run_exports: + weak: + - xorg-libsm >=1.2.6,<2.0a0 + size: 27590 + timestamp: 1741896361728 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_0.conda + sha256: 516d4060139dbb4de49a4dcdc6317a9353fb39ebd47789c14e6fe52de0deee42 + md5: 861fb6ccbc677bb9a9fb2468430b9c6a depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libxcb >=1.17.0,<2.0a0 license: MIT license_family: MIT purls: [] - size: 12282786 - timestamp: 1720853454991 -- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - sha256: 2e64307532f482a0929412976c8450c719d558ba20c0962832132fd0d07ba7a7 - md5: d68d48a3060eb5abdc1cdc8e2a3a5966 + run_exports: + weak: + - xorg-libx11 >=1.8.13,<2.0a0 + size: 839652 + timestamp: 1770819209719 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + sha256: 6bc6ab7a90a5d8ac94c7e300cc10beb0500eeba4b99822768ca2f2ef356f731b + md5: b2895afaf55bf96a8c8282a2e47a5de0 depends: - - __osx >=10.13 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 11761697 - timestamp: 1720853679409 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620 - md5: 5eb22c1d7b3fc4abb50d92d621583137 + run_exports: + weak: + - xorg-libxau >=1.0.12,<2.0a0 + size: 15321 + timestamp: 1762976464266 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + sha256: 832f538ade441b1eee863c8c91af9e69b356cd3e9e1350fff4fe36cc573fc91a + md5: 2ccd714aa2242315acaf0a67faea780b depends: - - __osx >=11.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 license: MIT license_family: MIT purls: [] - size: 11857802 - timestamp: 1720853997952 -- conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda - sha256: 1d04369a1860a1e9e371b9fc82dd0092b616adcf057d6c88371856669280e920 - md5: 8579b6bb8d18be7c0b27fb08adeeeb40 + run_exports: + weak: + - xorg-libxcursor >=1.2.3,<2.0a0 + size: 32533 + timestamp: 1730908305254 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + sha256: 25d255fb2eef929d21ff660a0c687d38a6d2ccfbcbf0cc6aa738b12af6e9d142 + md5: 1dafce8548e38671bea82e3f5c6ce22f depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 14544252 - timestamp: 1720853966338 -- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - sha256: d7a472c9fd479e2e8dcb83fb8d433fce971ea369d704ece380e876f9c3494e87 - md5: 39a4f67be3286c86d696df570b1201b7 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/idna?source=hash-mapping - size: 49765 - timestamp: 1733211921194 -- pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl - name: importlib-metadata - version: 8.7.1 - sha256: 5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 - requires_dist: - - zipp>=3.20 - - pytest>=6,!=8.1.* ; extra == 'test' - - packaging ; extra == 'test' - - pyfakefs ; extra == 'test' - - flufl-flake8 ; extra == 'test' - - pytest-perf>=0.9.2 ; extra == 'test' - - jaraco-test>=5.4 ; extra == 'test' - - sphinx>=3.5 ; extra == 'doc' - - jaraco-packaging>=9.3 ; extra == 'doc' - - rst-linker>=1.9 ; extra == 'doc' - - furo ; extra == 'doc' - - sphinx-lint ; extra == 'doc' - - jaraco-tidelift>=1.4 ; extra == 'doc' - - ipython ; extra == 'perf' - - pytest-checkdocs>=2.4 ; extra == 'check' - - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - pytest-cov ; extra == 'cover' - - pytest-enabler>=3.4 ; extra == 'enabler' - - pytest-mypy>=1.0.1 ; extra == 'type' - - mypy<1.19 ; platform_python_implementation == 'PyPy' and extra == 'type' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/intel-gmmlib-22.8.2-hb700be7_0.conda - sha256: 6bc45d77fb625cb9cd154cfb8c0783a3f21123dd9512b91439675c5f6163c29e - md5: 478edf896b4dfca175c27b052d76fbc2 + run_exports: + weak: + - xorg-libxdmcp >=1.1.5,<2.0a0 + size: 20591 + timestamp: 1762976546182 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-hb03c661_0.conda + sha256: 79c60fc6acfd3d713d6340d3b4e296836a0f8c51602327b32794625826bd052f + md5: 34e54f03dfea3e7a2dcf1453a85f1085 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libstdcxx >=14 + - xorg-libx11 >=1.8.12,<2.0a0 license: MIT license_family: MIT purls: [] - size: 999849 - timestamp: 1757639263833 -- conda: https://conda.anaconda.org/conda-forge/linux-64/intel-media-driver-25.3.4-hecca717_0.conda - sha256: 286679d4c175e8db2d047be766d1629f1ea5828bff9fe7e6aac2e6f0fad2b427 - md5: 7ae2034a0e2e24eb07468f1a50cdf0bb + run_exports: + weak: + - xorg-libxext >=1.3.7,<2.0a0 + size: 50326 + timestamp: 1769445253162 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda + sha256: 83c4c99d60b8784a611351220452a0a85b080668188dce5dfa394b723d7b64f4 + md5: ba231da7fccf9ea1e768caf5c7099b84 depends: - __glibc >=2.17,<3.0.a0 - - intel-gmmlib >=22.8.1,<23.0a0 - libgcc >=14 - - libstdcxx >=14 - - libva >=2.22.0,<3.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 license: MIT license_family: MIT purls: [] - size: 8424610 - timestamp: 1757591682198 -- pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl - name: jaraco-classes - version: 3.4.0 - sha256: f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 - requires_dist: - - more-itertools - - sphinx>=3.5 ; extra == 'docs' - - jaraco-packaging>=9.3 ; extra == 'docs' - - rst-linker>=1.9 ; extra == 'docs' - - furo ; extra == 'docs' - - sphinx-lint ; extra == 'docs' - - jaraco-tidelift>=1.4 ; extra == 'docs' - - pytest>=6 ; extra == 'testing' - - pytest-checkdocs>=2.4 ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-mypy ; extra == 'testing' - - pytest-enabler>=2.2 ; extra == 'testing' - - pytest-ruff>=0.2.1 ; extra == 'testing' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl - name: jaraco-context - version: 6.0.2 - sha256: 55fc21af4b4f9ca94aa643b6ee7fe13b1e4c01abf3aeb98ca4ad9c80b741c786 - requires_dist: - - backports-tarfile ; python_full_version < '3.12' - - pytest>=6,!=8.1.* ; extra == 'test' - - jaraco-test>=5.6.0 ; extra == 'test' - - portend ; extra == 'test' - - sphinx>=3.5 ; extra == 'doc' - - jaraco-packaging>=9.3 ; extra == 'doc' - - rst-linker>=1.9 ; extra == 'doc' - - furo ; extra == 'doc' - - sphinx-lint ; extra == 'doc' - - jaraco-tidelift>=1.4 ; extra == 'doc' - - pytest-checkdocs>=2.4 ; extra == 'check' - - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - pytest-cov ; extra == 'cover' - - pytest-enabler>=3.4 ; extra == 'enabler' - - pytest-mypy>=1.0.1 ; extra == 'type' - - mypy<1.19 ; platform_python_implementation == 'PyPy' and extra == 'type' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - name: jaraco-functools - version: 4.4.0 - sha256: 9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176 - requires_dist: - - more-itertools - - pytest>=6,!=8.1.* ; extra == 'test' - - jaraco-classes ; extra == 'test' - - sphinx>=3.5 ; extra == 'doc' - - jaraco-packaging>=9.3 ; extra == 'doc' - - rst-linker>=1.9 ; extra == 'doc' - - furo ; extra == 'doc' - - sphinx-lint ; extra == 'doc' - - jaraco-tidelift>=1.4 ; extra == 'doc' - - pytest-checkdocs>=2.4 ; extra == 'check' - - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - pytest-cov ; extra == 'cover' - - pytest-enabler>=3.4 ; extra == 'enabler' - - pytest-mypy>=1.0.1 ; extra == 'type' - - mypy<1.19 ; platform_python_implementation == 'PyPy' and extra == 'type' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - name: jeepney - version: 0.9.0 - sha256: 97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 - requires_dist: - - pytest ; extra == 'test' - - pytest-trio ; extra == 'test' - - pytest-asyncio>=0.17 ; extra == 'test' - - testpath ; extra == 'test' - - trio ; extra == 'test' - - async-timeout ; python_full_version < '3.11' and extra == 'test' - - trio ; extra == 'trio' - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - sha256: f1ac18b11637ddadc05642e8185a851c7fab5998c6f5470d716812fae943b2af - md5: 446bd6c8cb26050d528881df495ce646 + run_exports: + weak: + - xorg-libxfixes >=6.0.2,<7.0a0 + size: 20071 + timestamp: 1759282564045 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + sha256: 044c7b3153c224c6cedd4484dd91b389d2d7fd9c776ad0f4a34f099b3389f4a1 + md5: 96d57aba173e878a2089d5638016dc5e depends: - - markupsafe >=2.0 - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jinja2?source=hash-mapping - size: 112714 - timestamp: 1741263433881 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - sha256: ac377ef7762e49cb9c4f985f1281eeff471e9adc3402526eea78e6ac6589cf1d - md5: 341fd940c242cf33e832c0402face56f + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - xorg-libxrender >=0.9.12,<0.10.0a0 + size: 33005 + timestamp: 1734229037766 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda + sha256: 58e8fc1687534124832d22e102f098b5401173212ac69eb9fd96b16a3e2c8cb2 + md5: 303f7a0e9e0cd7d250bb6b952cecda90 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - xorg-libxscrnsaver >=1.2.4,<2.0a0 + size: 14412 + timestamp: 1727899730073 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.3-ha02ee65_0.conda + sha256: 2553fd3ec0a1020b2ca05ca10b0036a596cb0d4bf3645922fcf69dacce0e6679 + md5: 6a1b6af49a334e4e06b9f103367762bf + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - liblzma 5.8.3 hb03c661_0 + - liblzma-devel 5.8.3 hb03c661_0 + - xz-gpl-tools 5.8.3 ha02ee65_0 + - xz-tools 5.8.3 hb03c661_0 + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 24360 + timestamp: 1775825568523 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.3-ha02ee65_0.conda + sha256: 8f139666ea18dc8340a44a54056627dd4e89e242e8cd136ab2467d6dc2c192ba + md5: 8f5e2c6726c1339287a3c76a2c138ac7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - liblzma 5.8.3 hb03c661_0 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 34213 + timestamp: 1775825548743 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.3-hb03c661_0.conda + sha256: 162ebd76803464b8c8ebc7d45df32edf0ec717b3bf369a437ae3b0254f22dc2e + md5: b62b615caa60812640f24db3a8d0fc87 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - liblzma 5.8.3 hb03c661_0 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later + purls: [] + run_exports: {} + size: 95955 + timestamp: 1775825530484 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a depends: - - attrs >=22.2.0 - - jsonschema-specifications >=2023.3.6 - - python >=3.9 - - referencing >=0.28.4 - - rpds-py >=0.7.1 - - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 license: MIT license_family: MIT + purls: [] + run_exports: + weak: + - yaml >=0.2.5,<0.3.0a0 + size: 85189 + timestamp: 1753484064210 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.24.5-py311h3778330_0.conda + sha256: 46826f747feab7499ad15fa395c3ee99c51756ea77ffd859b69ac9aec16d4e93 + md5: 63c08dc16a8ef875ba32bbc2494f1108 + depends: + - __glibc >=2.17,<3.0.a0 + - idna >=2.0 + - libgcc >=14 + - multidict >=4.0 + - propcache >=0.2.1 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: Apache purls: - - pkg:pypi/jsonschema?source=hash-mapping - size: 81688 - timestamp: 1755595646123 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 - md5: 439cd0f567d697b20a8f45cb70a1005a + - pkg:pypi/yarl?source=compressed-mapping + run_exports: {} + size: 173395 + timestamp: 1784526608705 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zig-0.13.0-h97ab28e_4.conda + sha256: a16f2b0e9b0071e108a4282ff520e258f81d9556fc464c35fb861fe53ff69404 + md5: a48ae474571e9e7d587e2ce06fb25e75 depends: - - python >=3.10 - - referencing >=0.31.0 - - python + - __glibc >=2.28,<3.0.a0 + - libclang-cpp18.1 >=18.1.8,<18.2.0a0 + - libgcc >=13 + - libllvm18 >=18.1.8,<18.2.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.6,<1.6.0a0 license: MIT license_family: MIT - purls: - - pkg:pypi/jsonschema-specifications?source=hash-mapping - size: 19236 - timestamp: 1757335715225 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda - sha256: 56a7a7e907f15cca8c4f9b0c99488276d4cb10821d2d15df9245662184872e81 - md5: b7d89d860ebcda28a5303526cdee68ab + purls: [] + run_exports: {} + size: 24809472 + timestamp: 1729875565378 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda + sha256: d534a6518c2d8eccfa6579d75f665261484f0f2f7377b50402446a9433d46234 + md5: ca45bfd4871af957aaa5035593d5efd2 depends: - - __unix - - platformdirs >=2.5 - - python >=3.8 - - traitlets >=5.3 + - python + - cffi >=1.11 + - zstd >=1.5.7,<1.5.8.0a0 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.11.* *_cp311 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-core?source=hash-mapping - size: 59562 - timestamp: 1748333186063 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda - sha256: 928c2514c2974fda78447903217f01ca89a77eefedd46bf6a2fe97072df57e8d - md5: 324e60a0d3f39f268e899709575ea3cd + - pkg:pypi/zstandard?source=hash-mapping + run_exports: {} + size: 466893 + timestamp: 1762512695614 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 depends: - - __win - - cpython - - platformdirs >=2.5 - - python >=3.8 - - pywin32 >=300 - - traitlets >=5.3 + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/jupyter-core?source=hash-mapping - size: 59972 - timestamp: 1748333368923 -- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_8.conda - sha256: 305c22a251db227679343fd73bfde121e555d466af86e537847f4c8b9436be0d - md5: ff007ab0f0fdc53d245972bba8a6d40c - constrains: - - sysroot_linux-64 ==2.28 - license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later - license_family: GPL purls: [] - size: 1272697 - timestamp: 1752669126073 -- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_8.conda - sha256: 9d0a86bd0c52c39db8821405f6057bc984789d36e15e70fa5c697f8ba83c1a19 - md5: 2ab884dda7f1a08758fe12c32cc31d08 + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: a2527b1d81792a0ccd2c05850960df119c2b6d8f5fdec97f2db7d25dc23b1068 + md5: 468fd3bb9e1f671d36c2cbc677e56f1d + depends: + - libgomp >=7.5.0 constrains: - - sysroot_linux-aarch64 ==2.28 - license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later - license_family: GPL + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1244709 - timestamp: 1752669116535 -- pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - name: keyring - version: 25.7.0 - sha256: be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f - requires_dist: - - pywin32-ctypes>=0.2.0 ; sys_platform == 'win32' - - secretstorage>=3.2 ; sys_platform == 'linux' - - jeepney>=0.4.2 ; sys_platform == 'linux' - - importlib-metadata>=4.11.4 ; python_full_version < '3.12' - - jaraco-classes - - jaraco-functools - - jaraco-context - - pytest>=6,!=8.1.* ; extra == 'test' - - pyfakefs ; extra == 'test' - - sphinx>=3.5 ; extra == 'doc' - - jaraco-packaging>=9.3 ; extra == 'doc' - - rst-linker>=1.9 ; extra == 'doc' - - furo ; extra == 'doc' - - sphinx-lint ; extra == 'doc' - - jaraco-tidelift>=1.4 ; extra == 'doc' - - pytest-checkdocs>=2.4 ; extra == 'check' - - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - pytest-cov ; extra == 'cover' - - pytest-enabler>=3.4 ; extra == 'enabler' - - pytest-mypy>=1.0.1 ; extra == 'type' - - pygobject-stubs ; extra == 'type' - - shtab ; extra == 'type' - - types-pywin32 ; extra == 'type' - - shtab>=1.1.0 ; extra == 'completion' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 - md5: b38117a3c920364aff79f870c984b4a3 + run_exports: + strong: + - _openmp_mutex >=4.5 + size: 28926 + timestamp: 1770939656741 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.9.5-py311hcd402e7_0.conda + sha256: a385a27e4510a55d7094eca5a09cd11d3c1c35a91925e51acef47c85636cc440 + md5: e717043d9f39fb3a3a6dff8d085e5a4d depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - aiosignal >=1.1.2 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - libgcc-ng >=12 + - multidict >=4.5,<7.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + - yarl >=1.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/aiohttp?source=hash-mapping + run_exports: {} + size: 805564 + timestamp: 1713965086056 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/alsa-lib-1.2.16.1-he30d5cf_0.conda + sha256: 105e4c19cfa770affcb9a64b9d2451f406914cd09a67664009910869fa01a639 + md5: 5427b5dcb268bddf1a69c16d1cb77a47 + depends: + - libgcc >=14 license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 134088 - timestamp: 1754905959823 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda - sha256: 5ce830ca274b67de11a7075430a72020c1fb7d486161a82839be15c2b84e9988 - md5: e7df0aab10b9cbb73ab2a467ebfaf8c7 + run_exports: + weak: + - alsa-lib >=1.2.16.1,<1.3.0a0 + size: 621865 + timestamp: 1781522013595 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aom-3.9.1-hcccb83c_0.conda + sha256: ac438ce5d3d3673a9188b535fc7cda413b479f0d52536aeeac1bd82faa656ea0 + md5: cc744ac4efe5bcaa8cca51ff5b850df0 depends: - - libgcc >=13 - license: LGPL-2.1-or-later + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 129048 - timestamp: 1754906002667 -- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 - md5: 3f43953b7d3fb3aaa1d0d0723d91e368 + run_exports: + weak: + - aom >=3.9.1,<3.10.0a0 + size: 3250813 + timestamp: 1718551360260 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/attr-2.5.2-he30d5cf_1.conda + sha256: 35a0229a82e1131add6816316081667ad83aa2ede31041e156c4572d010deb9d + md5: 3cadbca708894f28398e5cce1ce7cf7e + depends: + - libattr 2.5.2 he30d5cf_1 + - libgcc >=14 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + run_exports: + weak: + - libattr >=2.5.2,<2.6.0a0 + size: 33708 + timestamp: 1773595939135 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binaryen-117-h2f0025b_0.conda + sha256: 3820ab878d1a20792271a37440da1d304b36e26effff6f302592d5098cefa496 + md5: 69f34782ba69df988531f13d6bcc4385 depends: - - keyutils >=1.6.1,<2.0a0 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - libgcc-ng >=12 - libstdcxx-ng >=12 - - openssl >=3.3.1,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 5372762 + timestamp: 1710444374732 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils-2.46.1-default_hf1166c9_102.conda + sha256: 548d128bc257badd2d9dbdff8173eb693fcbec84ceeb0fa74cbfed141ed42ad9 + md5: 7a2f1cdac4c9fd2e87533ff2d2dcb702 + depends: + - binutils_impl_linux-aarch64 >=2.46.1,<2.46.2.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + run_exports: {} + size: 35377 + timestamp: 1784214574154 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.46.1-default_h5f4c503_102.conda + sha256: eebe159bf600943552e4319ff4ce27b6a2dadf0dcc5443e76dcee9259a408e11 + md5: 58f37d76b8234c69dbf2939d511bea0b + depends: + - ld_impl_linux-aarch64 2.46.1 default_h1979696_102 + - sysroot_linux-aarch64 + - zstd >=1.5.7,<1.6.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + run_exports: {} + size: 4677171 + timestamp: 1784214549910 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_linux-aarch64-2.46.1-default_hf1166c9_102.conda + sha256: d41099a3fe2809a3ab3e4c718ecff4d5f8879e7949f3c8e8efa97fa06a90248c + md5: 9a340123cb7217eae51c5cae24484631 + depends: + - binutils_impl_linux-aarch64 2.46.1 default_h5f4c503_102 + license: GPL-3.0-only + license_family: GPL + purls: [] + run_exports: {} + size: 36196 + timestamp: 1784214578949 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py311h14a79a7_1.conda + sha256: bf73f124e8dd683c5f414b9bea077246fcdec3f6c530bd83234b5eb329b52423 + md5: 292e7c014bfab5c77a2ff9c92728bb50 + depends: + - libgcc >=14 + - libstdcxx >=14 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + constrains: + - libbrotlicommon 1.2.0 he30d5cf_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + run_exports: {} + size: 373346 + timestamp: 1764017600174 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + sha256: b3495077889dde6bb370938e7db82be545c73e8589696ad0843a32221520ad4c + md5: 840d8fc0d7b3209be93080bc20e07f2d + depends: + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 + size: 192412 + timestamp: 1771350241232 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.8-he30d5cf_0.conda + sha256: 24f78776eda069a6c1566fd6d65379e45747d0a7cce9f864686690806543c408 + md5: a8a5833ef43f9b31a4d548071ea5ba75 + depends: + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 1370023 - timestamp: 1719463201255 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda - sha256: 0ec272afcf7ea7fbf007e07a3b4678384b7da4047348107b2ae02630a570a815 - md5: 29c10432a2ca1472b53f299ffb2ffa37 + run_exports: + weak: + - c-ares >=1.34.8,<2.0a0 + size: 220677 + timestamp: 1784091035199 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-compiler-1.6.0-h31becfc_0.conda + sha256: 36bc9d1673939980e7692ccce27e677dd4477d4c727ea173ec4210605b73927d + md5: b98866e63b17433ea5921a826c93cb97 depends: - - keyutils >=1.6.1,<2.0a0 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - openssl >=3.3.1,<4.0a0 + - binutils + - gcc + - gcc_linux-aarch64 12.* + license: BSD + purls: [] + run_exports: {} + size: 6213 + timestamp: 1689097449087 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda + sha256: 37cfff940d2d02259afdab75eb2dbac42cf830adadee78d3733d160a1de2cc66 + md5: cd55953a67ec727db5dc32b167201aa6 + depends: + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libstdcxx >=13 + - libxcb >=1.17.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + - xorg-libice >=1.1.2,<2.0a0 + - xorg-libsm >=1.2.5,<2.0a0 + - xorg-libx11 >=1.8.11,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.1-only or MPL-1.1 + purls: [] + run_exports: + weak: + - cairo >=1.18.4,<2.0a0 + size: 966667 + timestamp: 1741554768968 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cargo-llvm-cov-0.8.7-h1ebd7d5_0.conda + sha256: 96463914f68926fdc41d13fcd5a2a4c1b0ea634855e7a8c5aaa2bf6e3ae6dc94 + md5: d11eaddabd09979a818b0c923fdb8853 + depends: + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: {} + size: 1220142 + timestamp: 1778642125918 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cargo-nextest-0.9.140-h069e38c_0.conda + sha256: 2532d1d3d0cf9500d85217781bc435c98f19a434b5a74913df792d11a81beb6f + md5: 9757e007ff71e5d5db1edb5cdcdb0900 + depends: + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 6935346 + timestamp: 1783308157665 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cargo-zigbuild-0.20.1-h069e38c_1.conda + sha256: 501e0e040fe896088325cf46f34ad0dce1d42ff48e5fd16695a018cffcf8c0f7 + md5: ea8657f0795e44fb0a19ee58d51298fe + depends: + - zig >=0.9.0 + - libgcc >=14 + constrains: + - __glibc >=2.17 license: MIT license_family: MIT purls: [] - size: 1474620 - timestamp: 1719463205834 -- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - sha256: 83b52685a4ce542772f0892a0f05764ac69d57187975579a0835ff255ae3ef9c - md5: d4765c524b1d91567886bde656fb514b + run_exports: {} + size: 1129151 + timestamp: 1753434923633 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.1.0-py311h460c349_0.conda + sha256: 3aa9627b8fa8e66f8224a97f060977af01b05ee5b5f37960395ae49fbf55457c + md5: 0a84b3abbe8e783800b564791914754a depends: - - __osx >=10.13 - - libcxx >=16 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - openssl >=3.3.1,<4.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 license: MIT license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + run_exports: {} + size: 332185 + timestamp: 1783425394422 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16-16.0.6-default_hf07bfb7_15.conda + sha256: 94710355171eeb54c62306bd7858c04a5b429382e972f254110bac822e1cc388 + md5: e8d44b518e1d877e99b85a2404ea7b6c + depends: + - libclang-cpp16 16.0.6 default_hf07bfb7_15 + - libgcc >=14 + - libllvm16 >=16.0.6,<16.1.0a0 + - libstdcxx >=14 + constrains: + - clang-tools 16.0.6 + - llvm-tools 16.0.6 + - clangdev 16.0.6 + - clangxx 16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 1185323 - timestamp: 1719463492984 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b - md5: c6dc8a0fdec13a0565936655c33069a1 + run_exports: {} + size: 780063 + timestamp: 1756169863023 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-16.0.6-default_h3935787_15.conda + sha256: 8bc5e189a65f25c3492604b1d096306d7d4abdaf49d760d992cb6dfba5208963 + md5: cc3c7361a42241d132da775e34628510 depends: - - __osx >=11.0 - - libcxx >=16 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - openssl >=3.3.1,<4.0a0 - license: MIT - license_family: MIT + - binutils_impl_linux-aarch64 + - clang-16 16.0.6 default_hf07bfb7_15 + - libgcc-devel_linux-aarch64 + - sysroot_linux-aarch64 + constrains: + - clang-tools 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 1155530 - timestamp: 1719463474401 -- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - sha256: 18e8b3430d7d232dad132f574268f56b3eb1a19431d6d5de8c53c29e6c18fa81 - md5: 31aec030344e962fbd7dbbbbd68e60a9 + run_exports: {} + size: 91765 + timestamp: 1756169905137 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16-16.0.6-default_hf07bfb7_15.conda + sha256: 791e0d76e5cfe0b67ceb92a8479463767b724a4bcc2fb3b47da8b8292a6b7e5e + md5: 4c46a862eacaaf54b3f51b3c384e352c depends: - - openssl >=3.3.1,<4.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libgcc >=14 + - libllvm16 >=16.0.6,<16.1.0a0 + - libstdcxx >=14 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 712034 - timestamp: 1719463874284 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lame-3.100-h166bdaf_1003.tar.bz2 - sha256: aad2a703b9d7b038c0f745b853c6bb5f122988fe1a7a096e0e606d9cbec4eaab - md5: a8832b479f93521a9e7b5b743803be51 + run_exports: {} + size: 132889 + timestamp: 1756170067259 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-format-16.0.6-default_hf07bfb7_15.conda + sha256: 73e4acdbdfbc75e01ee83ba8456dcfdc70132b76b0bb2918b3b15159fb50b27f + md5: 4719e45a61adee841186fe0c71e2acd0 depends: - - libgcc-ng >=12 - license: LGPL-2.0-only - license_family: LGPL + - clang-format-16 16.0.6 default_hf07bfb7_15 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libgcc >=14 + - libllvm16 >=16.0.6,<16.1.0a0 + - libstdcxx >=14 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 508258 - timestamp: 1664996250081 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lame-3.100-h4e544f5_1003.tar.bz2 - sha256: 2502904a42df6d94bd743f7b73915415391dd6d31d5f50cb57c0a54a108e7b0a - md5: ab05bcf82d8509b4243f07e93bada144 + run_exports: {} + size: 91926 + timestamp: 1756170098419 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/clang-tools-16.0.6-default_hf07bfb7_15.conda + sha256: a5761f23a044f8a5429fa7a17cd0240934fbd3e241fa2526682d4934ac36e23e + md5: 536fb2e63e6e5211067127498ceb8e7a depends: - - libgcc-ng >=12 - license: LGPL-2.0-only - license_family: LGPL + - clang-format 16.0.6 default_hf07bfb7_15 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libclang13 >=16.0.6 + - libgcc >=14 + - libllvm16 >=16.0.6,<16.1.0a0 + - libstdcxx >=14 + - libxml2 >=2.13.8,<2.14.0a0 + constrains: + - clangdev 16.0.6 + - clang 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 604863 - timestamp: 1664997611416 -- conda: https://conda.anaconda.org/conda-forge/osx-64/lame-3.100-hb7f2c08_1003.tar.bz2 - sha256: 0f943b08abb4c748d73207594321b53bad47eea3e7d06b6078e0f6c59ce6771e - md5: 3342b33c9a0921b22b767ed68ee25861 - license: LGPL-2.0-only - license_family: LGPL + run_exports: {} + size: 27244600 + timestamp: 1756170139674 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cmake-3.27.6-hef020d8_0.conda + sha256: 099e3d6deac7fc29251552f87b59ee7299582caf291a20de71107327a4aded57 + md5: e20b2e0185007a671ebbb72f4353d70b + depends: + - bzip2 >=1.0.8,<2.0a0 + - libcurl >=8.3.0,<9.0a0 + - libexpat >=2.5.0,<3.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + - libuv >=1.46.0,<2.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - ncurses >=6.4,<7.0a0 + - rhash >=1.4.4,<2.0a0 + - xz >=5.2.6,<6.0a0 + - zstd >=1.5.5,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 542681 - timestamp: 1664996421531 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lame-3.100-h1a8c8d9_1003.tar.bz2 - sha256: f40ce7324b2cf5338b766d4cdb8e0453e4156a4f83c2f31bbfff750785de304c - md5: bff0e851d66725f78dc2fd8b032ddb7e - license: LGPL-2.0-only - license_family: LGPL + run_exports: {} + size: 17776308 + timestamp: 1695269663260 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cxx-compiler-1.6.0-h2a328a1_0.conda + sha256: aebe297f355fb3a5101eb11a5233d94c3445d2f1bbf4c0d7e3ff88b98d399694 + md5: 3847c922cacfe5a3d7ee663ffde014a4 + depends: + - c-compiler 1.6.0 h31becfc_0 + - gxx + - gxx_linux-aarch64 12.* + license: BSD purls: [] - size: 528805 - timestamp: 1664996399305 -- conda: https://conda.anaconda.org/conda-forge/win-64/lame-3.100-hcfcfb64_1003.tar.bz2 - sha256: 824988a396b97bb9138823a1b3aabd8326e06da5834b3011253d72bb45fd3a88 - md5: d92e64077c44c9e32c72d4b5799d47e4 + run_exports: {} + size: 6220 + timestamp: 1689097451413 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dav1d-1.2.1-h31becfc_0.conda + sha256: 33fe66d025cf5bac7745196d1a3dd7a437abcf2dbce66043e9745218169f7e17 + md5: 6e5a87182d66b2d1328a96b61ca43a62 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vs2015_runtime >=14.29.30139 - license: LGPL-2.0-only - license_family: LGPL + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 570583 - timestamp: 1664996824680 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ld64-951.9-ha02d983_1.conda - sha256: 4a27102c8451ce30b3c2d90722826e8bd02e9bb3b92cd5afaa08c65bbe6447f5 - md5: 8991ffc3c5c410692d8740de4cb92849 + run_exports: + weak: + - dav1d >=1.2.1,<1.2.2.0a0 + size: 347363 + timestamp: 1685696690003 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-h70963c4_1.conda + sha256: 3af801577431af47c0b72a82bb93c654f03072dece0a2a6f92df8a6802f52a22 + md5: a4b6b82427d15f0489cef0df2d82f926 depends: - - ld64_osx-64 951.9 h3516399_1 - - libllvm16 >=16.0.6,<16.1.0a0 - constrains: - - cctools 1010.6.* - - cctools_osx-64 1010.6.* - license: APSL-2.0 - license_family: Other + - libstdcxx >=14 + - libgcc >=14 + - libglib >=2.86.2,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + license: AFL-2.1 OR GPL-2.0-or-later + purls: [] + run_exports: + weak: + - dbus >=1.16.2,<2.0a0 + size: 480416 + timestamp: 1764536098891 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/doxygen-1.9.7-h7b6a552_1.conda + sha256: cb4e2a628da54bf13d2decd9bbe982c611c216eb82b5ab826da59397492babd8 + md5: f619530bed063f8498eb2e15de71cf32 + depends: + - libgcc-ng >=12 + - libiconv >=1.17,<2.0a0 + - libstdcxx-ng >=12 + license: GPL-2.0-only + license_family: GPL purls: [] - size: 18850 - timestamp: 1726771680769 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64-951.9-h634c8be_1.conda - sha256: d347ecd273ea7552ae703a37650ea211ff640ed8fd921fe6f1ede49dcdc1358c - md5: 294a282b67deea1f0ea1c7d8be2bb5c5 + run_exports: {} + size: 5785379 + timestamp: 1687332318274 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fd-find-10.4.2-h1ebd7d5_0.conda + sha256: e875c95350dcd53ebf2ae46ae17c155a925fb5e88e3b51e891373154d65b05c2 + md5: 571107d5bfaaf5e18cb42e81a14cd6c9 depends: - - ld64_osx-arm64 951.9 h0605c9f_1 - - libllvm16 >=16.0.6,<16.1.0a0 + - libgcc >=14 constrains: - - cctools_osx-arm64 1010.6.* - - cctools 1010.6.* - license: APSL-2.0 - license_family: Other + - __glibc >=2.17 + license: MIT + license_family: MIT purls: [] - size: 18928 - timestamp: 1726771322773 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ld64_osx-64-951.9-h3516399_1.conda - sha256: 03417d5a379bf8e7b2ac99000d9af836cae53b843e02de7cea066c4ddd88767c - md5: 4656f00ccd13a49804387450302c4f45 + run_exports: {} + size: 1113801 + timestamp: 1773352768018 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ffmpeg-7.1.1-gpl_h8d881e6_910.conda + sha256: 3b9e3373977e49add71c770b386ddeabbb5f6c43ab4837790f5c9011a5ad050d + md5: a375807e930c22669ae4250745a5c71a depends: - - __osx >=10.13 - - libcxx - - libllvm16 >=16.0.6,<16.1.0a0 - - sigtool - - tapi >=1300.6.5,<1301.0a0 + - alsa-lib >=1.2.14,<1.3.0a0 + - aom >=3.9.1,<3.10.0a0 + - bzip2 >=1.0.8,<2.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - gmp >=6.3.0,<7.0a0 + - harfbuzz >=11.4.5 + - lame >=3.100,<3.101.0a0 + - libass >=0.17.4,<0.17.5.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libopenvino >=2025.2.0,<2025.2.1.0a0 + - libopenvino-arm-cpu-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-auto-batch-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-auto-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-hetero-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 + - libopus >=1.5.2,<2.0a0 + - librsvg >=2.58.4,<3.0a0 + - libstdcxx >=14 + - libvorbis >=1.3.7,<1.4.0a0 + - libvpx >=1.14.1,<1.15.0a0 + - libxcb >=1.17.0,<2.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - openh264 >=2.6.0,<2.6.1.0a0 + - openssl >=3.5.2,<4.0a0 + - pulseaudio-client >=17.0,<17.1.0a0 + - sdl2 >=2.32.54,<3.0a0 + - svt-av1 >=3.1.2,<3.1.3.0a0 + - x264 >=1!164.3095,<1!165 + - x265 >=3.5,<3.6.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 constrains: - - clang >=16.0.6,<17.0a0 - - cctools 1010.6.* - - cctools_osx-64 1010.6.* - - ld 951.9.* - license: APSL-2.0 - license_family: Other + - __cuda >=12.8 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 1088101 - timestamp: 1726771578888 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-951.9-h0605c9f_1.conda - sha256: 2183f5fc32084bbaa83a84817cfc68091e9e739a048a185dcfa55be908b9fe54 - md5: 77076839b5a8ac684c7971641d69b97a + run_exports: + weak: + - ffmpeg >=7.1.1,<8.0a0 + size: 10195859 + timestamp: 1757215115776 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/flatbuffers-25.12.19-h7ac5ae9_0.conda + sha256: bb7d543a9841cc7fa061d3fb60c65fb441ab25dbe30ed015f3f667a1b453602d + md5: 3e38efef94cc13471dac4a7cfd9fcf16 depends: - - __osx >=11.0 - - libcxx - - libllvm16 >=16.0.6,<16.1.0a0 - - sigtool - - tapi >=1300.6.5,<1301.0a0 - constrains: - - clang >=16.0.6,<17.0a0 - - cctools_osx-arm64 1010.6.* - - ld 951.9.* - - cctools 1010.6.* - license: APSL-2.0 - license_family: Other + - libstdcxx >=14 + - libgcc >=14 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 1006497 - timestamp: 1726771248963 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda - sha256: 1a620f27d79217c1295049ba214c2f80372062fd251b569e9873d4a953d27554 - md5: 0be7c6e070c19105f966d3758448d018 - depends: - - __glibc >=2.17,<3.0.a0 - constrains: - - binutils_impl_linux-64 2.44 - license: GPL-3.0-only - license_family: GPL + run_exports: + weak: + - flatbuffers >=25.12.19,<25.12.20.0a0 + size: 1699667 + timestamp: 1766388901342 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.18.1-hba86a56_0.conda + sha256: 2ccfd118269d363a5506161c4a0d96da46d2f01beecc74e0540a54b4737d0e45 + md5: f4d29a0cd77104a683607319a542ac7e + depends: + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libgcc >=14 + - libuuid >=2.42.1,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 676044 - timestamp: 1752032747103 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.44-h5e2c951_1.conda - sha256: 80e75aed7ea8af589b9171e90d042a20f111bbb21f62d06f32ec124ec9fd1f58 - md5: c10832808cf155953061892b3656470a - constrains: - - binutils_impl_linux-aarch64 2.44 - license: GPL-3.0-only - license_family: GPL + run_exports: + weak: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + size: 290522 + timestamp: 1780450108132 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.3-h8af1aa0_1.conda + sha256: 1112c56bc19cbce233b30d9d31ce8eb6fcc100c9baa5145315aaa1e3a25b5178 + md5: 5e8e88bfb3fbb0df0f9f8bb890721e07 + depends: + - libfreetype 2.14.3 h8af1aa0_1 + - libfreetype6 2.14.3 hdae7a39_1 + license: GPL-2.0-only OR FTL purls: [] - size: 708449 - timestamp: 1752032823484 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda - sha256: 412381a43d5ff9bbed82cd52a0bbca5b90623f62e41007c9c42d3870c60945ff - md5: 9344155d33912347b37f0ae6c410a835 + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + size: 174060 + timestamp: 1780933507786 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda + sha256: 1bfcd715bcb49a0b22d5d1899a22c6ff884b06f8e141eb746f3949752469a422 + md5: f3ac54914f7d3e1d68cb8d891765e5f9 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - license: Apache-2.0 - license_family: Apache + - libgcc >=14 + license: LGPL-2.1-or-later purls: [] - size: 264243 - timestamp: 1745264221534 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-hfdc4d58_1.conda - sha256: f01df5bbf97783fac9b89be602b4d02f94353f5221acfd80c424ec1c9a8d276c - md5: 60dceb7e876f4d74a9cbd42bbbc6b9cf + run_exports: + weak: + - fribidi >=1.0.16,<2.0a0 + size: 62909 + timestamp: 1757438620177 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.8.0-py311h91c1192_0.conda + sha256: e4b711eaf3ff15f3680bee3a40ee97c359b0a6af92505dadb6c80c8d48a1be4f + md5: 5f823e9c08b7c4626cdfd60b65a13f9c depends: - - libgcc >=13 - - libstdcxx >=13 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 license: Apache-2.0 - license_family: Apache + license_family: APACHE + purls: + - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} + size: 54712 + timestamp: 1779999836554 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc-12.4.0-h7e62973_2.conda + sha256: 62b7d45f5e8042890d7d6cacfdabaa0f2e5c9b8fe0f9b12d4f81fc078b66b347 + md5: e605824a02a81b3e3256636524c229d5 + depends: + - gcc_impl_linux-aarch64 12.4.0.* + license: BSD-3-Clause + license_family: BSD purls: [] - size: 227184 - timestamp: 1745265544057 -- conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hcca01a6_1.conda - sha256: cc1f1d7c30aa29da4474ec84026ec1032a8df1d7ec93f4af3b98bb793d01184e - md5: 21f765ced1a0ef4070df53cb425e1967 + run_exports: {} + size: 55373 + timestamp: 1740240463826 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_impl_linux-aarch64-12.4.0-h628656a_2.conda + sha256: d5434b7ece8e6c3d65a65b67f2c5e8f3c2379f8677a7b2aed214b63082fb9b88 + md5: 2f7cb25395310fa69c251dea18769124 depends: - - __osx >=10.13 - - libcxx >=18 - license: Apache-2.0 - license_family: Apache + - binutils_impl_linux-aarch64 >=2.40 + - libgcc >=12.4.0 + - libgcc-devel_linux-aarch64 12.4.0 h7b3af7c_102 + - libgomp >=12.4.0 + - libsanitizer 12.4.0 h469570c_2 + - libstdcxx >=12.4.0 + - sysroot_linux-aarch64 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 248882 - timestamp: 1745264331196 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda - sha256: 12361697f8ffc9968907d1a7b5830e34c670e4a59b638117a2cdfed8f63a38f8 - md5: a74332d9b60b62905e3d30709df08bf1 + run_exports: {} + size: 58914699 + timestamp: 1740240285252 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gcc_linux-aarch64-12.4.0-heb3b579_10.conda + sha256: 1ff4bb3d09d84c42fb1f338c2f76f2ab4ea989e8469583c47ce4b1843a522523 + md5: aa8fc7586ec58fcc44e4b9f4895181fe depends: - - __osx >=11.0 - - libcxx >=18 - license: Apache-2.0 - license_family: Apache + - binutils_linux-aarch64 + - gcc_impl_linux-aarch64 12.4.0.* + - sysroot_linux-aarch64 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 188306 - timestamp: 1745264362794 -- conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda - sha256: 868a3dff758cc676fa1286d3f36c3e0101cca56730f7be531ab84dc91ec58e9d - md5: c1b81da6d29a14b542da14a36c9fbf3f + run_exports: + strong: + - libgcc >=12 + size: 32648 + timestamp: 1745040658439 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.7-h90308e0_0.conda + sha256: f406e0b58a51da8648b100316c7b5893e5585256b67eb410126e2357a901f0d2 + md5: 6b26b46bbc0761e0f256699eab75202d depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - libgcc >=14 + - libglib >=2.88.2,<3.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + run_exports: + weak: + - gdk-pixbuf >=2.44.7,<3.0a0 + size: 588681 + timestamp: 1782593151876 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gh-2.96.0-h22914b5_0.conda + sha256: 484968e637fdb3cd55280b9106e7becbc0af1fe7c323df8ecfd64013a1806693 + md5: ef5c40ce73d5cadcca1fd58ede3ace62 license: Apache-2.0 - license_family: Apache + license_family: APACHE purls: [] - size: 164701 - timestamp: 1745264384716 -- conda: https://conda.anaconda.org/conda-forge/linux-64/level-zero-1.26.1-hb700be7_0.conda - sha256: 070cade1dec8f1352b26282c17a21df20c5ff7b58444a686222f5073cc904b7b - md5: d5d28ca40c9aefdb7617e8cdb7c218c2 + run_exports: {} + size: 12135397 + timestamp: 1783039136710 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gmp-6.3.0-h0a1ffab_2.conda + sha256: a5e341cbf797c65d2477b27d99091393edbaa5178c7d69b7463bb105b0488e69 + md5: 7cbfb3a8bb1b78a7f5518654ac6725ad + depends: + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: GPL-2.0-or-later OR LGPL-3.0-or-later + purls: [] + run_exports: + weak: + - gmp >=6.3.0,<7.0a0 + size: 417323 + timestamp: 1718980707330 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.15-hfae3067_0.conda + sha256: 3e529c517a76a1f4497c51eeedeeb927d33f732dcdb48055a020a83eb3e4e95c + md5: 4db044857ab1d09b2e8f0013c65387c1 depends: - - __glibc >=2.17,<3.0.a0 - libgcc >=14 - libstdcxx >=14 - license: MIT - license_family: MIT + license: LGPL-2.0-or-later + license_family: LGPL purls: [] - size: 638588 - timestamp: 1764980459016 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250512.1-cxx17_hba17884_0.conda - sha256: dcd1429a1782864c452057a6c5bc1860f2b637dc20a2b7e6eacd57395bbceff8 - md5: 83b160d4da3e1e847bf044997621ed63 + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 + size: 103119 + timestamp: 1780455096710 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx-12.4.0-h7e62973_2.conda + sha256: f54f7ec55907e31bde2681256d7135215c4ee3f7dcf4d6aebbaebf17ef66efcb + md5: 37d28c3a8d6a9408b8c9b043e74500fa depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - constrains: - - libabseil-static =20250512.1=cxx17* - - abseil-cpp =20250512.1 - license: Apache-2.0 - license_family: Apache + - gcc 12.4.0.* + - gxx_impl_linux-aarch64 12.4.0.* + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1310612 - timestamp: 1750194198254 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libabseil-20250512.1-cxx17_h201e9ed_0.conda - sha256: 28bb0a5f3177bb3b45a89d309b93bef65645671d1c97ae7bbcfa74481bf33f3c - md5: 4db30fe7ba05e2ce66595ed646064861 + run_exports: {} + size: 54875 + timestamp: 1740240579366 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_impl_linux-aarch64-12.4.0-h0bf7a72_2.conda + sha256: 07edf2303b2816b8d23191c15f40bda6824f4b3f4ba4892d8c27afd0c923e069 + md5: aeaa0618193ad8aa23457cd15eabfd61 depends: - - libgcc >=13 - - libstdcxx >=13 - constrains: - - abseil-cpp =20250512.1 - - libabseil-static =20250512.1=cxx17* - license: Apache-2.0 - license_family: Apache + - gcc_impl_linux-aarch64 12.4.0 h628656a_2 + - libstdcxx-devel_linux-aarch64 12.4.0 h7b3af7c_102 + - sysroot_linux-aarch64 + - tzdata + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 1327580 - timestamp: 1750194149128 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda - sha256: a878efebf62f039a1f1733c1e150a75a99c7029ece24e34efdf23d56256585b1 - md5: ddf1acaed2276c7eb9d3c76b49699a11 + run_exports: {} + size: 11915546 + timestamp: 1740240545209 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_linux-aarch64-12.4.0-h3f57e68_10.conda + sha256: 19edef472580cef8c145ccb307dd71ed2b7c18ac86e43aafce356047ce0f8352 + md5: ba65e3da87da43ba05bed772c89d084d depends: - - __osx >=10.13 - - libcxx >=18 - constrains: - - abseil-cpp =20250512.1 - - libabseil-static =20250512.1=cxx17* - license: Apache-2.0 - license_family: Apache + - binutils_linux-aarch64 + - gcc_linux-aarch64 12.4.0 heb3b579_10 + - gxx_impl_linux-aarch64 12.4.0.* + - sysroot_linux-aarch64 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1162435 - timestamp: 1750194293086 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda - sha256: 7f0ee9ae7fa2cf7ac92b0acf8047c8bac965389e48be61bf1d463e057af2ea6a - md5: 360dbb413ee2c170a0a684a33c4fc6b8 + run_exports: + strong: + - libstdcxx >=12 + - libgcc >=12 + size: 30955 + timestamp: 1745040677759 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-11.5.1-he4899c9_0.conda + sha256: c12b8f527fac572307d00ebf407b7663d23f19406d7ccdc02eafb168ff3d09ad + md5: 7f100c1ba5a0f5f3a23bb9481c70e880 depends: - - __osx >=11.0 - - libcxx >=18 - constrains: - - libabseil-static =20250512.1=cxx17* - - abseil-cpp =20250512.1 - license: Apache-2.0 - license_family: Apache + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libgcc >=14 + - libglib >=2.86.0,<3.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 1174081 - timestamp: 1750194620012 -- conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250814.1-cxx17_habfad5f_0.conda - sha256: d3c537290d1c76bb87ba4aefdf22615072ec4eeff99e60a09473ef5f3198e218 - md5: 8449690f173048e90e0759cbe4f159aa + run_exports: + weak: + - harfbuzz >=11.5.1 + size: 2073367 + timestamp: 1758644209045 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda + sha256: 813298f2e54ef087dbfc9cc2e56e08ded41de65cff34c639cc8ba4e27e4540c9 + md5: 268203e8b983fddb6412b36f2024e75c depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - abseil-cpp =20250814.1 - - libabseil-static =20250814.1=cxx17* - license: Apache-2.0 - license_family: Apache + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - icu >=75.1,<76.0a0 + size: 12282786 + timestamp: 1720853454991 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda + sha256: 5ce830ca274b67de11a7075430a72020c1fb7d486161a82839be15c2b84e9988 + md5: e7df0aab10b9cbb73ab2a467ebfaf8c7 + depends: + - libgcc >=13 + license: LGPL-2.1-or-later purls: [] - size: 1828247 - timestamp: 1758644485703 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-0.25.1-h3f43e3d_1.conda - sha256: cb728a2a95557bb6a5184be2b8be83a6f2083000d0c7eff4ad5bbe5792133541 - md5: 3b0d184bc9404516d418d4509e418bdc + run_exports: + weak: + - keyutils >=1.6.3,<2.0a0 + size: 129048 + timestamp: 1754906002667 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-h2fb54aa_1.conda + sha256: b644718416a22b5d57c1194ea7207b7f8d33d6a2b42775763782d76cd7457aea + md5: 5fd2304064ef6199d1f91ec60ee7b820 depends: - - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 - libgcc >=14 - libstdcxx >=14 - license: LGPL-2.1-or-later + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT purls: [] - size: 53582 - timestamp: 1753342901341 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-0.25.1-h5e0f5ae_0.conda - sha256: 146be90c237cf3d8399e44afe5f5d21ef9a15a7983ccea90e72d4ae0362f9b28 - md5: 1c5813f6be57f087b6659593248daf00 + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1518484 + timestamp: 1781859412954 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lame-3.100-h4e544f5_1003.tar.bz2 + sha256: 2502904a42df6d94bd743f7b73915415391dd6d31d5f50cb57c0a54a108e7b0a + md5: ab05bcf82d8509b4243f07e93bada144 depends: - - libgcc >=13 - - libstdcxx >=13 - license: LGPL-2.1-or-later + - libgcc-ng >=12 + license: LGPL-2.0-only + license_family: LGPL purls: [] - size: 53434 - timestamp: 1751557548397 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.25.1-h3f43e3d_1.conda - sha256: 2fc95060efc3d76547b7872875af0b7212d4b1407165be11c5f830aeeb57fc3a - md5: fd9cf4a11d07f0ef3e44fc061611b1ed + run_exports: + weak: + - lame >=3.100,<3.101.0a0 + size: 604863 + timestamp: 1664997611416 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.46.1-default_h1979696_102.conda + sha256: 2c4901f4227b0850328ed0c69f958b30ad2cd18982f7a31c7c1f911827004d08 + md5: 489444d0acb2a579d2a002d85a08c059 depends: - - __glibc >=2.17,<3.0.a0 - - libasprintf 0.25.1 h3f43e3d_1 - - libgcc >=14 - license: LGPL-2.1-or-later + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-aarch64 2.46.1 + license: GPL-3.0-only + license_family: GPL purls: [] - size: 34734 - timestamp: 1753342921605 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-devel-0.25.1-h5e0f5ae_0.conda - sha256: cc2bb8ca349ba4dd4af7971a3dba006bc8643353acd9757b4d645a817ec0f899 - md5: 5df92d925fba917586f3ca31c96d8e6d + run_exports: {} + size: 905305 + timestamp: 1784214534868 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.1.0-h52b7260_0.conda + sha256: 8957fd460c1c132c8031f65fd5f56ec3807fd71b7cab2c5e2b0937b13404ab36 + md5: d13423b06447113a90b5b1366d4da171 depends: - - libasprintf 0.25.1 h5e0f5ae_0 - - libgcc >=13 - license: LGPL-2.1-or-later + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 + license_family: Apache purls: [] - size: 34824 - timestamp: 1751557562978 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.4-h96ad9f0_0.conda - sha256: 035eb8b54e03e72e42ef707420f9979c7427776ea99e0f1e3c969f92eb573f19 - md5: d3be7b2870bf7aff45b12ea53165babd + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 + size: 240444 + timestamp: 1773114901155 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libabseil-20250512.1-cxx17_h201e9ed_0.conda + sha256: 28bb0a5f3177bb3b45a89d309b93bef65645671d1c97ae7bbcfa74481bf33f3c + md5: 4db30fe7ba05e2ce66595ed646064861 depends: - libgcc >=13 - - __glibc >=2.17,<3.0.a0 - - libzlib >=1.3.1,<2.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - fribidi >=1.0.10,<2.0a0 - - libiconv >=1.18,<2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - harfbuzz >=11.0.1 - license: ISC + - libstdcxx >=13 + constrains: + - abseil-cpp =20250512.1 + - libabseil-static =20250512.1=cxx17* + license: Apache-2.0 + license_family: Apache purls: [] - size: 152179 - timestamp: 1749328931930 + run_exports: + weak: + - libabseil >=20250512.1,<20250513.0a0 + - libabseil =*=cxx17* + size: 1327580 + timestamp: 1750194149128 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libass-0.17.4-hcfe818d_0.conda sha256: cb19ad0b8f9cb469c78d26af9c49c790e5f746bb8a348ec10b681a98f05d1dc7 md5: 8df67d209c9f7e8d40281a4ebf8ffd6d @@ -6880,78 +8086,38 @@ packages: - libzlib >=1.3.1,<2.0a0 license: ISC purls: [] + run_exports: + weak: + - libass >=0.17.4,<0.17.5.0a0 size: 171287 timestamp: 1749328949722 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.4-h87c4fc2_0.conda - sha256: 7ddcb016d016919f1735fd2c6b826bb4d7dabd995d053b748d41ef47343fe001 - md5: 3db36f8bfe00ab9cda1e72cd59fdd415 - depends: - - __osx >=10.13 - - libiconv >=1.18,<2.0a0 - - harfbuzz >=11.0.1 - - fribidi >=1.0.10,<2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libzlib >=1.3.1,<2.0a0 - license: ISC - purls: [] - size: 157712 - timestamp: 1749329008301 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libass-0.17.4-hcbd7ca7_0.conda - sha256: 079f5fdf7aace970a0db91cd2cc493c754dfdc4520d422ecec43d2561021167a - md5: 0977f4a79496437ff3a2c97d13c4c223 - depends: - - __osx >=11.0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - libzlib >=1.3.1,<2.0a0 - - fribidi >=1.0.10,<2.0a0 - - libiconv >=1.18,<2.0a0 - - harfbuzz >=11.0.1 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - license: ISC - purls: [] - size: 138339 - timestamp: 1749328988096 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcap-2.71-h39aace5_0.conda - sha256: 2bbefac94f4ab8ff7c64dc843238b6c8edcc9ff1f2b5a0a48407a904dc7ccfb2 - md5: dd19e4e3043f6948bd7454b946ee0983 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libattr-2.5.2-he30d5cf_1.conda + sha256: 50fec389e6eaa8a1baff79a48b242850e638f2ca92689a7c8e7c1e724ee42114 + md5: cdfbc8a5f16a7ed3d4f02779d5f7fbcf depends: - - __glibc >=2.17,<3.0.a0 - - attr >=2.5.1,<2.6.0a0 - - libgcc >=13 - license: BSD-3-Clause - license_family: BSD + - libgcc >=14 + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 102268 - timestamp: 1729940917945 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.71-h51d75a7_0.conda - sha256: 2b66e66e6a0768e833e7edc764649679881ec0a6b37d9bf254b1ceb3b8b434ef - md5: 29f6092b6e938516ca0b042837e64fa5 + run_exports: + weak: + - libattr >=2.5.2,<2.6.0a0 + size: 54504 + timestamp: 1773595923052 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcap-2.75-h51d75a7_0.conda + sha256: d77e8bd8d5714a80c1fa88037e71d5c29f21bae1e9281528006c9c5a6175ac1a + md5: c5456e13665779bf7a62dc7724ca2938 depends: - attr >=2.5.1,<2.6.0a0 - libgcc >=13 license: BSD-3-Clause license_family: BSD purls: [] - size: 106877 - timestamp: 1729940936697 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp16-16.0.6-default_hddf928d_15.conda - sha256: 218ea23f992734c3cb40bca39266768240f8f099a23c5d69305692f3485f1bea - md5: ebf034fe29aad0a581668bcbf8ca4431 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libllvm16 >=16.0.6,<16.1.0a0 - - libstdcxx >=14 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - purls: [] - size: 18328968 - timestamp: 1756166766219 + run_exports: + weak: + - libcap >=2.75,<2.76.0a0 + size: 108212 + timestamp: 1741177682469 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang-cpp16-16.0.6-default_hf07bfb7_15.conda sha256: 66ffdca9539147635d3600f8c6466271efb220077a981193ff752530c128148d md5: 14cec7ec0d7d5064e290ba1dd38ebc90 @@ -6962,45 +8128,26 @@ packages: license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] + run_exports: + weak: + - libclang-cpp16 >=16.0.6,<16.1.0a0 size: 17905583 timestamp: 1756169791231 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h4651f56_15.conda - sha256: 04f882afadb3af2e373efb5f542e8ff6b3aaea8326bf85b7445b9c727d1e0135 - md5: 5d3cb1a184771445034f2113ba543827 - depends: - - __osx >=10.13 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - purls: [] - size: 12759044 - timestamp: 1756166818220 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp16-16.0.6-default_h3c2e7ce_15.conda - sha256: 96beef959638d73da280e9551b9028df48f7f671df237c6bb7c7495816e96fa8 - md5: 2589c8f983f4676b005a4e8fb227212d - depends: - - __osx >=11.0 - - libcxx >=16.0.6 - - libllvm16 >=16.0.6,<16.1.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - purls: [] - size: 11797889 - timestamp: 1756165841886 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-21.1.0-default_h746c552_1.conda - sha256: e6c0123b888d6abf03c66c52ed89f9de1798dde930c5fd558774f26e994afbc6 - md5: 327c78a8ce710782425a89df851392f7 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang-cpp18.1-18.1.8-default_he95a3c9_18.conda + sha256: 208b5f871c4a627a3c023986bfafd3dba86e2b5df04da774cf3e17248ccc1c41 + md5: 92d8da1bd04f104980f7c578a92ffdcb depends: - - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libllvm21 >=21.1.0,<21.2.0a0 + - libllvm18 >=18.1.8,<18.2.0a0 - libstdcxx >=14 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 12358102 - timestamp: 1757383373129 + run_exports: + weak: + - libclang-cpp18.1 >=18.1.8,<18.2.0a0 + size: 19159057 + timestamp: 1773512248220 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libclang13-21.1.0-default_h94a09a5_1.conda sha256: 8d9840b6375bc3e947dbbbc4fb41006cd3c4a4f82bfdc248cd3cd8e810884fc2 md5: daf07a8287e12c3812d98bca3812ecf2 @@ -7011,3315 +8158,4326 @@ packages: license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] + run_exports: + weak: + - libclang13 >=21.1.0 size: 12123786 timestamp: 1757386604184 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libclang13-21.1.0-default_h7f9524c_1.conda - sha256: 7a39bb169f583c4da4ebc47729d8cf2c41763364010e7c12956dc0c0a86741d6 - md5: 8c5c6f63bb40997ae614b23a770b0369 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.21.0-hc57f145_1.conda + sha256: 277a4c0277ae4cd31307d50332aa7f975bd5df3bb6b9710a388ac4bab255c824 + md5: 24f82179a8c74c644c01df15f158bc1b depends: - - __osx >=10.13 - - libcxx >=21.1.0 - - libllvm21 >=21.1.0,<21.2.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - krb5 >=1.22.2,<1.23.0a0 + - libgcc >=14 + - libnghttp2 >=1.68.1,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT purls: [] - size: 9005813 - timestamp: 1757400178887 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.0-default_h6e8f826_1.conda - sha256: d4517eb5c79e386eacdfa0424c94c822a04cf0d344d6730483de1dcbce24a5dd - md5: a29a6b4c1a926fbb64813ecab5450483 + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 504521 + timestamp: 1782802479892 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda + sha256: 48814b73bd462da6eed2e697e30c060ae16af21e9fbed30d64feaf0aad9da392 + md5: a9138815598fe6b91a1d6782ca657b0c depends: - - __osx >=11.0 - - libcxx >=21.1.0 - - libllvm21 >=21.1.0,<21.2.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc >=14 + license: MIT + license_family: MIT purls: [] - size: 8513708 - timestamp: 1757383978186 -- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-21.1.1-default_ha2db4b5_0.conda - sha256: 6d73ef2edf64ff3759a380ed12bb1bf5a17d6035386c07377c34fbd6fa9c3d9d - md5: 17f5b2e04b696f148b1b8ff1d5d55b75 + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 + size: 71117 + timestamp: 1761979776756 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.127-he30d5cf_0.conda + sha256: 2a941ffcd6b09380344c2cb5b198d2743ce4fc30ec9a5c8c83e53368d8015aef + md5: 987d35ad350bb552a30f3d314f6c7655 depends: - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - zstd >=1.5.7,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc >=14 + - libpciaccess >=0.19,<0.20.0a0 + license: MIT + license_family: MIT purls: [] - size: 28988003 - timestamp: 1757621024964 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda - sha256: b6c5cf340a4f80d70d64b3a29a7d9885a5918d16a5cb952022820e6d3e79dc8b - md5: 45f6713cb00f124af300342512219182 + run_exports: + weak: + - libdrm >=2.4.127,<2.5.0a0 + size: 345283 + timestamp: 1778975814771 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda + sha256: c0b27546aa3a23d47919226b3a1635fccdb4f24b94e72e206a751b33f46fd8d6 + md5: fb640d776fc92b682a14e001980825b1 depends: - - __glibc >=2.17,<3.0.a0 - - krb5 >=1.21.3,<1.22.0a0 + - ncurses - libgcc >=13 - - libnghttp2 >=1.64.0,<2.0a0 - - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: curl + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libedit >=3.1.20250104,<3.2.0a0 + size: 148125 + timestamp: 1738479808948 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_3.conda + sha256: b987d3874edfcd9c7ddca86c003cb04ae51160a72c173a24cd46ab9eeb8886ab + md5: ec017f25e5d01ef9dd81e95ff73ff051 + depends: + - libglvnd 1.7.0 hd24410f_3 + license: LicenseRef-libglvnd + purls: [] + run_exports: {} + size: 54600 + timestamp: 1779728234591 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda + sha256: 973af77e297f1955dd1f69c2cbdc5ab9dfc88388a5576cd152cda178af0fd006 + md5: a9a13cb143bbaa477b1ebaefbe47a302 + depends: + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libev >=4.33,<4.34.0a0 + size: 115123 + timestamp: 1702146237623 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.8.1-hfae3067_1.conda + sha256: 20a5726bc8705d91437c9e6ef83b30da64a1719b869656d20a1ee818333ea5ac + md5: fac3b65a605cd253037fdf3daf2de8d9 + depends: + - libgcc >=14 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 77649 + timestamp: 1781203572523 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + sha256: 3df4c539449aabc3443bbe8c492c01d401eea894603087fca2917aa4e1c2dea9 + md5: 2f364feefb6a7c00423e80dcb12db62a + depends: + - libgcc >=14 + license: MIT license_family: MIT purls: [] - size: 449910 - timestamp: 1749033146806 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.14.1-h6702fde_0.conda - sha256: 13f7cc9f6b4bdc9a3544339abf2662bc61018c415fe7a1518137db782eb85343 - md5: 1d92dbf43358f0774dc91764fa77a9f5 + run_exports: + weak: + - libffi >=3.5.2,<3.6.0a0 + size: 55952 + timestamp: 1769456078358 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libflac-1.5.0-he9c94f4_1.conda + sha256: 175cdc1865c3d6becc87e96bf44010a8e14f3021600ddad59417ed36e677b1ea + md5: cbe37f1d15f60b5e5272955b55b65325 + depends: + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - libogg >=1.3.5,<1.4.0a0 + - libstdcxx >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libflac >=1.5.0,<1.6.0a0 + size: 397272 + timestamp: 1764526699497 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.3-h8af1aa0_1.conda + sha256: db75d0fc080992dc67db8e24d7bb2a2f2a0b25bfce8870fa45a82a4b5f6111a2 + md5: a13e600f9d18488b1fd1257344dbfdaa + depends: + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL + purls: [] + run_exports: {} + size: 8381 + timestamp: 1780933505754 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.3-hdae7a39_1.conda + sha256: 34fe8276befd6c42956c4acd969caeddbdc7ea8e6ed054b8388709b6c3e94ba4 + md5: 426cc33f8745ce11a73baf73db3954a7 + depends: + - libgcc >=14 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL + purls: [] + run_exports: {} + size: 424236 + timestamp: 1780933505195 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_19.conda + sha256: 4592b096e553f67799ae70d4b6167eeda3ec74587d68c7aecbf4e7b1df136681 + md5: f35b3f52d0a2ec4ffe3c89ba135cdb9a + depends: + - _openmp_mutex >=4.5 + constrains: + - libgomp 15.2.0 h8acb6b2_19 + - libgcc-ng ==15.2.0=*_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: {} + size: 622462 + timestamp: 1778268755949 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_19.conda + sha256: 1137f93f477f56199ded24117430045a0c02cbe8b10031beac3b9ad2138539d3 + md5: 770cf892e5530f43e63cadc673e85653 + depends: + - libgcc 15.2.0 h8acb6b2_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: + strong: + - libgcc + size: 27738 + timestamp: 1778268759211 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcrypt-lib-1.12.2-he30d5cf_0.conda + sha256: 5c1fa4dfe58fedf2077a0cc831565f32fe3fede70503fa91bcaf0b0a7ee6a5d9 + md5: f21411a9929d61875e41ab16da1657be + depends: + - libgcc >=14 + - libgpg-error >=1.61,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - libgcrypt-lib >=1.12.2,<2.0a0 + size: 695193 + timestamp: 1779976293723 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_3.conda + sha256: 05c75a2034bdbca29bab467d02ad770ed5e524e4f0670432258f2d8487c95348 + md5: 6e893c36f31502dd195d3d58f455fdbd + depends: + - libglvnd 1.7.0 hd24410f_3 + - libglx 1.7.0 hd24410f_3 + license: LicenseRef-libglvnd + purls: [] + run_exports: {} + size: 148112 + timestamp: 1779728248678 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.88.2-h96a7f82_0.conda + sha256: 6a13152a81513117b8d41bf64dea56c731d877bcced5a24fab738e3c0f9ac58c + md5: 31d404d8c0755d0f9062a4459f5a1084 + depends: + - libgcc >=14 + - libffi >=3.5.2,<3.6.0a0 + - pcre2 >=10.47,<10.48.0a0 + - libzlib >=1.3.2,<2.0a0 + - libiconv >=1.18,<2.0a0 + constrains: + - glib >2.66 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 4943441 + timestamp: 1782464048865 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_3.conda + sha256: ca124e53765a2b123e0ca6ce809c7caf188bb26e5fe125b69099378276d5e66f + md5: a2ad848c0aab2e326c6af08ea20502f4 + license: LicenseRef-libglvnd + purls: [] + run_exports: {} + size: 146645 + timestamp: 1779728228274 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_3.conda + sha256: 2698b415b9f7b692cd64e34db623e1a6e54ed54e78b0b4e5d4ea6762791e9118 + md5: 338faf34b78d053841098c0528699e34 depends: - - krb5 >=1.21.3,<1.22.0a0 - - libgcc >=13 - - libnghttp2 >=1.64.0,<2.0a0 - - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: curl - license_family: MIT + - libglvnd 1.7.0 hd24410f_3 + - xorg-libx11 >=1.8.13,<2.0a0 + license: LicenseRef-libglvnd + purls: [] + run_exports: {} + size: 76704 + timestamp: 1779728242753 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda + sha256: 2370ef0ffcbae5bede3c4bf136add4abc257245eb91f724c99bb4a43116c5a83 + md5: c5e8a379c4a2ec2aea4ba22758c001d9 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 469143 - timestamp: 1749033114882 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.14.1-h5dec5d8_0.conda - sha256: ca0d8d12056227d6b47122cfb6d68fc5a3a0c6ab75a0e908542954fc5f84506c - md5: 8738cd19972c3599400404882ddfbc24 + run_exports: + strong: + - _openmp_mutex >=4.5 + size: 587387 + timestamp: 1778268674393 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgpg-error-1.61-h7ac5ae9_0.conda + sha256: ef22c28bc58c66287ed1001ce14e884c42b97d7ad8c08309c74e10ee2bd46b3b + md5: c05cf4ee0b7c616d7838a469b6271e42 depends: - - __osx >=10.13 - - krb5 >=1.21.3,<1.22.0a0 - - libnghttp2 >=1.64.0,<2.0a0 - - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: curl - license_family: MIT + - libgcc >=14 + - libstdcxx >=14 + license: LGPL-2.1-only purls: [] - size: 424040 - timestamp: 1749033558114 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda - sha256: 0055b68137309db41ec34c938d95aec71d1f81bd9d998d5be18f32320c3ccba0 - md5: 1af57c823803941dfc97305248a56d57 + run_exports: + weak: + - libgpg-error >=1.61,<2.0a0 + size: 333760 + timestamp: 1778157463840 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libhwloc-2.12.1-default_h6f258fa_1000.conda + sha256: d25c10fd894ce6c5d3eba5667bef98be0e82d8e4d2ec20425d89a5baee715304 + md5: eea9ada077bda5f4a32889b9285af9c0 depends: - - __osx >=11.0 - - krb5 >=1.21.3,<1.22.0a0 - - libnghttp2 >=1.64.0,<2.0a0 - - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: curl - license_family: MIT + - libgcc >=14 + - libstdcxx >=14 + - libxml2 >=2.13.8,<2.14.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 403456 - timestamp: 1749033320430 -- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda - sha256: b2cface2cf35d8522289df7fffc14370596db6f6dc481cc1b6ca313faeac19d8 - md5: 836b9c08f34d2017dbcaec907c6a1138 + run_exports: + weak: + - libhwloc >=2.12.1,<2.12.2.0a0 + size: 2468653 + timestamp: 1752761831524 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda + sha256: 1473451cd282b48d24515795a595801c9b65b567fe399d7e12d50b2d6cdb04d9 + md5: 5a86bf847b9b926f3a4f203339748d78 depends: - - krb5 >=1.21.3,<1.22.0a0 - - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: curl - license_family: MIT + - libgcc >=14 + license: LGPL-2.1-only purls: [] - size: 368346 - timestamp: 1749033492826 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-21.1.1-h3d58e20_0.conda - sha256: dd207d8882854f22072b7fd4f03726e0e182e0666986ec880168f1753f7415dc - md5: 7f5b7dfca71a5c165ce57f46e9e48480 + run_exports: + weak: + - libiconv >=1.18,<2.0a0 + size: 791226 + timestamp: 1754910975665 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.2.0-he30d5cf_0.conda + sha256: da3e45974c1f23c76264d6358463861236862052ca156e34b502ef3c27e36fb8 + md5: de0780a3690bffe75ac3fa70db84596d depends: - - __osx >=10.13 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc >=14 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib purls: [] - size: 571163 - timestamp: 1757525814844 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.1-hf598326_0.conda - sha256: 6af03355967b7b097d5820dde05e0c709945fdb01f4bc56d11499d8bf7435239 - md5: d5790f3769fedeea4e021483272bdc53 + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 714602 + timestamp: 1783731816001 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm16-16.0.6-h2edbd07_4.conda + sha256: 058ff3b819b7d3066c1059ad17b730868c1e6e3baf732b91e6a945dc01f821ea + md5: 680291df42c567776f99f5a8335515b5 depends: - - __osx >=11.0 + - libgcc >=13 + - libstdcxx >=13 + - libxml2 >=2.13.5,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.6,<1.6.0a0 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 568291 - timestamp: 1757525671408 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-devel-16.0.6-h8f8a49f_2.conda - sha256: 1c1c6f6f4eca07be3f03929c59c2dd077da3c676fbf5e92c0df3bad2a4f069ab - md5: 677580dee2d1412311d9dd9bf6bfa6b7 + run_exports: + weak: + - libllvm16 >=16.0.6,<16.1.0a0 + size: 34544032 + timestamp: 1739798290457 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm18-18.1.8-default_hbd976d5_9.conda + sha256: d01e11431cf7e1d310f9c2e01071f640c238174517546ef58b63793be7eb2a6e + md5: 63fb359152df898d0e0ae027e8d894fb depends: - - libcxx >=16.0.6 + - libgcc >=14 + - libstdcxx >=14 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 716532 - timestamp: 1725067685814 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-16.0.6-h86353a2_2.conda - sha256: fb51aaeb9911d9999afaf0a3dc8f4eee97c524aac4ec152217372e8645ef8856 - md5: f81c638415433ea5bb5024b49cda17ea + run_exports: + weak: + - libllvm18 >=18.1.8,<18.2.0a0 + size: 38172187 + timestamp: 1756461840807 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm21-21.1.0-h2b567e5_0.conda + sha256: 1a393ebae1d2014dc350d472836f5087bd2040d48fa9410952cfc2faa6fd817e + md5: 2f7ec415da2566effa22beb4ba47bfb4 depends: - - libcxx >=16.0.6 + - libgcc >=14 + - libstdcxx >=14 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 717680 - timestamp: 1725067968232 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda - sha256: 8420748ea1cc5f18ecc5068b4f24c7a023cc9b20971c99c824ba10641fb95ddf - md5: 64f0c503da58ec25ebd359e4d990afa8 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 72573 - timestamp: 1747040452262 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.24-he377734_0.conda - sha256: dd0e4baa983803227ec50457731d6f41258b90b3530f579b5d3151d5a98af191 - md5: f0b3d6494663b3385bf87fc206d7451a - depends: - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 70417 - timestamp: 1747040440762 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.24-hcc1b750_0.conda - sha256: 2733a4adf53daca1aa4f41fe901f0f8ee9e4c509abd23ffcd7660013772d6f45 - md5: f0a46c359722a3e84deb05cd4072d153 - depends: - - __osx >=10.13 - license: MIT - license_family: MIT - purls: [] - size: 69751 - timestamp: 1747040526774 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda - sha256: 417d52b19c679e1881cce3f01cad3a2d542098fa2d6df5485aac40f01aede4d1 - md5: 3baf58a5a87e7c2f4d243ce2f8f2fe5c - depends: - - __osx >=11.0 - license: MIT - license_family: MIT - purls: [] - size: 54790 - timestamp: 1747040549847 -- conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda - sha256: 65347475c0009078887ede77efe60db679ea06f2b56f7853b9310787fe5ad035 - md5: 08d988e266c6ae77e03d164b83786dc4 + run_exports: + weak: + - libllvm21 >=21.1.0,<21.2.0a0 + size: 43185742 + timestamp: 1756287405599 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda + sha256: d61962b9cd54c3554361550203c64d5b65b71e3058a285b66e4b04b9769f0a5c + md5: 76298a9e6d71ee6e832a8d0d7373b261 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT + - libgcc >=14 + constrains: + - xz 5.8.3.* + license: 0BSD purls: [] - size: 156292 - timestamp: 1747040812624 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda - sha256: c076a213bd3676cc1ef22eeff91588826273513ccc6040d9bea68bccdc849501 - md5: 9314bc5a1fe7d1044dc9dfd3ef400535 + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 126102 + timestamp: 1775828008518 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-devel-5.8.3-he30d5cf_0.conda + sha256: 584cbcebe2f8c4c3e81eb46e3ed085bc49f709a9aeb92847ac20e1aa25c4b7b6 + md5: 349d74fc98742dd532de0ae6368fd19d depends: - - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libpciaccess >=0.18,<0.19.0a0 - license: MIT - license_family: MIT + - liblzma 5.8.3 he30d5cf_0 + license: 0BSD purls: [] - size: 310785 - timestamp: 1757212153962 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda - sha256: 4e6cdb5dd37db794b88bec714b4418a0435b04d14e9f7afc8cc32f2a3ced12f2 - md5: 2079727b538f6dd16f3fa579d4c3c53f + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 493047 + timestamp: 1775828222341 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.68.1-hd3077d7_0.conda + sha256: 13782715b9eeebc4ad16d36e84ca569d1495e3516aea3fe546a32caa0a597d82 + md5: be5f0f007a4500a226ef001115535a3d depends: + - c-ares >=1.34.6,<2.0a0 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 - libgcc >=14 - - libpciaccess >=0.18,<0.19.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 344548 - timestamp: 1757212128414 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 - md5: c277e0a4d549b03ac1e9d6cbbe3d017b + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 + size: 726928 + timestamp: 1773854039807 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda + sha256: c0dc4d84198e3eef1f37321299e48e2754ca83fd12e6284754e3cb231357c3a5 + md5: d5d58b2dc3e57073fe22303f5fed4db7 depends: - - ncurses - - __glibc >=2.17,<3.0.a0 - libgcc >=13 - - ncurses >=6.5,<7.0a0 - license: BSD-2-Clause - license_family: BSD + license: LGPL-2.1-only + license_family: GPL purls: [] - size: 134676 - timestamp: 1738479519902 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda - sha256: c0b27546aa3a23d47919226b3a1635fccdb4f24b94e72e206a751b33f46fd8d6 - md5: fb640d776fc92b682a14e001980825b1 + run_exports: + weak: + - libnsl >=2.0.1,<2.1.0a0 + size: 34831 + timestamp: 1750274211000 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libogg-1.3.5-h86ecc28_1.conda + sha256: 2c1b7c59badc2fd6c19b6926eabfce906c996068d38c2972bd1cfbe943c07420 + md5: 319df383ae401c40970ee4e9bc836c7a depends: - - ncurses - libgcc >=13 - - ncurses >=6.5,<7.0a0 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 148125 - timestamp: 1738479808948 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda - sha256: 6cc49785940a99e6a6b8c6edbb15f44c2dd6c789d9c283e5ee7bdfedd50b4cd6 - md5: 1f4ed31220402fcddc083b4bff406868 - depends: - - ncurses - - __osx >=10.13 - - ncurses >=6.5,<7.0a0 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 115563 - timestamp: 1738479554273 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 - md5: 44083d2d2c2025afca315c7a172eab2b - depends: - - ncurses - - __osx >=11.0 - - ncurses >=6.5,<7.0a0 - license: BSD-2-Clause + license: BSD-3-Clause license_family: BSD purls: [] - size: 107691 - timestamp: 1738479560845 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda - sha256: 7fd5408d359d05a969133e47af580183fbf38e2235b562193d427bb9dad79723 - md5: c151d5eb730e9b7480e6d48c0fc44048 - depends: - - __glibc >=2.17,<3.0.a0 - - libglvnd 1.7.0 ha4b6fd6_2 - license: LicenseRef-libglvnd - purls: [] - size: 44840 - timestamp: 1731330973553 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda - sha256: 8962abf38a58c235611ce356b9899f6caeb0352a8bce631b0bcc59352fda455e - md5: cf105bce884e4ef8c8ccdca9fe6695e7 + run_exports: + weak: + - libogg >=1.3.5,<1.4.0a0 + size: 220653 + timestamp: 1745826021156 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-2025.2.0-hcd21e76_1.conda + sha256: f5c7a24d9918b1f637ca11a7c0b5594e14469ccc5b1f3bafcd248df252d2bdfb + md5: 76baf6bb7a63e310210d91595e245d24 depends: - - libglvnd 1.7.0 hd24410f_2 - license: LicenseRef-libglvnd + - libgcc >=14 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 53551 - timestamp: 1731330990477 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 - md5: 172bf1cd1ff8629f2b1179945ed45055 + run_exports: + weak: + - libopenvino >=2025.2.0,<2025.2.1.0a0 + size: 5535917 + timestamp: 1753203182299 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-arm-cpu-plugin-2025.2.0-hcd21e76_1.conda + sha256: 018a0ea563bc2e91efee8a07f7b2ff769cd66d03d1c466c8bb7407075023ac85 + md5: 794c3f49774bd710aec2b0602ae38313 depends: - - libgcc-ng >=12 - license: BSD-2-Clause - license_family: BSD + - libgcc >=14 + - libopenvino 2025.2.0 hcd21e76_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 112766 - timestamp: 1702146165126 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda - sha256: 973af77e297f1955dd1f69c2cbdc5ab9dfc88388a5576cd152cda178af0fd006 - md5: a9a13cb143bbaa477b1ebaefbe47a302 + run_exports: {} + size: 9257629 + timestamp: 1753203203327 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-auto-batch-plugin-2025.2.0-h3890994_1.conda + sha256: 59a159c547fca34e8a0c600fcca428793da2ad4ecef0f47b58f1ea16d756c521 + md5: ad9768777a654205fa46aed8a829bd7e depends: - - libgcc-ng >=12 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 115123 - timestamp: 1702146237623 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda - sha256: 0d238488564a7992942aa165ff994eca540f687753b4f0998b29b4e4d030ff43 - md5: 899db79329439820b7e8f8de41bca902 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 106663 - timestamp: 1702146352558 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda - sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f - md5: 36d33e440c31857372a72137f78bacf5 - license: BSD-2-Clause - license_family: BSD + - libgcc >=14 + - libopenvino 2025.2.0 hcd21e76_1 + - libstdcxx >=14 + - tbb >=2021.13.0 purls: [] - size: 107458 - timestamp: 1702146414478 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 - md5: 4211416ecba1866fab0c6470986c22d6 + run_exports: {} + size: 111599 + timestamp: 1753203233477 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-auto-plugin-2025.2.0-h3890994_1.conda + sha256: 3353f616cf72dad02d974698a74fa89eb5ff1beeaa64cebcdd1f87c52d2a0516 + md5: 4cec7bb2362ece08d0d1799f1ed4fbe7 depends: - - __glibc >=2.17,<3.0.a0 - libgcc >=14 - constrains: - - expat 2.7.1.* - license: MIT - license_family: MIT + - libopenvino 2025.2.0 hcd21e76_1 + - libstdcxx >=14 + - tbb >=2021.13.0 purls: [] - size: 74811 - timestamp: 1752719572741 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.1-hfae3067_0.conda - sha256: 378cabff44ea83ce4d9f9c59f47faa8d822561d39166608b3e65d1e06c927415 - md5: f75d19f3755461db2eb69401f5514f4c + run_exports: {} + size: 235379 + timestamp: 1753203244808 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-hetero-plugin-2025.2.0-he07c6df_1.conda + sha256: 97f6a555d73d96efe26521527ce4e4c6ea49e46d5e5fd07a5e535e7de34bb6b5 + md5: 00d0206cb4358182c856700e1c1dae8b depends: - libgcc >=14 - constrains: - - expat 2.7.1.* - license: MIT - license_family: MIT + - libopenvino 2025.2.0 hcd21e76_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 purls: [] - size: 74309 - timestamp: 1752719762749 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.7.1-h21dd04a_0.conda - sha256: 689862313571b62ee77ee01729dc093f2bf25a2f99415fcfe51d3a6cd31cce7b - md5: 9fdeae0b7edda62e989557d645769515 + run_exports: {} + size: 187747 + timestamp: 1753203256494 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-ir-frontend-2025.2.0-he07c6df_1.conda + sha256: 935341a98e129d3fd792609de5e85b959c3b31661d1a95c2a655771611383a05 + md5: f86c16f077043c9b1e87dbc07bf5ec42 depends: - - __osx >=10.13 - constrains: - - expat 2.7.1.* - license: MIT - license_family: MIT + - libgcc >=14 + - libopenvino 2025.2.0 hcd21e76_1 + - libstdcxx >=14 + - pugixml >=1.15,<1.16.0a0 purls: [] - size: 72450 - timestamp: 1752719744781 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda - sha256: 8fbb17a56f51e7113ed511c5787e0dec0d4b10ef9df921c4fd1cccca0458f648 - md5: b1ca5f21335782f71a8bd69bdc093f67 + run_exports: + weak: + - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 + size: 195451 + timestamp: 1753203267888 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-onnx-frontend-2025.2.0-h07d5dce_1.conda + sha256: 576c1ba122fb58d1c0ea6540d5480809196a884d3e56c05ab49b97ccc99e2c90 + md5: f8d90a982f95366614c568eac3157a90 depends: - - __osx >=11.0 - constrains: - - expat 2.7.1.* - license: MIT - license_family: MIT + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libopenvino 2025.2.0 hcd21e76_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 purls: [] - size: 65971 - timestamp: 1752719657566 -- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda - sha256: 8432ca842bdf8073ccecf016ccc9140c41c7114dc4ec77ca754551c01f780845 - md5: 3608ffde260281fa641e70d6e34b1b96 + run_exports: + weak: + - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 + size: 1530030 + timestamp: 1753203281815 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-paddle-frontend-2025.2.0-h07d5dce_1.conda + sha256: b080ca352d8d4526b73815bdbdb12ba5caf5de4621c10e9ad41eac73a7a6a713 + md5: 098597aa6f19b2851f295f47c7105658 depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - expat 2.7.1.* - license: MIT - license_family: MIT + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libopenvino 2025.2.0 hcd21e76_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 purls: [] - size: 141322 - timestamp: 1752719767870 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - sha256: 764432d32db45466e87f10621db5b74363a9f847d2b8b1f9743746cd160f06ab - md5: ede4673863426c0883c0063d853bbd85 + run_exports: + weak: + - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 + size: 674194 + timestamp: 1753203295461 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-pytorch-frontend-2025.2.0-hfae3067_1.conda + sha256: 0dddd3e274c156a2b8ced3009444d99c04d75ab50a748968b94d3890b6dfab65 + md5: d00d92fbb31f8f9dc2cfb78f44286925 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT + - libgcc >=14 + - libopenvino 2025.2.0 hcd21e76_1 + - libstdcxx >=14 purls: [] - size: 57433 - timestamp: 1743434498161 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.6-he21f813_1.conda - sha256: 608b8c8b0315423e524b48733d91edd43f95cb3354a765322ac306a858c2cd2e - md5: 15a131f30cae36e9a655ca81fee9a285 + run_exports: + weak: + - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 + size: 1123835 + timestamp: 1753203307507 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-frontend-2025.2.0-h38473e3_1.conda + sha256: fcdb5623415c9f5d8c8635f579e5706647e2c97b543ebba621b5b31df096de3d + md5: b42a48c1052c5b576170212c2a834614 depends: - - libgcc >=13 - license: MIT - license_family: MIT + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libopenvino 2025.2.0 hcd21e76_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - libstdcxx >=14 + - snappy >=1.2.2,<1.3.0a0 purls: [] - size: 55847 - timestamp: 1743434586764 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.6-h281671d_1.conda - sha256: 6394b1bc67c64a21a5cc73d1736d1d4193a64515152e861785c44d2cfc49edf3 - md5: 4ca9ea59839a9ca8df84170fab4ceb41 + run_exports: + weak: + - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 + size: 1224816 + timestamp: 1753203320621 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-lite-frontend-2025.2.0-hfae3067_1.conda + sha256: cd4651c37e45fe6779a32ebfb3000fb3e9742409cd9bd0ac141c130b2f8f8d56 + md5: 274b11e7ed763c4964a6b6d2130ec1cb depends: - - __osx >=10.13 - license: MIT - license_family: MIT + - libgcc >=14 + - libopenvino 2025.2.0 hcd21e76_1 + - libstdcxx >=14 purls: [] - size: 51216 - timestamp: 1743434595269 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda - sha256: c6a530924a9b14e193ea9adfe92843de2a806d1b7dbfd341546ece9653129e60 - md5: c215a60c2935b517dcda8cad4705734d + run_exports: + weak: + - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 + size: 456714 + timestamp: 1753203333676 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopus-1.6.1-h80f16a2_0.conda + sha256: 059214f037fa5e51080f5aced39466993b2311a01d871086bd6d2a59bfbf59b5 + md5: c781f98ca7b987f968369bc768b2cd55 depends: - - __osx >=11.0 - license: MIT - license_family: MIT + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 39839 - timestamp: 1743434670405 -- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda - sha256: d3b0b8812eab553d3464bbd68204f007f1ebadf96ce30eb0cbc5159f72e353f5 - md5: 85d8fa5e55ed8f93f874b3b23ed54ec6 + run_exports: + weak: + - libopus >=1.6.1,<2.0a0 + size: 383586 + timestamp: 1768497303687 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.19-he30d5cf_0.conda + sha256: 5d26d751b7cc4b66e28ed1ae75900956600aaa5c5d874d5a8cf106d3aff834d3 + md5: 462239e256bc180c9c45dd049ba797ee depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 44978 - timestamp: 1743435053850 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libflac-1.4.3-h59595ed_0.conda - sha256: 65908b75fa7003167b8a8f0001e11e58ed5b1ef5e98b96ab2ba66d7c1b822c7d - md5: ee48bf17cc83a00f59ca1494d5646869 + run_exports: + weak: + - libpciaccess >=0.19,<0.20.0a0 + size: 30294 + timestamp: 1773533057559 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.58-h1abf092_0.conda + sha256: 483eaa53da40a6a3e558709d9f7b1ca388735364ae21a1ba58cf942514649c92 + md5: f51503ac45a4888bce71af9027a2ecc9 depends: - - gettext >=0.21.1,<1.0a0 - - libgcc-ng >=12 - - libogg 1.3.* - - libogg >=1.3.4,<1.4.0a0 - - libstdcxx-ng >=12 - license: BSD-3-Clause - license_family: BSD + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: zlib-acknowledgement purls: [] - size: 394383 - timestamp: 1687765514062 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libflac-1.4.3-h2f0025b_0.conda - sha256: b54935360349d3418b0663d787f20b3cba0b7ce3fcdf3ba5e7ef02b884759049 - md5: 520b12eab32a92e19b1f239ac545ec03 + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 + size: 341202 + timestamp: 1776315188425 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-6.31.1-h61c7711_5.conda + sha256: 08f0fd616f4482963e27e0cd7fd0d1913f33a153d3d97028beb8bc2892ff30a7 + md5: ce4282bd8d1af8a2c70d4d05223b89f7 depends: - - gettext >=0.21.1,<1.0a0 - - libgcc-ng >=12 - - libogg 1.3.* - - libogg >=1.3.4,<1.4.0a0 - - libstdcxx-ng >=12 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 371550 - timestamp: 1687765491794 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.0-ha770c72_1.conda - sha256: 66c4349ed5a8d4aefab57db275d417192c0e982db5d0631d08cdda1b4db7b5fb - md5: 9a8133acc0913a6f5d83cb8a1bad4f2d + run_exports: + weak: + - libprotobuf >=6.31.1,<6.31.2.0a0 + size: 3897821 + timestamp: 1780003417336 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.58.4-h3ac5bce_3.conda + sha256: e305cf09ec904625a66c7db1305595691c633276b7e34521537cef88edc5249a + md5: b115c14b3919823fbe081366d2b15d86 depends: - - libfreetype6 >=2.14.0 - license: GPL-2.0-only OR FTL + - cairo >=1.18.4,<2.0a0 + - freetype >=2.13.3,<3.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - harfbuzz >=11.0.0,<12.0a0 + - libgcc >=13 + - libglib >=2.84.0,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libxml2 >=2.13.7,<2.14.0a0 + - pango >=1.56.3,<2.0a0 + constrains: + - __glibc >=2.17 + license: LGPL-2.1-or-later purls: [] - size: 7689 - timestamp: 1757461576463 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.0-h8af1aa0_1.conda - sha256: 7a16867f66bb7cc91ac811daf3b9adf34a0cf4d2b70aafff7b5a89cd740b6dec - md5: 29a557dc8cc13abac1f98487558a5883 + run_exports: + weak: + - librsvg >=2.58.4,<3.0a0 + size: 6274749 + timestamp: 1743376660664 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsanitizer-12.4.0-h469570c_2.conda + sha256: b1c8db474fb2e2249544a17c78e6306829bc42ae7dc97e3dcf16291cded7ed9e + md5: 5a300cbd50f7e0fc582d325ac3c28c50 depends: - - libfreetype6 >=2.14.0 - license: GPL-2.0-only OR FTL + - libgcc >=12.4.0 + - libstdcxx >=12.4.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 7739 - timestamp: 1757517667317 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.0-h694c41f_1.conda - sha256: c9e9c347a3577a03fdd370148be3a9f1bf3e05fb5ee007422390b8b9dc56d133 - md5: 5b44e5691928a99306a20aa53afb86fd + run_exports: + weak: + - libsanitizer 12.4.0 + size: 3926612 + timestamp: 1740240236305 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsndfile-1.2.2-h30591a0_2.conda + sha256: f0b6844c09cdec608ca504bd97c5d64a5596a25f66ad806381f9d63dfc89e432 + md5: 362bc94148039b77c6a42b1f7e7ef537 depends: - - libfreetype6 >=2.14.0 - license: GPL-2.0-only OR FTL + - lame >=3.100,<3.101.0a0 + - libflac >=1.5.0,<1.6.0a0 + - libgcc >=14 + - libogg >=1.3.5,<1.4.0a0 + - libopus >=1.5.2,<2.0a0 + - libstdcxx >=14 + - libvorbis >=1.3.7,<1.4.0a0 + - mpg123 >=1.32.9,<1.33.0a0 + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 7781 - timestamp: 1757462057420 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.0-hce30654_1.conda - sha256: e2fd0fd4d389319a88558b2147d9a01b8743d0b51e5cce50034d453f96185e55 - md5: f184605f0569afc90a7821827f91ee50 + run_exports: + weak: + - libsndfile >=1.2.2,<1.3.0a0 + size: 406978 + timestamp: 1765181892661 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.3-h022381a_0.conda + sha256: 26ccc8affb892f2fe7da77beced5232b877f2c5af68ce26c2286eebbf063c096 + md5: 0c469d05c6cc2977d75020dd2d29aa42 depends: - - libfreetype6 >=2.14.0 - license: GPL-2.0-only OR FTL + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing purls: [] - size: 7781 - timestamp: 1757461902487 -- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.0-h57928b3_1.conda - sha256: 78caa501efa6a1b8a7f0ef795ab77a410dc643385fb4c1c06cabc49c3410f064 - md5: d4fb1747ece30e131769299072e239d8 + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 963901 + timestamp: 1782519046518 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-h18c354c_0.conda + sha256: 1e289bcce4ee6a5817a19c66e296f3c644dcfa6e562e5c1cba807270798814e7 + md5: eecc495bcfdd9da8058969656f916cc2 depends: - - libfreetype6 >=2.14.0 - license: GPL-2.0-only OR FTL + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 8128 - timestamp: 1757517996460 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.0-h73754d4_1.conda - sha256: 93b5aa0ae9398d87694cc491b280f0dbb1e4253bc65317559b8e1a1e8d0d1d02 - md5: df6bf113081fdea5b363eb5a7a5ceb69 + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 + size: 311396 + timestamp: 1745609845915 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_19.conda + sha256: 1dadc45e599f510dd5f97141dddcdbb9844d9f1430c1f3a38075cf1c58f87b4e + md5: 543fbc8d71f2a0baf04cf88ce96cb8bb depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libpng >=1.6.50,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 + - libgcc 15.2.0 h8acb6b2_19 constrains: - - freetype >=2.14.0 - license: GPL-2.0-only OR FTL + - libstdcxx-ng ==15.2.0=*_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 386783 - timestamp: 1757461576073 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.0-hdae7a39_1.conda - sha256: fd190007783491af23ae0c9c3406737534834828b97f3e5d559d911f8a4ded49 - md5: 95ac2e908ace9fc6da67b6d385cd2240 + run_exports: {} + size: 5546559 + timestamp: 1778268777463 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.2.0-hdbbeba8_19.conda + sha256: 56b5ec297a988961486694f1c598889c3a697d77a0b42b8cea3faaa12e9bd360 + md5: c82ed61c3ec470c5ec624580e6ba16e4 depends: - - libgcc >=14 - - libpng >=1.6.50,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - constrains: - - freetype >=2.14.0 - license: GPL-2.0-only OR FTL + - libstdcxx 15.2.0 hef695bb_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 423016 - timestamp: 1757517666727 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.0-h6912278_1.conda - sha256: e6278a98c99d8cc0b4409c5cedc1d2905826ae37db62ef7bb65e3cafb860de74 - md5: ebfad8c56f5a71f57ec7c6fb2333458e + run_exports: + strong: + - libstdcxx + size: 27803 + timestamp: 1778268813278 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsystemd0-257.7-h2bb824b_0.conda + sha256: 35ecfc98c22d4f035b051fe72398206607d48944e7bd4f60431e63eb95538e0d + md5: 63b49a2d12a1739f72be430c2ed58727 depends: - - __osx >=10.13 - - libpng >=1.6.50,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - constrains: - - freetype >=2.14.0 - license: GPL-2.0-only OR FTL + - libcap >=2.75,<2.76.0a0 + - libgcc >=13 + - libgcrypt-lib >=1.11.1,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: LGPL-2.1-or-later purls: [] - size: 374870 - timestamp: 1757462055592 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.0-h6da58f4_1.conda - sha256: 2fdd9a9c2118ac0050a38cc9b5e1b0a1b14bf5ffcee9fb726eed33dd99f35b79 - md5: 1ee5067901740fbbc916ae977a5daa1a + run_exports: {} + size: 510879 + timestamp: 1750949944203 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.2-hdb009f0_0.conda + sha256: ef1006578ef7e3f7c420e89d87846213ceb3fdcef2626af2558cbede53d36839 + md5: 20166e2297c1cac346b544d5f6197440 depends: - - __osx >=11.0 - - libpng >=1.6.50,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - constrains: - - freetype >=2.14.0 - license: GPL-2.0-only OR FTL + - lerc >=4.1.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libgcc >=14 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND purls: [] - size: 346703 - timestamp: 1757461898383 -- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.0-hdbac1cb_1.conda - sha256: 377e94973b5b816822424eb75080283b87ae057c157194124c9284a016db8b05 - md5: 10dd24f0c2a81775f09952badfb52019 + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 508982 + timestamp: 1783084925965 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev-257.4-h7b9e449_1.conda + sha256: 46558b5480ab6339cbd7d9c022531e2cc4ae280162622616cdec9863d3ff6ee3 + md5: 3af08e501eb0c1cf33ea0e4c697bb65b depends: - - libpng >=1.6.50,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - freetype >=2.14.0 - license: GPL-2.0-only OR FTL + - libudev1 257.4 h7b9e449_1 + license: LGPL-2.1-or-later purls: [] - size: 340416 - timestamp: 1757517995741 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda - sha256: 0caed73aac3966bfbf5710e06c728a24c6c138605121a3dacb2e03440e8baa6a - md5: 264fbfba7fb20acf3b29cde153e345ce + run_exports: + weak: + - libudev1 >=257.4 + size: 20296 + timestamp: 1741629517133 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev1-257.4-h7b9e449_1.conda + sha256: 3eff7ed43eb78a1f11d51e99ee3a49ed7da508ee1077ea9dbbaa49f9e30b9000 + md5: 2221b6437dcc3859343592eff80dc9a4 depends: - - __glibc >=2.17,<3.0.a0 - - _openmp_mutex >=4.5 - constrains: - - libgomp 15.1.0 h767d61c_5 - - libgcc-ng ==15.1.0=*_5 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libcap >=2.75,<2.76.0a0 + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + run_exports: {} + size: 154086 + timestamp: 1741629515475 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libunwind-1.6.2-h01db608_0.tar.bz2 + sha256: 7862d36ffc9f6b2ed3381ce77c78b9e5691d7353a19dd2050630868e192adf6f + md5: 93b7bbf9099cfe09e67c0abe34bb7885 + depends: + - libgcc-ng >=9.4.0 + - libstdcxx-ng >=9.4.0 + license: MIT + license_family: MIT purls: [] - size: 824191 - timestamp: 1757042543820 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.1.0-he277a41_5.conda - sha256: 99d44310fa159590766d77fdd2d90d26a13406f703591f64f4fb78ec7cfe142e - md5: 1c5fcbb9e0d333dc1d9206b0847e2d93 + run_exports: + weak: + - libunwind >=1.6.2,<1.7.0a0 + size: 90479 + timestamp: 1638452154070 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liburing-2.9-h17cf362_0.conda + sha256: 2922ab8ac4cdd966c1b13dad6ccc4c07c7db2054400843ee443ffd5e7b3f292e + md5: 8eef9430276ab3dbe6ad5b8f23ff5e26 depends: - - _openmp_mutex >=4.5 - constrains: - - libgcc-ng ==15.1.0=*_5 - - libgomp 15.1.0 he277a41_5 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libgcc >=13 + - libstdcxx >=13 + license: MIT + license_family: MIT purls: [] - size: 511668 - timestamp: 1757043002003 -- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.4.0-h1762d19_102.conda - sha256: 4f8486faaa5696a4115a621100acda0f64b49631f2c4bc6046e0f72496348d76 - md5: 5c9ee54252cddf9f83dc48f6ceef0ba4 + run_exports: + weak: + - liburing >=2.9,<2.10.0a0 + size: 123614 + timestamp: 1738605619021 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libusb-1.0.29-h06eaf92_0.conda + sha256: a60aae6b529cd7caa7842f9781ef95b93014e618f71fb005e404af434d76a33f + md5: 9a86e7473e16fe25c5c47f6c1376ac82 depends: - - __unix - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libgcc >=13 + - libudev1 >=257.4 + license: LGPL-2.1-or-later purls: [] - size: 2558737 - timestamp: 1740240187748 -- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.1.0-h4c094af_105.conda - sha256: 714648a02a42bf9c9ee63be4d56ee88de0c66e3b1c8f041995512173b0482278 - md5: a38922dbdf037d78b3d00d6d0a0399da + run_exports: + weak: + - libusb >=1.0.29,<2.0a0 + size: 93129 + timestamp: 1748856228398 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42.2-h1022ec0_0.conda + sha256: 7663489f97c104ae3814db10f384932c74b439f3c1fd4247e4fe3599830c090a + md5: 58fa42bc4bc71fc329889497ec15effb depends: - - __unix - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 2728198 - timestamp: 1757042471636 -- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-12.4.0-h7b3af7c_102.conda - sha256: d6723763270f1ce823b728ae2818994a8920dee11c24ecacd1a100cacc8a99fd - md5: 2cbe18ad69722b174d3f536f92e4fc25 + run_exports: + weak: + - libuuid >=2.42.2,<3.0a0 + size: 43248 + timestamp: 1781625528371 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.52.1-h80f16a2_0.conda + sha256: 3e2ead35f47d01364031f323f1be984018c8f19a3a264f952ddcd043685a1c86 + md5: ac7bcbd2c77691cd6d1ede8c029e8c8a depends: - - __unix - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libgcc >=14 + license: MIT + license_family: MIT purls: [] - size: 311781 - timestamp: 1740240133346 -- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-15.1.0-hd0aa34e_105.conda - sha256: def949291fae8e7fc0b9767901aa636c5db9686f18905e98b0dca93527bf9e1c - md5: eb065dde527d40e21c80c7762d162d51 + run_exports: + weak: + - libuv >=1.52.1,<2.0a0 + size: 456627 + timestamp: 1779396031450 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvorbis-1.3.7-h7ac5ae9_2.conda + sha256: 066708ca7179a1c6e5639d015de7ed6e432b93ad50525843db67d57eb1ba1faf + md5: 9d099329070afe52d797462ca7bf35f3 depends: - - __unix - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libogg + - libstdcxx >=14 + - libgcc >=14 + - libogg >=1.3.5,<1.4.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 2126099 - timestamp: 1757042933559 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_5.conda - sha256: f54bb9c3be12b24be327f4c1afccc2969712e0b091cdfbd1d763fb3e61cda03f - md5: 069afdf8ea72504e48d23ae1171d951c + run_exports: + weak: + - libvorbis >=1.3.7,<1.4.0a0 + size: 289391 + timestamp: 1753879417231 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvpx-1.14.1-h0a1ffab_0.conda + sha256: 918493354f78cb3bb2c3d91264afbcb312b2afe287237e7d1c85ee7e96d15b47 + md5: 3cb63f822a49e4c406639ebf8b5d87d7 depends: - - libgcc 15.1.0 h767d61c_5 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 29187 - timestamp: 1757042549554 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.1.0-he9431aa_5.conda - sha256: 560f36e3dafdc88b7122accbf4310266ca379cff43164008af97310df162ff50 - md5: 4391c20e103a64d4218ec82413407a40 + run_exports: + weak: + - libvpx >=1.14.1,<1.15.0a0 + size: 1211700 + timestamp: 1717859955539 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda + sha256: b03700a1f741554e8e5712f9b06dd67e76f5301292958cd3cb1ac8c6fdd9ed25 + md5: 24e92d0942c799db387f5c9d7b81f1af depends: - - libgcc 15.1.0 he277a41_5 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libgcc >=14 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 29202 - timestamp: 1757043005856 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcrypt-lib-1.11.1-hb9d3cd8_0.conda - sha256: dc9c7d7a6c0e6639deee6fde2efdc7e119e7739a6b229fa5f9049a449bae6109 - md5: 8504a291085c9fb809b66cabd5834307 + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 + size: 359496 + timestamp: 1752160685488 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda + sha256: 461cab3d5650ac6db73a367de5c8eca50363966e862dcf60181d693236b1ae7b + md5: cd14ee5cca2464a425b1dbfc24d90db2 depends: - - __glibc >=2.17,<3.0.a0 - libgcc >=13 - - libgpg-error >=1.55,<2.0a0 - license: LGPL-2.1-or-later + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT purls: [] - size: 590353 - timestamp: 1747060639058 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcrypt-lib-1.11.1-h86ecc28_0.conda - sha256: 5c572886ae3bf8f55fbc8f18275317679b559a9dd00cf1f128d24057dc6de70e - md5: 50df370cbbbcfb4aa67556879e6643a1 + run_exports: + weak: + - libxcb >=1.17.0,<2.0a0 + size: 397493 + timestamp: 1727280745441 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda + sha256: 6b46c397644091b8a26a3048636d10b989b1bf266d4be5e9474bf763f828f41f + md5: b4df5d7d4b63579d081fd3a4cf99740e depends: - - libgcc >=13 - - libgpg-error >=1.55,<2.0a0 + - libgcc-ng >=12 license: LGPL-2.1-or-later purls: [] - size: 652592 - timestamp: 1747060671875 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.25.1-h3f43e3d_1.conda - sha256: 50a9e9815cf3f5bce1b8c5161c0899cc5b6c6052d6d73a4c27f749119e607100 - md5: 2f4de899028319b27eb7a4023be5dfd2 + run_exports: + weak: + - libxcrypt >=4.4.36 + size: 114269 + timestamp: 1702724369203 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.11.0-h95ca766_0.conda + sha256: b23355766092c62b32a7fc8d5729f40d693d2d8491f52e12f3a2f184ec552f6a + md5: 21efa5fee8795bc04bd79bfc02f05c65 depends: - - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libiconv >=1.18,<2.0a0 - license: GPL-3.0-or-later - license_family: GPL - purls: [] - size: 188293 - timestamp: 1753342911214 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-0.25.1-h5ad3122_0.conda - sha256: c8e5590166f4931a3ab01e444632f326e1bb00058c98078eb46b6e8968f1b1e9 - md5: ad7b109fbbff1407b1a7eeaa60d7086a - depends: - - libgcc >=13 - license: GPL-3.0-or-later - license_family: GPL + - libstdcxx >=14 + - libxcb >=1.17.0,<2.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - xkeyboard-config + - xorg-libxau >=1.0.12,<2.0a0 + license: MIT/X11 Derivative + license_family: MIT purls: [] - size: 225352 - timestamp: 1751557555903 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.25.1-h3f43e3d_1.conda - sha256: c7ea10326fd450a2a21955987db09dde78c99956a91f6f05386756a7bfe7cc04 - md5: 3f7a43b3160ec0345c9535a9f0d7908e + run_exports: + weak: + - libxkbcommon >=1.11.0,<2.0a0 + size: 811243 + timestamp: 1754703942072 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.9-he58860d_0.conda + sha256: e7a1c9cf56046b85383f99d0931a3b8a603419c830d45cf1c8691f13aae3f655 + md5: 1e22b9412f9cb2eb7e5a65dd9475534a depends: - - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 - libgcc >=14 - - libgettextpo 0.25.1 h3f43e3d_1 - libiconv >=1.18,<2.0a0 - license: GPL-3.0-or-later - license_family: GPL + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 37407 - timestamp: 1753342931100 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-devel-0.25.1-h5ad3122_0.conda - sha256: a26e1982d062daba5bdd3a90a2ef77b323803d21d27cf4e941135f07037d6649 - md5: 0d9d56bac6e4249da2bede0588ae1c1b - depends: - - libgcc >=13 - - libgettextpo 0.25.1 h5ad3122_0 - license: GPL-3.0-or-later - license_family: GPL + run_exports: + weak: + - libxml2 >=2.13.9,<2.14.0a0 + size: 737147 + timestamp: 1761766137531 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda + sha256: eb111e32e5a7313a5bf799c7fb2419051fa2fe7eff74769fac8d5a448b309f7f + md5: 502006882cf5461adced436e410046d1 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other purls: [] - size: 37460 - timestamp: 1751557569909 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.1.0-hb74de2c_1.conda - sha256: 1f8f5b2fdd0d2559d0f3bade8da8f57e9ee9b54685bd6081c6d6d9a2b0239b41 - md5: 4281bd1c654cb4f5cab6392b3330451f + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 69833 + timestamp: 1774072605429 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lychee-0.23.0-hb434046_0.conda + sha256: 82ac7b7b6d1e9e8c929ac21993a0e036bfd4ee2d3db54913d4635476067901eb + md5: 347b587a5e72646fefa74431216c1c4a depends: - - llvm-openmp >=8.0.0 + - libgcc >=14 + - openssl >=3.5.5,<4.0a0 constrains: - - libgfortran 15.1.0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - __glibc >=2.17 + license: Apache-2.0 OR MIT purls: [] - size: 759679 - timestamp: 1756238772083 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda - sha256: dc2752241fa3d9e40ce552c1942d0a4b5eeb93740c9723873f6fcf8d39ef8d2d - md5: 928b8be80851f5d8ffb016f9c81dae7a + run_exports: {} + size: 5422618 + timestamp: 1771270379661 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.10.0-h5ad3122_1.conda + sha256: 67e55058d275beea76c1882399640c37b5be8be4eb39354c94b610928e9a0573 + md5: 6654e411da94011e8fbe004eacb8fe11 depends: - - __glibc >=2.17,<3.0.a0 - - libglvnd 1.7.0 ha4b6fd6_2 - - libglx 1.7.0 ha4b6fd6_2 - license: LicenseRef-libglvnd + - libgcc >=13 + - libstdcxx >=13 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 134712 - timestamp: 1731330998354 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_2.conda - sha256: 3e954380f16255d1c8ae5da3bd3044d3576a0e1ac2e3c3ff2fe8f2f1ad2e467a - md5: 0d00176464ebb25af83d40736a2cd3bb + run_exports: + weak: + - lz4-c >=1.10.0,<1.11.0a0 + size: 184953 + timestamp: 1733740984533 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py311h2dad8b0_1.conda + sha256: 6e831e7d699fbb5a11f086e40cd9ce5c2cd37c7e92b44835b773a22bec273f51 + md5: eab18e6c4b54950bc224d9dd300611ed depends: - - libglvnd 1.7.0 hd24410f_2 - - libglx 1.7.0 hd24410f_2 - license: LicenseRef-libglvnd + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + run_exports: {} + size: 27101 + timestamp: 1772446335262 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mpg123-1.32.9-h65af167_0.conda + sha256: d65d5a00278544639ba4f99887154be00a1f57afb0b34d80b08e5cba40a17072 + md5: cdf140c7690ab0132106d3bc48bce47d + depends: + - libgcc >=13 + - libstdcxx >=13 + license: LGPL-2.1-only + license_family: LGPL purls: [] - size: 145442 - timestamp: 1731331005019 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda - sha256: 33336bd55981be938f4823db74291e1323454491623de0be61ecbe6cf3a4619c - md5: b8e4c93f4ab70c3b6f6499299627dbdc + run_exports: + weak: + - mpg123 >=1.32.9,<1.33.0a0 + size: 558708 + timestamp: 1730581372400 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.7.1-py311h164a683_0.conda + sha256: 8064a56a1544aa717d9d0c49ba54505623c4e6b4e5e977d4bab2a203611af625 + md5: 43839d26a947bd3e912bb3338fbd8212 depends: - - __glibc >=2.17,<3.0.a0 - - libffi >=3.4.6,<3.5.0a0 - libgcc >=14 - - libiconv >=1.18,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - pcre2 >=10.46,<10.47.0a0 - constrains: - - glib 2.86.0 *_0 - license: LGPL-2.1-or-later + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/multidict?source=hash-mapping + run_exports: {} + size: 103225 + timestamp: 1771610871669 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mypy-1.14.1-py311ha879c10_0.conda + sha256: dc6f8258ebb3539b6ab27b5a78a1d2339b99a19c6396d29ffa3286664b0d671d + md5: e9e333fbbbc7571fb70f8e47edafdddd + depends: + - libgcc >=13 + - mypy_extensions >=1.0.0 + - psutil >=4.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + - typing_extensions >=4.1.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=hash-mapping + run_exports: {} + size: 16065092 + timestamp: 1735600817630 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nasm-2.16.03-h68df207_1.conda + sha256: af354688ee0ab41bd6d538b5c12fc392825da0e9549d5b4256ec13704b177bbd + md5: 277a1d8aa07160de3d02302364fd4dde + depends: + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 3978602 - timestamp: 1757403291664 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.0-h7cdfd2c_0.conda - sha256: c5e9508a9904d01b7f22e14caec099e9ac8d19834f48bd39cd5fca651a8cd542 - md5: 015bb144ea0e07dc75c33f37e1bd718c + run_exports: {} + size: 1332204 + timestamp: 1721654126314 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda + sha256: 369db85c5cd8d99dde364ce70725d76511d9c8199e5b820c740414091bf5bcca + md5: b2a43456aa56fe80c2477a5094899eff depends: - - libffi >=3.4.6,<3.5.0a0 - libgcc >=14 - - libiconv >=1.18,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - pcre2 >=10.46,<10.47.0a0 - constrains: - - glib 2.86.0 *_0 - license: LGPL-2.1-or-later + license: X11 AND BSD-3-Clause purls: [] - size: 4087725 - timestamp: 1757403280137 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.86.0-h7cafd41_0.conda - sha256: 0950997e833d3f6a91200c92a1d602e14728916f95cdcbcdb69b12c462206d5e - md5: 39fb5e0b9b76a73e18581b3839a3af3d + run_exports: + weak: + - ncurses >=6.6,<7.0a0 + size: 960036 + timestamp: 1777422174534 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.11.1-hdd96247_0.conda + sha256: 2ba2e59f619c58d748f4b1b858502587691a7ed0fa9ac2c26ac04091908d95ae + md5: 58f4c67113cda9171e3c03d3e62731e1 depends: - - __osx >=10.13 - - libffi >=3.4.6,<3.5.0a0 - - libiconv >=1.18,<2.0a0 - - libintl >=0.25.1,<1.0a0 - - libzlib >=1.3.1,<2.0a0 - - pcre2 >=10.46,<10.47.0a0 - constrains: - - glib 2.86.0 *_0 - license: LGPL-2.1-or-later + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: Apache-2.0 + license_family: Apache purls: [] - size: 3722414 - timestamp: 1757404071834 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.0-h1bb475b_0.conda - sha256: 92d17f998e14218810493c9190c8721bf7f7f006bfc5c00dbba1cede83c02f1a - md5: 9e065148e6013b7d7cae64ed01ab7081 + run_exports: {} + size: 2398482 + timestamp: 1676839419214 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-22.21.1-h43d1aef_0.conda + sha256: d9ba407d99dfb70cc11a4c39d2efcbbf054ffe85a13c57b4d46265b650779d8d + md5: 68a6ae2c0b9d1e6bce614dac3708b374 depends: - - __osx >=11.0 - - libffi >=3.4.6,<3.5.0a0 - - libiconv >=1.18,<2.0a0 - - libintl >=0.25.1,<1.0a0 + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.28,<3.0.a0 + - icu >=75.1,<76.0a0 + - openssl >=3.5.4,<4.0a0 + - libuv >=1.51.0,<2.0a0 - libzlib >=1.3.1,<2.0a0 - - pcre2 >=10.46,<10.47.0a0 - constrains: - - glib 2.86.0 *_0 - license: LGPL-2.1-or-later + license: MIT + license_family: MIT purls: [] - size: 3701880 - timestamp: 1757404501093 -- conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.0-h5f26cbf_0.conda - sha256: 02c2dcf1818d2614ad4472b196a2a7bb06490cd32fd0f43a30997097afca3a12 - md5: 30a7c2c9d7ba29bb1354cd68fcca9cda + run_exports: + weak: + - nodejs >=22.21.1,<23.0a0 + size: 25274442 + timestamp: 1765444714460 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openh264-2.6.0-h663e864_1.conda + sha256: c90d20ddcf28537ea6c1bd1c26a7abcba6baf9d7cdec493daa04bf0a968d1264 + md5: 0d86d4becd3cd1ce48011f71099211be depends: - - libffi >=3.4.6,<3.5.0a0 - - libiconv >=1.18,<2.0a0 - - libintl >=0.22.5,<1.0a0 - - libzlib >=1.3.1,<2.0a0 - - pcre2 >=10.46,<10.47.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - constrains: - - glib 2.86.0 *_0 - license: LGPL-2.1-or-later + - libgcc >=14 + - libstdcxx >=14 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 3794081 - timestamp: 1757403780432 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda - sha256: 1175f8a7a0c68b7f81962699751bb6574e6f07db4c9f72825f978e3016f46850 - md5: 434ca7e50e40f4918ab701e3facd59a0 + run_exports: + weak: + - openh264 >=2.6.0,<2.6.1.0a0 + size: 795565 + timestamp: 1782685979198 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.3-h546c87b_0.conda + sha256: da4a5df42614166b69c2f6d8602fc1425f7aaa699f77c3bafb5c7fe69b3d9fb7 + md5: fa6260b3e6eababf6ca85a7eb3336383 depends: - - __glibc >=2.17,<3.0.a0 - license: LicenseRef-libglvnd + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache purls: [] - size: 132463 - timestamp: 1731330968309 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_2.conda - sha256: 57ec3898a923d4bcc064669e90e8abfc4d1d945a13639470ba5f3748bd3090da - md5: 9e115653741810778c9a915a2f8439e7 - license: LicenseRef-libglvnd + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 3704664 + timestamp: 1781069675555 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda + sha256: dd36cd5b6bc1c2988291a6db9fa4eb8acade9b487f6f1da4eaa65a1eebb0a12d + md5: a22cc88bf6059c9bcc158c94c9aab5b8 + depends: + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libgcc >=13 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later purls: [] - size: 152135 - timestamp: 1731330986070 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda - sha256: 2d35a679624a93ce5b3e9dd301fff92343db609b79f0363e6d0ceb3a6478bfa7 - md5: c8013e438185f33b13814c5c488acd5c + run_exports: + weak: + - pango >=1.56.4,<2.0a0 + size: 468811 + timestamp: 1751293869070 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.47-hf841c20_0.conda + sha256: 04df2cee95feba440387f33f878e9f655521e69f4be33a0cd637f07d3d81f0f9 + md5: 1a30c42e32ca0ea216bd0bfe6f842f0b depends: - - __glibc >=2.17,<3.0.a0 - - libglvnd 1.7.0 ha4b6fd6_2 - - xorg-libx11 >=1.8.10,<2.0a0 - license: LicenseRef-libglvnd + - bzip2 >=1.0.8,<2.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 75504 - timestamp: 1731330988898 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_2.conda - sha256: 6591af640cb05a399fab47646025f8b1e1a06a0d4bbb4d2e320d6629b47a1c61 - md5: 1d4269e233636148696a67e2d30dad2a + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 + size: 1166552 + timestamp: 1763655534263 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_2.conda + sha256: 414acad3e460cfcf6bf3a764c4b4837359bda9fdc350a85b560e344a9e2306a5 + md5: af8ab93369e53542b135dab29eca49e8 depends: - - libglvnd 1.7.0 hd24410f_2 - - xorg-libx11 >=1.8.9,<2.0a0 - license: LicenseRef-libglvnd + - libstdcxx >=14 + - libgcc >=14 + license: MIT + license_family: MIT purls: [] - size: 77736 - timestamp: 1731330998960 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda - sha256: 125051d51a8c04694d0830f6343af78b556dd88cc249dfec5a97703ebfb1832d - md5: dcd5ff1940cd38f6df777cac86819d60 + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 304146 + timestamp: 1784286832656 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pkgconf-3.0.3-h80f16a2_0.conda + sha256: 6f560c5148ad9d47e7253f6206f950a75a8be184f4e86fc7efc66f6f83fbfbe8 + md5: f024d1c92fa17ec58a0ab13502dc31f2 depends: - - __glibc >=2.17,<3.0.a0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 447215 - timestamp: 1757042483384 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.1.0-he277a41_5.conda - sha256: 3573b6f0b9037ee69c1fb39a6614c05f919191149196f2b33fb2acdf7caece59 - md5: da1eb826fad1995cb91f385da6efb919 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - libgcc >=14 + license: MIT + license_family: MIT purls: [] - size: 450637 - timestamp: 1757042941171 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgpg-error-1.55-h3f2d84a_0.conda - sha256: 697334de4786a1067ea86853e520c64dd72b11a05137f5b318d8a444007b5e60 - md5: 2bd47db5807daade8500ed7ca4c512a4 + run_exports: + weak: + - pkgconf >=3.0.3,<4.0a0 + size: 144448 + timestamp: 1784088514572 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/prettier-3.8.1-h1e5041c_0.conda + sha256: c074be299472d8c75fd7358854f52e9a24bed29355767e87e50c97de7494f77f + md5: 2950e1f1239c6577eb31ed4251573da4 depends: - - libstdcxx >=13 - - libgcc >=13 - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-only + - nodejs + - nodejs >=22.21.1,<23.0a0 + license: MIT + license_family: MIT purls: [] - size: 312184 - timestamp: 1745575272035 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgpg-error-1.55-h5ad3122_0.conda - sha256: a744c0a137a084af7cee4a33de9bffb988182b5be4edb8a45d51d2a1efd3724c - md5: 39f742598d0f18c8e1cb01712bc03ee8 + run_exports: {} + size: 1104400 + timestamp: 1769199247268 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.5.2-py311h164a683_0.conda + sha256: 96e2c2843e690b15e96c4e7eebad72691d1a959727a93658534f20915089093e + md5: a669c6e71b76f7b7bd1c2ad2c645ed6d depends: - - libgcc >=13 - - libstdcxx >=13 - - libgcc >=13 - license: LGPL-2.1-only - purls: [] - size: 327973 - timestamp: 1745575312848 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.12.1-default_h3d81e11_1000.conda - sha256: eecaf76fdfc085d8fed4583b533c10cb7f4a6304be56031c43a107e01a56b7e2 - md5: d821210ab60be56dd27b5525ed18366d + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/propcache?source=hash-mapping + run_exports: {} + size: 52226 + timestamp: 1780037783775 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/protobuf-6.31.1-py311he3e547a_2.conda + sha256: 40a160e81173f0fdd19bb55eabc99974a35a9749fd2bf0217d6f9d17c058bee3 + md5: b77baff6dd1ceb08af318c066617d647 depends: - - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 - libgcc >=14 - libstdcxx >=14 - - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + constrains: + - libprotobuf 6.31.1 license: BSD-3-Clause license_family: BSD - purls: [] - size: 2450422 - timestamp: 1752761850672 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libhwloc-2.12.1-default_h6f258fa_1000.conda - sha256: d25c10fd894ce6c5d3eba5667bef98be0e82d8e4d2ec20425d89a5baee715304 - md5: eea9ada077bda5f4a32889b9285af9c0 + purls: + - pkg:pypi/protobuf?source=hash-mapping + run_exports: {} + size: 496479 + timestamp: 1760393576155 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.2.2-py311h51cfe5d_0.conda + sha256: b03dfb4dc12a3744f377d94bd267e1dc82dda4764b3d35d081cdb2482207565a + md5: a1a2849301364b492841e875fcaaf1e4 depends: + - python + - python 3.11.* *_cpython - libgcc >=14 - - libstdcxx >=14 - - libxml2 >=2.13.8,<2.14.0a0 + - python_abi 3.11.* *_cp311 license: BSD-3-Clause license_family: BSD - purls: [] - size: 2468653 - timestamp: 1752761831524 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.12.1-default_h8c32e24_1000.conda - sha256: 766146cbbfc1ec400a2b8502a30682d555db77a05918745828392839434b829b - md5: 622d2b076d7f0588ab1baa962209e6dd + purls: + - pkg:pypi/psutil?source=hash-mapping + run_exports: {} + size: 236635 + timestamp: 1769678160506 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda + sha256: 977dfb0cb3935d748521dd80262fe7169ab82920afd38ed14b7fee2ea5ec01ba + md5: bb5a90c93e3bac3d5690acf76b4a6386 depends: - - __osx >=10.13 - - libcxx >=19 - - libxml2 >=2.13.8,<2.14.0a0 - license: BSD-3-Clause - license_family: BSD + - libgcc >=13 + license: MIT + license_family: MIT purls: [] - size: 2381708 - timestamp: 1752761786288 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwloc-2.12.1-default_h88f92a7_1000.conda - sha256: 79a02778b06d9f22783050e5565c4497e30520cf2c8c29583c57b8e42068ae86 - md5: b32f2f83be560b0fb355a730e4057ec1 + run_exports: {} + size: 8342 + timestamp: 1726803319942 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pugixml-1.15-h6ef32b0_0.conda + sha256: adc17205a87e064508d809fe5542b7cf49f9b9a458418f8448e2fc895fcd04f3 + md5: 53e14f45d38558aa2b9a15b07416e472 depends: - - __osx >=11.0 - - libcxx >=19 - - libxml2 >=2.13.8,<2.14.0a0 - license: BSD-3-Clause - license_family: BSD + - libgcc >=13 + - libstdcxx >=13 + license: MIT + license_family: MIT purls: [] - size: 2355380 - timestamp: 1752761771779 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda - sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f - md5: 915f5995e94f60e9a4826e0b0920ee88 + run_exports: + weak: + - pugixml >=1.15,<1.16.0a0 + size: 113424 + timestamp: 1737355438448 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pulseaudio-client-17.0-h77cf2aa_2.conda + sha256: 588c9ba305e8ece39357b36174a371671916e878b98fdd7521296008a895adb1 + md5: 50f9b250973773b3a9888b57893cbdcd depends: - - __glibc >=2.17,<3.0.a0 + - dbus >=1.16.2,<2.0a0 - libgcc >=14 - license: LGPL-2.1-only + - libglib >=2.86.0,<3.0a0 + - libiconv >=1.18,<2.0a0 + - libsndfile >=1.2.2,<1.3.0a0 + - libsystemd0 >=257.7 + - libxcb >=1.17.0,<2.0a0 + constrains: + - pulseaudio 17.0 *_2 + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 790176 - timestamp: 1754908768807 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda - sha256: 1473451cd282b48d24515795a595801c9b65b567fe399d7e12d50b2d6cdb04d9 - md5: 5a86bf847b9b926f3a4f203339748d78 + run_exports: + weak: + - pulseaudio-client >=17.0,<17.1.0a0 + size: 767096 + timestamp: 1757472924483 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.11.15-h53314ec_1_cpython.conda + build_number: 1 + sha256: b3f14348c215820a932518ec56a734cd5762489cb9ed2ada932b4436e20aa0ae + md5: c93c2ea04a38e1c7d3d36f70de2b834a depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-aarch64 >=2.36.1 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 - libgcc >=14 - license: LGPL-2.1-only + - liblzma >=5.8.3,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libuuid >=2.42.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 purls: [] - size: 791226 - timestamp: 1754910975665 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda - sha256: a1c8cecdf9966921e13f0ae921309a1f415dfbd2b791f2117cf7e8f5e61a48b6 - md5: 210a85a1119f97ea7887188d176db135 + run_exports: + weak: + - python_abi 3.11.* *_cp311 + noarch: + - python + size: 15468763 + timestamp: 1781148364174 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py311h164a683_1.conda + sha256: e3175f507827cd575b5a7b7b50058cd253977f8286079cb3a6bf0aeaa878071b + md5: 7e1888f50cc191b7817848dbf6f90590 depends: - - __osx >=10.13 - license: LGPL-2.1-only + - libgcc >=14 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + run_exports: {} + size: 201473 + timestamp: 1770223445971 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + sha256: fe695f9d215e9a2e3dd0ca7f56435ab4df24f5504b83865e3d295df36e88d216 + md5: 3d49cad61f829f4f0e0611547a9cda12 + depends: + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL purls: [] - size: 737846 - timestamp: 1754908900138 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda - sha256: de0336e800b2af9a40bdd694b03870ac4a848161b35c8a2325704f123f185f03 - md5: 4d5a7445f0b25b6a3ddbb56e790f5251 + run_exports: + weak: + - readline >=8.3,<9.0a0 + size: 357597 + timestamp: 1765815673644 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rhash-1.4.6-h86ecc28_1.conda + sha256: 0fe6f40213f2d8af4fcb7388eeb782a4e496c8bab32c189c3a34b37e8004e5a4 + md5: 745d02c0c22ea2f28fbda2cb5dbec189 depends: - - __osx >=11.0 - license: LGPL-2.1-only + - libgcc >=13 + license: MIT + license_family: MIT purls: [] - size: 750379 - timestamp: 1754909073836 -- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 - md5: 64571d1dd6cdcfa25d0664a5950fdaa2 + run_exports: + weak: + - rhash >=1.4.6,<2.0a0 + size: 207475 + timestamp: 1748644952027 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-2026.6.3-py311h3b69377_0.conda + sha256: 89a978d723bf83affa24ae0b6f28caf3d66360db658309e1bcf7d62f360ac4cf + md5: da8adbb67521584fe0f37c2c43f4db77 depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: LGPL-2.1-only + - python + - libgcc >=14 + - python_abi 3.11.* *_cp311 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + run_exports: {} + size: 295876 + timestamp: 1782831351554 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruff-0.15.7-h9f438e6_1.conda + noarch: python + sha256: 12f3e09ad65e1b90ea9f9364198ceec9181bb812a4b36dece3d7b3f1f9259a84 + md5: b46ef22af5048a38a3051707e5db6ee1 + depends: + - python + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=hash-mapping + run_exports: {} + size: 8811982 + timestamp: 1774012576944 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl2-2.32.54-h5ad3122_0.conda + sha256: d83c13fc35ed447d186150d32b8bc48bdd73a047280ba6e06f151d4cce52639d + md5: 6b38021cb802b4e5bede7fe38c547883 + depends: + - libstdcxx >=13 + - libgcc >=13 + - libegl >=1.7.0,<2.0a0 + - libgl >=1.7.0,<2.0a0 + - sdl3 >=3.2.10,<4.0a0 + license: Zlib purls: [] - size: 696926 - timestamp: 1754909290005 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda - sha256: 8c352744517bc62d24539d1ecc813b9fdc8a785c780197c5f0b84ec5b0dfe122 - md5: a8e54eefc65645193c46e8b180f62d22 + run_exports: + weak: + - sdl2 >=2.32.54,<3.0a0 + size: 597383 + timestamp: 1745799910298 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl3-3.2.14-h7e2c5d6_0.conda + sha256: 83e07e24de6018133139d21e33cc61623864144cc1bc279d4affaf8d773fa52b + md5: ffe115848f7f2406decbe70ff4530c06 depends: - - __osx >=10.13 - - libiconv >=1.18,<2.0a0 - license: LGPL-2.1-or-later + - libstdcxx >=13 + - libgcc >=13 + - libxkbcommon >=1.9.2,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - libgl >=1.7.0,<2.0a0 + - libusb >=1.0.28,<2.0a0 + - pulseaudio-client >=17.0,<17.1.0a0 + - liburing >=2.9,<2.10.0a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - libudev1 >=257.4 + - libegl >=1.7.0,<2.0a0 + - libdrm >=2.4.124,<2.5.0a0 + - libunwind >=1.6.2,<1.7.0a0 + - dbus >=1.13.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 + - wayland >=1.23.1,<2.0a0 + license: Zlib purls: [] - size: 96909 - timestamp: 1753343977382 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda - sha256: 99d2cebcd8f84961b86784451b010f5f0a795ed1c08f1e7c76fbb3c22abf021a - md5: 5103f6a6b210a3912faf8d7db516918c + run_exports: + weak: + - sdl3 >=3.2.14,<4.0a0 + size: 1897812 + timestamp: 1747327559219 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.2.2-he774c54_1.conda + sha256: a8a79c53852fb07286407907402caa5a96b6e22b518c4f010be40647f9ee3726 + md5: 3dec912091fb88614afa0af2712c1362 depends: - - __osx >=11.0 - - libiconv >=1.18,<2.0a0 - license: LGPL-2.1-or-later + - libgcc >=14 + - libstdcxx >=14 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 90957 - timestamp: 1751558394144 -- conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - sha256: c7e4600f28bcada8ea81456a6530c2329312519efcf0c886030ada38976b0511 - md5: 2cf0cf76cc15d360dfa2f17fd6cf9772 + run_exports: + weak: + - snappy >=1.2.2,<1.3.0a0 + size: 47096 + timestamp: 1762948094646 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/svt-av1-3.1.2-hfae3067_0.conda + sha256: e4b482062da7cf259f21465274a0f3613d1dbd8ea649aca6072625f5038ac40d + md5: 7602d3004ed53b3f8e5e0e04e5de4de7 depends: - - libiconv >=1.17,<2.0a0 - license: LGPL-2.1-or-later + - libgcc >=14 + - libstdcxx >=14 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 95568 - timestamp: 1723629479451 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda - sha256: 98b399287e27768bf79d48faba8a99a2289748c65cd342ca21033fab1860d4a4 - md5: 9fa334557db9f63da6c9285fd2a48638 + run_exports: + weak: + - svt-av1 >=3.1.2,<3.1.3.0a0 + size: 2106252 + timestamp: 1756090698097 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/taplo-0.9.1-hb8f9562_0.conda + sha256: dbcd4fa63270cef1c777cdbba2b697845704470bb7f3011e2b1b318fb9eb59b7 + md5: 0cf5ee26646e7780a0f89e0fbeac329e depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - jpeg <0.0.0a - license: IJG AND BSD-3-Clause AND Zlib + - libgcc-ng >=12 + - openssl >=3.2.1,<4.0a0 + license: MIT + license_family: MIT purls: [] - size: 628947 - timestamp: 1745268527144 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.0-h86ecc28_0.conda - sha256: c7e4f017eeadcabb30e2a95dae95aad27271d633835e55e5dae23c932ae7efab - md5: a689388210d502364b79e8b19e7fa2cb + run_exports: {} + size: 3717546 + timestamp: 1710801928738 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tbb-2022.3.0-h0eac15c_1.conda + sha256: 3fd3d1ba6b81c5edee8d8fa0d2757f7ba3bf4d4a8ecc68f515c90e737eaa02e4 + md5: eda1e9439d903e3fdd7ff9e086da2018 depends: - - libgcc >=13 - constrains: - - jpeg <0.0.0a - license: IJG AND BSD-3-Clause AND Zlib + - libgcc >=14 + - libhwloc >=2.12.1,<2.12.2.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 653054 - timestamp: 1745268199701 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.1.0-h6e16a3a_0.conda - sha256: 9c0009389c1439ec96a08e3bf7731ac6f0eab794e0a133096556a9ae10be9c27 - md5: 87537967e6de2f885a9fcebd42b7cb10 + run_exports: {} + size: 144223 + timestamp: 1762511489745 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5cf4473_3.conda + build_number: 103 + sha256: cd51fbda051a9f3679d10ef4a94cd1ff38c10533b82845dadce8ba87245ba4ce + md5: 89e78452e06563964e419059ee45584a depends: - - __osx >=10.13 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 constrains: - - jpeg <0.0.0a - license: IJG AND BSD-3-Clause AND Zlib + - xorg-libx11 >=1.8.13,<2.0a0 + license: TCL purls: [] - size: 586456 - timestamp: 1745268522731 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda - sha256: 78df2574fa6aa5b6f5fc367c03192f8ddf8e27dc23641468d54e031ff560b9d4 - md5: 01caa4fbcaf0e6b08b3aef1151e91745 + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3683040 + timestamp: 1784229053797 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ty-0.0.31-h47ce4e6_0.conda + noarch: python + sha256: 95cc44da5bff86fdfcd5db03f79424c9a8e90cce961ab553348f2a46260ff671 + md5: 5a932a9579df431f51b59e3587206a84 depends: - - __osx >=11.0 + - python + - libgcc >=14 + - _python_abi3_support 1.* + - cpython >=3.10 constrains: - - jpeg <0.0.0a - license: IJG AND BSD-3-Clause AND Zlib - purls: [] - size: 553624 - timestamp: 1745268405713 -- conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda - sha256: e61b0adef3028b51251124e43eb6edf724c67c0f6736f1628b02511480ac354e - md5: 7c51d27540389de84852daa1cdb9c63c + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ty?source=hash-mapping + run_exports: {} + size: 9121062 + timestamp: 1776273658766 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/typos-1.48.0-h069e38c_0.conda + sha256: 3141ffeddc27e9a98298dade1faf6c233701bdff47209f3871d6688188adeed5 + md5: d2ff6fd5e80452e6b88ecfd5da3f4a73 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - libgcc >=14 constrains: - - jpeg <0.0.0a - license: IJG AND BSD-3-Clause AND Zlib + - __glibc >=2.17 + license: MIT OR Apache-2.0 purls: [] - size: 838154 - timestamp: 1745268437136 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm16-16.0.6-ha7bfdaf_4.conda - sha256: 421fed3a23f5657c2f6ab672b253ae3fce6039c109be6484bd9ce6a16e90bc2b - md5: 5cf4080515925080bff5ac96d82a3bfa + run_exports: {} + size: 3903453 + timestamp: 1782859635286 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wasm-pack-0.15.0-h069e38c_0.conda + sha256: 9e964e24d9f677fcdf062c9d5b3789e070dc6cbebaa35c546b9aae7f1844dbeb + md5: 18fa656fab4fa678356f5ade7982ddfc depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - libxml2 >=2.13.5,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: MIT OR Apache-2.0 purls: [] - size: 35234903 - timestamp: 1739806428307 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm16-16.0.6-h2edbd07_4.conda - sha256: 058ff3b819b7d3066c1059ad17b730868c1e6e3baf732b91e6a945dc01f821ea - md5: 680291df42c567776f99f5a8335515b5 + run_exports: {} + size: 2131446 + timestamp: 1780752599860 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.26.0-h4f8a99f_0.conda + sha256: 27d8782a2f6cd193f2f446de829f7a92f3b34cc885e2d59d4b8326934023d8fd + md5: 8da6920e59279dedb8c737cff347cfee depends: - - libgcc >=13 - - libstdcxx >=13 - - libxml2 >=2.13.5,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT purls: [] - size: 34544032 - timestamp: 1739798290457 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda - sha256: ad848dc0bb02b1dbe54324ee5700b050a2e5f63c095f5229b2de58249a3e268e - md5: 8fd56c0adc07a37f93bd44aa61a97c90 + run_exports: + weak: + - wayland >=1.26.0,<2.0a0 + size: 341752 + timestamp: 1784249192116 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x264-1!164.3095-h4e544f5_2.tar.bz2 + sha256: b48f150db8c052c197691c9d76f59e252d3a7f01de123753d51ebf2eed1cf057 + md5: 0efaf807a0b5844ce5f605bd9b668281 depends: - - libcxx >=16 - - libxml2 >=2.12.1,<2.14.0a0 - - libzlib >=1.2.13,<2.0.0a0 - - zstd >=1.5.5,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc-ng >=12 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 25196932 - timestamp: 1701379796962 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm16-16.0.6-hc4b4ae8_4.conda - sha256: 1cdaa0cf825d75758e67a2f0f3118a770272d0f8b30388b897a00730ac830484 - md5: 88bab67516b973b3f1a72021d2ac2ab6 + run_exports: + weak: + - x264 >=1!164.3095,<1!165 + size: 1000661 + timestamp: 1660324722559 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x265-3.5-hdd96247_3.tar.bz2 + sha256: cb2227f2441499900bdc0168eb423d7b2056c8fd5a3541df4e2d05509a88c668 + md5: 786853760099c74a1d4f0da98dd67aea depends: - - __osx >=11.0 - - libcxx >=18 - - libxml2 >=2.13.5,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc-ng >=10.3.0 + - libstdcxx-ng >=10.3.0 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 23532169 - timestamp: 1739798547548 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm21-21.1.0-hecd9e04_0.conda - sha256: d190f1bf322149321890908a534441ca2213a9a96c59819da6cabf2c5b474115 - md5: 9ad637a7ac380c442be142dfb0b1b955 + run_exports: + weak: + - x265 >=3.5,<3.6.0a0 + size: 1018181 + timestamp: 1646610147365 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.48-h80f16a2_0.conda + sha256: 96078068df25ddccc60958be740e6fa99efb1e0fa2dae2f84e775201bf84d70c + md5: 3dbc6d9e1f8a8768e7ef9f57585a43ca depends: - - __glibc >=2.17,<3.0.a0 - libgcc >=14 - - libstdcxx >=14 - - libxml2 >=2.13.8,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - xorg-libx11 >=1.8.13,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 44363060 - timestamp: 1756291822911 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm21-21.1.0-h2b567e5_0.conda - sha256: 1a393ebae1d2014dc350d472836f5087bd2040d48fa9410952cfc2faa6fd817e - md5: 2f7ec415da2566effa22beb4ba47bfb4 + run_exports: {} + size: 442725 + timestamp: 1782027381059 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda + sha256: a2ba1864403c7eb4194dacbfe2777acf3d596feae43aada8d1b478617ce45031 + md5: c8d8ec3e00cd0fd8a231789b91a7c5b7 depends: - - libgcc >=14 - - libstdcxx >=14 - - libxml2 >=2.13.8,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc >=13 + license: MIT + license_family: MIT purls: [] - size: 43185742 - timestamp: 1756287405599 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm21-21.1.0-h9b4ebcc_0.conda - sha256: fa24fbdeeb3cd8861c15bb06019d6482c7f686304f0883064d91f076e331fc25 - md5: 49233c30d20fbe080285fd286e9267fb + run_exports: + weak: + - xorg-libice >=1.1.2,<2.0a0 + size: 60433 + timestamp: 1734229908988 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda + sha256: b86a819cd16f90c01d9d81892155126d01555a20dabd5f3091da59d6309afd0a + md5: 2d1409c50882819cb1af2de82e2b7208 depends: - - __osx >=10.13 - - libcxx >=19 - - libxml2 >=2.13.8,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - xorg-libice >=1.1.2,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 31441188 - timestamp: 1756284335102 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.0-h846d351_0.conda - sha256: 4b22efda81b517da3f54dc138fd03a9f9807bdbc8911273777ae0182aab0b115 - md5: a8ec02cc70f4c56b5daaa5be62943065 + run_exports: + weak: + - xorg-libsm >=1.2.6,<2.0a0 + size: 28701 + timestamp: 1741897678254 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.13-h63a1b12_0.conda + sha256: cf886160e2ff580d77f7eb8ec1a77c41c2c5b05343e329bc35f0ddf40b8d92ab + md5: 22dd10425ef181e80e130db50675d615 depends: - - __osx >=11.0 - - libcxx >=19 - - libxml2 >=2.13.8,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: Apache + - libgcc >=14 + - libxcb >=1.17.0,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 29414704 - timestamp: 1756282753920 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 - md5: 1a580f7796c7bf6393fddb8bbbde58dc + run_exports: + weak: + - xorg-libx11 >=1.8.13,<2.0a0 + size: 869058 + timestamp: 1770819244991 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-he30d5cf_1.conda + sha256: e9f6e931feeb2f40e1fdbafe41d3b665f1ab6cb39c5880a1fcf9f79a3f3c84a5 + md5: 1c246e1105000c3660558459e2fd6d43 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - xz 5.8.1.* - license: 0BSD + - libgcc >=14 + license: MIT + license_family: MIT purls: [] - size: 112894 - timestamp: 1749230047870 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.1-h86ecc28_2.conda - sha256: 498ea4b29155df69d7f20990a7028d75d91dbea24d04b2eb8a3d6ef328806849 - md5: 7d362346a479256857ab338588190da0 + run_exports: + weak: + - xorg-libxau >=1.0.12,<2.0a0 + size: 16317 + timestamp: 1762977521691 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda + sha256: c5d3692520762322a9598e7448492309f5ee9d8f3aff72d787cf06e77c42507f + md5: f2054759c2203d12d0007005e1f1296d depends: - libgcc >=13 - constrains: - - xz 5.8.1.* - license: 0BSD - purls: [] - size: 125103 - timestamp: 1749232230009 -- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.1-hd471939_2.conda - sha256: 7e22fd1bdb8bf4c2be93de2d4e718db5c548aa082af47a7430eb23192de6bb36 - md5: 8468beea04b9065b9807fc8b9cdc5894 - depends: - - __osx >=10.13 - constrains: - - xz 5.8.1.* - license: 0BSD + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT purls: [] - size: 104826 - timestamp: 1749230155443 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285 - md5: d6df911d4564d77c4374b02552cb17d1 + run_exports: + weak: + - xorg-libxcursor >=1.2.3,<2.0a0 + size: 34596 + timestamp: 1730908388714 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-he30d5cf_1.conda + sha256: 128d72f36bcc8d2b4cdbec07507542e437c7d67f677b7d77b71ed9eeac7d6df1 + md5: bff06dcde4a707339d66d45d96ceb2e2 depends: - - __osx >=11.0 - constrains: - - xz 5.8.1.* - license: 0BSD + - libgcc >=14 + license: MIT + license_family: MIT purls: [] - size: 92286 - timestamp: 1749230283517 -- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - sha256: 55764956eb9179b98de7cc0e55696f2eff8f7b83fc3ebff5e696ca358bca28cc - md5: c15148b2e18da456f5108ccb5e411446 + run_exports: + weak: + - xorg-libxdmcp >=1.1.5,<2.0a0 + size: 21039 + timestamp: 1762979038025 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.7-he30d5cf_0.conda + sha256: db2188bc0d844d4e9747bac7f6c1d067e390bd769c5ad897c93f1df759dc5dba + md5: fb42b683034619915863d68dd9df03a3 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - xz 5.8.1.* - license: 0BSD + - libgcc >=14 + - xorg-libx11 >=1.8.12,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 104935 - timestamp: 1749230611612 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-devel-5.8.1-hb9d3cd8_2.conda - sha256: 329e66330a8f9cbb6a8d5995005478188eb4ba8a6b6391affa849744f4968492 - md5: f61edadbb301530bd65a32646bd81552 + run_exports: + weak: + - xorg-libxext >=1.3.7,<2.0a0 + size: 52409 + timestamp: 1769446753771 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda + sha256: 8cb9c88e25c57e47419e98f04f9ef3154ad96b9f858c88c570c7b91216a64d0e + md5: e8b4056544341daf1d415eaeae7a040c depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - liblzma 5.8.1 hb9d3cd8_2 - license: 0BSD + - libgcc >=14 + - xorg-libx11 >=1.8.12,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 439868 - timestamp: 1749230061968 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-devel-5.8.1-h86ecc28_2.conda - sha256: 3bd4de89c0cf559a944408525460b3de5495b4c21fb92c831ff0cc96398a7272 - md5: 236d1ebc954a963b3430ce403fbb0896 + run_exports: + weak: + - xorg-libxfixes >=6.0.2,<7.0a0 + size: 20704 + timestamp: 1759284028146 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda + sha256: ffd77ee860c9635a28cfda46163dcfe9224dc6248c62404c544ae6b564a0be1f + md5: ae2c2dd0e2d38d249887727db2af960e depends: - libgcc >=13 - - liblzma 5.8.1 h86ecc28_2 - license: 0BSD + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 440873 - timestamp: 1749232400775 -- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.1-hd471939_2.conda - sha256: a020ad9f1e27d4f7a522cbbb9613b99f64a5cc41f80caf62b9fdd1cf818acf18 - md5: 2e16f5b4f6c92b96f6a346f98adc4e3e + run_exports: + weak: + - xorg-libxrender >=0.9.12,<0.10.0a0 + size: 33649 + timestamp: 1734229123157 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.8.3-hd704e39_0.conda + sha256: a12cc406fc348d3b3a6c2bc6e2a1a82b2f6865d67f1f41498f7301aea77ae9c3 + md5: 515cd9d9970d3addd52b8f85ee1beab9 depends: - - __osx >=10.13 - - liblzma 5.8.1 hd471939_2 - license: 0BSD + - libgcc >=14 + - liblzma 5.8.3 he30d5cf_0 + - liblzma-devel 5.8.3 he30d5cf_0 + - xz-gpl-tools 5.8.3 hd704e39_0 + - xz-tools 5.8.3 he30d5cf_0 + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later purls: [] - size: 116356 - timestamp: 1749230171181 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.1-h39f12f2_2.conda - sha256: 974804430e24f0b00f3a48b67ec10c9f5441c9bb3d82cc0af51ba45b8a75a241 - md5: 1201137f1a5ec9556032ffc04dcdde8d + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 24330 + timestamp: 1775828875434 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-gpl-tools-5.8.3-hd704e39_0.conda + sha256: ad964969ff1a72d8cc8d60a6bbd66ca5f79c00f2e3dec612caae1c2c5b9e2d16 + md5: f357f5285cc0c4df2305dd6ccb977ec5 depends: - - __osx >=11.0 - - liblzma 5.8.1 h39f12f2_2 - license: 0BSD + - libgcc >=14 + - liblzma 5.8.3 he30d5cf_0 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later purls: [] - size: 116244 - timestamp: 1749230297170 -- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.1-h2466b09_2.conda - sha256: 1ccff927a2d768403bad85e36ca3e931d96890adb4f503e1780c3412dd1e1298 - md5: 42c90c4941c59f1b9f8fab627ad8ae76 + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 34353 + timestamp: 1775828661986 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-tools-5.8.3-he30d5cf_0.conda + sha256: 86d48bb0ac214d1d72e05e765523d5847f3ccd872c142304d3c307cc95c93881 + md5: 6cf7b9f879c7665749c7ddd45cc5c163 depends: - - liblzma 5.8.1 h2466b09_2 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: 0BSD + - libgcc >=14 + - liblzma 5.8.3 he30d5cf_0 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later purls: [] - size: 129344 - timestamp: 1749230637001 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - sha256: a4a7dab8db4dc81c736e9a9b42bdfd97b087816e029e221380511960ac46c690 - md5: b499ce4b026493a13774bcf0f4c33849 + run_exports: {} + size: 102970 + timestamp: 1775828449899 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda + sha256: 66265e943f32ce02396ad214e27cb35f5b0490b3bd4f064446390f9d67fa5d88 + md5: 032d8030e4a24fe1f72c74423a46fb88 depends: - - __glibc >=2.17,<3.0.a0 - - c-ares >=1.34.5,<2.0a0 - - libev >=4.33,<4.34.0a0 - - libev >=4.33,<5.0a0 - libgcc >=14 - - libstdcxx >=14 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.2,<4.0a0 license: MIT license_family: MIT purls: [] - size: 666600 - timestamp: 1756834976695 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.67.0-ha888d0e_0.conda - sha256: b03f406fd5c3f865a5e08c89b625245a9c4e026438fd1a445e45e6a0d69c2749 - md5: 981082c1cc262f514a5a2cf37cab9b81 + run_exports: + weak: + - yaml >=0.2.5,<0.3.0a0 + size: 88088 + timestamp: 1753484092643 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.24.5-py311h164a683_0.conda + sha256: 6656962517b612e90ec441bf113f0e347b9e6582daef85ab678ce1248dc9bbf6 + md5: 3de5f3bc98f2ca2ab340d29d12fbd31f depends: - - c-ares >=1.34.5,<2.0a0 - - libev >=4.33,<4.34.0a0 - - libev >=4.33,<5.0a0 + - idna >=2.0 - libgcc >=14 - - libstdcxx >=14 + - multidict >=4.0 + - propcache >=0.2.1 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/yarl?source=hash-mapping + run_exports: {} + size: 171978 + timestamp: 1784526491187 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zig-0.13.0-h49d127f_4.conda + sha256: cfa7120b621ef5b82c93fb6fe1998a915a2390446a94226d0257936df413fa54 + md5: 22c5daa3067b5125fa279b4436ba0fb3 + depends: + - libclang-cpp18.1 >=18.1.8,<18.2.0a0 + - libgcc >=13 + - libllvm18 >=18.1.8,<18.2.0a0 + - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.2,<4.0a0 + - sysroot_linux-aarch64 >=2.28 + - zstd >=1.5.6,<1.6.0a0 license: MIT license_family: MIT purls: [] - size: 728661 - timestamp: 1756835019535 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.67.0-h3338091_0.conda - sha256: c48d7e1cc927aef83ff9c48ae34dd1d7495c6ccc1edc4a3a6ba6aff1624be9ac - md5: e7630cef881b1174d40f3e69a883e55f + run_exports: {} + size: 24664928 + timestamp: 1729823148699 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.25.0-py311h51cfe5d_1.conda + sha256: ddeec193065b235166fb9f8ca4e5cbb931215ab90cbd17e9f9d753c8966b57b1 + md5: c8b3365fe290eeee3084274948012394 + depends: + - python + - cffi >=1.11 + - zstd >=1.5.7,<1.5.8.0a0 + - python 3.11.* *_cpython + - libgcc >=14 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zstandard?source=hash-mapping + run_exports: {} + size: 459426 + timestamp: 1762512724303 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + sha256: 569990cf12e46f9df540275146da567d9c618c1e9c7a0bc9d9cfefadaed20b75 + md5: c3655f82dcea2aa179b291e7099c1fcc depends: - - __osx >=10.13 - - c-ares >=1.34.5,<2.0a0 - - libcxx >=19 - - libev >=4.33,<4.34.0a0 - - libev >=4.33,<5.0a0 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.2,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 + size: 614429 + timestamp: 1764777145593 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + sha256: 2a7204314663eeda5dec482a956f0e2eaf289bd5b9953eaaaad0e81aa64638f2 + md5: 3845f3d75991bae0fb90884662f4327c + depends: + - cpython + - python-gil license: MIT license_family: MIT purls: [] - size: 605680 - timestamp: 1756835898134 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda - sha256: a07cb53b5ffa2d5a18afc6fd5a526a5a53dd9523fbc022148bd2f9395697c46d - md5: a4b4dd73c67df470d091312ab87bf6ae + run_exports: {} + size: 8144 + timestamp: 1784221492234 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + sha256: 8dc149a6828d19bf104ea96382a9d04dae185d4a03cc6beb1bc7b84c428e3ca2 + md5: 421a865222cd0c9d83ff08bc78bf3a61 depends: - - __osx >=11.0 - - c-ares >=1.34.5,<2.0a0 - - libcxx >=19 - - libev >=4.33,<4.34.0a0 - - libev >=4.33,<5.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.2,<4.0a0 + - frozenlist >=1.1.0 + - python >=3.9 + - typing_extensions >=4.2 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/aiosignal?source=hash-mapping + run_exports: {} + size: 13688 + timestamp: 1751626573984 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab + md5: c6b0543676ecb1fb2d7643941fe375f2 + depends: + - python >=3.10 + - python license: MIT license_family: MIT - purls: [] - size: 575454 - timestamp: 1756835746393 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 - md5: d864d34357c3b65a4b731f78c0801dc4 + purls: + - pkg:pypi/attrs?source=hash-mapping + run_exports: {} + size: 64927 + timestamp: 1773935801332 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-h4c7d964_0.conda + sha256: 7f458e4a82514d7bebbfef23d92817794a16aaf1c748a15f04870d4fb49aeab2 + md5: b9696b2cf00dfeec138c70cee38ed192 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-only - license_family: GPL + - __win + license: ISC purls: [] - size: 33731 - timestamp: 1750274110928 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda - sha256: c0dc4d84198e3eef1f37321299e48e2754ca83fd12e6284754e3cb231357c3a5 - md5: d5d58b2dc3e57073fe22303f5fed4db7 + run_exports: {} + size: 129352 + timestamp: 1781709016515 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + sha256: f8e3c730fa14ee3f170493779f06522c4acf89169f43db4f039727709b6419cf + md5: a9965dd99f683c5f444428f896635716 depends: - - libgcc >=13 - license: LGPL-2.1-only - license_family: GPL + - __unix + license: ISC purls: [] - size: 34831 - timestamp: 1750274211 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.5-hd0c01bc_1.conda - sha256: ffb066ddf2e76953f92e06677021c73c85536098f1c21fcd15360dbc859e22e4 - md5: 68e52064ed3897463c0e958ab5c8f91b + run_exports: {} + size: 128866 + timestamp: 1781708962055 +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 depends: - - libgcc >=13 - - __glibc >=2.17,<3.0.a0 + - python >=3.9 license: BSD-3-Clause license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping + run_exports: {} + size: 27011 + timestamp: 1733218222191 +- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-64-16.0.6-ha38d28d_2.conda + sha256: 75270bd8e306967f6e1a8c17d14f2dfe76602a5c162088f3ea98034fe3d71e0c + md5: 7a46507edc35c6c8818db0adaf8d787f + depends: + - clang 16.0.6.* + - clangxx 16.0.6.* + constrains: + - compiler-rt 16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE purls: [] - size: 218500 - timestamp: 1745825989535 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libogg-1.3.5-h86ecc28_1.conda - sha256: 2c1b7c59badc2fd6c19b6926eabfce906c996068d38c2972bd1cfbe943c07420 - md5: 319df383ae401c40970ee4e9bc836c7a + run_exports: {} + size: 9895261 + timestamp: 1701467223753 +- conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt_osx-arm64-16.0.6-h3808999_2.conda + sha256: 61f1a10e6e8ec147f17c5e36cf1c2fe77ac6d1907b05443fa319fd59be20fa33 + md5: 8c7d77d888e1a218cccd9e82b1458ec6 depends: - - libgcc >=13 - license: BSD-3-Clause - license_family: BSD + - clang 16.0.6.* + - clangxx 16.0.6.* + constrains: + - compiler-rt 16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE purls: [] - size: 220653 - timestamp: 1745826021156 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda - sha256: 26691d40c70e83d3955a8daaee713aa7d087aa351c5a1f43786bbb0e871f29da - md5: d0f30c7fe90d08e9bd9c13cd60be6400 + run_exports: {} + size: 9829914 + timestamp: 1701467293179 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.11.15-py311hd8ed1ab_1.conda + noarch: generic + sha256: cfe29a7e71ab4553b9715ee4b6788824853ade58b8661ff3363acb3e762046a5 + md5: 842533c9d507e2025a4933a091dfa983 depends: - - __osx >=10.13 + - python >=3.11,<3.12.0a0 + - python_abi * *_cp311 + license: Python-2.0 + purls: [] + run_exports: {} + size: 48482 + timestamp: 1781148385557 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b + md5: 0c96522c6bdaed4b1566d11387caaf45 license: BSD-3-Clause license_family: BSD purls: [] - size: 215854 - timestamp: 1745826006966 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libogg-1.3.5-h48c0fde_1.conda - sha256: 28bd1fe20fe43da105da41b95ac201e95a1616126f287985df8e86ddebd1c3d8 - md5: 29b8b11f6d7e6bd0e76c029dcf9dd024 + run_exports: {} + size: 397370 + timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c + md5: 34893075a5c9e55cdafac56607368fc6 + license: OFL-1.1 + license_family: Other + purls: [] + run_exports: {} + size: 96530 + timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 + md5: 4d59c254e01d9cde7957100457e2d5fb + license: OFL-1.1 + license_family: Other + purls: [] + run_exports: {} + size: 700814 + timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 + md5: 49023d73832ef61042f6a237cb2687e7 + license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 + license_family: Other + purls: [] + run_exports: {} + size: 1620504 + timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 + md5: fee5683a3f04bd15cbd8318b096a27ab depends: - - __osx >=11.0 + - fonts-conda-forge license: BSD-3-Clause license_family: BSD purls: [] - size: 216719 - timestamp: 1745826006052 -- conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda - sha256: c63e5fb169dbd192aacdcee6e37235407f106b8ca9c9036942a25e0366cbc73c - md5: b67ed8c9ca072695ff482e50d888a523 - depends: - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 + run_exports: {} + size: 3667 + timestamp: 1566974674465 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + sha256: 54eea8469786bc2291cc40bca5f46438d3e062a399e8f53f013b6a9f50e98333 + md5: a7970cd949a077b7cb9696379d338681 + depends: + - font-ttf-ubuntu + - font-ttf-inconsolata + - font-ttf-dejavu-sans-mono + - font-ttf-source-code-pro license: BSD-3-Clause license_family: BSD purls: [] - size: 35040 - timestamp: 1745826086628 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2025.2.0-hb617929_1.conda - sha256: 235e7d474c90ad9d8955401b8a91dbe373aa1dc65db3c8232a5e22e4eaf41976 - md5: 1da20cc4ff32dc74424dec68ec087dba + run_exports: {} + size: 4059 + timestamp: 1762351264405 +- conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + sha256: dbbec21a369872c8ebe23cb9a3b9d63638479ee30face165aa0fccc96e93eec3 + md5: 7c14f3706e099f8fcd47af2d494616cc depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 6244771 - timestamp: 1753211097492 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-2025.2.0-hcd21e76_1.conda - sha256: f5c7a24d9918b1f637ca11a7c0b5594e14469ccc5b1f3bafcd248df252d2bdfb - md5: 76baf6bb7a63e310210d91595e245d24 + - python >=3.9 + - smmap >=3.0.1,<6 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/gitdb?source=hash-mapping + run_exports: {} + size: 53136 + timestamp: 1735887290843 +- conda: https://conda.anaconda.org/conda-forge/noarch/gitignore-parser-0.1.13-pyhd8ed1ab_0.conda + sha256: 44d49cf04aa46769e2d8a3b2cb12c94ea5ca572f459a29c8545f68cbe277f65d + md5: 1c7086a72e284675506c76b05acbe8b6 depends: - - libgcc >=14 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 5535917 - timestamp: 1753203182299 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2025.2.0-h346e020_1.conda - sha256: 9ce68ea62066f60083611be69314c1664747d73b80407ad41438e08922c4407b - md5: 0e6b6a6c7640260ae38c963d16719bac + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/gitignore-parser?source=hash-mapping + run_exports: {} + size: 12133 + timestamp: 1756163102170 +- conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.53-pyhd8ed1ab_0.conda + sha256: d158acd1f01ac9224abb6e541842f9368d61ba7d0095306a73e372b0bef916fe + md5: 795439d20df9a8e8ede6d76922729419 depends: - - __osx >=11.0 - - libcxx >=19 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 4741821 - timestamp: 1753201195860 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-2025.2.0-h56e7ac4_1.conda - sha256: 6f74a2d9d39df7d98e5be28028b927746d6213102aad94eea2131f05879a5af4 - md5: 0d6535fb8c6e34dcc1c8e63b3a6d2a98 + - gitdb >=4.0.1,<5 + - python >=3.10 + - typing_extensions >=3.10.0.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/gitpython?source=compressed-mapping + run_exports: {} + size: 165002 + timestamp: 1784567010841 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 depends: - - __osx >=11.0 - - libcxx >=19 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 4367075 - timestamp: 1753200563969 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-arm-cpu-plugin-2025.2.0-hcd21e76_1.conda - sha256: 018a0ea563bc2e91efee8a07f7b2ff769cd66d03d1c466c8bb7407075023ac85 - md5: 794c3f49774bd710aec2b0602ae38313 + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + run_exports: {} + size: 95967 + timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + sha256: fdcea5d7cb314485d3907192ef024c704311548c5b0cbeb390cd1951051e29d2 + md5: b395909221b9bd1df066e5930e18855b depends: - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 9257629 - timestamp: 1753203203327 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-arm-cpu-plugin-2025.2.0-h56e7ac4_1.conda - sha256: a8975d1430afdab3e373c99d66d6bc4d6d6842a6448bcc3b32b2eb1d60d25729 - md5: 376ff75a12a871f211e943357229c32b + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=compressed-mapping + run_exports: {} + size: 32884 + timestamp: 1782283986153 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 7919701 - timestamp: 1753200600045 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-batch-plugin-2025.2.0-hed573e4_1.conda - sha256: 193f760e828b0dd5168dd1d28580d4bf429c5f14a4eee5e0c02ff4c6d4cf8093 - md5: 94f9d17be1d658213b66b22f63cc6578 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + run_exports: {} + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda + sha256: c75632ea624aa450a394f570749420c5a2e0997d0216bc29d5d45b0f39df0426 + md5: 577b04680ae422adb86fc60d7b940659 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - - tbb >=2021.13.0 - purls: [] - size: 114760 - timestamp: 1753211116381 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-auto-batch-plugin-2025.2.0-h3890994_1.conda - sha256: 59a159c547fca34e8a0c600fcca428793da2ad4ecef0f47b58f1ea16d756c521 - md5: ad9768777a654205fa46aed8a829bd7e + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=compressed-mapping + run_exports: {} + size: 163869 + timestamp: 1781620148226 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d depends: - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libstdcxx >=14 - - tbb >=2021.13.0 - purls: [] - size: 111599 - timestamp: 1753203233477 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2025.2.0-heda8b29_1.conda - sha256: 23649063fcbc666cad1bb4b4d430a6320c7c371367b0ed5d68608bcd5c94d568 - md5: 5ce82393e4b6d012250f79ad4f853867 + - markupsafe >=2.0 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jinja2?source=hash-mapping + run_exports: {} + size: 120685 + timestamp: 1764517220861 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 + md5: ada41c863af263cc4c5fcbaff7c3e4dc depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - - tbb >=2021.13.0 - purls: [] - size: 106879 - timestamp: 1753201232911 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-auto-batch-plugin-2025.2.0-he81eb65_1.conda - sha256: becf0dd673803ba43fbca7ac2731227855ee3c3bdc72e242a97f101a85d26c31 - md5: 45b44ad26a4b5d386feb079cc93996d8 + - attrs >=22.2.0 + - jsonschema-specifications >=2023.3.6 + - python >=3.10 + - referencing >=0.28.4 + - rpds-py >=0.25.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema?source=hash-mapping + run_exports: {} + size: 82356 + timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 + md5: 439cd0f567d697b20a8f45cb70a1005a depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - - tbb >=2021.13.0 - purls: [] - size: 105074 - timestamp: 1753200643185 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-plugin-2025.2.0-hed573e4_1.conda - sha256: a6f9f996e64e6d2f295f017a833eda7018ff58b6894503272d72f0002dfd6f33 - md5: 071b3a82342715a411f216d379ab6205 + - python >=3.10 + - referencing >=0.31.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema-specifications?source=hash-mapping + run_exports: {} + size: 19236 + timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyh6dadd2b_0.conda + sha256: ed709a6c25b731e01563521ef338b93986cd14b5bc17f35e9382000864872ccc + md5: a8db462b01221e9f5135be466faeb3e0 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - - tbb >=2021.13.0 - purls: [] - size: 250500 - timestamp: 1753211127339 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-auto-plugin-2025.2.0-h3890994_1.conda - sha256: 3353f616cf72dad02d974698a74fa89eb5ff1beeaa64cebcdd1f87c52d2a0516 - md5: 4cec7bb2362ece08d0d1799f1ed4fbe7 + - __win + - pywin32 + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + run_exports: {} + size: 64679 + timestamp: 1760643889625 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a + md5: b38fe4e78ee75def7e599843ef4c1ab0 depends: - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libstdcxx >=14 - - tbb >=2021.13.0 + - __unix + - python + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + run_exports: {} + size: 65503 + timestamp: 1760643864586 +- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda + sha256: 41557eeadf641de6aeae49486cef30d02a6912d8da98585d687894afd65b356a + md5: 86d9cba083cd041bfbf242a01a7a1999 + constrains: + - sysroot_linux-64 ==2.28 + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL purls: [] - size: 235379 - timestamp: 1753203244808 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2025.2.0-heda8b29_1.conda - sha256: 9625b18fa136b9f841b2651c834e89e8f2f90cb13e33dacba67af2ee88c175a9 - md5: 950fd5d5e34f2845f63eebf96c761d4c - depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - - tbb >=2021.13.0 + run_exports: {} + size: 1278712 + timestamp: 1765578681495 +- conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-aarch64-4.18.0-h05a177a_9.conda + sha256: 5d224bf4df9bac24e69de41897c53756108c5271a0e5d2d2f66fd4e2fbc1d84b + md5: bb3b7cad9005f2cbf9d169fb30263f3e + constrains: + - sysroot_linux-aarch64 ==2.28 + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL purls: [] - size: 221142 - timestamp: 1753201253766 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-auto-plugin-2025.2.0-he81eb65_1.conda - sha256: 5a515ec892e74682c3b8a9c68c4920444eaeb41de50c2bf3b4fc5cce6a4bfa9c - md5: 3c40649c696a02029642b08a27c041d0 + run_exports: {} + size: 1248134 + timestamp: 1765578613607 +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.4.0-h1762d19_102.conda + sha256: 4f8486faaa5696a4115a621100acda0f64b49631f2c4bc6046e0f72496348d76 + md5: 5c9ee54252cddf9f83dc48f6ceef0ba4 depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - - tbb >=2021.13.0 + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 216636 - timestamp: 1753200660470 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-hetero-plugin-2025.2.0-hd41364c_1.conda - sha256: f43f9049338ef9735b6815bac3f483d1e3adddecbfdeb13be365bc3f601fe156 - md5: 77c0c7028a8110076d40314dc7b1fa98 + run_exports: {} + size: 2558737 + timestamp: 1740240187748 +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-15.2.0-hcc6f6b0_119.conda + sha256: 38a557eba305468ac1f90ac85e50d8defd76141cb0b8a43b2fc1aca71dd5d5f2 + md5: 683fcb168e1df9a21fa80d5aa2d9330b depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 194815 - timestamp: 1753211138624 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-hetero-plugin-2025.2.0-he07c6df_1.conda - sha256: 97f6a555d73d96efe26521527ce4e4c6ea49e46d5e5fd07a5e535e7de34bb6b5 - md5: 00d0206cb4358182c856700e1c1dae8b + run_exports: {} + size: 3095909 + timestamp: 1778268932148 +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-12.4.0-h7b3af7c_102.conda + sha256: d6723763270f1ce823b728ae2818994a8920dee11c24ecacd1a100cacc8a99fd + md5: 2cbe18ad69722b174d3f536f92e4fc25 depends: - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 187747 - timestamp: 1753203256494 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2025.2.0-hd57c75b_1.conda - sha256: c48f09ce035ffee361ff020d586d76e4f7e464b58739f6d8b43cd242dc476f7a - md5: 554269b84c8a8c945056fd7d3ff28a67 + run_exports: {} + size: 311781 + timestamp: 1740240133346 +- conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-aarch64-15.2.0-h55c397f_119.conda + sha256: fe600a63a39281e6994e27fe79360cd6bd8e576c3ce1af32ce8673b011f46c21 + md5: 18ad0f0b94071d91fa962a1bf3983a78 depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - - pugixml >=1.15,<1.16.0a0 + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 180453 - timestamp: 1753201276333 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-hetero-plugin-2025.2.0-h273c05f_1.conda - sha256: 132e845f2001241eb112eea01aa2596e61562ca0be7dcd0e7be6056c2ad583f2 - md5: 98479fa3c1442811d65d44f695d6f271 + run_exports: {} + size: 2353893 + timestamp: 1778268665954 +- conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.4.0-h1762d19_102.conda + sha256: 5e86d884d6877ce428d90a484cdc66d5968bf81dc189393239c43fe9b831da7d + md5: aa2ae7befd3d165f3cfc4d3b39cebeb5 depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - - pugixml >=1.15,<1.16.0a0 + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 173628 - timestamp: 1753200679078 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-cpu-plugin-2025.2.0-hb617929_1.conda - sha256: a4a1cd320fa010a45d01f438dc3431b7a60271ee19188a901f884399fe744268 - md5: e4cc6db5bdc8b554c06bf569de57f85f + run_exports: {} + size: 11883113 + timestamp: 1740240215984 +- conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-aarch64-12.4.0-h7b3af7c_102.conda + sha256: 277208c0d21a068c1bb1bf1b2ae92f159ba866cfc75a882569b286e339d6c518 + md5: d5b8708faacba4063d7a150cf9ec94f7 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 + - __unix + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL purls: [] - size: 12377488 - timestamp: 1753211149903 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2025.2.0-h346e020_1.conda - sha256: 9f27cf634bba0d35d00f0b89e423246495dd3e9ea531d0ba60373c343df68349 - md5: bbaf847551103a59236544c674886b6d + run_exports: {} + size: 10156474 + timestamp: 1740240151058 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + sha256: 0c4c35376fe920714390d46e4b8d31c876d65f18e1655899e0763ec25f2a902f + md5: 6d03368f2b2b0a5fb6839df53b2eb5e0 depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 10731860 - timestamp: 1753201313287 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-gpu-plugin-2025.2.0-hb617929_1.conda - sha256: 03ebf700586775144ca5913f401393a386b9a1d7a7cfcba4494830063ca5eb92 - md5: b846fe6c158ca417e246122172d68d3a + - mdurl >=0.1,<1 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/markdown-it-py?source=hash-mapping + run_exports: {} + size: 69017 + timestamp: 1778169663339 +- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 + md5: 592132998493b3ff25fd7479396e8351 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - - ocl-icd >=2.3.3,<3.0a0 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 10815480 - timestamp: 1753211182626 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-npu-plugin-2025.2.0-hb617929_1.conda - sha256: b6dbc342293d6ce0c7b37c9f29f734b3e1856cff9405a02fb33cedd1b36528e6 - md5: 86fd4c25f6accaf646c86adf0f1382d3 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mdurl?source=hash-mapping + run_exports: {} + size: 14465 + timestamp: 1733255681319 +- conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + sha256: 6ed158e4e5dd8f6a10ad9e525631e35cee8557718f83de7a4e3966b1f772c4b1 + md5: e9c622e0d00fa24a6292279af3ab6d06 depends: - - __glibc >=2.17,<3.0.a0 - - level-zero >=1.23.1,<2.0a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 - - tbb >=2021.13.0 - purls: [] - size: 1261488 - timestamp: 1753211212823 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-ir-frontend-2025.2.0-hd41364c_1.conda - sha256: 334733396d4c9a9b2b2d7d7d850e8ee8deca1f9becd0368d106010076ceb20ca - md5: 75e595d9f2019a60f6dcb500266da615 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy-extensions?source=hash-mapping + run_exports: {} + size: 11766 + timestamp: 1745776666688 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 + md5: bbe1963f1e47f594070ffe87cdf612ea depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 - purls: [] - size: 204890 - timestamp: 1753211224567 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-ir-frontend-2025.2.0-he07c6df_1.conda - sha256: 935341a98e129d3fd792609de5e85b959c3b31661d1a95c2a655771611383a05 - md5: f86c16f077043c9b1e87dbc07bf5ec42 + - jsonschema >=2.6 + - jupyter_core >=4.12,!=5.0.* + - python >=3.9 + - python-fastjsonschema >=2.15 + - traitlets >=5.1 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbformat?source=hash-mapping + run_exports: {} + size: 100945 + timestamp: 1733402844974 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.9.1-pyhd8ed1ab_0.conda + sha256: e5029a1ca06d5f02c9366ec8686f64c942d77f297bfa90a0a8215083d85a7a01 + md5: 948b10290b9f4ebbb26de6da5c6b7c51 + depends: + - nbformat + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/nbstripout?source=hash-mapping + run_exports: {} + size: 24331 + timestamp: 1771778886946 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + sha256: da157b19bcd398b9804c5c52fc000fcb8ab0525bdb9c70f95beaa0bb42f85af1 + md5: 3bfed7e6228ebf2f7b9eaa47f1b4e2aa + depends: + - python >=3.8 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=hash-mapping + run_exports: {} + size: 60164 + timestamp: 1733203368787 +- conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh8b19718_0.conda + sha256: 29b7d75bf81ad11645a8e320b369abdc90a92b93f2a9178e853d9dddf82e5106 + md5: 511fbc2c63d2c73650ad1755e4d357ba + depends: + - python >=3.10,<3.13.0a0 + - setuptools + - wheel + license: MIT + license_family: MIT + purls: + - pkg:pypi/pip?source=hash-mapping + run_exports: {} + size: 1203173 + timestamp: 1780262795392 +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.10.1-pyhcf101f3_0.conda + sha256: 289a778958d7d65784f5435668b803e6e929e5a8dfd4e3f4e31f395f199f786f + md5: 441b03a60effe8be3b7df4805a8682a3 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/platformdirs?source=compressed-mapping + run_exports: {} + size: 26412 + timestamp: 1784376970393 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + sha256: e27e0473fc6723311a0bd48b89b616fa1b996a2f7a2b555338cbbcfb9c640568 + md5: 9c5491066224083c41b6d5635ed7107b + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pycparser?source=compressed-mapping + run_exports: {} + size: 55886 + timestamp: 1779293633166 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda + sha256: cf70b2f5ad9ae472b71235e5c8a736c9316df3705746de419b59d442e8348e86 + md5: 16c18772b340887160c79a6acc022db0 + depends: + - python >=3.10 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=hash-mapping + run_exports: {} + size: 893031 + timestamp: 1774796815820 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca + md5: e2fd202833c4a981ce8a65974fe4abd1 + depends: + - __win + - python >=3.9 + - win_inet_pton + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + run_exports: {} + size: 21784 + timestamp: 1733217448189 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + run_exports: {} + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + sha256: df9aa74e9e28e8d1309274648aac08ec447a92512c33f61a8de0afa9ce32ebe8 + md5: 23029aae904a2ba587daba708208012f depends: - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libstdcxx >=14 - - pugixml >=1.15,<1.16.0a0 - purls: [] - size: 195451 - timestamp: 1753203267888 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2025.2.0-hd57c75b_1.conda - sha256: 09f8d1e8ca01f248e1ac3d069749acc888710268cfab3b514829820c3774c8d9 - md5: 47e5f6af801546ebf4658f00be37ff60 + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fastjsonschema?source=hash-mapping + run_exports: {} + size: 244628 + timestamp: 1755304154927 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.15-hd8ed1ab_1.conda + sha256: 9eed0e05f90866823f7dbb2092c79076b8f11a34c7171165df02532d0ff34cce + md5: 336ca63d560b4a4004d4c0fdf78a9075 depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - - pugixml >=1.15,<1.16.0a0 + - cpython 3.11.15.* + - python_abi * *_cp311 + license: Python-2.0 purls: [] - size: 184658 - timestamp: 1753201367805 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-ir-frontend-2025.2.0-h273c05f_1.conda - sha256: d6a94e82f03568db207b36cf4c2fa4d36677f15a1171472eb9382905a0c78f5b - md5: b3f148dcd1e80f102338d79ce3fe1102 - depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - - pugixml >=1.15,<1.16.0a0 + run_exports: {} + size: 48417 + timestamp: 1781148405955 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda + build_number: 8 + sha256: fddf123692aa4b1fc48f0471e346400d9852d96eeed77dbfdd746fa50a8ff894 + md5: 8fcb6b0e2161850556231336dae58358 + constrains: + - python 3.11.* *_cpython + license: BSD-3-Clause + license_family: BSD purls: [] - size: 173701 - timestamp: 1753200697088 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-onnx-frontend-2025.2.0-h1862bb8_1.conda - sha256: 3937b028e7192ed3805581ac0ea171725843056c8544537754fad45a1791e864 - md5: 68f5ad9d8e3979362bb9dfc9388980aa + run_exports: {} + size: 7003 + timestamp: 1752805919375 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 + md5: 870293df500ca7e18bedefa5838a22ab depends: - - __glibc >=2.17,<3.0.a0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - - libstdcxx >=14 - purls: [] - size: 1724503 - timestamp: 1753211235981 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-onnx-frontend-2025.2.0-h07d5dce_1.conda - sha256: 576c1ba122fb58d1c0ea6540d5480809196a884d3e56c05ab49b97ccc99e2c90 - md5: f8d90a982f95366614c568eac3157a90 + - attrs >=22.2.0 + - python >=3.10 + - rpds-py >=0.7.0 + - typing_extensions >=4.4.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/referencing?source=hash-mapping + run_exports: {} + size: 51788 + timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda + sha256: edfb44d0b6468a8dfced728534c755101f06f1a9870a7ad329ec51389f16b086 + md5: a247579d8a59931091b16a1e932bbed6 depends: - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - - libstdcxx >=14 - purls: [] - size: 1530030 - timestamp: 1753203281815 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2025.2.0-ha4fb624_1.conda - sha256: 69e9bf3e93ea8572c512871668e2893c8ff74a8800b6d7153fe1ecf6e7702604 - md5: 7562969356f607c1899079b8c617f1d0 + - markdown-it-py >=2.2.0 + - pygments >=2.13.0,<3.0.0 + - python >=3.10 + - typing_extensions >=4.0.0,<5.0.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rich?source=hash-mapping + run_exports: {} + size: 200840 + timestamp: 1760026188268 +- conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhcf101f3_1.conda + sha256: bea67173ed67c73cf16691ef72e58059492ac1ed1c880cfbeb6f1295c5add7d6 + md5: 8e7be844ccb9706a999a337e056606ab depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - purls: [] - size: 1361773 - timestamp: 1753201390071 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-onnx-frontend-2025.2.0-h6386500_1.conda - sha256: 8188f5fc49ff977b1dacbc49c67effb3696bd34329703be08f9d56b112da38d8 - md5: 6a9b2e48da9e5a9e5dbbc2acd97661ad + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/semver?source=hash-mapping + run_exports: {} + size: 22532 + timestamp: 1767294175877 +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-83.0.0-pyh332efcf_0.conda + sha256: 48a9f96016505debadfc67f06de7ac548decbc38d327409b24b0432ef6f16335 + md5: 6bf6acbab2499830180ec88c3aff2fa4 depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - purls: [] - size: 1300903 - timestamp: 1753200716085 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-paddle-frontend-2025.2.0-h1862bb8_1.conda - sha256: c7ac3d4187323ab37ef62ec0896a41c8ca7da426c7f587494c72fe74852269e5 - md5: a032d03468dee9fb5b8eaf635b4571c2 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/setuptools?source=compressed-mapping + run_exports: {} + size: 642081 + timestamp: 1783619174976 +- conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.3-pyhcf101f3_1.conda + sha256: bc86861086db65c9b56dd6a1605755f41a9875b94fc3b69e137f686700d91aa1 + md5: b899f1195be56d8215ed1973a8f409c3 depends: - - __glibc >=2.17,<3.0.a0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - - libstdcxx >=14 - purls: [] - size: 744746 - timestamp: 1753211248776 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-paddle-frontend-2025.2.0-h07d5dce_1.conda - sha256: b080ca352d8d4526b73815bdbdb12ba5caf5de4621c10e9ad41eac73a7a6a713 - md5: 098597aa6f19b2851f295f47c7105658 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/smmap?source=hash-mapping + run_exports: {} + size: 27262 + timestamp: 1781021238494 +- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda + sha256: c47299fe37aebb0fcf674b3be588e67e4afb86225be4b0d452c7eb75c086b851 + md5: 13dc3adbc692664cd3beabd216434749 depends: - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - - libstdcxx >=14 + - __glibc >=2.28 + - kernel-headers_linux-64 4.18.0 he073ed8_9 + - tzdata + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL purls: [] - size: 674194 - timestamp: 1753203295461 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2025.2.0-ha4fb624_1.conda - sha256: a55b2ec77b20828551f37199b0f156de985d8e33ec31e16f77f588d674aa5fa3 - md5: 6d7ffc6166d1347d0c35b04dd04b9bf6 + run_exports: + strong: + - __glibc >=2.28,<3.0.a0 + size: 24008591 + timestamp: 1765578833462 +- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_9.conda + sha256: 1bd2db6b2e451247bab103e4a0128cf6c7595dd72cb26d70f7fadd9edd1d1bc3 + md5: fdf07ab944a222ff28c754914fdb0740 depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 + - __glibc >=2.28 + - kernel-headers_linux-aarch64 4.18.0 h05a177a_9 + - tzdata + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later + license_family: GPL purls: [] - size: 468414 - timestamp: 1753201414650 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-paddle-frontend-2025.2.0-h6386500_1.conda - sha256: 94e19e5fab3c6a50ce15fb3a404d81405fe642cce147dc3f6d2a02d2afaf8741 - md5: 0f2a4bd28364a0cf19bfd96c6e2fa052 + run_exports: + strong: + - __glibc >=2.28,<3.0.a0 + size: 23644746 + timestamp: 1765578629426 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + sha256: cb77c660b646c00a48ef942a9e1721ee46e90230c7c570cdeb5a893b5cce9bff + md5: d2732eb636c264dc9aa4cbee404b1a53 depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - purls: [] - size: 450125 - timestamp: 1753200737670 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2025.2.0-hecca717_1.conda - sha256: 2d4a680a16509b8dd06ccd7a236655e46cc7c242bb5b6e88b83a834b891658db - md5: cd40cf2d10a3279654c9769f3bc8caf5 + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=hash-mapping + run_exports: {} + size: 20973 + timestamp: 1760014679845 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda + sha256: 53cc436ab92d38683df1320e4468a8b978428e800195bf1c8c2460e90b0bc117 + md5: 074d0ce7a6261ab8b497c3518796ef3e depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - purls: [] - size: 1243134 - timestamp: 1753211260154 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-pytorch-frontend-2025.2.0-hfae3067_1.conda - sha256: 0dddd3e274c156a2b8ced3009444d99c04d75ab50a748968b94d3890b6dfab65 - md5: d00d92fbb31f8f9dc2cfb78f44286925 + - python >=3.7 + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomlkit?source=hash-mapping + run_exports: {} + size: 37132 + timestamp: 1700046842169 +- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda + sha256: 32c39424090a8cafe7994891a816580b3bd253eb4d4f5473bdefcf6a81ebc061 + md5: 92718e1f892e1e4623dcc59b9f9c4e55 depends: - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libstdcxx >=14 - purls: [] - size: 1123835 - timestamp: 1753203307507 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2025.2.0-hbc7d668_1.conda - sha256: d52231c562fe544c2a5f95df6397b5f7e9778cc19cef698da30f80b872bb7207 - md5: 186bf8821732296cc1de55cfebf76446 + - colorama + - python >=3.7 + license: MPL-2.0 or MIT + purls: + - pkg:pypi/tqdm?source=hash-mapping + run_exports: {} + size: 89367 + timestamp: 1730145312554 +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.1-pyhcf101f3_0.conda + sha256: b89a823edf524956b94a2a4db974866e4501f05c68976eff458c5dcf07f88431 + md5: 37e3be7b6e2977d37b8fa5da229f5dc0 depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - purls: [] - size: 850745 - timestamp: 1753201436800 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-pytorch-frontend-2025.2.0-hec049ff_1.conda - sha256: 3b9d03eb5332626e35dd5ffc9a5c46b77c5ad8e0a61f16616255ce511323915e - md5: 5e2ab51b1fc44850320061e235112b84 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/traitlets?source=compressed-mapping + run_exports: {} + size: 115158 + timestamp: 1780507822178 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + sha256: 2d888f90af0686044882c74193ec80a90ec1943145d94a7b1b048958acda1848 + md5: c70ad746c22219b9700931707482992c depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=compressed-mapping + run_exports: {} + size: 52631 + timestamp: 1783002732887 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + sha256: b928c30ddcb0e3f544c6eade8352737e6e610e263276b90232db6a578ef899d8 + md5: fcb489df604d100968b737f2cb6076c6 + license: LicenseRef-Public-Domain purls: [] - size: 820657 - timestamp: 1753200755855 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2025.2.0-h0767aad_1.conda - sha256: 311ec1118448a28e76f0359c4393c7f7f5e64761c48ac7b169bf928a391eae77 - md5: f71c6b4e342b560cc40687063ef62c50 + run_exports: {} + size: 118849 + timestamp: 1784250406640 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + sha256: 4fb9789154bd666ca74e428d973df81087a697dbb987775bc3198d2215f240f8 + md5: 436c165519e140cb08d246a4472a9d6a depends: - - __glibc >=2.17,<3.0.a0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - - libstdcxx >=14 - - snappy >=1.2.2,<1.3.0a0 - purls: [] - size: 1325059 - timestamp: 1753211272484 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-frontend-2025.2.0-h38473e3_1.conda - sha256: fcdb5623415c9f5d8c8635f579e5706647e2c97b543ebba621b5b31df096de3d - md5: b42a48c1052c5b576170212c2a834614 + - brotli-python >=1.0.9 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.9 + - zstandard >=0.18.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping + run_exports: {} + size: 101735 + timestamp: 1750271478254 +- conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda + sha256: b72270395326dc56de9bd6ca82f63791b3c8c9e2b98e25242a9869a4ca821895 + md5: f622897afff347b715d046178ad745a5 depends: - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - - libstdcxx >=14 - - snappy >=1.2.2,<1.3.0a0 + - __win + license: MIT + license_family: MIT purls: [] - size: 1224816 - timestamp: 1753203320621 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2025.2.0-hd87add6_1.conda - sha256: 1f785acc3c4ed6aad94053bfa48d52d76e8d6ff369064331e70421b7c87fd61d - md5: e4f76aeb995f50f7a1a533affd6c12a4 - depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - - snappy >=1.2.2,<1.3.0a0 + run_exports: {} + size: 238764 + timestamp: 1745560912727 +- conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.49-hd8ed1ab_0.conda + sha256: 04ce686cd187d379344f9b2be7b4da5f431b265dc0944a6b764fab9da9171948 + md5: 0839a3421140d4a9ba93fb988698fc00 + license: MIT + license_family: MIT purls: [] - size: 987782 - timestamp: 1753201460022 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-frontend-2025.2.0-hee62d61_1.conda - sha256: 4828d3fd7e59c8533cf46b7e3b09985f14fd3e7a43a92ecdbc371f823ed221c1 - md5: ebc006303a61e7110e3b219a839637df + run_exports: {} + size: 147954 + timestamp: 1780946721169 +- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 + sha256: bd4f11ff075ff251ade9f57686f31473e25be46ab282d9603f551401250f9f44 + md5: c829cfb8cb826acb9de0ac1a2df0a940 depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - - libprotobuf >=6.31.1,<6.31.2.0a0 - - snappy >=1.2.2,<1.3.0a0 - purls: [] - size: 934382 - timestamp: 1753200778004 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hecca717_1.conda - sha256: 581f4951e645e820c4a6ffe40fb0174b56d6e31fb1fefd2d64913fea01f8f69e - md5: fd9dacd7101f80ff1110ea6b76adb95d + - python >=3.7 + license: MIT + license_family: MIT + purls: + - pkg:pypi/wheel?source=hash-mapping + run_exports: {} + size: 32521 + timestamp: 1668051714265 +- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f + md5: 46e441ba871f524e2b067929da3051c2 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libopenvino 2025.2.0 hb617929_1 - - libstdcxx >=14 - purls: [] - size: 497047 - timestamp: 1753211285617 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenvino-tensorflow-lite-frontend-2025.2.0-hfae3067_1.conda - sha256: cd4651c37e45fe6779a32ebfb3000fb3e9742409cd9bd0ac141c130b2f8f8d56 - md5: 274b11e7ed763c4964a6b6d2130ec1cb + - __win + - python >=3.9 + license: LicenseRef-Public-Domain + purls: + - pkg:pypi/win-inet-pton?source=hash-mapping + run_exports: {} + size: 9555 + timestamp: 1733130678956 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aiohttp-3.9.5-py311he705e18_0.conda + sha256: 6e1c28d255830f350ccc135db4932153a978956d480e7bcd26c1663e19db4f9d + md5: a955769e6187495614f719668695e28f depends: - - libgcc >=14 - - libopenvino 2025.2.0 hcd21e76_1 - - libstdcxx >=14 + - aiosignal >=1.1.2 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - multidict >=4.5,<7.0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - yarl >=1.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/aiohttp?source=hash-mapping + run_exports: {} + size: 779497 + timestamp: 1713965157234 +- conda: https://conda.anaconda.org/conda-forge/osx-64/aom-3.9.1-hf036a51_0.conda + sha256: 3032f2f55d6eceb10d53217c2a7f43e1eac83603d91e21ce502e8179e63a75f5 + md5: 3f17bc32cb7fcb2b4bf3d8d37f656eb8 + depends: + - __osx >=10.13 + - libcxx >=16 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 456714 - timestamp: 1753203333676 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hbc7d668_1.conda - sha256: fde90b9981ba17a436ae7ce17e1caf4ea3f97c1a5cf55f5bed0d97c0d4a094f4 - md5: b04dfe98f9fa74ffa9f385cf57c4c455 + run_exports: + weak: + - aom >=3.9.1,<3.10.0a0 + size: 2749186 + timestamp: 1718551450314 +- conda: https://conda.anaconda.org/conda-forge/osx-64/binaryen-117-h73e2aa4_0.conda + sha256: f1dae7bbbdae9ee2f4b3479b51578fc67e77d54c5c235a5e5c7c1c58b2fff13e + md5: 029b1d804ba237f99163740225d53abc depends: - - __osx >=11.0 - - libcxx >=19 - - libopenvino 2025.2.0 h346e020_1 + - libcxx >=16 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 391641 - timestamp: 1753201485293 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-lite-frontend-2025.2.0-hec049ff_1.conda - sha256: 79f30d362a978300739b2f3b28dca0e0abca405a08637b445556737a92f5a80d - md5: 9ec0b186ee2d356aae50bb791bd54bfb + run_exports: {} + size: 3797571 + timestamp: 1709093347983 +- conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.2.0-py311h7e844b6_1.conda + sha256: 292026d98fd60bb25852792e2fd6ee97be35515057cfe258416ea6e1998e3564 + md5: ae49e04114f7f1673920fdbf326a047f depends: - - __osx >=11.0 + - __osx >=10.13 - libcxx >=19 - - libopenvino 2025.2.0 h56e7ac4_1 - purls: [] - size: 389727 - timestamp: 1753200797326 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopus-1.5.2-hd0c01bc_0.conda - sha256: 786d43678d6d1dc5f88a6bad2d02830cfd5a0184e84a8caa45694049f0e3ea5f - md5: b64523fb87ac6f87f0790f324ad43046 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + constrains: + - libbrotlicommon 1.2.0 h8616949_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + run_exports: {} + size: 389997 + timestamp: 1764017848151 +- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + sha256: 9f242f13537ef1ce195f93f0cc162965d6cc79da578568d6d8e50f70dd025c42 + md5: 4173ac3b19ec0a4f400b4f782910368b depends: - - libgcc >=13 - - __glibc >=2.17,<3.0.a0 - license: BSD-3-Clause + - __osx >=10.13 + license: bzip2-1.0.6 license_family: BSD purls: [] - size: 312472 - timestamp: 1744330953241 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopus-1.5.2-h86ecc28_0.conda - sha256: c887543068308fb0fd50175183a3513f60cd8eb1defc23adc3c89769fde80d48 - md5: 44b2cfec6e1b94723a960f8a5e6206ae + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 + size: 133427 + timestamp: 1771350680709 +- conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.34.8-ha1e9b39_0.conda + sha256: b879a20c5b29237707db9eac7f99b2f5128bb0095a3dbe8449557350ea510af9 + md5: 99bc571aba2ddd7130cb315fe38f6dd0 depends: - - libgcc >=13 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + license: MIT + license_family: MIT purls: [] - size: 357115 - timestamp: 1744331282621 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.5.2-he3325bb_0.conda - sha256: 1ca09dddde2f1b7bab1a8b1e546910be02e32238ebaa2f19e50e443b17d0660f - md5: dd0f9f16dfae1d1518312110051586f6 + run_exports: + weak: + - c-ares >=1.34.8,<2.0a0 + size: 188777 + timestamp: 1784091418806 +- conda: https://conda.anaconda.org/conda-forge/osx-64/c-compiler-1.6.0-h282daa2_0.conda + sha256: c52dcdd9b5fc9fd9a7eb028b7d4bb9f11f4ba3a7361e904d2b28bc12053bac23 + md5: 2b801fd417843897458f4f8e132e05bb depends: - - __osx >=10.13 - license: BSD-3-Clause - license_family: BSD + - cctools >=949.0.1 + - clang_osx-64 16.* + - ld64 >=530 + - llvm-openmp + license: BSD purls: [] - size: 331776 - timestamp: 1744331054952 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopus-1.5.2-h48c0fde_0.conda - sha256: 3a01094a59dd59d7a5a1c8e838c2ef3fccf9e098af575c38c26fceb56c6bb917 - md5: 882feb9903f31dca2942796a360d1007 + run_exports: {} + size: 6375 + timestamp: 1701504699534 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.4-h950ec3b_0.conda + sha256: d4297c3a9bcff9add3c5a46c6e793b88567354828bcfdb6fc9f6b1ab34aa4913 + md5: 32403b4ef529a2018e4d8c4f2a719f16 depends: - - __osx >=11.0 - license: BSD-3-Clause - license_family: BSD + - __osx >=10.13 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libcxx >=18 + - libexpat >=2.6.4,<3.0a0 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + license: LGPL-2.1-only or MPL-1.1 purls: [] - size: 299498 - timestamp: 1744330988108 -- conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.5.2-h2466b09_0.conda - sha256: 4c5e04de758450f9427a75095a54957de521b57234711374fac1cdc89fc7a9ca - md5: 67c18f2110921f6307a608050cd153f8 + run_exports: + weak: + - cairo >=1.18.4,<2.0a0 + size: 893252 + timestamp: 1741554808521 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cargo-llvm-cov-0.8.7-h009cd8f_0.conda + sha256: f8c47148aa17505288261f148994bd387c5615d120f0ab6603960e85f2f943f3 + md5: fc649ea1d1063a80d18bb4b56b410513 depends: - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: Apache-2.0 + license_family: Apache purls: [] - size: 289268 - timestamp: 1744330990400 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda - sha256: 0bd91de9b447a2991e666f284ae8c722ffb1d84acb594dbd0c031bd656fa32b2 - md5: 70e3400cbbfa03e96dcde7fc13e38c7b + run_exports: {} + size: 1253647 + timestamp: 1778642456224 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cargo-nextest-0.9.140-h19f9e61_0.conda + sha256: e046ab4c3bc5ff36f0d0566a78d8a0780cd7ffdfbd558a6721ad89a50e3c36d6 + md5: c452ce982dfba14c18c9d067848376f8 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - __osx >=11.0 + constrains: + - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 28424 - timestamp: 1749901812541 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.18-h86ecc28_0.conda - sha256: 7641dfdfe9bda7069ae94379e9924892f0b6604c1a016a3f76b230433bb280f2 - md5: 5044e160c5306968d956c2a0a2a440d6 + run_exports: {} + size: 7041520 + timestamp: 1783308151495 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cctools-1010.6-h40f6528_1.conda + sha256: 3e6ab1eb84f55df432af6b1893067c0dfa86e312c04d91824b199c125cf729e1 + md5: 7e7eb6bef28acef1112673443a8d692b depends: - - libgcc >=13 - license: MIT - license_family: MIT + - cctools_osx-64 1010.6 heaa7f0c_1 + - ld64 951.9 ha02d983_1 + - libllvm16 >=16.0.6,<16.1.0a0 + license: APSL-2.0 + license_family: Other purls: [] - size: 29512 - timestamp: 1749901899881 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda - sha256: e75a2723000ce3a4b9fd9b9b9ce77553556c93e475a4657db6ed01abc02ea347 - md5: 7af8e91b0deb5f8e25d1a595dea79614 + run_exports: {} + size: 21588 + timestamp: 1726771695380 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cctools_osx-64-1010.6-heaa7f0c_1.conda + sha256: 2769f7bde9888d100a9997da14aabef345a8ee0850fe2c90e2ca2306e7fe79bd + md5: eaedf7d6a7b93b35381f7a0b4663922a depends: - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 + - __osx >=10.13 + - ld64_osx-64 >=951.9,<951.10.0a0 + - libcxx + - libllvm16 >=16.0.6,<16.1.0a0 - libzlib >=1.3.1,<2.0a0 - license: zlib-acknowledgement + - llvm-tools 16.0.* + - sigtool + constrains: + - ld64 951.9.* + - cctools 1010.6.* + - clang 16.0.* + license: APSL-2.0 + license_family: Other purls: [] - size: 317390 - timestamp: 1753879899951 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.50-h1abf092_1.conda - sha256: e1effd7335ec101bb124f41a5f79fabb5e7b858eafe0f2db4401fb90c51505a7 - md5: ed42935ac048d73109163d653d9445a0 + run_exports: {} + size: 1099432 + timestamp: 1726771664399 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-2.1.0-py311hc34a7ac_0.conda + sha256: 822c327e635b8ab16122967a85f67b43f3b49958c2acaffec0593dc7d41ad8c1 + md5: 96c7ed5fac11001cdfd1d996eb8fa03a depends: - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 - license: zlib-acknowledgement + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pycparser + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + run_exports: {} + size: 300498 + timestamp: 1783424543956 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16-16.0.6-default_h4651f56_15.conda + sha256: 53fefb4b47993b9cfc1b7d2f85fe66b8a62e312658eaa23f054b2d5e31bb529e + md5: 696148e51e076dea8f4d39b60280f933 + depends: + - __osx >=10.13 + - libclang-cpp16 16.0.6 default_h4651f56_15 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + constrains: + - clang-tools 16.0.6 + - clangxx 16.0.6 + - clangdev 16.0.6 + - llvm-tools 16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: {} + size: 762659 + timestamp: 1756166994264 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-16.0.6-default_h510d6ca_15.conda + sha256: 377762f985606a4a5104cf7810acf0e0371bca30eef66b99cdfad92dd359f72a + md5: 6a57e5f291f44a7b1360372db400d672 + depends: + - clang-16 16.0.6 default_h4651f56_15 + constrains: + - clang-tools 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 339168 - timestamp: 1753879915462 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.50-h84aeda2_1.conda - sha256: 8d92c82bcb09908008d8cf5fab75e20733810d40081261d57ef8cd6495fc08b4 - md5: 1fe32bb16991a24e112051cc0de89847 + run_exports: {} + size: 92070 + timestamp: 1756167200107 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16-16.0.6-default_h4651f56_15.conda + sha256: 4881cded9d4551050a04184f380b4bb50d75e4c7868136b4d9f7d96481e8affb + md5: 79ce8c25f88855de6a6acc2c8a31430b depends: - __osx >=10.13 - - libzlib >=1.3.1,<2.0a0 - license: zlib-acknowledgement + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 297609 - timestamp: 1753879919854 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda - sha256: a2e0240fb0c79668047b528976872307ea80cb330baf8bf6624ac2c6443449df - md5: 4d0f5ce02033286551a32208a5519884 + run_exports: {} + size: 128881 + timestamp: 1756167683330 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-format-16.0.6-default_h4651f56_15.conda + sha256: a6d474ca999b1a0fabc34c9e92a68a6c4ef2eaf6c4a86a0881c6c4b14c22884f + md5: 4f39be6579f9ec9afad0b15fec159fc0 depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - license: zlib-acknowledgement + - __osx >=10.13 + - clang-format-16 16.0.6 default_h4651f56_15 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 287056 - timestamp: 1753879907258 -- conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda - sha256: e84b041f91c94841cb9b97952ab7f058d001d4a15ed4ce226ec5fdb267cc0fa5 - md5: 3ae6e9f5c47c495ebeed95651518be61 + run_exports: {} + size: 92323 + timestamp: 1756167820703 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clang-tools-16.0.6-default_h4651f56_15.conda + sha256: 324e12f7d311d584d28098e048bf3610c7a12eea96ddc0d55616025c214d0fd8 + md5: 006fd8afef42dd85907aca61970d553a depends: - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - libzlib >=1.3.1,<2.0a0 - license: zlib-acknowledgement + - __osx >=10.13 + - clang-format 16.0.6 default_h4651f56_15 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libclang13 >=16.0.6 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + constrains: + - clangdev 16.0.6 + - clang 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 382709 - timestamp: 1753879944850 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.31.1-h49aed37_2.conda - sha256: 1679f16c593d769f3dab219adb1117cbaaddb019080c5a59f79393dc9f45b84f - md5: 94cb88daa0892171457d9fdc69f43eca + run_exports: {} + size: 17932186 + timestamp: 1756168027313 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clang_impl_osx-64-16.0.6-h8787910_19.conda + sha256: 7c8146bb69ddf42af2e30d83ad357985732052eccfbaf279d433349e0c1324de + md5: 64155ef139280e8c181dad866dea2980 depends: - - __glibc >=2.17,<3.0.a0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libstdcxx >=14 - - libzlib >=1.3.1,<2.0a0 + - cctools_osx-64 + - clang 16.0.6.* + - compiler-rt 16.0.6.* + - ld64_osx-64 + - llvm-tools 16.0.6.* license: BSD-3-Clause license_family: BSD purls: [] - size: 4645876 - timestamp: 1760550892361 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-6.31.1-h2cf3c76_2.conda - sha256: e1bfa4ee03ddfa3a5e347d6796757a373878b2f277ed48dbc32412b05e16e776 - md5: 8eb7b485dcbb81166e340a07ccb40e67 + run_exports: {} + size: 17589 + timestamp: 1723069343993 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clang_osx-64-16.0.6-hb91bd55_19.conda + sha256: d38be1dc9476fdc60dfbd428df0fb3e284ee9101e7eeaa1764b54b11bab54105 + md5: 760ecbc6f4b6cecbe440b0080626286f depends: - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libstdcxx >=14 - - libzlib >=1.3.1,<2.0a0 + - clang_impl_osx-64 16.0.6 h8787910_19 license: BSD-3-Clause license_family: BSD purls: [] - size: 4465754 - timestamp: 1760550264433 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h03562ea_2.conda - sha256: 40a32a77cdb7f7b49187a4c9faf5c7812d95233288ab96b06e0dd9978ecd8e6d - md5: 39b7711c03a0d0533e832e734641e56e + run_exports: {} + size: 20580 + timestamp: 1723069348997 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx-16.0.6-default_h1b9e3cd_15.conda + sha256: 945d52e908b9a52b3a290eedcf7a7865f80334f2cc1dacc7a2809f5189388086 + md5: 75da7c70527c5330f3a88ea8138d0303 depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD + - clang 16.0.6 default_h510d6ca_15 + - libcxx-devel 16.0.6.* + constrains: + - libcxx-devel 16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 3550823 - timestamp: 1760550860606 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h658db43_2.conda - sha256: a01c3829eb0e3c1354ee7d61c5cde9a79dcebe6ccc7114c2feadf30aecbc7425 - md5: 155d3d17eaaf49ddddfe6c73842bc671 + run_exports: {} + size: 92193 + timestamp: 1756167234819 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_impl_osx-64-16.0.6-h6d92fbe_19.conda + sha256: c99c773d76a93066f1e78d368f934cd904b4f39a3939bf1d5a5cf26e3b812dbc + md5: 9ffa16e2bd7eb5b8b1a0d19185710cd3 depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libzlib >=1.3.1,<2.0a0 + - clang_osx-64 16.0.6 hb91bd55_19 + - clangxx 16.0.6.* + - libcxx >=16 + - libllvm16 >=16.0.6,<16.1.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 2982875 - timestamp: 1760550241203 -- conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.32.1-h514701f_1.conda - sha256: 6d28b9643a45c27f8f63b6349b73c40a9322084e767f9088fbac20c6467d8073 - md5: 3690234545b61fd615e17e2ffdf0ab84 + run_exports: {} + size: 17642 + timestamp: 1723069387016 +- conda: https://conda.anaconda.org/conda-forge/osx-64/clangxx_osx-64-16.0.6-hb91bd55_19.conda + sha256: 8c2cf371561f8de565aa721520d34e14ff9cf9b7e3a868879ec2f99760c433cc + md5: 81d40fad4c14cc7a893f2e274647c7a4 depends: - - libabseil * cxx17* - - libabseil >=20250814.1,<20250815.0a0 - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - clang_osx-64 16.0.6 hb91bd55_19 + - clangxx_impl_osx-64 16.0.6 h6d92fbe_19 license: BSD-3-Clause license_family: BSD purls: [] - size: 7302269 - timestamp: 1760478705997 -- conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda - sha256: a45ef03e6e700cc6ac6c375e27904531cf8ade27eb3857e080537ff283fb0507 - md5: d27665b20bc4d074b86e628b3ba5ab8b - depends: - - __glibc >=2.17,<3.0.a0 - - cairo >=1.18.4,<2.0a0 - - freetype >=2.13.3,<3.0a0 - - gdk-pixbuf >=2.42.12,<3.0a0 - - harfbuzz >=11.0.0,<12.0a0 - - libgcc >=13 - - libglib >=2.84.0,<3.0a0 - - libpng >=1.6.47,<1.7.0a0 - - libxml2 >=2.13.7,<2.14.0a0 - - pango >=1.56.3,<2.0a0 - constrains: - - __glibc >=2.17 - license: LGPL-2.1-or-later - purls: [] - size: 6543651 - timestamp: 1743368725313 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.58.4-h3ac5bce_3.conda - sha256: e305cf09ec904625a66c7db1305595691c633276b7e34521537cef88edc5249a - md5: b115c14b3919823fbe081366d2b15d86 - depends: - - cairo >=1.18.4,<2.0a0 - - freetype >=2.13.3,<3.0a0 - - gdk-pixbuf >=2.42.12,<3.0a0 - - harfbuzz >=11.0.0,<12.0a0 - - libgcc >=13 - - libglib >=2.84.0,<3.0a0 - - libpng >=1.6.47,<1.7.0a0 - - libxml2 >=2.13.7,<2.14.0a0 - - pango >=1.56.3,<2.0a0 - constrains: - - __glibc >=2.17 - license: LGPL-2.1-or-later - purls: [] - size: 6274749 - timestamp: 1743376660664 -- conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.58.4-h21a6cfa_3.conda - sha256: 87432fca28ddfaaf82b3cd12ce4e31fcd963428d1f2c5e2a3aef35dd30e56b71 - md5: 213dcdb373bf108d1beb18d33075f51d + run_exports: + strong: + - libcxx >=16 + size: 19289 + timestamp: 1723069392162 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cmake-3.27.6-hf40c264_0.conda + sha256: 9216698f88b82e99db950f8c372038931c54ea3e0b0b05e2a3ce03ec4b405df7 + md5: 771da6a52aaf0f9d84114d0ed0d0299f depends: - - __osx >=10.13 - - cairo >=1.18.4,<2.0a0 - - gdk-pixbuf >=2.42.12,<3.0a0 - - libglib >=2.84.0,<3.0a0 - - libxml2 >=2.13.7,<2.14.0a0 - - pango >=1.56.3,<2.0a0 - constrains: - - __osx >=10.13 - license: LGPL-2.1-or-later + - bzip2 >=1.0.8,<2.0a0 + - libcurl >=8.3.0,<9.0a0 + - libcxx >=15.0.7 + - libexpat >=2.5.0,<3.0a0 + - libuv >=1.46.0,<2.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - ncurses >=6.4,<7.0a0 + - rhash >=1.4.4,<2.0a0 + - xz >=5.2.6,<6.0a0 + - zstd >=1.5.5,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 4946543 - timestamp: 1743368938616 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.58.4-h266df6f_3.conda - sha256: 0ec066d7f22bcd9acb6ca48b2e6a15e9be4f94e67cb55b0a2c05a37ac13f9315 - md5: 95d6ad8fb7a2542679c08ce52fafbb6c + run_exports: {} + size: 16525734 + timestamp: 1695270838345 +- conda: https://conda.anaconda.org/conda-forge/osx-64/compiler-rt-16.0.6-ha38d28d_2.conda + sha256: de0e2c94d9a04f60ec9aedde863d6c1fad3f261bdb63ec8adc70e2d9ecdb07bb + md5: 3b9e8c5c63b8e86234f499490acd85c2 depends: - - __osx >=11.0 - - cairo >=1.18.4,<2.0a0 - - gdk-pixbuf >=2.42.12,<3.0a0 - - libglib >=2.84.0,<3.0a0 - - libxml2 >=2.13.7,<2.14.0a0 - - pango >=1.56.3,<2.0a0 - constrains: - - __osx >=11.0 - license: LGPL-2.1-or-later + - clang 16.0.6.* + - clangxx 16.0.6.* + - compiler-rt_osx-64 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE purls: [] - size: 4607782 - timestamp: 1743369546790 -- conda: https://conda.anaconda.org/conda-forge/win-64/librsvg-2.58.4-h5ce5fed_3.conda - sha256: 8910bc40a52f2b979ced95137f09b8faf0113e14c430ca8fa7dd94dc88dafb83 - md5: 34fefcb3aed33ea39f1b040f5b9849e3 + run_exports: {} + size: 94198 + timestamp: 1701467261175 +- conda: https://conda.anaconda.org/conda-forge/osx-64/cxx-compiler-1.6.0-h7728843_0.conda + sha256: 3d609b7cf397b1d9f8627dedd0abd95a9daffa919d9593b56096a4e6e4a8597e + md5: 52efcad0d146779100e46c973cc1cb56 depends: - - cairo >=1.18.4,<2.0a0 - - gdk-pixbuf >=2.42.12,<3.0a0 - - libglib >=2.84.0,<3.0a0 - - libxml2 >=2.13.7,<2.14.0a0 - - pango >=1.56.3,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.42.34438 - license: LGPL-2.1-or-later + - c-compiler 1.6.0 h282daa2_0 + - clangxx_osx-64 16.* + license: BSD purls: [] - size: 3919170 - timestamp: 1743369262131 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.4.0-ha732cd4_2.conda - sha256: d9a23eee55fc2a901e67565c328c37e7c2336ca805d985ad4a67b7837fb4e40a - md5: e729f335fee31fd68429187c9e0f97c2 + run_exports: {} + size: 6415 + timestamp: 1701504710176 +- conda: https://conda.anaconda.org/conda-forge/osx-64/dav1d-1.2.1-h0dc2134_0.conda + sha256: ec71a835866b42e946cd2039a5f7a6458851a21890d315476f5e66790ac11c96 + md5: 9d88733c715300a39f8ca2e936b7808d + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - dav1d >=1.2.1,<1.2.2.0a0 + size: 668439 + timestamp: 1685696184631 +- conda: https://conda.anaconda.org/conda-forge/osx-64/dbus-1.16.2-h6e7f9a9_1.conda + sha256: 80ea0a20236ecb7006f7a89235802a34851eaac2f7f4323ca7acc094bcf7f372 + md5: cdbed7d22d4bdd74e60ce78bc7c6dd58 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=12.4.0 - - libstdcxx >=12.4.0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - __osx >=10.13 + - libcxx >=19 + - libexpat >=2.7.3,<3.0a0 + - libglib >=2.86.2,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: AFL-2.1 OR GPL-2.0-or-later purls: [] - size: 3955974 - timestamp: 1740240321338 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsanitizer-12.4.0-h469570c_2.conda - sha256: b1c8db474fb2e2249544a17c78e6306829bc42ae7dc97e3dcf16291cded7ed9e - md5: 5a300cbd50f7e0fc582d325ac3c28c50 + run_exports: + weak: + - dbus >=1.16.2,<2.0a0 + size: 407670 + timestamp: 1764536068038 +- conda: https://conda.anaconda.org/conda-forge/osx-64/doxygen-1.9.7-hd7636e7_1.conda + sha256: b3a43f399a710dbfff7f0380d43db3c7155ae128af5f14a0a23ac51a48209123 + md5: 00ada1ebe41c7febae72032969017b09 depends: - - libgcc >=12.4.0 - - libstdcxx >=12.4.0 - license: GPL-3.0-only WITH GCC-exception-3.1 + - libcxx >=15.0.7 + - libiconv >=1.17,<2.0a0 + license: GPL-2.0-only license_family: GPL purls: [] - size: 3926612 - timestamp: 1740240236305 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsndfile-1.2.2-hc60ed4a_1.conda - sha256: f709cbede3d4f3aee4e2f8d60bd9e256057f410bd60b8964cb8cf82ec1457573 - md5: ef1910918dd895516a769ed36b5b3a4e + run_exports: {} + size: 5344962 + timestamp: 1687332955991 +- conda: https://conda.anaconda.org/conda-forge/osx-64/fd-find-10.4.2-h009cd8f_0.conda + sha256: 472f04555f084c7c3f554a2d65c540b386ab55ed66055823dc59c029321afcb0 + md5: 7898be59567e317ac459b27fddf57cbd depends: - - lame >=3.100,<3.101.0a0 - - libflac >=1.4.3,<1.5.0a0 - - libgcc-ng >=12 - - libogg >=1.3.4,<1.4.0a0 - - libopus >=1.3.1,<2.0a0 - - libstdcxx-ng >=12 - - libvorbis >=1.3.7,<1.4.0a0 - - mpg123 >=1.32.1,<1.33.0a0 - license: LGPL-2.1-or-later - license_family: LGPL + - __osx >=11.0 + constrains: + - __osx >=10.13 + license: MIT + license_family: MIT purls: [] - size: 354372 - timestamp: 1695747735668 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsndfile-1.2.2-h79657aa_1.conda - sha256: 8fcd5e45d6fb071e8baf492ebb8710203fd5eedf0cb791e007265db373c89942 - md5: ad8e62c0faec46b1442f960489c80b49 + run_exports: {} + size: 1133353 + timestamp: 1773353161332 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ffmpeg-7.1.1-gpl_hf226373_110.conda + sha256: 167c459251ecd586be917042df0432e6c90c115f881231af962f5c35fd35c8f7 + md5: b63b503d159f1eb6c9d98587c65c59b3 depends: + - __osx >=10.13 + - aom >=3.9.1,<3.10.0a0 + - bzip2 >=1.0.8,<2.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - gmp >=6.3.0,<7.0a0 + - harfbuzz >=11.4.5 - lame >=3.100,<3.101.0a0 - - libflac >=1.4.3,<1.5.0a0 - - libgcc-ng >=12 - - libogg >=1.3.4,<1.4.0a0 - - libopus >=1.3.1,<2.0a0 - - libstdcxx-ng >=12 + - libass >=0.17.4,<0.17.5.0a0 + - libcxx >=19 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libopenvino >=2025.2.0,<2025.2.1.0a0 + - libopenvino-auto-batch-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-auto-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-hetero-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-intel-cpu-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 + - libopus >=1.5.2,<2.0a0 + - librsvg >=2.58.4,<3.0a0 - libvorbis >=1.3.7,<1.4.0a0 - - mpg123 >=1.32.1,<1.33.0a0 - license: LGPL-2.1-or-later - license_family: LGPL - purls: [] - size: 396501 - timestamp: 1695747749825 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - sha256: 6d9c32fc369af5a84875725f7ddfbfc2ace795c28f246dc70055a79f9b2003da - md5: 0b367fad34931cb79e0d6b7e5c06bb1c - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 - license: blessing - purls: [] - size: 932581 - timestamp: 1753948484112 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.50.4-h022381a_0.conda - sha256: a361dc926f232e7f3aa664dbd821f12817601c07d2c8751a0668c2fb07d0e202 - md5: 0ad1b73a3df7e3376c14efe6dabe6987 - depends: - - libgcc >=14 + - libvpx >=1.14.1,<1.15.0a0 + - libxml2 >=2.13.8,<2.14.0a0 - libzlib >=1.3.1,<2.0a0 - license: blessing + - openh264 >=2.6.0,<2.6.1.0a0 + - openssl >=3.5.2,<4.0a0 + - sdl2 >=2.32.54,<3.0a0 + - svt-av1 >=3.1.2,<3.1.3.0a0 + - x264 >=1!164.3095,<1!165 + - x265 >=3.5,<3.6.0a0 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 931661 - timestamp: 1753948557036 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.50.4-h39a8b3b_0.conda - sha256: 466366b094c3eb4b1d77320530cbf5400e7a10ab33e4824c200147488eebf7a6 - md5: 156bfb239b6a67ab4a01110e6718cbc4 + run_exports: + weak: + - ffmpeg >=7.1.1,<8.0a0 + size: 10215471 + timestamp: 1757215303226 +- conda: https://conda.anaconda.org/conda-forge/osx-64/flatbuffers-25.12.19-h06076ce_0.conda + sha256: f23fb474996e6ff0375309ea32e41eacddc41cb717dee4afb6535d69058dd82c + md5: ba143d1c0e58368ecd9a9985b1bc3dfb depends: + - libcxx >=19 - __osx >=10.13 - - libzlib >=1.3.1,<2.0a0 - license: blessing + license: Apache-2.0 + license_family: APACHE purls: [] - size: 980121 - timestamp: 1753948554003 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda - sha256: 802ebe62e6bc59fc26b26276b793e0542cfff2d03c086440aeaf72fb8bbcec44 - md5: 1dcb0468f5146e38fae99aef9656034b + run_exports: + weak: + - flatbuffers >=25.12.19,<25.12.20.0a0 + size: 1636310 + timestamp: 1766388873085 +- conda: https://conda.anaconda.org/conda-forge/osx-64/fontconfig-2.18.1-h7a4440b_0.conda + sha256: 134aed823beae85798607e32b78aa1368afbfbea145a43c974d88269f1013287 + md5: 17925ae2a399d859c0b978934df591e3 depends: - __osx >=11.0 - - icu >=75.1,<76.0a0 - - libzlib >=1.3.1,<2.0a0 - license: blessing + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libintl >=0.25.1,<1.0a0 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 902645 - timestamp: 1753948599139 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda - sha256: 5dc4f07b2d6270ac0c874caec53c6984caaaa84bc0d3eb593b0edf3dc8492efa - md5: ccb20d946040f86f0c05b644d5eadeca - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: blessing + run_exports: + weak: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + size: 247884 + timestamp: 1780450811484 +- conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.14.3-h694c41f_1.conda + sha256: c67130a919d3c7733fce056cc2ce8cec2935e295547d5d70bcbf35e4351d543b + md5: 48fc845b770770e9c7db8743f6d53d44 + depends: + - libfreetype 2.14.3 h694c41f_1 + - libfreetype6 2.14.3 h58fbd8d_1 + license: GPL-2.0-only OR FTL purls: [] - size: 1288499 - timestamp: 1753948889360 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda - sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661 - md5: eecce068c7e4eddeb169591baac20ac4 + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + size: 174300 + timestamp: 1780934162319 +- conda: https://conda.anaconda.org/conda-forge/osx-64/fribidi-1.0.16-h8616949_0.conda + sha256: 53dd0a6c561cf31038633aaa0d52be05da1f24e86947f06c4e324606c72c7413 + md5: 4422491d30462506b9f2d554ab55e33d depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - license: BSD-3-Clause - license_family: BSD + - __osx >=10.13 + license: LGPL-2.1-or-later purls: [] - size: 304790 - timestamp: 1745608545575 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-h18c354c_0.conda - sha256: 1e289bcce4ee6a5817a19c66e296f3c644dcfa6e562e5c1cba807270798814e7 - md5: eecc495bcfdd9da8058969656f916cc2 + run_exports: + weak: + - fribidi >=1.0.16,<2.0a0 + size: 60923 + timestamp: 1757438791418 +- conda: https://conda.anaconda.org/conda-forge/osx-64/frozenlist-1.8.0-py311ha09d3ca_0.conda + sha256: e944858b369b90d4f262471ed9bd9e4a23f5f62fe59d2dab89152d1def6d0f54 + md5: eb2165c2c36bb77df65cc77ff2f98cf8 depends: - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libcxx >=19 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} + size: 50913 + timestamp: 1780000106588 +- conda: https://conda.anaconda.org/conda-forge/osx-64/gdk-pixbuf-2.44.7-hae309b2_0.conda + sha256: b936e714e8b449ea1fe3f9773392794795b3c3142666d56806ee3c4c9e8f70e4 + md5: 4b55a5953b14e83c7a52d864ddfac733 + depends: + - __osx >=11.0 + - libglib >=2.88.2,<3.0a0 + - libintl >=0.25.1,<1.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 311396 - timestamp: 1745609845915 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda - sha256: 00654ba9e5f73aa1f75c1f69db34a19029e970a4aeb0fa8615934d8e9c369c3c - md5: a6cb15db1c2dc4d3a5f6cf3772e09e81 + run_exports: + weak: + - gdk-pixbuf >=2.44.7,<3.0a0 + size: 556945 + timestamp: 1782591683373 +- conda: https://conda.anaconda.org/conda-forge/osx-64/gh-2.96.0-h5839d16_0.conda + sha256: c21b3f4914a52e19bae2ea6deb9c8ea05f7d40539cf81363ccc1d1bf7386cd55 + md5: aa8f94bbfc02dc3635ed9fbae4445a33 + constrains: + - __osx >=10.12 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 13289179 + timestamp: 1783039197970 +- conda: https://conda.anaconda.org/conda-forge/osx-64/gmp-6.3.0-hf036a51_2.conda + sha256: 75aa5e7a875afdcf4903b7dc98577672a3dc17b528ac217b915f9528f93c85fc + md5: 427101d13f19c4974552a4e5b072eef1 depends: - __osx >=10.13 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - license: BSD-3-Clause - license_family: BSD + - libcxx >=16 + license: GPL-2.0-or-later OR LGPL-3.0-or-later purls: [] - size: 284216 - timestamp: 1745608575796 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda - sha256: 8bfe837221390ffc6f111ecca24fa12d4a6325da0c8d131333d63d6c37f27e0a - md5: b68e8f66b94b44aaa8de4583d3d4cc40 + run_exports: + weak: + - gmp >=6.3.0,<7.0a0 + size: 428919 + timestamp: 1718981041839 +- conda: https://conda.anaconda.org/conda-forge/osx-64/graphite2-1.3.15-hcc62823_0.conda + sha256: aaebae3c0e713579e52de6fd4eec54a172e28c7f90d90da4583e91b1634a7fee + md5: 6a0525cf3166f16b9e156fb6b2cac5c0 depends: - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libcxx >=19 + license: LGPL-2.0-or-later + license_family: LGPL purls: [] - size: 279193 - timestamp: 1745608793272 -- conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda - sha256: cbdf93898f2e27cefca5f3fe46519335d1fab25c4ea2a11b11502ff63e602c09 - md5: 9dce2f112bfd3400f4f432b3d0ac07b2 + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 + size: 85964 + timestamp: 1780454502704 +- conda: https://conda.anaconda.org/conda-forge/osx-64/harfbuzz-12.2.0-hc5d3ef4_0.conda + sha256: 352c0fe4445599c3081a41e16b91d66041f9115b9490b7f3daea63897f593385 + md5: 05a72f9d35dddd5bf534d7da4929297c depends: + - __osx >=10.13 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libcxx >=19 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libglib >=2.86.1,<3.0a0 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD + license: MIT + license_family: MIT purls: [] - size: 292785 - timestamp: 1745608759342 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda - sha256: 0f5f61cab229b6043541c13538d75ce11bd96fb2db76f94ecf81997b1fde6408 - md5: 4e02a49aaa9d5190cb630fa43528fbe6 + run_exports: + weak: + - harfbuzz >=12.2.0 + size: 1875555 + timestamp: 1762373120771 +- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda + sha256: 2e64307532f482a0929412976c8450c719d558ba20c0962832132fd0d07ba7a7 + md5: d68d48a3060eb5abdc1cdc8e2a3a5966 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc 15.1.0 h767d61c_5 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - __osx >=10.13 + license: MIT + license_family: MIT purls: [] - size: 3896432 - timestamp: 1757042571458 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.1.0-h3f4de04_5.conda - sha256: 012b552fdb3fc4f703341b4c6d56313951f3fa8e817a7e7ecaef99d51920faad - md5: 06758dc7550f212f095936e35255f32e + run_exports: + weak: + - icu >=75.1,<76.0a0 + size: 11761697 + timestamp: 1720853679409 +- conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.22.2-h3ddfcb2_1.conda + sha256: c6342c340b18651d14b6134e223904da6f6099665e45449efb683d4c68b28432 + md5: e070b249c4f9c6bddb7984a1a794e8df depends: - - libgcc 15.1.0 he277a41_5 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - __osx >=11.0 + - libcxx >=19 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT purls: [] - size: 3827611 - timestamp: 1757043023868 -- conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.4.0-h1762d19_102.conda - sha256: 5e86d884d6877ce428d90a484cdc66d5968bf81dc189393239c43fe9b831da7d - md5: aa2ae7befd3d165f3cfc4d3b39cebeb5 + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1195956 + timestamp: 1781860554632 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lame-3.100-hb7f2c08_1003.tar.bz2 + sha256: 0f943b08abb4c748d73207594321b53bad47eea3e7d06b6078e0f6c59ce6771e + md5: 3342b33c9a0921b22b767ed68ee25861 + license: LGPL-2.0-only + license_family: LGPL + purls: [] + run_exports: + weak: + - lame >=3.100,<3.101.0a0 + size: 542681 + timestamp: 1664996421531 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ld64-951.9-ha02d983_1.conda + sha256: 4a27102c8451ce30b3c2d90722826e8bd02e9bb3b92cd5afaa08c65bbe6447f5 + md5: 8991ffc3c5c410692d8740de4cb92849 depends: - - __unix - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - ld64_osx-64 951.9 h3516399_1 + - libllvm16 >=16.0.6,<16.1.0a0 + constrains: + - cctools 1010.6.* + - cctools_osx-64 1010.6.* + license: APSL-2.0 + license_family: Other purls: [] - size: 11883113 - timestamp: 1740240215984 -- conda: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-aarch64-12.4.0-h7b3af7c_102.conda - sha256: 277208c0d21a068c1bb1bf1b2ae92f159ba866cfc75a882569b286e339d6c518 - md5: d5b8708faacba4063d7a150cf9ec94f7 + run_exports: {} + size: 18850 + timestamp: 1726771680769 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ld64_osx-64-951.9-h3516399_1.conda + sha256: 03417d5a379bf8e7b2ac99000d9af836cae53b843e02de7cea066c4ddd88767c + md5: 4656f00ccd13a49804387450302c4f45 depends: - - __unix - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - __osx >=10.13 + - libcxx + - libllvm16 >=16.0.6,<16.1.0a0 + - sigtool + - tapi >=1300.6.5,<1301.0a0 + constrains: + - clang >=16.0.6,<17.0a0 + - cctools 1010.6.* + - cctools_osx-64 1010.6.* + - ld 951.9.* + license: APSL-2.0 + license_family: Other purls: [] - size: 10156474 - timestamp: 1740240151058 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda - sha256: 7b8cabbf0ab4fe3581ca28fe8ca319f964078578a51dd2ca3f703c1d21ba23ff - md5: 8bba50c7f4679f08c861b597ad2bda6b + run_exports: {} + size: 1088101 + timestamp: 1726771578888 +- conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.1.0-h35c7297_0.conda + sha256: f918716c71c8bebbc0c40e1050878aa512fea92c1d17c363ca35650bc60f6c35 + md5: d2fe7e177d1c97c985140bd54e2a5e33 depends: - - libstdcxx 15.1.0 h8f9b012_5 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - __osx >=11.0 + - libcxx >=19 + license: Apache-2.0 + license_family: Apache purls: [] - size: 29233 - timestamp: 1757042603319 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.1.0-hf1166c9_5.conda - sha256: 67567a6ceb581b5ece3e9a43cbf37e8781313917c3227eb53e9d31ba61d02277 - md5: 08ea9416b779ffbe8e11b5b835919468 + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 + size: 215089 + timestamp: 1773114468701 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250512.1-cxx17_hfc00f1c_0.conda + sha256: a878efebf62f039a1f1733c1e150a75a99c7029ece24e34efdf23d56256585b1 + md5: ddf1acaed2276c7eb9d3c76b49699a11 depends: - - libstdcxx 15.1.0 h3f4de04_5 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - __osx >=10.13 + - libcxx >=18 + constrains: + - abseil-cpp =20250512.1 + - libabseil-static =20250512.1=cxx17* + license: Apache-2.0 + license_family: Apache purls: [] - size: 29229 - timestamp: 1757043052495 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsystemd0-256.9-h2774228_0.conda - sha256: a93e45c12c2954942a994ff3ffc8b9a144261288032da834ed80a6210708ad49 - md5: 7b283ff97a87409a884bc11283855c17 + run_exports: + weak: + - libabseil >=20250512.1,<20250513.0a0 + - libabseil =*=cxx17* + size: 1162435 + timestamp: 1750194293086 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libass-0.17.4-h87c4fc2_0.conda + sha256: 7ddcb016d016919f1735fd2c6b826bb4d7dabd995d053b748d41ef47343fe001 + md5: 3db36f8bfe00ab9cda1e72cd59fdd415 depends: - - __glibc >=2.17,<3.0.a0 - - libcap >=2.71,<2.72.0a0 - - libgcc >=13 - - libgcrypt-lib >=1.11.0,<2.0a0 - - lz4-c >=1.9.3,<1.10.0a0 - - xz >=5.2.6,<6.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: LGPL-2.1-or-later + - __osx >=10.13 + - libiconv >=1.18,<2.0a0 + - harfbuzz >=11.0.1 + - fribidi >=1.0.10,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libzlib >=1.3.1,<2.0a0 + license: ISC purls: [] - size: 410424 - timestamp: 1733312416327 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsystemd0-256.9-hd54d049_0.conda - sha256: d04ea4fa1b3282029039ec28054f53b0c5b3ef044303450e5684e2a690e7aa52 - md5: 9ee06ecb3e342bf03e163af5080acd9f + run_exports: + weak: + - libass >=0.17.4,<0.17.5.0a0 + size: 157712 + timestamp: 1749329008301 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.2.0-h8616949_1.conda + sha256: 4c19b211b3095f541426d5a9abac63e96a5045e509b3d11d4f9482de53efe43b + md5: f157c098841474579569c85a60ece586 depends: - - libcap >=2.71,<2.72.0a0 - - libgcc >=13 - - libgcrypt-lib >=1.11.0,<2.0a0 - - lz4-c >=1.9.3,<1.10.0a0 - - xz >=5.2.6,<6.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: LGPL-2.1-or-later + - __osx >=10.13 + license: MIT + license_family: MIT purls: [] - size: 430930 - timestamp: 1733311785480 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda - sha256: c62694cd117548d810d2803da6d9063f78b1ffbf7367432c5388ce89474e9ebe - md5: b6093922931b535a7ba566b6f384fbe6 + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + size: 78854 + timestamp: 1764017554982 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.2.0-h8616949_1.conda + sha256: 729158be90ae655a4e0427fe4079767734af1f9b69ff58cf94ca6e8d4b3eb4b7 + md5: 63186ac7a8a24b3528b4b14f21c03f54 depends: - - __glibc >=2.17,<3.0.a0 - - lerc >=4.0.0,<5.0a0 - - libdeflate >=1.24,<1.25.0a0 - - libgcc >=14 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libstdcxx >=14 - - libwebp-base >=1.6.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: HPND + - __osx >=10.13 + - libbrotlicommon 1.2.0 h8616949_1 + license: MIT + license_family: MIT purls: [] - size: 433078 - timestamp: 1755011934951 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.0-h7a57436_6.conda - sha256: 7ffe5cd8455bc0b5d4b6f092ae552dd6e1feac8e512f206ac8e03adda1b494bc - md5: 360b68f57756b64922d5d3af5e986fa9 + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 30835 + timestamp: 1764017584474 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.2.0-h8616949_1.conda + sha256: 8ece7b41b6548d6601ac2c2cd605cf2261268fc4443227cc284477ed23fbd401 + md5: 12a58fd3fc285ce20cf20edf21a0ff8f depends: - - lerc >=4.0.0,<5.0a0 - - libdeflate >=1.24,<1.25.0a0 - - libgcc >=14 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libstdcxx >=14 - - libwebp-base >=1.6.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: HPND + - __osx >=10.13 + - libbrotlicommon 1.2.0 h8616949_1 + license: MIT + license_family: MIT purls: [] - size: 481479 - timestamp: 1755012014975 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.0-h59ddb5d_6.conda - sha256: 656dc01238d4b766e35976319aba2a9b3ea707b467b7a5aad94ef49a150be7a8 - md5: 1cb7b8054ffa9460ca3dd782062f3074 + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 + size: 310355 + timestamp: 1764017609985 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libclang-cpp16-16.0.6-default_h4651f56_15.conda + sha256: 04f882afadb3af2e373efb5f542e8ff6b3aaea8326bf85b7445b9c727d1e0135 + md5: 5d3cb1a184771445034f2113ba543827 depends: - __osx >=10.13 - - lerc >=4.0.0,<5.0a0 - - libcxx >=19 - - libdeflate >=1.24,<1.25.0a0 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libwebp-base >=1.6.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: HPND + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 401676 - timestamp: 1755012183336 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.0-h025e3ab_6.conda - sha256: d6ed4b307dde5d66b73aa3f155b3ed40ba9394947cfe148e2cd07605ef4b410b - md5: d0862034c2c563ef1f52a3237c133d8d + run_exports: + weak: + - libclang-cpp16 >=16.0.6,<16.1.0a0 + size: 12759044 + timestamp: 1756166818220 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libclang13-21.1.0-default_h7f9524c_1.conda + sha256: 7a39bb169f583c4da4ebc47729d8cf2c41763364010e7c12956dc0c0a86741d6 + md5: 8c5c6f63bb40997ae614b23a770b0369 depends: - - __osx >=11.0 - - lerc >=4.0.0,<5.0a0 - - libcxx >=19 - - libdeflate >=1.24,<1.25.0a0 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libwebp-base >=1.6.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: HPND + - __osx >=10.13 + - libcxx >=21.1.0 + - libllvm21 >=21.1.0,<21.2.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 372136 - timestamp: 1755012109767 -- conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.0-h550210a_6.conda - sha256: fd27821c8cfc425826f13760c3263d7b3b997c5372234cefa1586ff384dcc989 - md5: 72d45aa52ebca91aedb0cfd9eac62655 + run_exports: + weak: + - libclang13 >=21.1.0 + size: 9005813 + timestamp: 1757400178887 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.21.0-h8f0b9e4_1.conda + sha256: 33ba88482d9607db195d2b5920e29f4d2744a4780fbbfc47f2e58e4b59a5530c + md5: 83fdd27f6406225d15e4f44b5b45aa96 depends: - - lerc >=4.0.0,<5.0a0 - - libdeflate >=1.24,<1.25.0a0 - - libjpeg-turbo >=3.1.0,<4.0a0 - - liblzma >=5.8.1,<6.0a0 - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - __osx >=11.0 + - krb5 >=1.22.2,<1.23.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 - zstd >=1.5.7,<1.6.0a0 - license: HPND + license: curl + license_family: MIT purls: [] - size: 983988 - timestamp: 1755012056987 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libudev1-257.4-h9a4d06a_0.conda - sha256: 65ebc2185cdc008f8da92864e8063e60293c59134b11b13e4bc44fd6f6e04eec - md5: 8b87f46f586167c54b2d4c0fd4a72001 + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 430084 + timestamp: 1782803235400 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-22.1.8-h19cb2f5_0.conda + sha256: 57ee997f1f800cf38abc743c0f0a9ddfe6a101c697c35510452ce6f4ddf96361 + md5: 0f600157f28fc7bc9549ecafdfa5bc12 depends: - - __glibc >=2.17,<3.0.a0 - - libcap >=2.71,<2.72.0a0 - - libgcc >=13 - license: LGPL-2.1-or-later + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 143836 - timestamp: 1741612453664 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libudev1-257.4-h1187dce_0.conda - sha256: 1389af70858732b9bf6384c2af9b1da4b261bc8d889bb6a25d853a75cbb04073 - md5: 0a0bd551a68587c7dd852324da97b853 + run_exports: {} + size: 566717 + timestamp: 1781672189697 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-devel-16.0.6-h8f8a49f_2.conda + sha256: 1c1c6f6f4eca07be3f03929c59c2dd077da3c676fbf5e92c0df3bad2a4f069ab + md5: 677580dee2d1412311d9dd9bf6bfa6b7 depends: - - libcap >=2.71,<2.72.0a0 - - libgcc >=13 - license: LGPL-2.1-or-later + - libcxx >=16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 153980 - timestamp: 1741612457053 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libunwind-1.6.2-h9c3ff4c_0.tar.bz2 - sha256: f2ac872920833960e514ce9efd8f7c08ce66dd870738d73839d1bce1ac497de6 - md5: a730b2badd586580c5752cc73842e068 + run_exports: {} + size: 716532 + timestamp: 1725067685814 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.25-h517ebb2_0.conda + sha256: 025f8b1e85dd8254e0ca65f011919fb1753070eb507f03bca317871a884d24de + md5: 31aa65919a729dc48180893f62c25221 depends: - - libgcc-ng >=9.4.0 - - libstdcxx-ng >=9.4.0 + - __osx >=10.13 license: MIT license_family: MIT purls: [] - size: 75491 - timestamp: 1638450786937 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libunwind-1.6.2-h01db608_0.tar.bz2 - sha256: 7862d36ffc9f6b2ed3381ce77c78b9e5691d7353a19dd2050630868e192adf6f - md5: 93b7bbf9099cfe09e67c0abe34bb7885 + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 + size: 70840 + timestamp: 1761980008502 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20250104-pl5321ha958ccf_0.conda + sha256: 6cc49785940a99e6a6b8c6edbb15f44c2dd6c789d9c283e5ee7bdfedd50b4cd6 + md5: 1f4ed31220402fcddc083b4bff406868 depends: - - libgcc-ng >=9.4.0 - - libstdcxx-ng >=9.4.0 - license: MIT - license_family: MIT + - ncurses + - __osx >=10.13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 90479 - timestamp: 1638452154070 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liburing-2.9-h84d6215_0.conda - sha256: bfa34a5a929d792dfcfbbe2d9ee21bd870d73d646512e21c871dab0b80194468 - md5: ecd409e7bfcf4ee73f74d7a2cc91a4c3 + run_exports: + weak: + - libedit >=3.1.20250104,<3.2.0a0 + size: 115563 + timestamp: 1738479554273 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + sha256: 0d238488564a7992942aa165ff994eca540f687753b4f0998b29b4e4d030ff43 + md5: 899db79329439820b7e8f8de41bca902 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libev >=4.33,<4.34.0a0 + size: 106663 + timestamp: 1702146352558 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + sha256: 9c96cc05e056e1bba5b545cbbd57b6e01db622dc2c82934caaaa25cfb22fe666 + md5: dcfdea7b7013beef0a4d744d776ea38f depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 + - __osx >=11.0 + constrains: + - expat 2.8.1.* license: MIT license_family: MIT purls: [] - size: 121336 - timestamp: 1738604403935 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liburing-2.9-h17cf362_0.conda - sha256: 2922ab8ac4cdd966c1b13dad6ccc4c07c7db2054400843ee443ffd5e7b3f292e - md5: 8eef9430276ab3dbe6ad5b8f23ff5e26 + run_exports: {} + size: 76020 + timestamp: 1781204303305 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + sha256: 951958d1792238006fdc6fce7f71f1b559534743b26cc1333497d46e5903a2d6 + md5: 66a0dc7464927d0853b590b6f53ba3ea depends: - - libgcc >=13 - - libstdcxx >=13 + - __osx >=10.13 license: MIT license_family: MIT purls: [] - size: 123614 - timestamp: 1738605619021 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libusb-1.0.29-h73b1eb8_0.conda - sha256: 89c84f5b26028a9d0f5c4014330703e7dff73ba0c98f90103e9cef6b43a5323c - md5: d17e3fb595a9f24fa9e149239a33475d - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libudev1 >=257.4 - license: LGPL-2.1-or-later - purls: [] - size: 89551 - timestamp: 1748856210075 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libusb-1.0.29-h06eaf92_0.conda - sha256: a60aae6b529cd7caa7842f9781ef95b93014e618f71fb005e404af434d76a33f - md5: 9a86e7473e16fe25c5c47f6c1376ac82 - depends: - - libgcc >=13 - - libudev1 >=257.4 - license: LGPL-2.1-or-later - purls: [] - size: 93129 - timestamp: 1748856228398 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda - sha256: b46c1c71d8be2d19615a10eaa997b3547848d1aee25a7e9486ad1ca8d61626a7 - md5: e5d5fd6235a259665d7652093dc7d6f1 + run_exports: + weak: + - libffi >=3.5.2,<3.6.0a0 + size: 53583 + timestamp: 1769456300951 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype-2.14.3-h694c41f_1.conda + sha256: 9029ed0c940be8161c86f5338eacfad1f61af216cdc508e386a648f6ef893a28 + md5: 7cec36e11e7c5a674a1d8c1d5082479e depends: - - __osx >=10.13 - license: LGPL-2.1-or-later + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL purls: [] - size: 85523 - timestamp: 1748856209535 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libusb-1.0.29-hbc156a2_0.conda - sha256: 5eee9a2bf359e474d4548874bcfc8d29ebad0d9ba015314439c256904e40aaad - md5: f6654e9e96e9d973981b3b2f898a5bfa + run_exports: {} + size: 8394 + timestamp: 1780934152050 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libfreetype6-2.14.3-h58fbd8d_1.conda + sha256: cc94862c51e68626fadddf68b523e5f752149186ccc498fa37976504e2e7ff55 + md5: 112cb22521fa3abf19bc0c93938576f5 depends: - __osx >=11.0 - license: LGPL-2.1-or-later + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL purls: [] - size: 83849 - timestamp: 1748856224950 -- conda: https://conda.anaconda.org/conda-forge/win-64/libusb-1.0.29-h1839187_0.conda - sha256: 9837f8e8de20b6c9c033561cd33b4554cd551b217e3b8d2862b353ed2c23d8b8 - md5: a656b2c367405cd24988cf67ff2675aa + run_exports: {} + size: 365107 + timestamp: 1780934149073 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libglib-2.88.2-hf28f236_0.conda + sha256: 445e6806480103c6411993e7c2fd5ad1c6cb14ef1fee9386b44adeb536834d07 + md5: 6ed62b59574adb4c9629ed6932a51de7 depends: - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libintl >=0.25.1,<1.0a0 + - pcre2 >=10.47,<10.48.0a0 + - libffi >=3.5.2,<3.6.0a0 + constrains: + - glib >2.66 license: LGPL-2.1-or-later purls: [] - size: 118204 - timestamp: 1748856290542 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda - sha256: 776e28735cee84b97e4d05dd5d67b95221a3e2c09b8b13e3d6dbe6494337d527 - md5: af930c65e9a79a3423d6d36e265cef65 + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 4510929 + timestamp: 1782464244345 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libhwloc-2.12.1-default_h8c32e24_1000.conda + sha256: 766146cbbfc1ec400a2b8502a30682d555db77a05918745828392839434b829b + md5: 622d2b076d7f0588ab1baa962209e6dd depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 + - __osx >=10.13 + - libcxx >=19 + - libxml2 >=2.13.8,<2.14.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 37087 - timestamp: 1757334557450 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.1-h3e4203c_0.conda - sha256: 4c27cf85e5f71d8d886b17743005bb95041299739f1c09a83f40e15fca24af56 - md5: 7a37d5ca406edc9ae46bb56932f9bea0 + run_exports: + weak: + - libhwloc >=2.12.1,<2.12.2.0a0 + size: 2381708 + timestamp: 1752761786288 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.18-h57a12c2_2.conda + sha256: a1c8cecdf9966921e13f0ae921309a1f415dfbd2b791f2117cf7e8f5e61a48b6 + md5: 210a85a1119f97ea7887188d176db135 depends: - - libgcc >=14 - license: BSD-3-Clause - license_family: BSD + - __osx >=10.13 + license: LGPL-2.1-only purls: [] - size: 39065 - timestamp: 1757334544078 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - sha256: c180f4124a889ac343fc59d15558e93667d894a966ec6fdb61da1604481be26b - md5: 0f03292cc56bf91a077a134ea8747118 + run_exports: + weak: + - libiconv >=1.18,<2.0a0 + size: 737846 + timestamp: 1754908900138 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libintl-0.25.1-h3184127_1.conda + sha256: 8c352744517bc62d24539d1ecc813b9fdc8a785c780197c5f0b84ec5b0dfe122 + md5: a8e54eefc65645193c46e8b180f62d22 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: MIT - license_family: MIT + - __osx >=10.13 + - libiconv >=1.18,<2.0a0 + license: LGPL-2.1-or-later purls: [] - size: 895108 - timestamp: 1753948278280 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.51.0-he30d5cf_1.conda - sha256: 7a0fb5638582efc887a18b7d270b0c4a6f6e681bf401cab25ebafa2482569e90 - md5: 8e62bf5af966325ee416f19c6f14ffa3 + run_exports: + weak: + - libintl >=0.25.1,<1.0a0 + size: 96909 + timestamp: 1753343977382 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.2.0-ha1e9b39_0.conda + sha256: 5c48ea89073ba28eb93df264587973d3905827e5b536a5320604ae3baaa73e13 + md5: d86c1b9259377fb4dcd76fe7472bd2d2 depends: - - libgcc >=14 - license: MIT - license_family: MIT + - __osx >=11.0 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 613600 + timestamp: 1783732260943 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm16-16.0.6-hbedff68_3.conda + sha256: ad848dc0bb02b1dbe54324ee5700b050a2e5f63c095f5229b2de58249a3e268e + md5: 8fd56c0adc07a37f93bd44aa61a97c90 + depends: + - libcxx >=16 + - libxml2 >=2.12.1,<2.14.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - zstd >=1.5.5,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 629238 - timestamp: 1753948296190 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.51.0-h58003a5_1.conda - sha256: d90dd0eee6f195a5bd14edab4c5b33be3635b674b0b6c010fb942b956aa2254c - md5: fbfc6cf607ae1e1e498734e256561dc3 + run_exports: + weak: + - libllvm16 >=16.0.6,<16.1.0a0 + size: 25196932 + timestamp: 1701379796962 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libllvm21-21.1.0-h9b4ebcc_0.conda + sha256: fa24fbdeeb3cd8861c15bb06019d6482c7f686304f0883064d91f076e331fc25 + md5: 49233c30d20fbe080285fd286e9267fb depends: - __osx >=10.13 - license: MIT - license_family: MIT + - libcxx >=19 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 422612 - timestamp: 1753948458902 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.51.0-h6caf38d_1.conda - sha256: 042c7488ad97a5629ec0a991a8b2a3345599401ecc75ad6a5af73b60e6db9689 - md5: c0d87c3c8e075daf1daf6c31b53e8083 + run_exports: + weak: + - libllvm21 >=21.1.0,<21.2.0a0 + size: 31441188 + timestamp: 1756284335102 +- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + sha256: d9e2006051529aec5578c6efeb13bb6a7200a014b2d5a77a579e83a8049d5f3c + md5: becdfbfe7049fa248e52aa37a9df09e2 depends: - __osx >=11.0 - license: MIT - license_family: MIT + constrains: + - xz 5.8.3.* + license: 0BSD purls: [] - size: 421195 - timestamp: 1753948426421 -- conda: https://conda.anaconda.org/conda-forge/win-64/libuv-1.51.0-hfd05255_1.conda - sha256: f03dc82e6fb1725788e73ae97f0cd3d820d5af0d351a274104a0767035444c59 - md5: 31e1545994c48efc3e6ea32ca02a8724 + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 105724 + timestamp: 1775826029494 +- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-devel-5.8.3-hbb4bfdb_0.conda + sha256: 05f845d7f29691f8410665297a4fd168261aaa2710993e9e21effd66365c080d + md5: a59a33afff299f2d95fdabbd1214f4f1 depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: MIT - license_family: MIT + - __osx >=11.0 + - liblzma 5.8.3 hbb4bfdb_0 + license: 0BSD purls: [] - size: 297087 - timestamp: 1753948490874 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libva-2.22.0-h4f16b4b_2.conda - sha256: e0df324fb02fa05a05824b8db886b06659432b5cff39495c59e14a37aa23d40f - md5: 2c65566e79dc11318ce689c656fb551c + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 118185 + timestamp: 1775826064340 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.68.1-h70048d4_0.conda + sha256: 899551e16aac9dfb85bfc2fd98b655f4d1b7fea45720ec04ccb93d95b4d24798 + md5: dba4c95e2fe24adcae4b77ebf33559ae depends: - - __glibc >=2.17,<3.0.a0 - - libdrm >=2.4.124,<2.5.0a0 - - libegl >=1.7.0,<2.0a0 - - libgcc >=13 - - libgl >=1.7.0,<2.0a0 - - libglx >=1.7.0,<2.0a0 - - libxcb >=1.17.0,<2.0a0 - - wayland >=1.23.1,<2.0a0 - - wayland-protocols - - xorg-libx11 >=1.8.11,<2.0a0 - - xorg-libxext >=1.3.6,<2.0a0 - - xorg-libxfixes >=6.0.1,<7.0a0 + - __osx >=11.0 + - c-ares >=1.34.6,<2.0a0 + - libcxx >=19 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 217567 - timestamp: 1740897682004 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h54a6638_2.conda - sha256: ca494c99c7e5ecc1b4cd2f72b5584cef3d4ce631d23511184411abcbb90a21a5 - md5: b4ecbefe517ed0157c37f8182768271c + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 + size: 606749 + timestamp: 1773854765508 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libogg-1.3.5-he3325bb_1.conda + sha256: 26691d40c70e83d3955a8daaee713aa7d087aa351c5a1f43786bbb0e871f29da + md5: d0f30c7fe90d08e9bd9c13cd60be6400 depends: - - libogg - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - libstdcxx >=14 - - libgcc >=14 - - libogg >=1.3.5,<1.4.0a0 + - __osx >=10.13 license: BSD-3-Clause license_family: BSD purls: [] - size: 285894 - timestamp: 1753879378005 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvorbis-1.3.7-h7ac5ae9_2.conda - sha256: 066708ca7179a1c6e5639d015de7ed6e432b93ad50525843db67d57eb1ba1faf - md5: 9d099329070afe52d797462ca7bf35f3 + run_exports: + weak: + - libogg >=1.3.5,<1.4.0a0 + size: 215854 + timestamp: 1745826006966 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-2025.2.0-h346e020_1.conda + sha256: 9ce68ea62066f60083611be69314c1664747d73b80407ad41438e08922c4407b + md5: 0e6b6a6c7640260ae38c963d16719bac depends: - - libogg - - libstdcxx >=14 - - libgcc >=14 - - libogg >=1.3.5,<1.4.0a0 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libcxx >=19 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 289391 - timestamp: 1753879417231 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libvorbis-1.3.7-ha059160_2.conda - sha256: 7b79c0e867db70c66e57ea0abf03ea940070ed8372289d6dc5db7ab59e30acc1 - md5: 8eadf13aee55e59089edaf2acaaaf4f7 + run_exports: + weak: + - libopenvino >=2025.2.0,<2025.2.1.0a0 + size: 4741821 + timestamp: 1753201195860 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-batch-plugin-2025.2.0-heda8b29_1.conda + sha256: 23649063fcbc666cad1bb4b4d430a6320c7c371367b0ed5d68608bcd5c94d568 + md5: 5ce82393e4b6d012250f79ad4f853867 depends: - - libogg + - __osx >=11.0 - libcxx >=19 - - __osx >=10.13 - - libogg >=1.3.5,<1.4.0a0 - license: BSD-3-Clause - license_family: BSD + - libopenvino 2025.2.0 h346e020_1 + - tbb >=2021.13.0 purls: [] - size: 279656 - timestamp: 1753879393065 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvorbis-1.3.7-h81086ad_2.conda - sha256: 95768e4eceaffb973081fd986d03da15d93aa10609ed202e6fd5ca1e490a3dce - md5: 719e7653178a09f5ca0aa05f349b41f7 + run_exports: {} + size: 106879 + timestamp: 1753201232911 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-auto-plugin-2025.2.0-heda8b29_1.conda + sha256: 9625b18fa136b9f841b2651c834e89e8f2f90cb13e33dacba67af2ee88c175a9 + md5: 950fd5d5e34f2845f63eebf96c761d4c depends: - - libogg - - libcxx >=19 - __osx >=11.0 - - libogg >=1.3.5,<1.4.0a0 - license: BSD-3-Clause - license_family: BSD + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 + - tbb >=2021.13.0 purls: [] - size: 259122 - timestamp: 1753879389702 -- conda: https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h5112557_2.conda - sha256: 429124709c73b2e8fae5570bdc6b42f5418a7551ba72e591bb960b752e87b365 - md5: 42a8a56c60882da5d451aa95b8455111 + run_exports: {} + size: 221142 + timestamp: 1753201253766 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-hetero-plugin-2025.2.0-hd57c75b_1.conda + sha256: c48f09ce035ffee361ff020d586d76e4f7e464b58739f6d8b43cd242dc476f7a + md5: 554269b84c8a8c945056fd7d3ff28a67 depends: - - libogg - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - libogg >=1.3.5,<1.4.0a0 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 + - pugixml >=1.15,<1.16.0a0 purls: [] - size: 243401 - timestamp: 1753879416570 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libvpl-2.15.0-h54a6638_1.conda - sha256: bf0010d93f5b154c59bd9d3cc32168698c1d24f2904729f4693917cce5b27a9f - md5: a41a299c157cc6d0eff05e5fc298cc45 + run_exports: {} + size: 180453 + timestamp: 1753201276333 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-intel-cpu-plugin-2025.2.0-h346e020_1.conda + sha256: 9f27cf634bba0d35d00f0b89e423246495dd3e9ea531d0ba60373c343df68349 + md5: bbaf847551103a59236544c674886b6d depends: - - libstdcxx >=14 - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - intel-media-driver >=25.3.3,<25.4.0a0 - - libva >=2.22.0,<3.0a0 - license: MIT - license_family: MIT + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 287944 - timestamp: 1757278954789 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda - sha256: e7d2daf409c807be48310fcc8924e481b62988143f582eb3a58c5523a6763b13 - md5: cde393f461e0c169d9ffb2fc70f81c33 + run_exports: {} + size: 10731860 + timestamp: 1753201313287 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-ir-frontend-2025.2.0-hd57c75b_1.conda + sha256: 09f8d1e8ca01f248e1ac3d069749acc888710268cfab3b514829820c3774c8d9 + md5: 47e5f6af801546ebf4658f00be37ff60 depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 + - pugixml >=1.15,<1.16.0a0 purls: [] - size: 1022466 - timestamp: 1717859935011 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libvpx-1.14.1-h0a1ffab_0.conda - sha256: 918493354f78cb3bb2c3d91264afbcb312b2afe287237e7d1c85ee7e96d15b47 - md5: 3cb63f822a49e4c406639ebf8b5d87d7 + run_exports: + weak: + - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 + size: 184658 + timestamp: 1753201367805 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-onnx-frontend-2025.2.0-ha4fb624_1.conda + sha256: 69e9bf3e93ea8572c512871668e2893c8ff74a8800b6d7153fe1ecf6e7702604 + md5: 7562969356f607c1899079b8c617f1d0 depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 purls: [] - size: 1211700 - timestamp: 1717859955539 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.14.1-hf036a51_0.conda - sha256: 47e70e76988c11de97d539794fd4b03db69b75289ac02cdc35ae5a595ffcd973 - md5: 9b8744a702ffb1738191e094e6eb67dc + run_exports: + weak: + - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 + size: 1361773 + timestamp: 1753201390071 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-paddle-frontend-2025.2.0-ha4fb624_1.conda + sha256: a55b2ec77b20828551f37199b0f156de985d8e33ec31e16f77f588d674aa5fa3 + md5: 6d7ffc6166d1347d0c35b04dd04b9bf6 depends: - - __osx >=10.13 - - libcxx >=16 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 purls: [] - size: 1297054 - timestamp: 1717860051058 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvpx-1.14.1-h7bae524_0.conda - sha256: 5d6458b5395cba0804846f156574aa8a34eef6d5f05d39e9932ddbb4215f8bd0 - md5: 95bee48afff34f203e4828444c2b2ae9 + run_exports: + weak: + - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 + size: 468414 + timestamp: 1753201414650 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-pytorch-frontend-2025.2.0-hbc7d668_1.conda + sha256: d52231c562fe544c2a5f95df6397b5f7e9778cc19cef698da30f80b872bb7207 + md5: 186bf8821732296cc1de55cfebf76446 depends: - __osx >=11.0 - - libcxx >=16 - license: BSD-3-Clause - license_family: BSD + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 purls: [] - size: 1178981 - timestamp: 1717860096742 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda - sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b - md5: aea31d2e5b1091feca96fcfe945c3cf9 + run_exports: + weak: + - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 + size: 850745 + timestamp: 1753201436800 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-frontend-2025.2.0-hd87add6_1.conda + sha256: 1f785acc3c4ed6aad94053bfa48d52d76e8d6ff369064331e70421b7c87fd61d + md5: e4f76aeb995f50f7a1a533affd6c12a4 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - constrains: - - libwebp 1.6.0 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - snappy >=1.2.2,<1.3.0a0 purls: [] - size: 429011 - timestamp: 1752159441324 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda - sha256: b03700a1f741554e8e5712f9b06dd67e76f5301292958cd3cb1ac8c6fdd9ed25 - md5: 24e92d0942c799db387f5c9d7b81f1af + run_exports: + weak: + - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 + size: 987782 + timestamp: 1753201460022 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopenvino-tensorflow-lite-frontend-2025.2.0-hbc7d668_1.conda + sha256: fde90b9981ba17a436ae7ce17e1caf4ea3f97c1a5cf55f5bed0d97c0d4a094f4 + md5: b04dfe98f9fa74ffa9f385cf57c4c455 depends: - - libgcc >=14 - constrains: - - libwebp 1.6.0 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h346e020_1 purls: [] - size: 359496 - timestamp: 1752160685488 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda - sha256: 00dbfe574b5d9b9b2b519acb07545380a6bc98d1f76a02695be4995d4ec91391 - md5: 7bb6608cf1f83578587297a158a6630b + run_exports: + weak: + - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 + size: 391641 + timestamp: 1753201485293 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libopus-1.6.1-hc6ced15_0.conda + sha256: 14389effc1a614456cfe013e4b34e0431f28c5e0047bb6fc80b7dbdab3df4d25 + md5: c009362fc3b273d1a671507cff70a3da depends: - __osx >=10.13 - constrains: - - libwebp 1.6.0 license: BSD-3-Clause license_family: BSD purls: [] - size: 365086 - timestamp: 1752159528504 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda - sha256: a4de3f371bb7ada325e1f27a4ef7bcc81b2b6a330e46fac9c2f78ac0755ea3dd - md5: e5e7d467f80da752be17796b87fe6385 + run_exports: + weak: + - libopus >=1.6.1,<2.0a0 + size: 346077 + timestamp: 1768497213180 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.58-he930e7c_0.conda + sha256: a669b22978e546484d18d99a210801b1823360a266d7035c713d8d1facd035f7 + md5: 9744d43d5200f284260637304a069ddd depends: - __osx >=11.0 - constrains: - - libwebp 1.6.0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 294974 - timestamp: 1752159906788 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda - sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa - md5: 92ed62436b625154323d40d5f2f11dd7 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - pthread-stubs - - xorg-libxau >=1.0.11,<2.0a0 - - xorg-libxdmcp - license: MIT - license_family: MIT - purls: [] - size: 395888 - timestamp: 1727278577118 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda - sha256: 461cab3d5650ac6db73a367de5c8eca50363966e862dcf60181d693236b1ae7b - md5: cd14ee5cca2464a425b1dbfc24d90db2 - depends: - - libgcc >=13 - - pthread-stubs - - xorg-libxau >=1.0.11,<2.0a0 - - xorg-libxdmcp - license: MIT - license_family: MIT + - libzlib >=1.3.2,<2.0a0 + license: zlib-acknowledgement purls: [] - size: 397493 - timestamp: 1727280745441 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c - md5: 5aa797f8787fe7a17d1b0821485b5adc + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 + size: 299206 + timestamp: 1776315286816 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-6.31.1-h774df25_5.conda + sha256: 337e8abeadb5f1303646c44b86d7b03767d81b3f7eec825d9f6825554a6fb054 + md5: 3a67e77f5eedfbd8aac22941685c3807 depends: - - libgcc-ng >=12 - license: LGPL-2.1-or-later + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 100393 - timestamp: 1702724383534 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda - sha256: 6b46c397644091b8a26a3048636d10b989b1bf266d4be5e9474bf763f828f41f - md5: b4df5d7d4b63579d081fd3a4cf99740e + run_exports: + weak: + - libprotobuf >=6.31.1,<6.31.2.0a0 + size: 3010593 + timestamp: 1780005465717 +- conda: https://conda.anaconda.org/conda-forge/osx-64/librsvg-2.58.4-h21a6cfa_3.conda + sha256: 87432fca28ddfaaf82b3cd12ce4e31fcd963428d1f2c5e2a3aef35dd30e56b71 + md5: 213dcdb373bf108d1beb18d33075f51d depends: - - libgcc-ng >=12 + - __osx >=10.13 + - cairo >=1.18.4,<2.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - libglib >=2.84.0,<3.0a0 + - libxml2 >=2.13.7,<2.14.0a0 + - pango >=1.56.3,<2.0a0 + constrains: + - __osx >=10.13 license: LGPL-2.1-or-later purls: [] - size: 114269 - timestamp: 1702724369203 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda - sha256: 23f47e86cc1386e7f815fa9662ccedae151471862e971ea511c5c886aa723a54 - md5: 74e91c36d0eef3557915c68b6c2bef96 + run_exports: + weak: + - librsvg >=2.58.4,<3.0a0 + size: 4946543 + timestamp: 1743368938616 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsigtool-0.1.3-hc0f2934_0.conda + sha256: f87b743d5ab11c1a8ddd800dd9357fc0fabe47686068232ddc1d1eed0d7321ec + md5: 3576aba85ce5e9ab15aa0ea376ab864b depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - - libxcb >=1.17.0,<2.0a0 - - libxml2 >=2.13.8,<2.14.0a0 - - xkeyboard-config - - xorg-libxau >=1.0.12,<2.0a0 - license: MIT/X11 Derivative + - __osx >=10.13 + - openssl >=3.5.4,<4.0a0 + license: MIT license_family: MIT purls: [] - size: 791328 - timestamp: 1754703902365 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.11.0-h95ca766_0.conda - sha256: b23355766092c62b32a7fc8d5729f40d693d2d8491f52e12f3a2f184ec552f6a - md5: 21efa5fee8795bc04bd79bfc02f05c65 + run_exports: {} + size: 38085 + timestamp: 1767044977731 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h77d7759_0.conda + sha256: 9aa1757e1aa94f1ddf81d46537d0c1aa2eb72d079f0cb97831422c332f6269ae + md5: 8d307fb88bcddb53f544f7417a402a10 depends: - - libgcc >=14 - - libstdcxx >=14 - - libxcb >=1.17.0,<2.0a0 - - libxml2 >=2.13.8,<2.14.0a0 - - xkeyboard-config - - xorg-libxau >=1.0.12,<2.0a0 - license: MIT/X11 Derivative - license_family: MIT + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing purls: [] - size: 811243 - timestamp: 1754703942072 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda - sha256: 03deb1ec6edfafc5aaeecadfc445ee436fecffcda11fcd97fde9b6632acb583f - md5: 10bcbd05e1c1c9d652fccb42b776a9fa + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 1008790 + timestamp: 1782519491771 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.1-hed3591d_0.conda + sha256: 00654ba9e5f73aa1f75c1f69db34a19029e970a4aeb0fa8615934d8e9c369c3c + md5: a6cb15db1c2dc4d3a5f6cf3772e09e81 depends: - - __glibc >=2.17,<3.0.a0 - - icu >=75.1,<76.0a0 - - libgcc >=14 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 + - __osx >=10.13 - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 698448 - timestamp: 1754315344761 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.8-he58860d_1.conda - sha256: 708ce24ebc1c3d11ac3757ae7a9ab628a1508e4427789a86197f38dad131dac9 - md5: 20d0cae4f8f49a79892d7e397310d81f + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 + size: 284216 + timestamp: 1745608575796 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.2-h95d6d7f_0.conda + sha256: 8e43d2a3538f45848c61cdda4baa6728ce0a48bc665b306de5a31dd3464a30c1 + md5: c92fd887c114aac4612bfc14b1516776 depends: - - icu >=75.1,<76.0a0 - - libgcc >=14 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + - __osx >=11.0 + - lerc >=4.1.0,<5.0a0 + - libcxx >=19 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND purls: [] - size: 739576 - timestamp: 1754315493293 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.8-he1bc88e_1.conda - sha256: 248871154c6f86f0c6d456872457ad4f5799e23c09512a473041da3b9b9ee83c - md5: 1d31029d8d2685d56a812dec48083483 + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 418681 + timestamp: 1783085828393 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libusb-1.0.29-h2287256_0.conda + sha256: b46c1c71d8be2d19615a10eaa997b3547848d1aee25a7e9486ad1ca8d61626a7 + md5: e5d5fd6235a259665d7652093dc7d6f1 depends: - __osx >=10.13 - - icu >=75.1,<76.0a0 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + license: LGPL-2.1-or-later purls: [] - size: 611430 - timestamp: 1754315569848 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.8-h4a9ca0c_1.conda - sha256: 365ad1fa0b213e3712d882f187e6de7f601a0e883717f54fe69c344515cdba78 - md5: 05774cda4a601fc21830842648b3fe04 + run_exports: + weak: + - libusb >=1.0.29,<2.0a0 + size: 85523 + timestamp: 1748856209535 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libuv-1.52.1-ha3d0635_0.conda + sha256: a77c3832a82b26afe8da3f4bbacca58a943cc62f2a5680547913650527a51299 + md5: 703303067839cd1da659528a84b3c0cc depends: - __osx >=11.0 - - icu >=75.1,<76.0a0 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libzlib >=1.3.1,<2.0a0 license: MIT license_family: MIT purls: [] - size: 582952 - timestamp: 1754315458016 -- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.8-h741aa76_1.conda - sha256: 32fa908bb2f2a6636dab0edaac1d4bf5ff62ad404a82d8bb16702bc5b8eb9114 - md5: aeb49dc1f5531de13d2c0d57ffa6d0c8 + run_exports: + weak: + - libuv >=1.52.1,<2.0a0 + size: 128150 + timestamp: 1779396112490 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libvorbis-1.3.7-ha059160_2.conda + sha256: 7b79c0e867db70c66e57ea0abf03ea940070ed8372289d6dc5db7ab59e30acc1 + md5: 8eadf13aee55e59089edaf2acaaaf4f7 depends: - - libiconv >=1.18,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: MIT - license_family: MIT + - libogg + - libcxx >=19 + - __osx >=10.13 + - libogg >=1.3.5,<1.4.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1519401 - timestamp: 1754315497781 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 - md5: edb0dca6bc32e4f4789199455a1dbeb8 + run_exports: + weak: + - libvorbis >=1.3.7,<1.4.0a0 + size: 279656 + timestamp: 1753879393065 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libvpx-1.14.1-hf036a51_0.conda + sha256: 47e70e76988c11de97d539794fd4b03db69b75289ac02cdc35ae5a595ffcd973 + md5: 9b8744a702ffb1738191e094e6eb67dc depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other + - __osx >=10.13 + - libcxx >=16 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 60963 - timestamp: 1727963148474 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda - sha256: 5a2c1eeef69342e88a98d1d95bff1603727ab1ff4ee0e421522acd8813439b84 - md5: 08aad7cbe9f5a6b460d0976076b6ae64 + run_exports: + weak: + - libvpx >=1.14.1,<1.15.0a0 + size: 1297054 + timestamp: 1717860051058 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libvulkan-loader-1.4.341.0-ha6bc089_0.conda + sha256: ce9bc992ffffdefbde5f7977b0a3ad9036650f8323611e4024908755891674e0 + md5: dcce6338514e65c2b7fdf172f1264561 depends: - - libgcc >=13 + - __osx >=10.13 + - libcxx >=19 constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other + - libvulkan-headers 1.4.341.0.* + license: Apache-2.0 + license_family: APACHE purls: [] - size: 66657 - timestamp: 1727963199518 -- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda - sha256: 8412f96504fc5993a63edf1e211d042a1fd5b1d51dedec755d2058948fcced09 - md5: 003a54a4e32b02f7355b50a837e699da + run_exports: + weak: + - libvulkan-loader >=1.4.341.0,<2.0a0 + size: 182703 + timestamp: 1770077140315 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.6.0-hb807250_0.conda + sha256: 00dbfe574b5d9b9b2b519acb07545380a6bc98d1f76a02695be4995d4ec91391 + md5: 7bb6608cf1f83578587297a158a6630b depends: - __osx >=10.13 constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 57133 - timestamp: 1727963183990 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b - md5: 369964e85dc26bfe78f41399b366c435 + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 + size: 365086 + timestamp: 1752159528504 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.13.9-he1bc88e_0.conda + sha256: 151e653e72b9de48bdeb54ae0664b490d679d724e618649997530a582a67a5fb + md5: af41ebf4621373c4eeeda69cc703f19c depends: - - __osx >=11.0 - constrains: - - zlib 1.3.1 *_2 - license: Zlib - license_family: Other + - __osx >=10.13 + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 46438 - timestamp: 1727963202283 -- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 - md5: 41fbfac52c601159df6c01f875de31b9 + run_exports: + weak: + - libxml2 >=2.13.9,<2.14.0a0 + size: 609937 + timestamp: 1761766325697 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + sha256: 4c6da089952b2d70150c74234679d6f7ac04f4a98f9432dec724968f912691e7 + md5: 30439ff30578e504ee5e0b390afc8c65 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - __osx >=11.0 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 55476 - timestamp: 1727963768015 -- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-21.1.0-hf4e0ed4_0.conda - sha256: 78336131a08990390003ef05d14ecb49f3a47e4dac60b1bcebeccd87fa402925 - md5: 5acc6c266fd33166fa3b33e48665ae0d - depends: - - __osx >=10.13 - constrains: - - openmp 21.1.0|21.1.0.* - - intel-openmp <0.0a0 - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE - purls: [] - size: 311174 - timestamp: 1756673275570 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.0-hbb9b287_0.conda - sha256: c6750073a128376a14bedacfa90caab4c17025c9687fcf6f96e863b28d543af4 - md5: e57d95fec6eaa747e583323cba6cfe5c + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 59000 + timestamp: 1774073052242 +- conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-22.1.8-h0d3cbff_0.conda + sha256: 7e8dcf03c2ef5491405d6d86eb892d14e99902f50f4eeb250db0cbdc58dd5818 + md5: 9d5828c46147a47f828ca47a18407621 depends: - __osx >=11.0 constrains: + - openmp 22.1.8|22.1.8.* - intel-openmp <0.0a0 - - openmp 21.1.0|21.1.0.* license: Apache-2.0 WITH LLVM-exception license_family: APACHE purls: [] - size: 286039 - timestamp: 1756673290280 + run_exports: + strong: + - llvm-openmp >=22.1.8 + size: 311645 + timestamp: 1781737360942 - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-tools-16.0.6-hbedff68_3.conda sha256: dff3ca83c6945f020ee6d3c62ddb3ed175ae8a357be3689a8836bcfe25ad9882 md5: e9356b0807462e8f84c1384a8da539a5 @@ -10336,195 +12494,27 @@ packages: license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] + run_exports: {} size: 22221159 timestamp: 1701379965425 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-16.0.6-hc4b4ae8_4.conda - sha256: 3fc56aa583f213f271f95cc51ead5b3f1b4f6c82531860c75161a76b86b8a944 - md5: d920ea6c48053a4587bdfd0002bfff51 - depends: - - __osx >=11.0 - - libllvm16 16.0.6 hc4b4ae8_4 - - libxml2 >=2.13.5,<2.14.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.6,<1.6.0a0 - constrains: - - llvmdev 16.0.6 - - clang 16.0.6.* - - clang-tools 16.0.6.* - - llvm 16.0.6.* - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - purls: [] - size: 20903239 - timestamp: 1739799054437 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lychee-0.23.0-he64ecbb_0.conda - sha256: 0b1bc4b4a8fde5bf474f5b63c64fe356b3f47034f1d485fedd630b2d17de8fb5 - md5: d89182800d61d9e4922f4f338cd28362 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - openssl >=3.5.5,<4.0a0 - constrains: - - __glibc >=2.17 - license: Apache-2.0 OR MIT - purls: [] - size: 5582609 - timestamp: 1771270353931 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lychee-0.23.0-hb434046_0.conda - sha256: 82ac7b7b6d1e9e8c929ac21993a0e036bfd4ee2d3db54913d4635476067901eb - md5: 347b587a5e72646fefa74431216c1c4a - depends: - - libgcc >=14 - - openssl >=3.5.5,<4.0a0 - constrains: - - __glibc >=2.17 - license: Apache-2.0 OR MIT - purls: [] - size: 5422618 - timestamp: 1771270379661 - conda: https://conda.anaconda.org/conda-forge/osx-64/lychee-0.23.0-h651e3a3_0.conda - sha256: 8037f7a2f536c5ded9353054377dd4f386a6cd52ee0fdfbd7e47911d63a8dc60 - md5: beea0174c7b90d94f2b5a7813faa7b64 - depends: - - __osx >=11.0 - - openssl >=3.5.5,<4.0a0 - constrains: - - __osx >=10.13 - license: Apache-2.0 OR MIT - purls: [] - size: 5527018 - timestamp: 1771270370972 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lychee-0.23.0-h17e24d4_0.conda - sha256: 412ccd60e68618a3ee449f6bd4152acdf249e62073e4dfa2e7c8d405fc62b1b0 - md5: 7fa0025f406d5cf3f1100f3a90156547 - depends: - - __osx >=11.0 - - openssl >=3.5.5,<4.0a0 - constrains: - - __osx >=11.0 - license: Apache-2.0 OR MIT - purls: [] - size: 5174311 - timestamp: 1771270398758 -- conda: https://conda.anaconda.org/conda-forge/win-64/lychee-0.23.0-hb3eb754_0.conda - sha256: 28ee5a30bea795d00df2151fa000f32c16a605e6e5743600f674cd2234864f67 - md5: cbe8bd414d963d87749cfd47659d55c4 - depends: - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - openssl >=3.5.5,<4.0a0 - license: Apache-2.0 OR MIT - purls: [] - size: 5763830 - timestamp: 1771270396939 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda - sha256: 1b4c105a887f9b2041219d57036f72c4739ab9e9fe5a1486f094e58c76b31f5f - md5: 318b08df404f9c9be5712aaa5a6f0bb0 - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 143402 - timestamp: 1674727076728 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.9.4-hd600fc2_0.conda - sha256: 076870eb72411f41c46598c7582a2f3f42ba94c526a2d60a0c8f70a0a7a64429 - md5: 500145a83ed07ce79c8cef24252f366b - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 163770 - timestamp: 1674727020254 -- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2 - sha256: 9de95a7996d5366ae0808eef2acbc63f9b11b874aa42375f55379e6715845dc6 - md5: 066552ac6b907ec6d72c0ddab29050dc - depends: - - m2w64-gcc-libs-core - - msys2-conda-epoch ==20160418 - license: GPL, LGPL, FDL, custom - purls: [] - size: 350687 - timestamp: 1608163451316 -- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2 - sha256: 3bd1ab02b7c89a5b153a17be03b36d833f1517ff2a6a77ead7c4a808b88196aa - md5: fe759119b8b3bfa720b8762c6fdc35de - depends: - - m2w64-gcc-libgfortran - - m2w64-gcc-libs-core - - m2w64-gmp - - m2w64-libwinpthread-git - - msys2-conda-epoch ==20160418 - license: GPL3+, partial:GCCRLE, partial:LGPL2+ - purls: [] - size: 532390 - timestamp: 1608163512830 -- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-core-5.3.0-7.tar.bz2 - sha256: 58afdfe859ed2e9a9b1cc06bc408720cb2c3a6a132e59d4805b090d7574f4ee0 - md5: 4289d80fb4d272f1f3b56cfe87ac90bd - depends: - - m2w64-gmp - - m2w64-libwinpthread-git - - msys2-conda-epoch ==20160418 - license: GPL3+, partial:GCCRLE, partial:LGPL2+ - purls: [] - size: 219240 - timestamp: 1608163481341 -- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2 - sha256: 7e3cd95f554660de45f8323fca359e904e8d203efaf07a4d311e46d611481ed1 - md5: 53a1c73e1e3d185516d7e3af177596d9 - depends: - - msys2-conda-epoch ==20160418 - license: LGPL3 - purls: [] - size: 743501 - timestamp: 1608163782057 -- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 - sha256: f63a09b2cae7defae0480f1740015d6235f1861afa6fe2e2d3e10bd0d1314ee0 - md5: 774130a326dee16f1ceb05cc687ee4f0 - depends: - - msys2-conda-epoch ==20160418 - license: MIT, BSD - purls: [] - size: 31928 - timestamp: 1608166099896 -- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.0.0-pyhd8ed1ab_0.conda - sha256: 7b1da4b5c40385791dbc3cc85ceea9fad5da680a27d5d3cb8bfaa185e304a89e - md5: 5b5203189eb668f042ac2b0826244964 - depends: - - mdurl >=0.1,<1 - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/markdown-it-py?source=hash-mapping - size: 64736 - timestamp: 1754951288511 -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py311h2dc5d0c_1.conda - sha256: 0291d90706ac6d3eea73e66cd290ef6d805da3fad388d1d476b8536ec92ca9a8 - md5: 6565a715337ae279e351d0abd8ffe88a + sha256: 8037f7a2f536c5ded9353054377dd4f386a6cd52ee0fdfbd7e47911d63a8dc60 + md5: beea0174c7b90d94f2b5a7813faa7b64 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 + - __osx >=11.0 + - openssl >=3.5.5,<4.0a0 constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 25354 - timestamp: 1733219879408 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.2-py311ha09ea12_1.conda - sha256: 0af0d9357e309876adf6ca61fa574afee74741fb1628755ce1f36028d294e854 - md5: eb3611be0cc15845bf6e5075adc520ee + - __osx >=10.13 + license: Apache-2.0 OR MIT + purls: [] + run_exports: {} + size: 5527018 + timestamp: 1771270370972 +- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.3-py311ha8ae342_1.conda + sha256: 8702d79aa3f5622d8ede5d5cc94faf135deab77b5caf95401f25f25179b22607 + md5: e14b7beea23c9e28b6500c24f5093d62 depends: - - libgcc >=13 + - __osx >=11.0 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 constrains: @@ -10533,2260 +12523,2666 @@ packages: license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 25787 - timestamp: 1733220925299 -- conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-3.0.2-py311ha3cf9ac_1.conda - sha256: e9965b5d4c29b17b1512035b24a7c126ed7bdb6b39103b52cae099d5bb4194a9 - md5: 1d6596ca7c7b66215c5c0d58b3cb0dd3 + run_exports: {} + size: 25987 + timestamp: 1772445597250 +- conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.7.1-py311h42ed68f_0.conda + sha256: 1a8fe170142be8d18c14f0893822079867e1507d64df2e472e71a7401ae34d07 + md5: 93619b2d972199a3320c1101eedaea89 depends: - __osx >=10.13 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD + license: Apache-2.0 + license_family: APACHE purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 24688 - timestamp: 1733219887972 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py311h4921393_1.conda - sha256: 4f738a7c80e34e5e5d558e946b06d08e7c40e3cc4bdf08140bf782c359845501 - md5: 249e2f6f5393bb6b36b3d3a3eebdcdf9 + - pkg:pypi/multidict?source=hash-mapping + run_exports: {} + size: 89614 + timestamp: 1771611130785 +- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.14.1-py311h4d7f069_0.conda + sha256: 5b5043cb2eeec8d0821130bf0e7ef62df44cbcff7220dca7e8497382f39f40b1 + md5: 285e86076c2a98bb57c7080a11095c69 depends: - - __osx >=11.0 + - __osx >=10.13 + - mypy_extensions >=1.0.0 + - psutil >=4.0 - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD + - typing_extensions >=4.1.0 + license: MIT + license_family: MIT purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 24976 - timestamp: 1733219849253 -- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py311h5082efb_1.conda - sha256: 6f756e13ccf1a521d3960bd3cadddf564e013e210eaeced411c5259f070da08e - md5: c1f2ddad665323278952a453912dc3bd + - pkg:pypi/mypy?source=hash-mapping + run_exports: {} + size: 12710578 + timestamp: 1735600553201 +- conda: https://conda.anaconda.org/conda-forge/osx-64/nasm-2.16.03-hfdf4475_1.conda + sha256: 67e4730cee8b72abdcd587e3407dad7eb5fb97b07754c673cb20583d2e528ac0 + md5: aa906b48511f43e9496d9afb0660b7df depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause + - __osx >=10.13 + license: BSD-2-Clause license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 28238 - timestamp: 1733220208800 -- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 - md5: 592132998493b3ff25fd7479396e8351 + purls: [] + run_exports: {} + size: 377182 + timestamp: 1721652783302 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + sha256: f5f7e006ff4271305ab4cc08eedd855c67a571793c3d18aff73f645f088a8cae + md5: 31b8740cf1b2588d4e61c81191004061 depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mdurl?source=hash-mapping - size: 14465 - timestamp: 1733255681319 -- conda: https://conda.anaconda.org/conda-forge/linux-64/meilisearch-1.5.1-he8a937b_0.conda - sha256: 233f9c2e3c83e2b27a7915cd21433c7f2566971470ec8f2f416cf298b9b73d97 - md5: d648052889e66626c93825ce8ee1d6f2 + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + run_exports: + weak: + - ncurses >=6.6,<7.0a0 + size: 831711 + timestamp: 1777423052277 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ninja-1.11.1-hb8565cd_0.conda + sha256: 6f738d9a26fa275317b95b2b96832daab9059ef64af9a338f904a3cb684ae426 + md5: 49ad513efe39447aa51affd47e3aa68f depends: - - libgcc-ng >=12 - license: MIT - license_family: MIT + - libcxx >=14.0.6 + license: Apache-2.0 + license_family: Apache purls: [] - size: 83512382 - timestamp: 1702682895721 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/meilisearch-1.5.1-h5ef7bb8_0.conda - sha256: c359718f193da18e77b7d19402d7453fa732978433ac562bbc86dfd17ef1bff8 - md5: 595899dbe10e2a0ab8e37f894f683082 + run_exports: {} + size: 121284 + timestamp: 1676837793132 +- conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-24.12.0-hb2861ea_0.conda + sha256: ff9de59616188f588070b382a66259aa7427be0fd5d8442d86493a85fce392ee + md5: 60d4a103fd38eac6b6df6ad9f6823516 + depends: + - libcxx >=19 + - __osx >=10.15 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - openssl >=3.5.4,<4.0a0 + - libnghttp2 >=1.67.0,<2.0a0 + - icu >=75.1,<76.0a0 + - c-ares >=1.34.6,<2.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libuv >=1.51.0,<2.0a0 + - libsqlite >=3.51.1,<4.0a0 license: MIT license_family: MIT purls: [] - size: 81671718 - timestamp: 1702680633448 -- pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - name: more-itertools - version: 10.8.0 - sha256: 52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/mpg123-1.32.9-hc50e24c_0.conda - sha256: 39c4700fb3fbe403a77d8cc27352fa72ba744db487559d5d44bf8411bb4ea200 - md5: c7f302fd11eeb0987a6a5e1f3aed6a21 + run_exports: + weak: + - nodejs >=24.12.0,<25.0a0 + size: 16917303 + timestamp: 1765888125784 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-hd629203_1.conda + sha256: bf665eb898b6105fd87a3a908301b8216463d2ee963b8e719801d3b1032148fd + md5: d685cb1e808a140561319c447ba9eed6 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - license: LGPL-2.1-only - license_family: LGPL + - __osx >=11.0 + - libcxx >=19 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 491140 - timestamp: 1730581373280 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mpg123-1.32.9-h65af167_0.conda - sha256: d65d5a00278544639ba4f99887154be00a1f57afb0b34d80b08e5cba40a17072 - md5: cdf140c7690ab0132106d3bc48bce47d + run_exports: + weak: + - openh264 >=2.6.0,<2.6.1.0a0 + size: 666034 + timestamp: 1782686541058 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + sha256: 819d4368d6b5b298fa40d4bc836c1250842489002cacf3fb918a13ee2033b7c6 + md5: 46be42ab403712fd349d007d763bf767 depends: - - libgcc >=13 - - libstdcxx >=13 - license: LGPL-2.1-only - license_family: LGPL + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache purls: [] - size: 558708 - timestamp: 1730581372400 -- conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2 - sha256: 99358d58d778abee4dca82ad29fb58058571f19b0f86138363c260049d4ac7f1 - md5: b0309b72560df66f71a9d5e34a5efdfa + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 2775300 + timestamp: 1781071391999 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-h6ef8af8_0.conda + sha256: baab8ebf970fb6006ad26884f75f151316e545c47fb308a1de2dd47ddd0381c5 + md5: 8c6316c058884ffda0af1f1272910f94 + depends: + - __osx >=10.13 + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later purls: [] - size: 3227 - timestamp: 1608166968312 -- conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.6.3-py311h2dc5d0c_0.conda - sha256: cde96613adebfa3a2c57abd4bf4026b6829d276fa95756ac6516115a7ff83b1f - md5: f368028b53e029409e2964707e03dcaf + run_exports: + weak: + - pango >=1.56.4,<2.0a0 + size: 432832 + timestamp: 1751292511389 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.47-h13923f0_0.conda + sha256: 8d64a9d36073346542e5ea042ef8207a45a0069a2e65ce3323ee3146db78134c + md5: 08f970fb2b75f5be27678e077ebedd46 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/multidict?source=hash-mapping - size: 97411 - timestamp: 1751310661884 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.6.3-py311h58d527c_0.conda - sha256: f8655863c4b2459af65e68ec9fd0726e676027f59722923e0a02911687751fbf - md5: b61c6bd3a01879c30d3c967cd54a5ca5 + - __osx >=10.13 + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 + size: 1106584 + timestamp: 1763655837207 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-h2fb4741_2.conda + sha256: 24fd53956b359b0cb79152522aadc2c216531880736e146cac3acaf137c48867 + md5: f8e55c2953f01b116eb61d764ed79095 depends: - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/multidict?source=hash-mapping - size: 100143 - timestamp: 1751310728158 -- conda: https://conda.anaconda.org/conda-forge/osx-64/multidict-6.6.3-py311h1cc1194_0.conda - sha256: b8a691f856b9b9139bb2588042ebe65f5aeda5d6f1e0a67bc4002980e4530012 - md5: 004066024ee31dc0f0bd22d4da0ca15b + - __osx >=11.0 + - libcxx >=19 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 320575 + timestamp: 1784286990554 +- conda: https://conda.anaconda.org/conda-forge/osx-64/prettier-3.8.1-h07b0e94_0.conda + sha256: ffaa0423766c436525f02ca93c9159e1f4e718e3432cacc2dac9f826290b9c24 + md5: 847c555170f38358a9266ec7cfd25a32 depends: + - nodejs - __osx >=10.13 + - nodejs >=24.12.0,<25.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 1103408 + timestamp: 1769199281565 +- conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.5.2-py311ha8ae342_0.conda + sha256: 11a77bd99d4d83a07929cd70cfe4af13e0fb3d520d38d1805d6a3e1e28d16075 + md5: bd7265ff62381b2c9b0bb0b3f56cc723 + depends: + - __osx >=11.0 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/multidict?source=hash-mapping - size: 89835 - timestamp: 1751310802904 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.6.3-py311h30e7462_0.conda - sha256: 4d175220d26e47265c9ed5f256fe68df4821e92e5c2cfc2fbe437f32c501c388 - md5: 069929b6e01d317f2d3775fffaba3db6 + - pkg:pypi/propcache?source=hash-mapping + run_exports: {} + size: 48898 + timestamp: 1780038168797 +- conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.31.1-py311h1c9791f_2.conda + sha256: d7a4d529e8c32a784f1b90aeda61b8867bb0456f80740e1f149cfc61988b7444 + md5: db13432bc0d826a5b62856ea0e071ff2 depends: - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libzlib >=1.3.1,<2.0a0 - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE + constrains: + - libprotobuf 6.31.1 + license: BSD-3-Clause + license_family: BSD purls: - - pkg:pypi/multidict?source=hash-mapping - size: 88450 - timestamp: 1751310825065 -- conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.6.3-py311h3f79411_0.conda - sha256: e696024cc1bf12d09e3866036acc633af1cae789ee83c0aaf87df53c56794e85 - md5: 923dca46fba0f7cfe2446f741126e00b + - pkg:pypi/protobuf?source=hash-mapping + run_exports: {} + size: 471373 + timestamp: 1760394226015 +- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.2.2-py311ha332486_0.conda + sha256: 374933b76115cd0946f3c47f20a7934ee0ce7bd4b667d14f7a704db1e85ee459 + md5: 29d06d0775445ec3f42f3a1be72b3f02 depends: - - python >=3.11,<3.12.0a0 + - python + - __osx >=10.13 - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: Apache-2.0 - license_family: APACHE + license: BSD-3-Clause + license_family: BSD purls: - - pkg:pypi/multidict?source=hash-mapping - size: 92269 - timestamp: 1751310800405 -- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.14.1-py311h9ecbd09_0.conda - sha256: 583282ca209e9dc9f91e28bb4d47bbf31456c2d437a4b4bdc3b1684b916b6264 - md5: 2bf2e229fee8e7649a7567dc61156437 + - pkg:pypi/psutil?source=hash-mapping + run_exports: {} + size: 242642 + timestamp: 1769678292465 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda + sha256: d22fd205d2db21c835e233c30e91e348735e18418c35327b0406d2d917e39a90 + md5: 7a1ad34efe728093c36a76afeaf30586 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - mypy_extensions >=1.0.0 - - psutil >=4.0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 + - __osx >=10.13 + - libcxx >=18 license: MIT license_family: MIT - purls: - - pkg:pypi/mypy?source=hash-mapping - size: 18730461 - timestamp: 1735601000085 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/mypy-1.14.1-py311ha879c10_0.conda - sha256: dc6f8258ebb3539b6ab27b5a78a1d2339b99a19c6396d29ffa3286664b0d671d - md5: e9e333fbbbc7571fb70f8e47edafdddd + purls: [] + run_exports: + weak: + - pugixml >=1.15,<1.16.0a0 + size: 97559 + timestamp: 1736601483485 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.15-ha9537fe_1_cpython.conda + build_number: 1 + sha256: 0cf3e36f0f9b6d40b844ed048ff495f9d381c200eb150d9152d847e5f56fac06 + md5: c652cafe724fd91cd8d56c1729c7676b + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.3,<6.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + run_exports: + weak: + - python_abi 3.11.* *_cp311 + noarch: + - python + size: 15650410 + timestamp: 1781150619639 +- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py311h53ebfaf_1.conda + sha256: db6ce0be1a9e0e57d829e6522ba932643fadd9e5238875ff299925246a01b6b2 + md5: 398a9df67c68780701415da829315730 depends: - - libgcc >=13 - - mypy_extensions >=1.0.0 - - psutil >=4.0 + - __osx >=10.13 - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 + - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT purls: - - pkg:pypi/mypy?source=hash-mapping - size: 16065092 - timestamp: 1735600817630 -- conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.14.1-py311h4d7f069_0.conda - sha256: 5b5043cb2eeec8d0821130bf0e7ef62df44cbcff7220dca7e8497382f39f40b1 - md5: 285e86076c2a98bb57c7080a11095c69 + - pkg:pypi/pyyaml?source=hash-mapping + run_exports: {} + size: 194735 + timestamp: 1770223769285 +- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + sha256: 4614af680aa0920e82b953fece85a03007e0719c3399f13d7de64176874b80d5 + md5: eefd65452dfe7cce476a519bece46704 + depends: + - __osx >=10.13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + run_exports: + weak: + - readline >=8.3,<9.0a0 + size: 317819 + timestamp: 1765813692798 +- conda: https://conda.anaconda.org/conda-forge/osx-64/rhash-1.4.6-h6e16a3a_1.conda + sha256: 65c946fc5a9bb71772a7ac9bad64ff08ac07f7d5311306c2dcc1647157b96706 + md5: d0fcaaeff83dd4b6fb035c2f36df198b depends: - __osx >=10.13 - - mypy_extensions >=1.0.0 - - psutil >=4.0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 license: MIT license_family: MIT - purls: - - pkg:pypi/mypy?source=hash-mapping - size: 12710578 - timestamp: 1735600553201 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.14.1-py311h917b07b_0.conda - sha256: 0d891fd3d73ddcece659e62b765ddd6023b1296d69943481dc9910107071307a - md5: c09549d23170ecaabfa4a8162b5d4f10 + purls: [] + run_exports: + weak: + - rhash >=1.4.6,<2.0a0 + size: 185180 + timestamp: 1748644989546 +- conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-2026.6.3-py311hcd3406c_0.conda + sha256: 049014c730fec9ed917f4e9ec460289db88daa184f1b60ab335c710e11af8b13 + md5: dcd3a3e2bff50b5154088ffcd0d137a8 depends: + - python - __osx >=11.0 - - mypy_extensions >=1.0.0 - - psutil >=4.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 + constrains: + - __osx >=11.0 license: MIT license_family: MIT purls: - - pkg:pypi/mypy?source=hash-mapping - size: 10180870 - timestamp: 1735600589567 -- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.14.1-py311he736701_0.conda - sha256: 12a90fb2507dd5c56a0e846bf828fe8b3197aa79ec8d655934d455d20101a640 - md5: a3f3aebd6fbdbdec85098e24d14f89aa + - pkg:pypi/rpds-py?source=hash-mapping + run_exports: {} + size: 297054 + timestamp: 1782831470109 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.7-h16586dd_1.conda + noarch: python + sha256: da9d7924d76798615c7919b7b3a77e40f848a89277fe996badb82fdc80d35ae7 + md5: 21c9b7a026f3f0244ab849aef6970138 depends: - - mypy_extensions >=1.0.0 - - psutil >=4.0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - typing_extensions >=4.1.0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - python + - __osx >=11.0 + constrains: + - __osx >=10.13 license: MIT license_family: MIT purls: - - pkg:pypi/mypy?source=hash-mapping - size: 10553025 - timestamp: 1735600107955 -- conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda - sha256: 6ed158e4e5dd8f6a10ad9e525631e35cee8557718f83de7a4e3966b1f772c4b1 - md5: e9c622e0d00fa24a6292279af3ab6d06 + - pkg:pypi/ruff?source=hash-mapping + run_exports: {} + size: 9184114 + timestamp: 1774012818963 +- conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.56-h2fb4741_0.conda + sha256: 22aacfc07fb6003992a281517514cbfc024fe52131bcb5645e7e13f3da28340b + md5: 1986c166d7888162b30cbe2f81c2f75c depends: - - python >=3.9 + - __osx >=11.0 + - libcxx >=19 + - sdl3 >=3.4.12,<4.0a0 + license: Zlib + purls: [] + run_exports: + weak: + - sdl2 >=2.32.56,<3.0a0 + size: 739868 + timestamp: 1783452029054 +- conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.4.12-hf9078ff_0.conda + sha256: 1341772ad10c042d2486c5b0d8c2fd412591dadb83b5921c1838231197f5c661 + md5: f18696d6257c9648562e4319e8acbf23 + depends: + - libcxx >=19 + - __osx >=11.0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - dbus >=1.16.2,<2.0a0 + - libusb >=1.0.29,<2.0a0 + license: Zlib + purls: [] + run_exports: + weak: + - sdl3 >=3.4.12,<4.0a0 + size: 1713182 + timestamp: 1782948155190 +- conda: https://conda.anaconda.org/conda-forge/osx-64/sigtool-0.1.3-hc0f2934_0.conda + sha256: 626bfe67b926107f84ec538e6d079552ea33bd169af3267bcdae37fae38a6cf5 + md5: 35241a0e86f03ddcff771a9a2070188d + depends: + - __osx >=10.13 + - libsigtool 0.1.3 hc0f2934_0 + - openssl >=3.5.4,<4.0a0 + - sigtool-codesign 0.1.3 hc0f2934_0 license: MIT license_family: MIT - purls: - - pkg:pypi/mypy-extensions?source=hash-mapping - size: 11766 - timestamp: 1745776666688 -- conda: https://conda.anaconda.org/conda-forge/linux-64/nasm-2.16.03-h4bc722e_1.conda - sha256: d01bfa655ad08d33dc5830a5166c7b664143df24fab59d41df15f076c58000b6 - md5: 35f8ab79609d5bc56d6d040f12dacf3a + purls: [] + run_exports: {} + size: 125857 + timestamp: 1767045035127 +- conda: https://conda.anaconda.org/conda-forge/osx-64/sigtool-codesign-0.1.3-hc0f2934_0.conda + sha256: b89d89d0b62e0a84093205607d071932cca228d4d6982a5b073eec7e765b146d + md5: 1261fc730f1d8af7eeea8a0024b23493 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 - license: BSD-2-Clause - license_family: BSD + - __osx >=10.13 + - libsigtool 0.1.3 hc0f2934_0 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT purls: [] - size: 1221519 - timestamp: 1721652638250 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nasm-2.16.03-h68df207_1.conda - sha256: af354688ee0ab41bd6d538b5c12fc392825da0e9549d5b4256ec13704b177bbd - md5: 277a1d8aa07160de3d02302364fd4dde + run_exports: {} + size: 123083 + timestamp: 1767045007433 +- conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h01f5ddf_1.conda + sha256: 1525e6d8e2edf32dabfe2a8e2fc8bf2df81c5ef9f0b5374a3d4ccfa672bfd949 + md5: 2e993292ec18af5cd480932d448598cf depends: - - libgcc-ng >=12 - license: BSD-2-Clause + - libcxx >=19 + - __osx >=10.13 + license: BSD-3-Clause license_family: BSD purls: [] - size: 1332204 - timestamp: 1721654126314 -- conda: https://conda.anaconda.org/conda-forge/osx-64/nasm-2.16.03-hfdf4475_1.conda - sha256: 67e4730cee8b72abdcd587e3407dad7eb5fb97b07754c673cb20583d2e528ac0 - md5: aa906b48511f43e9496d9afb0660b7df + run_exports: + weak: + - snappy >=1.2.2,<1.3.0a0 + size: 40023 + timestamp: 1762948053450 +- conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-3.1.2-h21dd04a_0.conda + sha256: e6fa8309eadc275aae8c456b9473be5b2b9413b43c6ef2fdbebe21fb3818dd55 + md5: c11ebe332911d9642f0678da49bedf44 depends: - __osx >=10.13 + - libcxx >=19 license: BSD-2-Clause license_family: BSD purls: [] - size: 377182 - timestamp: 1721652783302 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nasm-2.16.03-h99b78c6_1.conda - sha256: a1d8b8f6be3ccf94d8f29920a61fa83e4a94da84e8f0bdec4a5937092d56e59d - md5: c306196adb43e1300e1470dd65694ec5 + run_exports: + weak: + - svt-av1 >=3.1.2,<3.1.3.0a0 + size: 2390115 + timestamp: 1756086715447 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tapi-1300.6.5-h390ca13_0.conda + sha256: f97372a1c75b749298cb990405a690527e8004ff97e452ed2c59e4bc6a35d132 + md5: c6ee25eb54accb3f1c8fc39203acfaf1 depends: - - __osx >=11.0 - license: BSD-2-Clause - license_family: BSD + - __osx >=10.13 + - libcxx >=17.0.0.a0 + - ncurses >=6.5,<7.0a0 + license: NCSA + license_family: MIT purls: [] - size: 385586 - timestamp: 1721652965778 -- conda: https://conda.anaconda.org/conda-forge/win-64/nasm-2.16.03-hfd05255_1.conda - sha256: cce00ed17e684bf84c8cc592de578fedfb93b2d2357256c41c262b67ceacf6e7 - md5: ead716d50b01f09d327c781c05b25882 + run_exports: {} + size: 221236 + timestamp: 1725491044729 +- conda: https://conda.anaconda.org/conda-forge/osx-64/taplo-0.9.1-h236d3af_0.conda + sha256: 3e9032084b3f8d686b15f67500323ae2cae5637dc427b309b661a30026d8f00c + md5: 02c8d9c54b2887c5456fb7a0ecec62f3 depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.40.33810 - license: BSD-2-Clause - license_family: BSD + - openssl >=3.2.1,<4.0a0 + constrains: + - __osx >=10.12 + license: MIT + license_family: MIT purls: [] - size: 450395 - timestamp: 1721653214123 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 - md5: bbe1963f1e47f594070ffe87cdf612ea + run_exports: {} + size: 3773670 + timestamp: 1710793055293 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2022.3.0-hf0c99ee_1.conda + sha256: 56e32e8bd8f621ccd30574c2812f8f5bc42cc66a3fda8dd7e1b5e54d3f835faa + md5: 108a7d3b5f5b08ed346636ac5935a495 depends: - - jsonschema >=2.6 - - jupyter_core >=4.12,!=5.0.* - - python >=3.9 - - python-fastjsonschema >=2.15 - - traitlets >=5.1 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/nbformat?source=hash-mapping - size: 100945 - timestamp: 1733402844974 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbstripout-0.8.1-pyhd8ed1ab_0.conda - sha256: 45e7972348924fe5fe6bddf3b72ec79b679e4dfee1c1731d4fd9692fba13ceb4 - md5: 35e9b8d735ce9ee57686ec48556b1e51 + - __osx >=10.13 + - libcxx >=19 + - libhwloc >=2.12.1,<2.12.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 160700 + timestamp: 1762510382168 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hb794df6_3.conda + sha256: 670a364b8285887e5738880bb026f721af2662cfefe9ef64aa9e93eff1981535 + md5: bc699b366e49399bf8e5c6de99bb8cfb depends: - - nbformat - - python >=3.8 + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: TCL + purls: [] + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3516600 + timestamp: 1784229134070 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ty-0.0.31-h479939e_0.conda + noarch: python + sha256: aaf3c72e0ad337dc1c57f535cb77aacef03817ba8eb243888ebebe7cb4cb4968 + md5: 8c1e65a7ce27587a4398abbb4a65613b + depends: + - python + - __osx >=11.0 + - _python_abi3_support 1.* + - cpython >=3.10 + constrains: + - __osx >=10.13 license: MIT license_family: MIT purls: - - pkg:pypi/nbstripout?source=hash-mapping - size: 20982 - timestamp: 1731877844796 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 - md5: 47e340acb35de30501a76c7c799c41d7 + - pkg:pypi/ty?source=hash-mapping + run_exports: {} + size: 9197176 + timestamp: 1776273727251 +- conda: https://conda.anaconda.org/conda-forge/osx-64/typos-1.48.0-h19f9e61_0.conda + sha256: cb456e7a71045c982b372758c3dcab34bebdc23daa440e08ef692bff46026c76 + md5: 2548bc384e665a84e00c8fcee8afb256 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: X11 AND BSD-3-Clause + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT OR Apache-2.0 purls: [] - size: 891641 - timestamp: 1738195959188 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda - sha256: 91cfb655a68b0353b2833521dc919188db3d8a7f4c64bea2c6a7557b24747468 - md5: 182afabe009dc78d8b73100255ee6868 + run_exports: {} + size: 2862431 + timestamp: 1782859665351 +- conda: https://conda.anaconda.org/conda-forge/osx-64/wasm-pack-0.15.0-h19f9e61_0.conda + sha256: 054dd0fe133d2fe42e6e090c0800dd2d10c663aa01c0ad28802c7a7f619b02c4 + md5: afce185f83a2fa7a5acd17db16322db9 depends: - - libgcc >=13 - license: X11 AND BSD-3-Clause + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT OR Apache-2.0 + purls: [] + run_exports: {} + size: 2154262 + timestamp: 1780752593671 +- conda: https://conda.anaconda.org/conda-forge/osx-64/x264-1!164.3095-h775f41a_2.tar.bz2 + sha256: de611da29f4ed0733a330402e163f9260218e6ba6eae593a5f945827d0ee1069 + md5: 23e9c3180e2c0f9449bb042914ec2200 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 926034 - timestamp: 1738196018799 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_3.conda - sha256: ea4a5d27ded18443749aefa49dc79f6356da8506d508b5296f60b8d51e0c4bd9 - md5: ced34dd9929f491ca6dab6a2927aff25 + run_exports: + weak: + - x264 >=1!164.3095,<1!165 + size: 937077 + timestamp: 1660323305349 +- conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 + sha256: 6b6a57710192764d0538f72ea1ccecf2c6174a092e0bc76d790f8ca36bbe90e4 + md5: a3bf3e95b7795871a6734a784400fcea depends: - - __osx >=10.13 - license: X11 AND BSD-3-Clause + - libcxx >=12.0.1 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 822259 - timestamp: 1738196181298 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 - md5: 068d497125e4bf8a66bf707254fff5ae + run_exports: + weak: + - x265 >=3.5,<3.6.0a0 + size: 3433205 + timestamp: 1646610148268 +- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.3-h6a5a847_0.conda + sha256: ba5ad03c1c99c0bc62b92fb4630a33839e07b41f1b64c2d224f63a36b6ac1c00 + md5: 65aa14eb080715ecf13b15e5d85acde2 depends: - __osx >=11.0 - license: X11 AND BSD-3-Clause + - liblzma 5.8.3 hbb4bfdb_0 + - liblzma-devel 5.8.3 hbb4bfdb_0 + - xz-gpl-tools 5.8.3 h6a5a847_0 + - xz-tools 5.8.3 hbb4bfdb_0 + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later purls: [] - size: 797030 - timestamp: 1738196177597 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.11.1-h924138e_0.conda - sha256: b555247ac8859b4ff311e3d708a0640f1bfe9fae7125c485b444072474a84c41 - md5: 73a4953a2d9c115bdc10ff30a52f675f + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 24261 + timestamp: 1775826189380 +- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.3-h6a5a847_0.conda + sha256: e9bbba55933e2d962f65b689796561e9b687c36fb388b42eba5c0c561c6fe574 + md5: f2d1a60e16eb0da1cac4f9d0129957da depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 2251263 - timestamp: 1676837602636 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.11.1-hdd96247_0.conda - sha256: 2ba2e59f619c58d748f4b1b858502587691a7ed0fa9ac2c26ac04091908d95ae - md5: 58f4c67113cda9171e3c03d3e62731e1 + - __osx >=11.0 + - liblzma 5.8.3 hbb4bfdb_0 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 34168 + timestamp: 1775826151739 +- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.3-hbb4bfdb_0.conda + sha256: 57fc818b986bf86c5dec503047d5ba2f97bf76f2de225a3e6fea0c87c6e973dd + md5: 0a8d7aa810e8bef50429295f485fb14c depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: Apache-2.0 - license_family: Apache + - __osx >=11.0 + - liblzma 5.8.3 hbb4bfdb_0 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later purls: [] - size: 2398482 - timestamp: 1676839419214 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ninja-1.11.1-hb8565cd_0.conda - sha256: 6f738d9a26fa275317b95b2b96832daab9059ef64af9a338f904a3cb684ae426 - md5: 49ad513efe39447aa51affd47e3aa68f + run_exports: {} + size: 86264 + timestamp: 1775826113228 +- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda + sha256: a335161bfa57b64e6794c3c354e7d49449b28b8d8a7c4ed02bf04c3f009953f9 + md5: a645bb90997d3fc2aea0adf6517059bd depends: - - libcxx >=14.0.6 - license: Apache-2.0 - license_family: Apache + - __osx >=10.13 + license: MIT + license_family: MIT purls: [] - size: 121284 - timestamp: 1676837793132 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ninja-1.11.1-hffc8910_0.conda - sha256: a594e90b0ed8202c280fff4a008f6a355d0db54a62b17067dc4a950370ddffc0 - md5: fdecec4002f41cf6ea1eea5b52947ee0 + run_exports: + weak: + - yaml >=0.2.5,<0.3.0a0 + size: 79419 + timestamp: 1753484072608 +- conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.24.5-py311ha8ae342_0.conda + sha256: fd36d5e440a691c16043068b41259fa4954e242f31313d5868fc094ea56ad11c + md5: e8b8d5a9898aaff5cacf2d2f9c5dfe5a depends: - - libcxx >=14.0.6 + - __osx >=11.0 + - idna >=2.0 + - multidict >=4.0 + - propcache >=0.2.1 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 license: Apache-2.0 license_family: Apache + purls: + - pkg:pypi/yarl?source=hash-mapping + run_exports: {} + size: 165615 + timestamp: 1784526690425 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py311h62e9434_1.conda + sha256: 8b4e61e45260fdcdedd36192a225de724a0491548fd84a69679e872f467e55fd + md5: 9e05cc70a6656c2718783f011128991f + depends: + - python + - cffi >=1.11 + - zstd >=1.5.7,<1.5.8.0a0 + - __osx >=10.13 + - zstd >=1.5.7,<1.6.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zstandard?source=hash-mapping + run_exports: {} + size: 462796 + timestamp: 1762512690757 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + sha256: 47101a4055a70a4876ffc87b750ab2287b67eca793f21c8224be5e1ee6394d3f + md5: 727109b184d680772e3122f40136d5ca + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 107047 - timestamp: 1676837935565 -- conda: https://conda.anaconda.org/conda-forge/win-64/ninja-1.11.1-h91493d7_0.conda - sha256: 0ffb1912768af8354a930f482368ef170bf3d8217db328dfea1c8b09772c8c71 - md5: 44a99ef26178ea98626ff8e027702795 + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 + size: 528148 + timestamp: 1764777156963 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + build_number: 7 + sha256: 7acaa2e0782cad032bdaf756b536874346ac1375745fb250e9bdd6a48a7ab3cd + md5: a44032f282e7d2acdeb1c240308052dd + depends: + - llvm-openmp >=9.0.1 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - _openmp_mutex >=4.5 + size: 8325 + timestamp: 1764092507920 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.9.5-py311h05b510d_0.conda + sha256: 63ee70099b66bfa62751d1eb82831438426e3cfc9671a0b836dd9b9d94c92bd6 + md5: 69eee7117ab7f3ef9eb59a600a9079a3 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vs2015_runtime >=14.29.30139 - license: Apache-2.0 + - aiosignal >=1.1.2 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - multidict >=4.5,<7.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + - yarl >=1.0,<2.0 + license: MIT AND Apache-2.0 license_family: Apache - purls: [] - size: 279200 - timestamp: 1676838681615 -- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.17.1-heeeca48_0.conda - sha256: 5f2a59c43c871214f991c54022cf7ea6e5ec93d9a3a128fa1f9084712823b6f8 - md5: bb436044551ecc5c9f2a1cde9e712151 + purls: + - pkg:pypi/aiohttp?source=hash-mapping + run_exports: {} + size: 782527 + timestamp: 1713965372169 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aom-3.9.1-h7bae524_0.conda + sha256: ec238f18ce8140485645252351a0eca9ef4f7a1c568a420f240a585229bc12ef + md5: 7adba36492a1bb22d98ffffe4f6fc6de depends: - - __glibc >=2.28,<3.0.a0 - - libstdcxx >=14 - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 - - libuv >=1.51.0,<2.0a0 - - icu >=75.1,<76.0a0 - - openssl >=3.5.1,<4.0a0 - license: MIT - license_family: MIT + - __osx >=11.0 + - libcxx >=16 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 24474481 - timestamp: 1752839443324 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-24.4.1-hc854191_0.conda - sha256: ee8bfd840a9f424c438cb27924b7d1e7d76ad2738c3491282b43870d21b9ec25 - md5: a63b485569ea05f8618b76e312b7e2ec + run_exports: + weak: + - aom >=3.9.1,<3.10.0a0 + size: 2235747 + timestamp: 1718551382432 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/binaryen-117-hebf3989_0.conda + sha256: 9f4696ff6bf7a43261e549c1142dc24f45905fff68a6c0a1ebbdd0a84acd9056 + md5: 26d849f5539e7e20d8b7465a3616a622 depends: - - libgcc >=14 - - __glibc >=2.28,<3.0.a0 - - libstdcxx >=14 - - libgcc >=14 - - icu >=75.1,<76.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.1,<4.0a0 - - libuv >=1.51.0,<2.0a0 - license: MIT - license_family: MIT + - libcxx >=16 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 26871387 - timestamp: 1752839485839 -- conda: https://conda.anaconda.org/conda-forge/osx-64/nodejs-24.4.1-h2e7699b_0.conda - sha256: 1c9571726b5b5e85acfba50dda7ae9b22d2b29e590159a581bafde5bf2e04621 - md5: 9993063cfe84cf1fa928c7d021bd01a0 + run_exports: {} + size: 3466426 + timestamp: 1709092708128 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py311hdc60ec4_1.conda + sha256: 617545ec0e97d35ed2ff7852f2581a20c0dda80b366d0c42a43706687f971ba8 + md5: 150cbf381febcf0a5e470a8d066e1bc0 depends: - - __osx >=10.15 + - __osx >=11.0 - libcxx >=19 - - openssl >=3.5.1,<4.0a0 - - libuv >=1.51.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - icu >=75.1,<76.0a0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + constrains: + - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + run_exports: {} + size: 359588 + timestamp: 1764018467340 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/buf-1.66.0-h75b854d_0.conda + sha256: 81522a9cfda1f1b00104691afa996e2775d80327454d869db5a9729b33252813 + md5: b11300170f80c33477cba788ccd9cbe7 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 18918546 - timestamp: 1752839437994 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-24.4.1-hab9d20b_0.conda - sha256: c79d2c81f80a9adedc77362f2e8b10879ed0f9806deb6ba2464c1287a05f0b9b - md5: 463a537de602f8558604f27395b323d0 + run_exports: {} + size: 59020197 + timestamp: 1771990147629 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df + md5: 620b85a3f45526a8bc4d23fd78fc22f0 depends: - - libcxx >=19 - __osx >=11.0 - - openssl >=3.5.1,<4.0a0 - - libuv >=1.51.0,<2.0a0 - - icu >=75.1,<76.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT + license: bzip2-1.0.6 + license_family: BSD purls: [] - size: 17949155 - timestamp: 1752839389217 -- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-24.4.1-he453025_0.conda - sha256: 1bb0d9e370bb0ffa2071ccfdd0ef3cb90bd183b07c67b646d1aa5c743004d233 - md5: cde0d5793a73ab343b5764fa6c002771 + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 + size: 124834 + timestamp: 1771350416561 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.8-h84a0fba_0.conda + sha256: 2bfa6ed107d2d8b5835228f8392050cc12e4b6dd732ee044c4ab503b94ff7f31 + md5: 187f3b77e761a2f126f9e09f165cebba + depends: + - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 29967122 - timestamp: 1752839409586 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.3-hb9d3cd8_0.conda - sha256: 2254dae821b286fb57c61895f2b40e3571a070910fdab79a948ff703e1ea807b - md5: 56f8947aa9d5cf37b0b3d43b83f34192 + run_exports: + weak: + - c-ares >=1.34.8,<2.0a0 + size: 183515 + timestamp: 1784091337771 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-compiler-1.6.0-h6aa9301_0.conda + sha256: c7d7c09724e7c324ecd3ad2dee4f016149b93f9bd8ee67661cafb20993f5b8a9 + md5: 0b204833d66694f214a5b3d7d2b87700 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - opencl-headers >=2024.10.24 - license: BSD-2-Clause - license_family: BSD + - cctools >=949.0.1 + - clang_osx-arm64 16.* + - ld64 >=530 + - llvm-openmp + license: BSD purls: [] - size: 106742 - timestamp: 1743700382939 -- conda: https://conda.anaconda.org/conda-forge/linux-64/opencl-headers-2025.06.13-h5888daf_0.conda - sha256: 2b6ce54174ec19110e1b3c37455f7cd138d0e228a75727a9bba443427da30a36 - md5: 45c3d2c224002d6d0d7769142b29f986 + run_exports: {} + size: 6380 + timestamp: 1701504712958 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + sha256: 00439d69bdd94eaf51656fdf479e0c853278439d22ae151cabf40eb17399d95f + md5: 38f6df8bc8c668417b904369a01ba2e2 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - license: Apache-2.0 - license_family: APACHE + - __osx >=11.0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libcxx >=18 + - libexpat >=2.6.4,<3.0a0 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + license: LGPL-2.1-only or MPL-1.1 purls: [] - size: 55357 - timestamp: 1749853464518 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.6.0-hc22cd8d_0.conda - sha256: 3f231f2747a37a58471c82a9a8a80d92b7fece9f3fce10901a5ac888ce00b747 - md5: b28cf020fd2dead0ca6d113608683842 + run_exports: + weak: + - cairo >=1.18.4,<2.0a0 + size: 896173 + timestamp: 1741554795915 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cargo-llvm-cov-0.8.7-h748bcf4_0.conda + sha256: d8414be30616fe78fdea8c4be1011c38f172ea3843b65b4c61ca30256e78508f + md5: 5fa54e076a5d79033ec79f6336afa3d8 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - license: BSD-2-Clause - license_family: BSD + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: Apache-2.0 + license_family: Apache purls: [] - size: 731471 - timestamp: 1739400677213 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openh264-2.6.0-h0564a2a_0.conda - sha256: 3b7a519e3b7d7721a0536f6cba7f1909b878c71962ee67f02242958314748341 - md5: 0abed5d78c07a64e85c54f705ba14d30 + run_exports: {} + size: 1150283 + timestamp: 1778642519278 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cargo-nextest-0.9.140-h6fdd925_0.conda + sha256: 76211f85956ec6f37f38b1b6165332233c3f5e016bb090859881d9e1ac1f1a12 + md5: 85190477f1f0a580ac199d87cf3227b2 depends: - - libgcc >=13 - - libstdcxx >=13 - license: BSD-2-Clause - license_family: BSD + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT purls: [] - size: 774512 - timestamp: 1739400731652 -- conda: https://conda.anaconda.org/conda-forge/osx-64/openh264-2.6.0-h4883158_0.conda - sha256: a6d734ddbfed9b6b972e7564f5d5eeaab9db2ba128ef92677abd11d36192ff2f - md5: 774f56cba369e2286e4922c8f143694a + run_exports: {} + size: 6539037 + timestamp: 1783308109859 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools-1010.6-h4faf515_1.conda + sha256: e0a69226e1f70b79f41c471c86fd0c450cc4fd5ec3343cd7689eb1c016babc70 + md5: d200afcb0b601ad89c79212b9a124347 depends: - - __osx >=10.13 - - libcxx >=18 - license: BSD-2-Clause - license_family: BSD + - cctools_osx-arm64 1010.6 h4f2c9d0_1 + - ld64 951.9 h634c8be_1 + - libllvm16 >=16.0.6,<16.1.0a0 + license: APSL-2.0 + license_family: Other purls: [] - size: 660864 - timestamp: 1739400822452 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openh264-2.6.0-hb5b2745_0.conda - sha256: fbea05722a8e8abfb41c989e2cec7ba6597eabe27cb6b88ff0b6443a5abb9069 - md5: 6ff0890a94972aca7cc7f8f8ef1ff142 + run_exports: {} + size: 21621 + timestamp: 1726771337947 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cctools_osx-arm64-1010.6-h4f2c9d0_1.conda + sha256: 3585a1d44fae9fd6839734e25ddde9dfb1dbb99c6974deb7bdbc6470b54af76d + md5: 3cf0dad98fcf3cec8cf6372ba2954724 depends: - __osx >=11.0 - - libcxx >=18 - license: BSD-2-Clause - license_family: BSD + - ld64_osx-arm64 >=951.9,<951.10.0a0 + - libcxx + - libllvm16 >=16.0.6,<16.1.0a0 + - libzlib >=1.3.1,<2.0a0 + - llvm-tools 16.0.* + - sigtool + constrains: + - ld64 951.9.* + - cctools 1010.6.* + - clang 16.0.* + license: APSL-2.0 + license_family: Other purls: [] - size: 601538 - timestamp: 1739400923874 -- conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-hb17fa0b_0.conda - sha256: 914702d9a64325ff3afb072c8bc0f8cbea3f19955a8395a8c190e45604f83c76 - md5: ad4cac6ceb9e4c8e01802e3f15e87bb2 + run_exports: {} + size: 1091944 + timestamp: 1726771303834 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.1.0-py311h833bfeb_0.conda + sha256: fa96675649d057baaf2ad4fc73f836781a58d0e3270f4776a3f4ee457bacda37 + md5: eda60fa2b167c49a1b033450b4af6032 depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 411269 - timestamp: 1739401120354 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.0-h26f9b46_0.conda - sha256: a47271202f4518a484956968335b2521409c8173e123ab381e775c358c67fe6d - md5: 9ee58d5c534af06558933af3c845a780 + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - pycparser + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + run_exports: {} + size: 298491 + timestamp: 1783424587732 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16-16.0.6-default_h3c2e7ce_15.conda + sha256: 7412ca9b68eefe9ae8f509a4badac9e8a70f5d06024285604aad36fae9710317 + md5: 19739ec9eae7382a7be37881a95f30e2 depends: - - __glibc >=2.17,<3.0.a0 - - ca-certificates - - libgcc >=14 - license: Apache-2.0 + - __osx >=11.0 + - libclang-cpp16 16.0.6 default_h3c2e7ce_15 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + constrains: + - clang-tools 16.0.6 + - clangxx 16.0.6 + - llvm-tools 16.0.6 + - clangdev 16.0.6 + license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 3165399 - timestamp: 1762839186699 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.0-h8e36d6e_0.conda - sha256: 8dd3b4c31fe176a3e51c5729b2c7f4c836a2ce3bd5c82082dc2a503ba9ee0af3 - md5: 7624c6e01aecba942e9115e0f5a2af9d + run_exports: {} + size: 761355 + timestamp: 1756166017332 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-16.0.6-default_h3e759af_15.conda + sha256: 13772739cdacadffdc7b3b97dd2a5b4c1ea8526f6e48b2bb5c28d4be0de0200c + md5: 0d4af3afc0ec97952578b965da7fee34 depends: - - ca-certificates - - libgcc >=14 - license: Apache-2.0 + - clang-16 16.0.6 default_h3c2e7ce_15 + constrains: + - clang-tools 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 3705625 - timestamp: 1762841024958 -- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.0-h230baf5_0.conda - sha256: 36fe9fb316be22fcfb46d5fa3e2e85eec5ef84f908b7745f68f768917235b2d5 - md5: 3f50cdf9a97d0280655758b735781096 + run_exports: {} + size: 92190 + timestamp: 1756166136780 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16-16.0.6-default_h3c2e7ce_15.conda + sha256: b7049fd6cfab9306522c4e9399cd74692a6897f0d11a50a63ad92f15b40db2f6 + md5: af53806ee8d5023c799d6186fd1442f6 depends: - - __osx >=10.13 - - ca-certificates - license: Apache-2.0 + - __osx >=11.0 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 2778996 - timestamp: 1762840724922 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.0-h5503f6c_0.conda - sha256: ebe93dafcc09e099782fe3907485d4e1671296bc14f8c383cb6f3dfebb773988 - md5: b34dc4172653c13dcf453862f251af2b + run_exports: {} + size: 127337 + timestamp: 1756166443161 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-format-16.0.6-default_h3c2e7ce_15.conda + sha256: 211a4772f2912ac1ea68eaa8cebb411dec46efdc5ef4d899dba316e53aebcb7f + md5: da984913ad26e91c30015e58ec58d902 depends: - __osx >=11.0 - - ca-certificates - license: Apache-2.0 + - clang-format-16 16.0.6 default_h3c2e7ce_15 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 3108371 - timestamp: 1762839712322 -- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.0-h725018a_0.conda - sha256: 6d72d6f766293d4f2aa60c28c244c8efed6946c430814175f959ffe8cab899b3 - md5: 84f8fb4afd1157f59098f618cd2437e4 + run_exports: {} + size: 92531 + timestamp: 1756166546529 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang-tools-16.0.6-default_h3c2e7ce_15.conda + sha256: 791b96da9f4c361831c250f7d40f171ce0f41cb9d6d8891860c8f3f418b4177d + md5: 8ff3b05785dbd769ae3328d3199fef9d depends: - - ca-certificates - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: Apache-2.0 + - __osx >=11.0 + - clang-format 16.0.6 default_h3c2e7ce_15 + - libclang-cpp16 >=16.0.6,<16.1.0a0 + - libclang13 >=16.0.6 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + constrains: + - clangdev 16.0.6 + - clang 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception license_family: Apache purls: [] - size: 9440812 - timestamp: 1762841722179 -- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - sha256: da157b19bcd398b9804c5c52fc000fcb8ab0525bdb9c70f95beaa0bb42f85af1 - md5: 3bfed7e6228ebf2f7b9eaa47f1b4e2aa - depends: - - python >=3.8 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/packaging?source=hash-mapping - size: 60164 - timestamp: 1733203368787 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda - sha256: 3613774ad27e48503a3a6a9d72017087ea70f1426f6e5541dbdb59a3b626eaaf - md5: 79f71230c069a287efe3a8614069ddf1 - depends: - - __glibc >=2.17,<3.0.a0 - - cairo >=1.18.4,<2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - fribidi >=1.0.10,<2.0a0 - - harfbuzz >=11.0.1 - - libexpat >=2.7.0,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libgcc >=13 - - libglib >=2.84.2,<3.0a0 - - libpng >=1.6.49,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - license: LGPL-2.1-or-later - purls: [] - size: 455420 - timestamp: 1751292466873 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda - sha256: dd36cd5b6bc1c2988291a6db9fa4eb8acade9b487f6f1da4eaa65a1eebb0a12d - md5: a22cc88bf6059c9bcc158c94c9aab5b8 + run_exports: {} + size: 16980094 + timestamp: 1756166687551 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_impl_osx-arm64-16.0.6-hc421ffc_19.conda + sha256: e131b316c772b9ecd57f47e221b0b460d817650ee29de3a6d017ba17f834e3a3 + md5: 44d46e1690d60e9dfdf9ab9fc8a344f6 depends: - - cairo >=1.18.4,<2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - fribidi >=1.0.10,<2.0a0 - - harfbuzz >=11.0.1 - - libexpat >=2.7.0,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libgcc >=13 - - libglib >=2.84.2,<3.0a0 - - libpng >=1.6.49,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - license: LGPL-2.1-or-later + - cctools_osx-arm64 + - clang 16.0.6.* + - compiler-rt 16.0.6.* + - ld64_osx-arm64 + - llvm-tools 16.0.6.* + license: BSD-3-Clause + license_family: BSD purls: [] - size: 468811 - timestamp: 1751293869070 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pango-1.56.4-h6ef8af8_0.conda - sha256: baab8ebf970fb6006ad26884f75f151316e545c47fb308a1de2dd47ddd0381c5 - md5: 8c6316c058884ffda0af1f1272910f94 + run_exports: {} + size: 17659 + timestamp: 1723069383236 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clang_osx-arm64-16.0.6-h54d7cd3_19.conda + sha256: 1be2d2b837267e9cc61c1cb5e0ce780047ceb87063005144c1332a82a5996fb3 + md5: 1a9ab8ce6143c14e425059e61a4fb737 depends: - - __osx >=10.13 - - cairo >=1.18.4,<2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - fribidi >=1.0.10,<2.0a0 - - harfbuzz >=11.0.1 - - libexpat >=2.7.0,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libglib >=2.84.2,<3.0a0 - - libpng >=1.6.49,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - license: LGPL-2.1-or-later + - clang_impl_osx-arm64 16.0.6 hc421ffc_19 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 432832 - timestamp: 1751292511389 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda - sha256: 705484ad60adee86cab1aad3d2d8def03a699ece438c864e8ac995f6f66401a6 - md5: 7d57f8b4b7acfc75c777bc231f0d31be + run_exports: {} + size: 20589 + timestamp: 1723069388608 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx-16.0.6-default_hc1b5c72_15.conda + sha256: da0634e0c5f0d117169bf2d65d696277a07cd1ec18cd04bd98c90baded3541d0 + md5: f9cfd9b8b33f762dd456cc770fa5b29f depends: - - __osx >=11.0 - - cairo >=1.18.4,<2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - fribidi >=1.0.10,<2.0a0 - - harfbuzz >=11.0.1 - - libexpat >=2.7.0,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libglib >=2.84.2,<3.0a0 - - libpng >=1.6.49,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - license: LGPL-2.1-or-later + - clang 16.0.6 default_h3e759af_15 + - libcxx-devel 16.0.6.* + constrains: + - libcxx-devel 16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 426931 - timestamp: 1751292636271 -- conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda - sha256: dcda7e9bedc1c87f51ceef7632a5901e26081a1f74a89799a3e50dbdc801c0bd - md5: 452d6d3b409edead3bd90fc6317cd6d4 + run_exports: {} + size: 92329 + timestamp: 1756166158732 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_impl_osx-arm64-16.0.6-hcd7bac0_19.conda + sha256: 6847b38f815e43a01e7cfe78fc9d2d7ab90c749bce1301322707ccbad4f2d7a2 + md5: 263f7e2b3196bea030602830381cc84e depends: - - cairo >=1.18.4,<2.0a0 - - fontconfig >=2.15.0,<3.0a0 - - fonts-conda-ecosystem - - fribidi >=1.0.10,<2.0a0 - - harfbuzz >=11.0.1 - - libexpat >=2.7.0,<3.0a0 - - libfreetype >=2.13.3 - - libfreetype6 >=2.13.3 - - libglib >=2.84.2,<3.0a0 - - libpng >=1.6.49,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: LGPL-2.1-or-later + - clang_osx-arm64 16.0.6 h54d7cd3_19 + - clangxx 16.0.6.* + - libcxx >=16 + - libllvm16 >=16.0.6,<16.1.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 454854 - timestamp: 1751292618315 -- conda: https://conda.anaconda.org/conda-forge/linux-64/patchelf-0.17.2-h58526e2_0.conda - sha256: eb355ac225be2f698e19dba4dcab7cb0748225677a9799e9cc8e4cadc3cb738f - md5: ba76a6a448819560b5f8b08a9c74f415 + run_exports: {} + size: 17740 + timestamp: 1723069417515 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/clangxx_osx-arm64-16.0.6-h54d7cd3_19.conda + sha256: 6e4344d0bc29fc76e6c6c8aa463536ea0615ffe60512c883b8ae26d73ac4804d + md5: 26ffc845adddf183c15dd4285e97fc66 depends: - - libgcc-ng >=7.5.0 - - libstdcxx-ng >=7.5.0 - license: GPL-3.0-or-later - license_family: GPL + - clang_osx-arm64 16.0.6 h54d7cd3_19 + - clangxx_impl_osx-arm64 16.0.6 hcd7bac0_19 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 94048 - timestamp: 1673473024463 -- pypi: https://files.pythonhosted.org/packages/78/6b/14fc9049d78435fd29e82846c777bd7ed9c470013dc8d0260fff3ff1c11e/pathspec-1.0.2-py3-none-any.whl - name: pathspec - version: 1.0.2 - sha256: 62f8558917908d237d399b9b338ef455a814801a4688bc41074b25feefd93472 - requires_dist: - - hyperscan>=0.7 ; extra == 'hyperscan' - - typing-extensions>=4 ; extra == 'optional' - - google-re2>=1.1 ; extra == 're2' - - pytest>=9 ; extra == 'tests' - - typing-extensions>=4.15 ; extra == 'tests' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda - sha256: 5c7380c8fd3ad5fc0f8039069a45586aa452cf165264bc5a437ad80397b32934 - md5: 7fa07cb0fb1b625a089ccc01218ee5b1 + run_exports: + strong: + - libcxx >=16 + size: 19366 + timestamp: 1723069423746 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cmake-3.27.6-h1c59155_0.conda + sha256: 31be31e358e6f6f8818d8f9c9086da4404f8c6fc89d71d55887bed11ce6d463e + md5: 3c0dd04401438fec44cd113247ba2852 depends: - - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 + - libcurl >=8.3.0,<9.0a0 + - libcxx >=15.0.7 + - libexpat >=2.5.0,<3.0a0 + - libuv >=1.46.0,<2.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - ncurses >=6.4,<7.0a0 + - rhash >=1.4.4,<2.0a0 + - xz >=5.2.6,<6.0a0 + - zstd >=1.5.5,<1.6.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 1209177 - timestamp: 1756742976157 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.46-h15761aa_0.conda - sha256: 75800e60e0e44d957c691a964085f56c9ac37dcd75e6c6904809d7b68f39e4ea - md5: 5128cb5188b630a58387799ea1366e37 + run_exports: {} + size: 16007289 + timestamp: 1695270816826 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/compiler-rt-16.0.6-h3808999_2.conda + sha256: 67f6883f37ea720f97d016c3384962d86ec8853e5f4b0065aa77e335ca80193e + md5: 517f18b3260bb7a508d1f54a96e6285b depends: - - bzip2 >=1.0.8,<2.0a0 - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD + - clang 16.0.6.* + - clangxx 16.0.6.* + - compiler-rt_osx-arm64 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE purls: [] - size: 1161914 - timestamp: 1756742893031 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.46-ha3e7e28_0.conda - sha256: cb262b7f369431d1086445ddd1f21d40003bb03229dfc1d687e3a808de2663a6 - md5: 3b504da3a4f6d8b2b1f969686a0bf0c0 + run_exports: {} + size: 93724 + timestamp: 1701467327657 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cxx-compiler-1.6.0-h2ffa867_0.conda + sha256: c3a4ee7382e548f1e98ca1a348c941094b8d5f38c84d3258c00f9e493c591344 + md5: b3bf27600fda1f6770fd28c45805d689 depends: - - __osx >=10.13 - - bzip2 >=1.0.8,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause + - c-compiler 1.6.0 h6aa9301_0 + - clangxx_osx-arm64 16.* + license: BSD + purls: [] + run_exports: {} + size: 6399 + timestamp: 1701504753445 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/dav1d-1.2.1-hb547adb_0.conda + sha256: 93e077b880a85baec8227e8c72199220c7f87849ad32d02c14fb3807368260b8 + md5: 5a74cdee497e6b65173e10d94582fae6 + license: BSD-2-Clause license_family: BSD purls: [] - size: 1097626 - timestamp: 1756743061564 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda - sha256: 5bf2eeaa57aab6e8e95bea6bd6bb2a739f52eb10572d8ed259d25864d3528240 - md5: 0e6e82c3cc3835f4692022e9b9cd5df8 + run_exports: + weak: + - dav1d >=1.2.1,<1.2.2.0a0 + size: 316394 + timestamp: 1685695959391 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/dbus-1.16.2-h3ff7a7c_1.conda + sha256: a8207751ed261764061866880da38e4d3063e167178bfe85b6db9501432462ba + md5: 5a3506971d2d53023c1c4450e908a8da depends: + - libcxx >=19 - __osx >=11.0 - - bzip2 >=1.0.8,<2.0a0 + - libglib >=2.86.2,<3.0a0 - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD + - libexpat >=2.7.3,<3.0a0 + license: AFL-2.1 OR GPL-2.0-or-later + purls: [] + run_exports: + weak: + - dbus >=1.16.2,<2.0a0 + size: 393811 + timestamp: 1764536084131 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/doxygen-1.9.7-h0e2417a_1.conda + sha256: 4bfaf6721b163301135c2db1268b40a099f51e2a42fdec60262137c72e20b9eb + md5: 02c4969f0c780d47e3f95b43f18a8ad7 + depends: + - libcxx >=15.0.7 + - libiconv >=1.17,<2.0a0 + license: GPL-2.0-only + license_family: GPL + purls: [] + run_exports: {} + size: 5103390 + timestamp: 1687332854077 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fd-find-10.4.2-h748bcf4_0.conda + sha256: aeb06a67ac07856b790e5a086b3d390432d1d643458908b4cc3a5a6a09805500 + md5: eb518eb445bb6de558890e8899862c2a + depends: + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT purls: [] - size: 835080 - timestamp: 1756743041908 -- conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda - sha256: 29c2ed44a8534d27faad96bdce16efe29c2788f556f4c5409d4ae8ae074681ec - md5: 889053e920d15353c2665fa6310d7a7a + run_exports: {} + size: 1057813 + timestamp: 1773353306662 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ffmpeg-7.1.1-gpl_h93d53e2_110.conda + sha256: 68eae35a62e36844aaa4c246c65977403e7a46415a1c4785577154fedba5ec63 + md5: 8adffbcfe629e5817d3921718fd42e7d depends: + - __osx >=11.0 + - aom >=3.9.1,<3.10.0a0 - bzip2 >=1.0.8,<2.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - gmp >=6.3.0,<7.0a0 + - harfbuzz >=11.4.5 + - lame >=3.100,<3.101.0a0 + - libass >=0.17.4,<0.17.5.0a0 + - libcxx >=19 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libopenvino >=2025.2.0,<2025.2.1.0a0 + - libopenvino-arm-cpu-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-auto-batch-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-auto-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-hetero-plugin >=2025.2.0,<2025.2.1.0a0 + - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 + - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 + - libopus >=1.5.2,<2.0a0 + - librsvg >=2.58.4,<3.0a0 + - libvorbis >=1.3.7,<1.4.0a0 + - libvpx >=1.14.1,<1.15.0a0 + - libxml2 >=2.13.8,<2.14.0a0 - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 1034703 - timestamp: 1756743085974 -- pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - name: pexpect - version: 4.9.0 - sha256: 7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 - requires_dist: - - ptyprocess>=0.5 -- pypi: https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl - name: pillow - version: 12.2.0 - sha256: 71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 - requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl - name: pillow - version: 12.2.0 - sha256: 8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab - requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl - name: pillow - version: 12.2.0 - sha256: 6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 - requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - name: pillow - version: 12.2.0 - sha256: 3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705 - requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: pillow - version: 12.2.0 - sha256: e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 - requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.2-pyh8b19718_0.conda - sha256: ec9ed3cef137679f3e3a68e286c6efd52144684e1be0b05004d9699882dadcdd - md5: dfce4b2af4bfe90cdcaf56ca0b28ddf5 + - openh264 >=2.6.0,<2.6.1.0a0 + - openssl >=3.5.2,<4.0a0 + - sdl2 >=2.32.54,<3.0a0 + - svt-av1 >=3.1.2,<3.1.3.0a0 + - x264 >=1!164.3095,<1!165 + - x265 >=3.5,<3.6.0a0 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + run_exports: + weak: + - ffmpeg >=7.1.1,<8.0a0 + size: 9159034 + timestamp: 1757215368356 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/flatbuffers-25.12.19-h784d473_0.conda + sha256: 534bff044ed0be12787d5d41af4a14896d22ab0d92794083f4205ebcf0a93289 + md5: e1c1fce04be829b04b0a03e9194f848a depends: - - python >=3.9,<3.13.0a0 - - setuptools - - wheel - license: MIT - license_family: MIT - purls: - - pkg:pypi/pip?source=hash-mapping - size: 1177168 - timestamp: 1753924973872 -- conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.3-pyh8b19718_0.conda - sha256: b67692da1c0084516ac1c9ada4d55eaf3c5891b54980f30f3f444541c2706f1e - md5: c55515ca43c6444d2572e0f0d93cb6b9 + - __osx >=11.0 + - libcxx >=19 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - flatbuffers >=25.12.19,<25.12.20.0a0 + size: 1549199 + timestamp: 1766388878273 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.18.1-h2b252f5_0.conda + sha256: 8607d8d0b32f9f6fc61ea8c06b537486b78428a04516658222fa4d1d521af765 + md5: 9d928e6a62192141fb6540a3125b1345 depends: - - python >=3.10,<3.13.0a0 - - setuptools - - wheel + - __osx >=11.0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libintl >=0.25.1,<1.0a0 + - libzlib >=1.3.2,<2.0a0 license: MIT license_family: MIT + purls: [] + run_exports: + weak: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + size: 248677 + timestamp: 1780450500773 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.3-hce30654_1.conda + sha256: 96b33f1e2a32c602b167f43719e3acf89ec742b4a1e25e99ffd0e6f99b38d277 + md5: 7bd06ab4ed807154c2d9031eb5ebf025 + depends: + - libfreetype 2.14.3 hce30654_1 + - libfreetype6 2.14.3 hdfa99f5_1 + license: GPL-2.0-only OR FTL + purls: [] + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + size: 173518 + timestamp: 1780933616544 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda + sha256: d856dc6744ecfba78c5f7df3378f03a75c911aadac803fa2b41a583667b4b600 + md5: 04bdce8d93a4ed181d1d726163c2d447 + depends: + - __osx >=11.0 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - fribidi >=1.0.16,<2.0a0 + size: 59391 + timestamp: 1757438897523 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.8.0-py311hf75086c_0.conda + sha256: 32ab4112a1d2e119d8c5109f345a4f32b396db4597889958b62680a5bc1c73e9 + md5: abb28a2132a7c4587f406fab77b777ce + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: APACHE purls: - - pkg:pypi/pip?source=compressed-mapping - size: 1177534 - timestamp: 1762776258783 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda - sha256: 43d37bc9ca3b257c5dd7bf76a8426addbdec381f6786ff441dc90b1a49143b6a - md5: c01af13bdc553d1a8fbfff6e8db075f0 + - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} + size: 51197 + timestamp: 1780000393807 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.7-h4e57454_0.conda + sha256: 69bb2e62a93f6407c9e9ccd4d21fe4d4e5373d64eecd6c5df318144ca4c80953 + md5: f717a22e13a1499c9552ffff02d22d64 depends: - - libgcc >=14 - - libstdcxx >=14 - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 + - __osx >=11.0 + - libglib >=2.88.2,<3.0a0 + - libintl >=0.25.1,<1.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + run_exports: + weak: + - gdk-pixbuf >=2.44.7,<3.0a0 + size: 553902 + timestamp: 1782591436963 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gh-2.96.0-hf76c51c_0.conda + sha256: 56d2d61817b5ad372b1fc64e48ac939eabb3f03c0dad070b4940397308cd4528 + md5: ff30f92f378e6bb91a8bb9187c53b3c5 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 12008445 + timestamp: 1783039135497 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gmp-6.3.0-h7bae524_2.conda + sha256: 76e222e072d61c840f64a44e0580c2503562b009090f55aa45053bf1ccb385dd + md5: eed7278dfbab727b56f2c0b64330814b + depends: + - __osx >=11.0 + - libcxx >=16 + license: GPL-2.0-or-later OR LGPL-3.0-or-later + purls: [] + run_exports: + weak: + - gmp >=6.3.0,<7.0a0 + size: 365188 + timestamp: 1718981343258 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.15-hf6b4638_0.conda + sha256: c0a060d7b7a05669043ef3f68c7a1025c8594e1ab73735afb64c35e8baa41da5 + md5: 0d576cff278a2e60456d5b2c0a1ffda3 + depends: + - __osx >=11.0 + - libcxx >=19 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 + size: 82245 + timestamp: 1780454628763 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-12.2.0-haf38c7b_0.conda + sha256: 2f8d95fe1cb655fe3bac114062963f08cc77b31b042027ef7a04ebde3ce21594 + md5: 1c7ff9d458dd8220ac2ee71dd4af1be5 + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libcxx >=19 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libglib >=2.86.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 license: MIT license_family: MIT purls: [] - size: 450960 - timestamp: 1754665235234 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda - sha256: e6b0846a998f2263629cfeac7bca73565c35af13251969f45d385db537a514e4 - md5: 1587081d537bd4ae77d1c0635d465ba5 + run_exports: + weak: + - harfbuzz >=12.2.0 + size: 1537764 + timestamp: 1762373922469 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620 + md5: 5eb22c1d7b3fc4abb50d92d621583137 depends: - - libgcc >=14 - - libstdcxx >=14 - - libgcc >=14 + - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 357913 - timestamp: 1754665583353 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.46.4-ha059160_1.conda - sha256: ff8b679079df25aa3ed5daf3f4e3a9c7ee79e7d4b2bd8a21de0f8e7ec7207806 - md5: 742a8552e51029585a32b6024e9f57b4 + run_exports: + weak: + - icu >=75.1,<76.0a0 + size: 11857802 + timestamp: 1720853997952 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda + sha256: c740e4a2e7247776a9883158fdab50ae0732c8f67f96d8f1db8ad9da5e0b5222 + md5: 8780f41b013d19219faef9c82260744b depends: - - __osx >=10.13 + - __osx >=11.0 - libcxx >=19 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - openssl >=3.5.7,<4.0a0 license: MIT license_family: MIT purls: [] - size: 390942 - timestamp: 1754665233989 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda - sha256: 29c9b08a9b8b7810f9d4f159aecfd205fce051633169040005c0b7efad4bc718 - md5: 17c3d745db6ea72ae2fce17e7338547f + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1159780 + timestamp: 1781859501654 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lame-3.100-h1a8c8d9_1003.tar.bz2 + sha256: f40ce7324b2cf5338b766d4cdb8e0453e4156a4f83c2f31bbfff750785de304c + md5: bff0e851d66725f78dc2fd8b032ddb7e + license: LGPL-2.0-only + license_family: LGPL + purls: [] + run_exports: + weak: + - lame >=3.100,<3.101.0a0 + size: 528805 + timestamp: 1664996399305 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64-951.9-h634c8be_1.conda + sha256: d347ecd273ea7552ae703a37650ea211ff640ed8fd921fe6f1ede49dcdc1358c + md5: 294a282b67deea1f0ea1c7d8be2bb5c5 + depends: + - ld64_osx-arm64 951.9 h0605c9f_1 + - libllvm16 >=16.0.6,<16.1.0a0 + constrains: + - cctools_osx-arm64 1010.6.* + - cctools 1010.6.* + license: APSL-2.0 + license_family: Other + purls: [] + run_exports: {} + size: 18928 + timestamp: 1726771322773 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ld64_osx-arm64-951.9-h0605c9f_1.conda + sha256: 2183f5fc32084bbaa83a84817cfc68091e9e739a048a185dcfa55be908b9fe54 + md5: 77076839b5a8ac684c7971641d69b97a + depends: + - __osx >=11.0 + - libcxx + - libllvm16 >=16.0.6,<16.1.0a0 + - sigtool + - tapi >=1300.6.5,<1301.0a0 + constrains: + - clang >=16.0.6,<17.0a0 + - cctools_osx-arm64 1010.6.* + - ld 951.9.* + - cctools 1010.6.* + license: APSL-2.0 + license_family: Other + purls: [] + run_exports: {} + size: 1006497 + timestamp: 1726771248963 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.1.0-h1eee2c3_0.conda + sha256: 66e5ffd301a44da696f3efc2f25d6d94f42a9adc0db06c44ad753ab844148c51 + md5: 095e5749868adab9cae42d4b460e5443 depends: - __osx >=11.0 - libcxx >=19 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 + size: 164222 + timestamp: 1773114244984 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250512.1-cxx17_hd41c47c_0.conda + sha256: 7f0ee9ae7fa2cf7ac92b0acf8047c8bac965389e48be61bf1d463e057af2ea6a + md5: 360dbb413ee2c170a0a684a33c4fc6b8 + depends: + - __osx >=11.0 + - libcxx >=18 + constrains: + - libabseil-static =20250512.1=cxx17* + - abseil-cpp =20250512.1 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libabseil >=20250512.1,<20250513.0a0 + - libabseil =*=cxx17* + size: 1174081 + timestamp: 1750194620012 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libass-0.17.4-hcbd7ca7_0.conda + sha256: 079f5fdf7aace970a0db91cd2cc493c754dfdc4520d422ecec43d2561021167a + md5: 0977f4a79496437ff3a2c97d13c4c223 + depends: + - __osx >=11.0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - libzlib >=1.3.1,<2.0a0 + - fribidi >=1.0.10,<2.0a0 + - libiconv >=1.18,<2.0a0 + - harfbuzz >=11.0.1 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + license: ISC + purls: [] + run_exports: + weak: + - libass >=0.17.4,<0.17.5.0a0 + size: 138339 + timestamp: 1749328988096 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + sha256: a7cb9e660531cf6fbd4148cff608c85738d0b76f0975c5fc3e7d5e92840b7229 + md5: 006e7ddd8a110771134fcc4e1e3a6ffa + depends: + - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 248045 - timestamp: 1754665282033 -- conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda - sha256: 246fce4706b3f8b247a7d6142ba8d732c95263d3c96e212b9d63d6a4ab4aff35 - md5: 08c8fa3b419df480d985e304f7884d35 + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + size: 79443 + timestamp: 1764017945924 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + sha256: 2eae444039826db0454b19b52a3390f63bfe24f6b3e63089778dd5a5bf48b6bf + md5: 079e88933963f3f149054eec2c487bc2 depends: - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT purls: [] - size: 542795 - timestamp: 1754665193489 -- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - sha256: dfe0fa6e351d2b0cef95ac1a1533d4f960d3992f9e0f82aeb5ec3623a699896b - md5: cc9d9a3929503785403dbfad9f707145 + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 29452 + timestamp: 1764017979099 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + sha256: 01436c32bb41f9cb4bcf07dda647ce4e5deb8307abfc3abdc8da5317db8189d1 + md5: b2b7c8288ca1a2d71ff97a8e6a1e8883 depends: - - python >=3.10 - - python + - __osx >=11.0 + - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT - purls: - - pkg:pypi/platformdirs?source=compressed-mapping - size: 23653 - timestamp: 1756227402815 -- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl - name: pluggy - version: 1.6.0 - sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 - requires_dist: - - pre-commit ; extra == 'dev' - - tox ; extra == 'dev' - - pytest ; extra == 'testing' - - pytest-benchmark ; extra == 'testing' - - coverage ; extra == 'testing' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/prettier-3.6.2-h4c22ac6_1.conda - sha256: be8168057925ab344d97a3c261ab0a628509bfb4e5542d2a16cacefc92b20655 - md5: d5e01725eb018c1907f41fed6afbf81b + purls: [] + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 + size: 290754 + timestamp: 1764018009077 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang-cpp16-16.0.6-default_h3c2e7ce_15.conda + sha256: 96beef959638d73da280e9551b9028df48f7f671df237c6bb7c7495816e96fa8 + md5: 2589c8f983f4676b005a4e8fb227212d depends: - - nodejs - - __glibc >=2.17,<3.0.a0 - - nodejs >=22.17.0,<23.0a0 - license: MIT + - __osx >=11.0 + - libcxx >=16.0.6 + - libllvm16 >=16.0.6,<16.1.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: + weak: + - libclang-cpp16 >=16.0.6,<16.1.0a0 + size: 11797889 + timestamp: 1756165841886 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libclang13-21.1.0-default_h6e8f826_1.conda + sha256: d4517eb5c79e386eacdfa0424c94c822a04cf0d344d6730483de1dcbce24a5dd + md5: a29a6b4c1a926fbb64813ecab5450483 + depends: + - __osx >=11.0 + - libcxx >=21.1.0 + - libllvm21 >=21.1.0,<21.2.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: + weak: + - libclang13 >=21.1.0 + size: 8513708 + timestamp: 1757383978186 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda + sha256: ba74311dc4805af2ba1365d85814199a563cb09950f68b97fcd4e939dc090855 + md5: 27c5b464c5fbee7411aab3bc0195f9c2 + depends: + - __osx >=11.0 + - krb5 >=1.22.2,<1.23.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl license_family: MIT purls: [] - size: 1084709 - timestamp: 1752245753014 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/prettier-3.6.2-h70496c1_1.conda - sha256: 8fff7bd7eea756de4470e758324365593064fca73888a29265f7ce581796256a - md5: bdc884f77fe5e1a32f660d0444f431f6 + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 410285 + timestamp: 1782802574409 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.8-h55c6f16_0.conda + sha256: a2e7abab5add9750fab064c024394de48e49f97631c605ad5db5c8ac3fc769ef + md5: 89f76a2a21a3ec3ec983b5eb237c4113 depends: - - nodejs - - nodejs >=24.3.0,<25.0a0 + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: {} + size: 569349 + timestamp: 1781670209146 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-devel-16.0.6-h86353a2_2.conda + sha256: fb51aaeb9911d9999afaf0a3dc8f4eee97c524aac4ec152217372e8645ef8856 + md5: f81c638415433ea5bb5024b49cda17ea + depends: + - libcxx >=16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: {} + size: 717680 + timestamp: 1725067968232 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda + sha256: 5e0b6961be3304a5f027a8c00bd0967fc46ae162cffb7553ff45c70f51b8314c + md5: a6130c709305cd9828b4e1bd9ba0000c + depends: + - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 1085790 - timestamp: 1752245753237 -- conda: https://conda.anaconda.org/conda-forge/osx-64/prettier-3.6.2-h07b0e94_1.conda - sha256: 35dc836f5ec05974874b6f6478eb4cd1ff3dec29be7a8054eeef7b07ebbee361 - md5: 4d12a1c76891aaa752da7f1a94d098e1 + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 + size: 55420 + timestamp: 1761980066242 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 + md5: 44083d2d2c2025afca315c7a172eab2b + depends: + - ncurses + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libedit >=3.1.20250104,<3.2.0a0 + size: 107691 + timestamp: 1738479560845 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f + md5: 36d33e440c31857372a72137f78bacf5 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libev >=4.33,<4.34.0a0 + size: 107458 + timestamp: 1702146414478 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + sha256: 5af74261101e3c777399c6294b2b5d290e508153268eb2e9ff99c4d69834612f + md5: a915151d5d3c5bf039f5ccc8402a436f depends: - - nodejs - - __osx >=10.13 - - nodejs >=24.3.0,<25.0a0 + - __osx >=11.0 + constrains: + - expat 2.8.1.* license: MIT license_family: MIT purls: [] - size: 1083550 - timestamp: 1752245752827 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/prettier-3.6.2-h9907cc9_1.conda - sha256: 65b42f9bbffaa7432e65447442af100f1764556fec60dc2d3f1d1bea905edc11 - md5: 126573a0d34ba9a0ce0bcfc502af1a91 + run_exports: {} + size: 69362 + timestamp: 1781203631990 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 depends: - - nodejs - __osx >=11.0 - - nodejs >=24.3.0,<25.0a0 license: MIT license_family: MIT purls: [] - size: 1084179 - timestamp: 1752245795819 -- conda: https://conda.anaconda.org/conda-forge/win-64/prettier-3.6.2-hc21fffc_1.conda - sha256: b4e935c49424ee0045e9b056e1a01479401d813a4b0a1110a8e76e630dca1d1f - md5: 8a590f0da474edda47ed4dda15bb575f + run_exports: + weak: + - libffi >=3.5.2,<3.6.0a0 + size: 40979 + timestamp: 1769456747661 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_1.conda + sha256: d5637b01941c0fc8f5cbb1f170c238f4ee153b3c1708b9d50f4f1305438ff051 + md5: 0582e67cd14cfed773be2f3b1aba08e0 depends: - - nodejs - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - nodejs >=24.4.0,<25.0a0 - license: MIT - license_family: MIT + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL purls: [] - size: 1087013 - timestamp: 1752245767199 -- conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.3.1-py311h2dc5d0c_0.conda - sha256: 38ef315508a4c6c96985a990b172964a8ed737fe4e991d82ad9d2a77c45add1f - md5: c75eb8c91d69fe0385fce584f3ce193a - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/propcache?source=hash-mapping - size: 54558 - timestamp: 1744525097548 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.3.1-py311h58d527c_0.conda - sha256: 65d0f979c9f3e3972dc8ef178c5fbb0bf6858cd82521ec9e6be5b563c18756c3 - md5: 872b336081fdbcd407ba6eef96f6651a - depends: - - libgcc >=13 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/propcache?source=hash-mapping - size: 54289 - timestamp: 1744525129299 -- conda: https://conda.anaconda.org/conda-forge/osx-64/propcache-0.3.1-py311ha3cf9ac_0.conda - sha256: 5245afac67313565159345ff12fee41f91183a46f46b84e7a94a6d3a6bafaa90 - md5: 8fd57bbb0bdb21497cc53129a2f94e91 - depends: - - __osx >=10.13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/propcache?source=hash-mapping - size: 49875 - timestamp: 1744525202139 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.3.1-py311h4921393_0.conda - sha256: 559f330cc40372422f8d9d5068b905b80d6762a8c2c7aeb4886a98ed7023c686 - md5: 667f23d757cbfa63e8b3ecc7e7d34b18 + run_exports: {} + size: 8365 + timestamp: 1780933612390 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.3-hdfa99f5_1.conda + sha256: abbfffd8a8c776bb8b59a10c8247fc3aa6b17ba0051e9f6d199dca38479f214f + md5: a0bb0678f67c464938d3693fa96f6884 depends: - __osx >=11.0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/propcache?source=hash-mapping - size: 51291 - timestamp: 1744525140418 -- conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.3.1-py311h5082efb_0.conda - sha256: aa123cee8e1ad192896c79e39a88f131e289b66113c177e11933ec5812d69533 - md5: 7ea79e503415ce3f38a73669d3168cee - depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/propcache?source=hash-mapping - size: 50616 - timestamp: 1744525381124 -- pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl - name: proto-plus - version: 1.27.0 - sha256: 1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82 - requires_dist: - - protobuf>=3.19.0,<7.0.0 - - google-api-core>=1.31.5 ; extra == 'testing' - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.31.1-py311h425ed32_2.conda - sha256: f5216cb89239542d39b9dfc9a757157f8c779e88a769c165e275da035b38cd02 - md5: 28ef5e67a2544510913d04a4a6dd9e12 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL + purls: [] + run_exports: {} + size: 338442 + timestamp: 1780933611662 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + sha256: 06644fa4d34d57c9e48f4d84b1256f9e5f654fdb37f43acc8a58a396952d42b7 + md5: 644058123986582db33aebd4ae2ca184 depends: - - __glibc >=2.17,<3.0.a0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libstdcxx >=14 - - libzlib >=1.3.1,<2.0a0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 + - _openmp_mutex constrains: - - libprotobuf 6.31.1 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/protobuf?source=hash-mapping - size: 486563 - timestamp: 1760393355981 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/protobuf-6.31.1-py311he3e547a_2.conda - sha256: 40a160e81173f0fdd19bb55eabc99974a35a9749fd2bf0217d6f9d17c058bee3 - md5: b77baff6dd1ceb08af318c066617d647 + - libgcc-ng ==15.2.0=*_19 + - libgomp 15.2.0 19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: {} + size: 404080 + timestamp: 1778273064154 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_19.conda + sha256: d0a68b7a121d115b80c169e24d1265dcc25a3fe58d107df1bbc430797e226d88 + md5: ba36d8c606a6a53fe0b8c12d47267b3d depends: - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libgcc >=14 - - libstdcxx >=14 - - libzlib >=1.3.1,<2.0a0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 + - libgcc >=15.2.0 constrains: - - libprotobuf 6.31.1 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/protobuf?source=hash-mapping - size: 496479 - timestamp: 1760393576155 -- conda: https://conda.anaconda.org/conda-forge/osx-64/protobuf-6.31.1-py311h1c9791f_2.conda - sha256: d7a4d529e8c32a784f1b90aeda61b8867bb0456f80740e1f149cfc61988b7444 - md5: db13432bc0d826a5b62856ea0e071ff2 + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + run_exports: {} + size: 599691 + timestamp: 1778273075448 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.88.2-ha08bb59_0.conda + sha256: 68ac66904a284a7dfbb3bdf9225380eaf20750b2d414215e6d7af5caa3ed1a63 + md5: 03123cafdc96cef79fc8a615f9119b79 depends: - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - - libcxx >=19 - - libzlib >=1.3.1,<2.0a0 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 + - pcre2 >=10.47,<10.48.0a0 + - libiconv >=1.18,<2.0a0 + - libintl >=0.25.1,<1.0a0 + - libzlib >=1.3.2,<2.0a0 + - libffi >=3.5.2,<3.6.0a0 constrains: - - libprotobuf 6.31.1 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/protobuf?source=hash-mapping - size: 471373 - timestamp: 1760394226015 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.31.1-py311h93f9908_2.conda - sha256: cb419d29176f8334822563103dccb3f7e9296974bbd9fdbb96739529bdc4c6a2 - md5: df8f7f9164c209927985b2ec2d43d1d9 + - glib >2.66 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 4437447 + timestamp: 1782463971075 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwloc-2.12.1-default_h88f92a7_1000.conda + sha256: 79a02778b06d9f22783050e5565c4497e30520cf2c8c29583c57b8e42068ae86 + md5: b32f2f83be560b0fb355a730e4057ec1 depends: - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20250512.1,<20250513.0a0 - libcxx >=19 - - libzlib >=1.3.1,<2.0a0 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - constrains: - - libprotobuf 6.31.1 + - libxml2 >=2.13.8,<2.14.0a0 license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/protobuf?source=hash-mapping - size: 468653 - timestamp: 1760394086091 -- conda: https://conda.anaconda.org/conda-forge/win-64/protobuf-6.32.1-py311heca59f8_2.conda - sha256: b03ed3c30a4ebd9f5597c7f6aeba4c80160721c4c9496303d3ab2e0a8ee5d579 - md5: 1206f808089e2f72ea28c518c07bada2 + purls: [] + run_exports: + weak: + - libhwloc >=2.12.1,<2.12.2.0a0 + size: 2355380 + timestamp: 1752761771779 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + sha256: de0336e800b2af9a40bdd694b03870ac4a848161b35c8a2325704f123f185f03 + md5: 4d5a7445f0b25b6a3ddbb56e790f5251 depends: - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.2 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - libprotobuf 6.32.1 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/protobuf?source=hash-mapping - size: 492674 - timestamp: 1760430608792 -- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.3-py311haee01d2_0.conda - sha256: 6a0b791e00368b6b635c65d5fb31d385129da790d21923387c6b546230ffdf14 - md5: 2092b7977bc8e05eb17a1048724593a4 + - __osx >=11.0 + license: LGPL-2.1-only + purls: [] + run_exports: + weak: + - libiconv >=1.18,<2.0a0 + size: 750379 + timestamp: 1754909073836 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + sha256: 99d2cebcd8f84961b86784451b010f5f0a795ed1c08f1e7c76fbb3c22abf021a + md5: 5103f6a6b210a3912faf8d7db516918c depends: - - python - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 513789 - timestamp: 1762092898190 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.1.3-py311h51cfe5d_0.conda - sha256: f9cf8d817b4bcbf9f7d5f6ef502dc1bebe7aee3502c6cf52febd1762442c0fb5 - md5: d98d8c983ffb785bdc25659720f83072 + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - libintl >=0.25.1,<1.0a0 + size: 90957 + timestamp: 1751558394144 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_0.conda + sha256: e2b4fce7cf48705dc182a0aa6c478a47b16202aeb712242caf9c9ab6a19778fa + md5: 8f05a69b7e870574a2d366043f1515b1 depends: - - python - - libgcc >=14 - - python 3.11.* *_cpython - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 518968 - timestamp: 1762092942561 -- conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-7.1.3-py311h62e9434_0.conda - sha256: 2b16aa4a8d1df8810129d3edbe74d760c826dc998ee65a4b0db3957fc7d97374 - md5: 76219ffe585f6877d15dbbd28c5a93c4 + - __osx >=11.0 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 558409 + timestamp: 1783732115664 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm16-16.0.6-hc4b4ae8_4.conda + sha256: 1cdaa0cf825d75758e67a2f0f3118a770272d0f8b30388b897a00730ac830484 + md5: 88bab67516b973b3f1a72021d2ac2ab6 depends: - - python - - __osx >=10.13 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 524561 - timestamp: 1762092999229 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.3-py311h5bb9006_0.conda - sha256: ea916cf3e0ac41d9eb6d644b8317b2cf4878d974339e56ab081cc2544315d4d1 - md5: cf10a93bacbb33e52075e536051efe97 + - __osx >=11.0 + - libcxx >=18 + - libxml2 >=2.13.5,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.6,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: + weak: + - libllvm16 >=16.0.6,<16.1.0a0 + size: 23532169 + timestamp: 1739798547548 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libllvm21-21.1.0-h846d351_0.conda + sha256: 4b22efda81b517da3f54dc138fd03a9f9807bdbc8911273777ae0182aab0b115 + md5: a8ec02cc70f4c56b5daaa5be62943065 depends: - - python - __osx >=11.0 - - python 3.11.* *_cpython - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 526877 - timestamp: 1762093036674 -- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.3-py311hf893f09_0.conda - sha256: 62611ce54d62682f9e37100bb3af4da1a9da49d631fd505521e20647a6e2e171 - md5: 697ef79a96ce3fb39ca62aa58a8915c7 + - libcxx >=19 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: + weak: + - libllvm21 >=21.1.0,<21.2.0a0 + size: 29414704 + timestamp: 1756282753920 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + sha256: 34878d87275c298f1a732c6806349125cebbf340d24c6c23727268184bba051e + md5: b1fd823b5ae54fbec272cea0811bd8a9 depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/psutil?source=hash-mapping - size: 532233 - timestamp: 1762092931133 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda - sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 - md5: b3c17d95b5a10c6e64a21fa17573e70e + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 92472 + timestamp: 1775825802659 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-devel-5.8.3-h8088a28_0.conda + sha256: 3002be39c0e98ec6cd103b0dc2963dc9e0d7cab127fb2fe9a8de9707a76ed1f0 + md5: ebe1f5418d6e2d4bbc26b2c906a0a470 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT + - __osx >=11.0 + - liblzma 5.8.3 h8088a28_0 + license: 0BSD purls: [] - size: 8252 - timestamp: 1726802366959 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda - sha256: 977dfb0cb3935d748521dd80262fe7169ab82920afd38ed14b7fee2ea5ec01ba - md5: bb5a90c93e3bac3d5690acf76b4a6386 + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 118482 + timestamp: 1775825828010 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + sha256: 2bc7bc3978066f2c274ebcbf711850cc9ab92e023e433b9631958a098d11e10a + md5: 6ea18834adbc3b33df9bd9fb45eaf95b depends: - - libgcc >=13 + - __osx >=11.0 + - c-ares >=1.34.6,<2.0a0 + - libcxx >=19 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 8342 - timestamp: 1726803319942 -- pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - name: ptyprocess - version: 0.7.0 - sha256: 4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.15-h3f63f65_0.conda - sha256: 23c98a5000356e173568dc5c5770b53393879f946f3ace716bbdefac2a8b23d2 - md5: b11a4c6bf6f6f44e5e143f759ffa2087 + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 + size: 576526 + timestamp: 1773854624224 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libogg-1.3.5-h48c0fde_1.conda + sha256: 28bd1fe20fe43da105da41b95ac201e95a1616126f287985df8e86ddebd1c3d8 + md5: 29b8b11f6d7e6bd0e76c029dcf9dd024 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - license: MIT - license_family: MIT + - __osx >=11.0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 118488 - timestamp: 1736601364156 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pugixml-1.15-h6ef32b0_0.conda - sha256: adc17205a87e064508d809fe5542b7cf49f9b9a458418f8448e2fc895fcd04f3 - md5: 53e14f45d38558aa2b9a15b07416e472 + run_exports: + weak: + - libogg >=1.3.5,<1.4.0a0 + size: 216719 + timestamp: 1745826006052 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-2025.2.0-h56e7ac4_1.conda + sha256: 6f74a2d9d39df7d98e5be28028b927746d6213102aad94eea2131f05879a5af4 + md5: 0d6535fb8c6e34dcc1c8e63b3a6d2a98 depends: - - libgcc >=13 - - libstdcxx >=13 - license: MIT - license_family: MIT + - __osx >=11.0 + - libcxx >=19 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 113424 - timestamp: 1737355438448 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pugixml-1.15-h46091d4_0.conda - sha256: d22fd205d2db21c835e233c30e91e348735e18418c35327b0406d2d917e39a90 - md5: 7a1ad34efe728093c36a76afeaf30586 + run_exports: + weak: + - libopenvino >=2025.2.0,<2025.2.1.0a0 + size: 4367075 + timestamp: 1753200563969 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-arm-cpu-plugin-2025.2.0-h56e7ac4_1.conda + sha256: a8975d1430afdab3e373c99d66d6bc4d6d6842a6448bcc3b32b2eb1d60d25729 + md5: 376ff75a12a871f211e943357229c32b depends: - - __osx >=10.13 - - libcxx >=18 - license: MIT - license_family: MIT + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + - pugixml >=1.15,<1.16.0a0 + - tbb >=2021.13.0 purls: [] - size: 97559 - timestamp: 1736601483485 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pugixml-1.15-hd3d436d_0.conda - sha256: 5ad8d036040b095f85d23c70624d3e5e1e4c00bc5cea97831542f2dcae294ec9 - md5: b9a4004e46de7aeb005304a13b35cb94 + run_exports: {} + size: 7919701 + timestamp: 1753200600045 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-auto-batch-plugin-2025.2.0-he81eb65_1.conda + sha256: becf0dd673803ba43fbca7ac2731227855ee3c3bdc72e242a97f101a85d26c31 + md5: 45b44ad26a4b5d386feb079cc93996d8 depends: - __osx >=11.0 - - libcxx >=18 - license: MIT - license_family: MIT + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + - tbb >=2021.13.0 purls: [] - size: 91283 - timestamp: 1736601509593 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pulseaudio-client-17.0-hb77b528_0.conda - sha256: b27c0c8671bd95c205a61aeeac807c095b60bc76eb5021863f919036d7a964fc - md5: 07f45f1be1c25345faddb8db0de8039b + run_exports: {} + size: 105074 + timestamp: 1753200643185 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-auto-plugin-2025.2.0-he81eb65_1.conda + sha256: 5a515ec892e74682c3b8a9c68c4920444eaeb41de50c2bf3b4fc5cce6a4bfa9c + md5: 3c40649c696a02029642b08a27c041d0 depends: - - dbus >=1.13.6,<2.0a0 - - libgcc-ng >=12 - - libglib >=2.78.3,<3.0a0 - - libsndfile >=1.2.2,<1.3.0a0 - - libsystemd0 >=255 - constrains: - - pulseaudio 17.0 *_0 - license: LGPL-2.1-or-later - license_family: LGPL + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + - tbb >=2021.13.0 purls: [] - size: 757633 - timestamp: 1705690081905 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pulseaudio-client-17.0-h729494f_0.conda - sha256: 209eac3123ee2c84a35401626941c4aa64e04e2c9854084ddeba6432c6078a41 - md5: f35f57712d5c2abca98c85a51a408bc1 + run_exports: {} + size: 216636 + timestamp: 1753200660470 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-hetero-plugin-2025.2.0-h273c05f_1.conda + sha256: 132e845f2001241eb112eea01aa2596e61562ca0be7dcd0e7be6056c2ad583f2 + md5: 98479fa3c1442811d65d44f695d6f271 depends: - - dbus >=1.13.6,<2.0a0 - - libgcc-ng >=12 - - libglib >=2.78.3,<3.0a0 - - libsndfile >=1.2.2,<1.3.0a0 - - libsystemd0 >=255 - constrains: - - pulseaudio 17.0 *_0 - license: LGPL-2.1-or-later - license_family: LGPL + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + - pugixml >=1.15,<1.16.0a0 purls: [] - size: 766184 - timestamp: 1705690164726 -- pypi: https://files.pythonhosted.org/packages/14/e3/3a32500d845bdd94f6a2b4ed6244982f42ec2bc64602ea8fcfe900678ae7/py_spy-0.4.1-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - name: py-spy - version: 0.4.1 - sha256: 809094208c6256c8f4ccadd31e9a513fe2429253f48e20066879239ba12cd8cc - requires_dist: - - numpy ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/68/fb/bc7f639aed026bca6e7beb1e33f6951e16b7d315594e7635a4f7d21d63f4/py_spy-0.4.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl - name: py-spy - version: 0.4.1 - sha256: 6a80ec05eb8a6883863a367c6a4d4f2d57de68466f7956b6367d4edd5c61bb29 - requires_dist: - - numpy ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/df/79/9ed50bb0a9de63ed023aa2db8b6265b04a7760d98c61eb54def6a5fddb68/py_spy-0.4.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - name: py-spy - version: 0.4.1 - sha256: ee776b9d512a011d1ad3907ed53ae32ce2f3d9ff3e1782236554e22103b5c084 - requires_dist: - - numpy ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/e1/da/fcc9a9fcd4ca946ff402cff20348e838b051d69f50f5d1f5dca4cd3c5eb8/py_spy-0.4.1-py2.py3-none-win_amd64.whl - name: py-spy - version: 0.4.1 - sha256: d92e522bd40e9bf7d87c204033ce5bb5c828fca45fa28d970f58d71128069fdc - requires_dist: - - numpy ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl - name: pyasn1 - version: 0.6.1 - sha256: 0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl - name: pyasn1-modules - version: 0.4.2 - sha256: 29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a - requires_dist: - - pyasn1>=0.6.1,<0.7.0 - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 - md5: 12c566707c80111f9799308d9e265aef + run_exports: {} + size: 173628 + timestamp: 1753200679078 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-ir-frontend-2025.2.0-h273c05f_1.conda + sha256: d6a94e82f03568db207b36cf4c2fa4d36677f15a1171472eb9382905a0c78f5b + md5: b3f148dcd1e80f102338d79ce3fe1102 depends: - - python >=3.9 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pycparser?source=hash-mapping - size: 110100 - timestamp: 1733195786147 -- pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl - name: pygithub - version: 2.6.1 - sha256: 6f2fa6d076ccae475f9fc392cc6cdbd54db985d4f69b8833a28397de75ed6ca3 - requires_dist: - - pynacl>=1.4.0 - - requests>=2.14.0 - - pyjwt[crypto]>=2.4.0 - - typing-extensions>=4.0.0 - - urllib3>=1.26.0 - - deprecated - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a - md5: 6b6ece66ebcae2d5f326c77ef2c5a066 + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + - pugixml >=1.15,<1.16.0a0 + purls: [] + run_exports: + weak: + - libopenvino-ir-frontend >=2025.2.0,<2025.2.1.0a0 + size: 173701 + timestamp: 1753200697088 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-onnx-frontend-2025.2.0-h6386500_1.conda + sha256: 8188f5fc49ff977b1dacbc49c67effb3696bd34329703be08f9d56b112da38d8 + md5: 6a9b2e48da9e5a9e5dbbc2acd97661ad depends: - - python >=3.9 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/pygments?source=hash-mapping - size: 889287 - timestamp: 1750615908735 -- pypi: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl - name: pyjwt - version: 2.10.1 - sha256: dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb - requires_dist: - - cryptography>=3.4.0 ; extra == 'crypto' - - coverage[toml]==5.0.4 ; extra == 'dev' - - cryptography>=3.4.0 ; extra == 'dev' - - pre-commit ; extra == 'dev' - - pytest>=6.0.0,<7.0.0 ; extra == 'dev' - - sphinx ; extra == 'dev' - - sphinx-rtd-theme ; extra == 'dev' - - zope-interface ; extra == 'dev' - - sphinx ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - - zope-interface ; extra == 'docs' - - coverage[toml]==5.0.4 ; extra == 'tests' - - pytest>=6.0.0,<7.0.0 ; extra == 'tests' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - name: pynacl - version: 1.6.2 - sha256: 8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c - requires_dist: - - cffi>=1.4.1 ; python_full_version < '3.9' and platform_python_implementation != 'PyPy' - - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' - - pytest>=7.4.0 ; extra == 'tests' - - pytest-cov>=2.10.1 ; extra == 'tests' - - pytest-xdist>=3.5.0 ; extra == 'tests' - - hypothesis>=3.27.0 ; extra == 'tests' - - sphinx<7 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl - name: pynacl - version: 1.6.2 - sha256: 62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0 - requires_dist: - - cffi>=1.4.1 ; python_full_version < '3.9' and platform_python_implementation != 'PyPy' - - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' - - pytest>=7.4.0 ; extra == 'tests' - - pytest-cov>=2.10.1 ; extra == 'tests' - - pytest-xdist>=3.5.0 ; extra == 'tests' - - hypothesis>=3.27.0 ; extra == 'tests' - - sphinx<7 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - name: pynacl - version: 1.6.2 - sha256: 46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87 - requires_dist: - - cffi>=1.4.1 ; python_full_version < '3.9' and platform_python_implementation != 'PyPy' - - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' - - pytest>=7.4.0 ; extra == 'tests' - - pytest-cov>=2.10.1 ; extra == 'tests' - - pytest-xdist>=3.5.0 ; extra == 'tests' - - hypothesis>=3.27.0 ; extra == 'tests' - - sphinx<7 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl - name: pynacl - version: 1.6.2 - sha256: c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465 - requires_dist: - - cffi>=1.4.1 ; python_full_version < '3.9' and platform_python_implementation != 'PyPy' - - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' - - pytest>=7.4.0 ; extra == 'tests' - - pytest-cov>=2.10.1 ; extra == 'tests' - - pytest-xdist>=3.5.0 ; extra == 'tests' - - hypothesis>=3.27.0 ; extra == 'tests' - - sphinx<7 ; extra == 'docs' - - sphinx-rtd-theme ; extra == 'docs' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - name: pyproject-hooks - version: 1.2.0 - sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca - md5: e2fd202833c4a981ce8a65974fe4abd1 + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + purls: [] + run_exports: + weak: + - libopenvino-onnx-frontend >=2025.2.0,<2025.2.1.0a0 + size: 1300903 + timestamp: 1753200716085 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-paddle-frontend-2025.2.0-h6386500_1.conda + sha256: 94e19e5fab3c6a50ce15fb3a404d81405fe642cce147dc3f6d2a02d2afaf8741 + md5: 0f2a4bd28364a0cf19bfd96c6e2fa052 depends: - - __win - - python >=3.9 - - win_inet_pton + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + purls: [] + run_exports: + weak: + - libopenvino-paddle-frontend >=2025.2.0,<2025.2.1.0a0 + size: 450125 + timestamp: 1753200737670 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-pytorch-frontend-2025.2.0-hec049ff_1.conda + sha256: 3b9d03eb5332626e35dd5ffc9a5c46b77c5ad8e0a61f16616255ce511323915e + md5: 5e2ab51b1fc44850320061e235112b84 + depends: + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + purls: [] + run_exports: + weak: + - libopenvino-pytorch-frontend >=2025.2.0,<2025.2.1.0a0 + size: 820657 + timestamp: 1753200755855 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-frontend-2025.2.0-hee62d61_1.conda + sha256: 4828d3fd7e59c8533cf46b7e3b09985f14fd3e7a43a92ecdbc371f823ed221c1 + md5: ebc006303a61e7110e3b219a839637df + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + - libprotobuf >=6.31.1,<6.31.2.0a0 + - snappy >=1.2.2,<1.3.0a0 + purls: [] + run_exports: + weak: + - libopenvino-tensorflow-frontend >=2025.2.0,<2025.2.1.0a0 + size: 934382 + timestamp: 1753200778004 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenvino-tensorflow-lite-frontend-2025.2.0-hec049ff_1.conda + sha256: 79f30d362a978300739b2f3b28dca0e0abca405a08637b445556737a92f5a80d + md5: 9ec0b186ee2d356aae50bb791bd54bfb + depends: + - __osx >=11.0 + - libcxx >=19 + - libopenvino 2025.2.0 h56e7ac4_1 + purls: [] + run_exports: + weak: + - libopenvino-tensorflow-lite-frontend >=2025.2.0,<2025.2.1.0a0 + size: 389727 + timestamp: 1753200797326 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopus-1.6.1-h1a92334_0.conda + sha256: 5c95a5f7712f543c59083e62fc3a95efec8b7f3773fbf4542ad1fb87fbf51ff4 + md5: 7f414dd3fd1cb7a76e51fec074a9c49e + depends: + - __osx >=11.0 license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/pysocks?source=hash-mapping - size: 21784 - timestamp: 1733217448189 -- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 - md5: 461219d1a5bd61342293efa2c0c90eac + purls: [] + run_exports: + weak: + - libopus >=1.6.1,<2.0a0 + size: 308000 + timestamp: 1768497248058 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda + sha256: 66eae34546df1f098a67064970c92aa14ae7a7505091889e00468294d2882c36 + md5: 2259ae0949dbe20c0665850365109b27 depends: - - __unix - - python >=3.9 + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: zlib-acknowledgement + purls: [] + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 + size: 289546 + timestamp: 1776315246750 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-6.31.1-h29102cf_5.conda + sha256: c1998dd33ae1229bec55c40a01cdf88bc5839cab2549f9ba21ea84472bba3242 + md5: 9f05750adae48f79259ee79aa4b02e52 + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libzlib >=1.3.2,<2.0a0 license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/pysocks?source=hash-mapping - size: 21085 - timestamp: 1733217331982 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.13-h9e4cc4f_0_cpython.conda - sha256: 9979a7d4621049388892489267139f1aa629b10c26601ba5dce96afc2b1551d4 - md5: 8c399445b6dc73eab839659e6c7b5ad1 + purls: [] + run_exports: + weak: + - libprotobuf >=6.31.1,<6.31.2.0a0 + size: 3430992 + timestamp: 1780004171922 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.58.4-h266df6f_3.conda + sha256: 0ec066d7f22bcd9acb6ca48b2e6a15e9be4f94e67cb55b0a2c05a37ac13f9315 + md5: 95d6ad8fb7a2542679c08ce52fafbb6c depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - libgcc >=13 - - liblzma >=5.8.1,<6.0a0 - - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.50.0,<4.0a0 - - libuuid >=2.38.1,<3.0a0 - - libxcrypt >=4.4.36 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.0,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - libglib >=2.84.0,<3.0a0 + - libxml2 >=2.13.7,<2.14.0a0 + - pango >=1.56.3,<2.0a0 constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 + - __osx >=11.0 + license: LGPL-2.1-or-later purls: [] - size: 30629559 - timestamp: 1749050021812 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.11.13-h1683364_0_cpython.conda - sha256: b44a026ac1fb82f81ec59d4da49db25add375202f7f395b6c2cb1384ad6a33d6 - md5: 4efe51e746f7c0abc30338e6b3d13323 + run_exports: + weak: + - librsvg >=2.58.4,<3.0a0 + size: 4607782 + timestamp: 1743369546790 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsigtool-0.1.3-h98dc951_0.conda + sha256: 421f7bd7caaa945d9cd5d374cc3f01e75637ca7372a32d5e7695c825a48a30d1 + md5: c08557d00807785decafb932b5be7ef5 depends: - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-aarch64 >=2.36.1 - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - libgcc >=13 - - liblzma >=5.8.1,<6.0a0 - - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.50.0,<4.0a0 - - libuuid >=2.38.1,<3.0a0 - - libxcrypt >=4.4.36 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.0,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 + - __osx >=11.0 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT purls: [] - size: 15306062 - timestamp: 1749048115706 -- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.13-h9ccd52b_0_cpython.conda - sha256: d8e15db837c10242658979bc475298059bd6615524f2f71365ab8e54fbfea43c - md5: 6e28c31688c6f1fdea3dc3d48d33e1c0 + run_exports: {} + size: 36416 + timestamp: 1767045062496 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda + sha256: a73a8acd97a6599fd6e561514db9f101ca7fd984cdc0cfd91ba74c8aa9dbe067 + md5: 7184d95871a58b8258a8ea124ed5aabc + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 924912 + timestamp: 1782519136322 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + sha256: 8bfe837221390ffc6f111ecca24fa12d4a6325da0c8d131333d63d6c37f27e0a + md5: b68e8f66b94b44aaa8de4583d3d4cc40 depends: - - __osx >=10.13 - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - liblzma >=5.8.1,<6.0a0 - - libsqlite >=3.50.0,<4.0a0 - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - openssl >=3.5.0,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 15423460 - timestamp: 1749049420299 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.13-hc22306f_0_cpython.conda - sha256: 2c966293ef9e97e66b55747c7a97bc95ba0311ac1cf0d04be4a51aafac60dcb1 - md5: 95facc4683b7b3b9cf8ae0ed10f30dce + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 + size: 279193 + timestamp: 1745608793272 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.2-h282da08_0.conda + sha256: 253153cabf9469170e02b21a6bc9aa598048e7c34966b1103af52d27d2a708a4 + md5: 6fa87106b3c041ad6d965a9411e79b94 depends: - __osx >=11.0 - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - liblzma >=5.8.1,<6.0a0 - - libsqlite >=3.50.0,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.0,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 + - lerc >=4.1.0,<5.0a0 + - libcxx >=19 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND purls: [] - size: 14573820 - timestamp: 1749048947732 -- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.13-h3f84c4b_0_cpython.conda - sha256: 723dbca1384f30bd2070f77dd83eefd0e8d7e4dda96ac3332fbf8fe5573a8abb - md5: bedbb6f7bb654839719cd528f9b298ad + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 387825 + timestamp: 1783085754081 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libusb-1.0.29-hbc156a2_0.conda + sha256: 5eee9a2bf359e474d4548874bcfc8d29ebad0d9ba015314439c256904e40aaad + md5: f6654e9e96e9d973981b3b2f898a5bfa depends: - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - liblzma >=5.8.1,<6.0a0 - - libsqlite >=3.50.0,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - __osx >=11.0 + license: LGPL-2.1-or-later + purls: [] + run_exports: + weak: + - libusb >=1.0.29,<2.0a0 + size: 83849 + timestamp: 1748856224950 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libuv-1.52.1-h1a92334_0.conda + sha256: e23176af832f637693ebbb9bbe7d29c0f4cba662dabd001081d2aa6fc9f7f661 + md5: fa9fef7d9f33724b7c3899c883c25a3e + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libuv >=1.52.1,<2.0a0 + size: 122732 + timestamp: 1779396113397 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvorbis-1.3.7-h81086ad_2.conda + sha256: 95768e4eceaffb973081fd986d03da15d93aa10609ed202e6fd5ca1e490a3dce + md5: 719e7653178a09f5ca0aa05f349b41f7 + depends: + - libogg + - libcxx >=19 + - __osx >=11.0 + - libogg >=1.3.5,<1.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libvorbis >=1.3.7,<1.4.0a0 + size: 259122 + timestamp: 1753879389702 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvpx-1.14.1-h7bae524_0.conda + sha256: 5d6458b5395cba0804846f156574aa8a34eef6d5f05d39e9932ddbb4215f8bd0 + md5: 95bee48afff34f203e4828444c2b2ae9 + depends: + - __osx >=11.0 + - libcxx >=16 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libvpx >=1.14.1,<1.15.0a0 + size: 1178981 + timestamp: 1717860096742 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libvulkan-loader-1.4.341.0-h3feff0a_0.conda + sha256: d2790dafc9149b1acd45b9033d02cfa3f3e9ee5af97bd61e0a5718c414a0a135 + md5: 6b4c9a5b130759136a0dde0c373cb0ea + depends: + - __osx >=11.0 + - libcxx >=19 constrains: - - python_abi 3.11.* *_cp311 - license: Python-2.0 + - libvulkan-headers 1.4.341.0.* + license: Apache-2.0 + license_family: APACHE purls: [] - size: 18242669 - timestamp: 1749048351218 -- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - name: python-dateutil - version: 2.9.0.post0 - sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - requires_dist: - - six>=1.5 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' -- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - sha256: df9aa74e9e28e8d1309274648aac08ec447a92512c33f61a8de0afa9ce32ebe8 - md5: 23029aae904a2ba587daba708208012f + run_exports: + weak: + - libvulkan-loader >=1.4.341.0,<2.0a0 + size: 180304 + timestamp: 1770077143460 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + sha256: a4de3f371bb7ada325e1f27a4ef7bcc81b2b6a330e46fac9c2f78ac0755ea3dd + md5: e5e7d467f80da752be17796b87fe6385 depends: - - python >=3.9 - - python + - __osx >=11.0 + constrains: + - libwebp 1.6.0 license: BSD-3-Clause license_family: BSD - purls: - - pkg:pypi/fastjsonschema?source=hash-mapping - size: 244628 - timestamp: 1755304154927 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.13-hd8ed1ab_0.conda - sha256: 9bc2f57084388a955ba799240359d73083fb27c45bb02f3a3eff72b4948718c5 - md5: dc7aefbecef49699c2cd086f2431049d + purls: [] + run_exports: + weak: + - libwebp-base >=1.6.0,<2.0a0 + size: 294974 + timestamp: 1752159906788 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.9-h4a9ca0c_0.conda + sha256: 7ab9b3033f29ac262cd3c846887e5b512f5916c3074d10f298627d67b7a32334 + md5: 763c7e76295bf142145d5821f251b884 depends: - - cpython 3.11.13.* - - python_abi * *_cp311 - license: Python-2.0 + - __osx >=11.0 + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT purls: [] - size: 47472 - timestamp: 1749048180043 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.14-hd8ed1ab_2.conda - sha256: 9261923f0dc8a3c8c517c29d8f3b7eea80f2577b094198239895bd7a88b534c5 - md5: a4effc7e6eb335d0e1080a5554590425 + run_exports: + weak: + - libxml2 >=2.13.9,<2.14.0a0 + size: 581379 + timestamp: 1761766437117 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 + md5: bc5a5721b6439f2f62a84f2548136082 depends: - - cpython 3.11.14.* - - python_abi * *_cp311 - license: Python-2.0 + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other purls: [] - size: 47569 - timestamp: 1761172935811 -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.11-8_cp311.conda - build_number: 8 - sha256: fddf123692aa4b1fc48f0471e346400d9852d96eeed77dbfdd746fa50a8ff894 - md5: 8fcb6b0e2161850556231336dae58358 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 47759 + timestamp: 1774072956767 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.8-hc7d1edf_0.conda + sha256: ccbaad6bbc88f135ab849bc36af5fa6eda36a9ed18ce6f58e3dde3d11784c156 + md5: a9c118f6343fb6301b6f3b4e94c4c562 + depends: + - __osx >=11.0 constrains: - - python 3.11.* *_cpython - license: BSD-3-Clause - license_family: BSD + - intel-openmp <0.0a0 + - openmp 22.1.8|22.1.8.* + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE purls: [] - size: 7003 - timestamp: 1752805919375 -- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py311hefeebc8_1.conda - sha256: e3ef7e0cc53111ab81b8a9dd3eabc1374d7420d4c9fce3c8631e73310203ad55 - md5: c1cfe9f5d8e278cc4d2d4c7b0126634d + run_exports: + strong: + - llvm-openmp >=22.1.8 + size: 286313 + timestamp: 1781736516782 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-tools-16.0.6-hc4b4ae8_4.conda + sha256: 3fc56aa583f213f271f95cc51ead5b3f1b4f6c82531860c75161a76b86b8a944 + md5: d920ea6c48053a4587bdfd0002bfff51 depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.11.* *_cp311 - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/pywin32?source=hash-mapping - size: 6729388 - timestamp: 1756487145061 -- pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - name: pywin32-ctypes - version: 0.2.3 - sha256: 8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 - requires_python: '>=3.6' -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py311h3778330_0.conda - sha256: 7dc5c27c0c23474a879ef5898ed80095d26de7f89f4720855603c324cca19355 - md5: 707c3d23f2476d3bfde8345b4e7d7853 + - __osx >=11.0 + - libllvm16 16.0.6 hc4b4ae8_4 + - libxml2 >=2.13.5,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.6,<1.6.0a0 + constrains: + - llvmdev 16.0.6 + - clang 16.0.6.* + - clang-tools 16.0.6.* + - llvm 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: {} + size: 20903239 + timestamp: 1739799054437 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lychee-0.23.0-h17e24d4_0.conda + sha256: 412ccd60e68618a3ee449f6bd4152acdf249e62073e4dfa2e7c8d405fc62b1b0 + md5: 7fa0025f406d5cf3f1100f3a90156547 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 211606 - timestamp: 1758892088237 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py311h164a683_0.conda - sha256: ef6ca1f9f087731d7c224441b76ebad18afa723c20eb3a8cecfa163b4ee0b132 - md5: 3b798096c0411fe74c24a95c3965a62a + - __osx >=11.0 + - openssl >=3.5.5,<4.0a0 + constrains: + - __osx >=11.0 + license: Apache-2.0 OR MIT + purls: [] + run_exports: {} + size: 5174311 + timestamp: 1771270398758 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py311hc290fe0_1.conda + sha256: d635f2b1d9e19e8e68c5d33150f7e4f62df08ef2ef0e85977f743e81939afc01 + md5: ff068874356bbc7f9bd2d793f809f44b depends: - - libgcc >=14 + - __osx >=11.0 - python >=3.11,<3.12.0a0 - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 206495 - timestamp: 1758891830460 -- conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.3-py311he13f9b5_0.conda - sha256: be448cd6d759cd21d40bc9a3850672187a8d37fcd3abdc3f637abc0ca1ed2f44 - md5: 2d9ba0ec796516a17d3c87efdb881aff - depends: - - __osx >=10.13 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - - yaml >=0.2.5,<0.3.0a0 + - pkg:pypi/markupsafe?source=hash-mapping + run_exports: {} + size: 26511 + timestamp: 1772445369187 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/meilisearch-1.5.1-h5ef7bb8_0.conda + sha256: c359718f193da18e77b7d19402d7453fa732978433ac562bbc86dfd17ef1bff8 + md5: 595899dbe10e2a0ab8e37f894f683082 license: MIT license_family: MIT - purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 196463 - timestamp: 1758892069824 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311ha9b3269_0.conda - sha256: 747c1b94222481a727aeeb912407f862a93a1bb4e704be3a8236768182ac0290 - md5: 109a9c326951cc9ab5df6a06cf5b930a + purls: [] + run_exports: {} + size: 81671718 + timestamp: 1702680633448 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.7.1-py311ha275503_0.conda + sha256: 01aae5d525f7eec07bfe9d9cd82cae84d5889babdfe4bd3b674b734005289cfe + md5: a57b7e57a380097482d5a89a44f0a5c4 depends: - __osx >=11.0 - python >=3.11,<3.12.0a0 - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT + license: Apache-2.0 + license_family: APACHE purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 195537 - timestamp: 1758892104856 -- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_0.conda - sha256: 22dcc6c6779e5bd970a7f5208b871c02bf4985cf4d827d479c4a492ced8ce577 - md5: 4e9b677d70d641f233b29d5eab706e20 + - pkg:pypi/multidict?source=hash-mapping + run_exports: {} + size: 89354 + timestamp: 1771611632254 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.14.1-py311h917b07b_0.conda + sha256: 0d891fd3d73ddcece659e62b765ddd6023b1296d69943481dc9910107071307a + md5: c09549d23170ecaabfa4a8162b5d4f10 depends: + - __osx >=11.0 + - mypy_extensions >=1.0.0 + - psutil >=4.0 - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - yaml >=0.2.5,<0.3.0a0 + - typing_extensions >=4.1.0 license: MIT license_family: MIT purls: - - pkg:pypi/pyyaml?source=hash-mapping - size: 188290 - timestamp: 1758892467876 -- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c - md5: 283b96675859b20a825f8fa30f311446 - depends: - - libgcc >=13 - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 282480 - timestamp: 1740379431762 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda - sha256: 54bed3a3041befaa9f5acde4a37b1a02f44705b7796689574bcf9d7beaad2959 - md5: c0f08fc2737967edde1a272d4bf41ed9 + - pkg:pypi/mypy?source=hash-mapping + run_exports: {} + size: 10180870 + timestamp: 1735600589567 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nasm-2.16.03-h99b78c6_1.conda + sha256: a1d8b8f6be3ccf94d8f29920a61fa83e4a94da84e8f0bdec4a5937092d56e59d + md5: c306196adb43e1300e1470dd65694ec5 depends: - - libgcc >=13 - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 291806 - timestamp: 1740380591358 -- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h7cca4af_2.conda - sha256: 53017e80453c4c1d97aaf78369040418dea14cf8f46a2fa999f31bd70b36c877 - md5: 342570f8e02f2f022147a7f841475784 + run_exports: {} + size: 385586 + timestamp: 1721652965778 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d + md5: 343d10ed5b44030a2f67193905aea159 depends: - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL + - __osx >=11.0 + license: X11 AND BSD-3-Clause purls: [] - size: 256712 - timestamp: 1740379577668 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda - sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 - md5: 63ef3f6e6d6d5c589e64f11263dc5676 + run_exports: + weak: + - ncurses >=6.6,<7.0a0 + size: 805509 + timestamp: 1777423252320 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ninja-1.11.1-hffc8910_0.conda + sha256: a594e90b0ed8202c280fff4a008f6a355d0db54a62b17067dc4a950370ddffc0 + md5: fdecec4002f41cf6ea1eea5b52947ee0 depends: - - ncurses >=6.5,<7.0a0 - license: GPL-3.0-only - license_family: GPL + - libcxx >=14.0.6 + license: Apache-2.0 + license_family: Apache purls: [] - size: 252359 - timestamp: 1740379663071 -- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - sha256: e20909f474a6cece176dfc0dc1addac265deb5fa92ea90e975fbca48085b20c3 - md5: 9140f1c09dd5489549c6a33931b943c7 - depends: - - attrs >=22.2.0 - - python >=3.9 - - rpds-py >=0.7.0 - - typing_extensions >=4.4.0 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/referencing?source=hash-mapping - size: 51668 - timestamp: 1737836872415 -- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - name: requests - version: 2.32.5 - sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 - requires_dist: - - charset-normalizer>=2,<4 - - idna>=2.5,<4 - - urllib3>=1.21.1,<3 - - certifi>=2017.4.17 - - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' - requires_python: '>=3.9' -- pypi: ./rerun_pixi_env - name: rerun-pixi-env - version: 0.1.0 - sha256: b6cf6127b254f41f05edd3131566756841dbac04e9c56d9148e413465b122d23 - requires_python: '>=3.10' - editable: true -- conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - sha256: d5c73079c1dd2c2a313c3bfd81c73dbd066b7eb08d213778c8bff520091ae894 - md5: c1c9b02933fdb2cfb791d936c20e887e + run_exports: {} + size: 107047 + timestamp: 1676837935565 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-24.13.0-h3a0f24a_0.conda + sha256: 0df4fc02f331b432ada9c3ab423513d048651bd1718242ef19c7c6896550da95 + md5: 1c5b4679fead09823209626fe36225a0 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libcxx >=19 + - __osx >=11.0 + - icu >=75.1,<76.0a0 + - zstd >=1.5.7,<1.6.0a0 + - openssl >=3.5.5,<4.0a0 + - libnghttp2 >=1.67.0,<2.0a0 + - c-ares >=1.34.6,<2.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuv >=1.51.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 license: MIT license_family: MIT purls: [] - size: 193775 - timestamp: 1748644872902 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rhash-1.4.6-h86ecc28_1.conda - sha256: 0fe6f40213f2d8af4fcb7388eeb782a4e496c8bab32c189c3a34b37e8004e5a4 - md5: 745d02c0c22ea2f28fbda2cb5dbec189 + run_exports: + weak: + - nodejs >=24.13.0,<25.0a0 + size: 15863855 + timestamp: 1770653253696 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openh264-2.6.0-hdf0efb5_1.conda + sha256: b7a43bf86db73d41e87e342e44df201bd08e9e1276508ec317ee603a32abdf8b + md5: 16691d628bbf06c067c5325f6b2edf1c depends: - - libgcc >=13 - license: MIT - license_family: MIT + - __osx >=11.0 + - libcxx >=19 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 207475 - timestamp: 1748644952027 -- conda: https://conda.anaconda.org/conda-forge/osx-64/rhash-1.4.6-h6e16a3a_1.conda - sha256: 65c946fc5a9bb71772a7ac9bad64ff08ac07f7d5311306c2dcc1647157b96706 - md5: d0fcaaeff83dd4b6fb035c2f36df198b + run_exports: + weak: + - openh264 >=2.6.0,<2.6.1.0a0 + size: 603670 + timestamp: 1782686332958 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + sha256: b3e3ca895c336d4eb91c5d2f244a312bdb59a0de8cfa0cc4c179225ab2f6bbfb + md5: 8187a86242741725bfa74785fe812979 depends: - - __osx >=10.13 - license: MIT - license_family: MIT + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache purls: [] - size: 185180 - timestamp: 1748644989546 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rhash-1.4.6-h5505292_1.conda - sha256: f4957c05f4fbcd99577de8838ca4b5b1ae4b400a44be647a0159c14f85b9bfc0 - md5: 029e812c8ae4e0d4cf6ff4f7d8dc9366 + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 3102584 + timestamp: 1781069820667 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + sha256: 705484ad60adee86cab1aad3d2d8def03a699ece438c864e8ac995f6f66401a6 + md5: 7d57f8b4b7acfc75c777bc231f0d31be depends: - __osx >=11.0 - license: MIT - license_family: MIT + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later purls: [] - size: 185448 - timestamp: 1748645057503 -- conda: https://conda.anaconda.org/conda-forge/noarch/rich-14.2.0-pyhcf101f3_0.conda - sha256: edfb44d0b6468a8dfced728534c755101f06f1a9870a7ad329ec51389f16b086 - md5: a247579d8a59931091b16a1e932bbed6 - depends: - - markdown-it-py >=2.2.0 - - pygments >=2.13.0,<3.0.0 - - python >=3.10 - - typing_extensions >=4.0.0,<5.0.0 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/rich?source=hash-mapping - size: 200840 - timestamp: 1760026188268 -- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.27.1-py311h902ca64_1.conda - sha256: d9bc1564949ede4abd32aea34cf1997d704b6091e547f255dc0168996f5d5ec8 - md5: 622c389c080689ba1575a0750eb0209d + run_exports: + weak: + - pango >=1.56.4,<2.0a0 + size: 426931 + timestamp: 1751292636271 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-h30297fc_0.conda + sha256: 5e2e443f796f2fd92adf7978286a525fb768c34e12b1ee9ded4000a41b2894ba + md5: 9b4190c4055435ca3502070186eba53a depends: - - python - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - python_abi 3.11.* *_cp311 - constrains: - - __glibc >=2.17 + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 + size: 850231 + timestamp: 1763655726735 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h784d473_2.conda + sha256: b7d0a814a3f8f30bba28c83ccfd896e89f90ffd8a763c4cb5fa55abe7ba3cf0c + md5: 63b5e8afb859517d0073b295f1aa9589 + depends: + - libcxx >=19 + - __osx >=11.0 license: MIT license_family: MIT - purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 387057 - timestamp: 1756737832651 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.27.1-py311hc91c717_1.conda - sha256: 4e54bed932066c5ec7b917a4e9809fceac7fc6ab6dce0136eaa82e7b0a26cb71 - md5: 6315a262e3d9feb00eb5e768689d5a0f + purls: [] + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 198437 + timestamp: 1784287312271 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/prettier-3.8.1-h9907cc9_0.conda + sha256: b4494151f6f758c6d51973a8583fed7f63b186a1dc60e1e86b1dfe0add3e7b73 + md5: d59831f8460836da00ae6441c054e61a depends: - - python - - libgcc >=14 - - python_abi 3.11.* *_cp311 - constrains: - - __glibc >=2.17 + - nodejs + - __osx >=11.0 + - nodejs >=24.12.0,<25.0a0 license: MIT license_family: MIT + purls: [] + run_exports: {} + size: 1104682 + timestamp: 1769199263346 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.5.2-py311hc290fe0_0.conda + sha256: c3e726226ac17207dbca1d61415261dc30133b79fbc6dc1773a327b5c55a617b + md5: 757ef7785e30f794a6b52957af5d81fa + depends: + - __osx >=11.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: APACHE purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 386653 - timestamp: 1756737837272 -- conda: https://conda.anaconda.org/conda-forge/osx-64/rpds-py-0.27.1-py311hd3d88a1_1.conda - sha256: 85357c87af076680c071a8ea843bea554d58694d011104b721cc13bbf9ad0e75 - md5: 4b9839b15de18289ee5289a6dbcb8a45 + - pkg:pypi/propcache?source=hash-mapping + run_exports: {} + size: 49554 + timestamp: 1780038276062 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-6.31.1-py311h93f9908_2.conda + sha256: cb419d29176f8334822563103dccb3f7e9296974bbd9fdbb96739529bdc4c6a2 + md5: df8f7f9164c209927985b2ec2d43d1d9 depends: - - python - - __osx >=10.13 + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20250512.1,<20250513.0a0 + - libcxx >=19 + - libzlib >=1.3.1,<2.0a0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython - python_abi 3.11.* *_cp311 constrains: - - __osx >=10.13 - license: MIT - license_family: MIT + - libprotobuf 6.31.1 + license: BSD-3-Clause + license_family: BSD purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 376118 - timestamp: 1756737583772 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.27.1-py311h1c3fc1a_1.conda - sha256: 95714a24265b6b4d4b218e303dcb075ba435826cb1d5927792ec94a8196c3e72 - md5: 5236ffaff99e6421aa4431b4c00ca47a + - pkg:pypi/protobuf?source=hash-mapping + run_exports: {} + size: 468653 + timestamp: 1760394086091 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.2.2-py311he363849_0.conda + sha256: 2b774e8f4ccaac8783d1908f281e4bb20b7036d6708dc913ee7811b070f5b4b5 + md5: 7cff50265141513c960f00ba586780ea depends: - python - python 3.11.* *_cpython - __osx >=11.0 - python_abi 3.11.* *_cp311 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT + license: BSD-3-Clause + license_family: BSD purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 362213 - timestamp: 1756737586989 -- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.27.1-py311hf51aa87_1.conda - sha256: e61607627213b70e7be73570e7ef5e2d36b583512def108aaf78a6ab16f0cdd9 - md5: 3c5b42969dae70e100154750d29d43cc + - pkg:pypi/psutil?source=hash-mapping + run_exports: {} + size: 245203 + timestamp: 1769678306347 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pugixml-1.15-hd3d436d_0.conda + sha256: 5ad8d036040b095f85d23c70624d3e5e1e4c00bc5cea97831542f2dcae294ec9 + md5: b9a4004e46de7aeb005304a13b35cb94 depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.11.* *_cp311 + - __osx >=11.0 + - libcxx >=18 license: MIT license_family: MIT - purls: - - pkg:pypi/rpds-py?source=hash-mapping - size: 247101 - timestamp: 1756737437304 -- pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - name: rsa - version: 4.9.1 - sha256: 68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 - requires_dist: - - pyasn1>=0.1.3 - requires_python: '>=3.6,<4' -- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.7-h7805a7d_1.conda - noarch: python - sha256: 2985cfff61368323db477c2a0d7f100a57f6cb34aafec51ae96b6fc409d9090f - md5: f5678c1a929d9efe3c2397675ae90a3c + purls: [] + run_exports: + weak: + - pugixml >=1.15,<1.16.0a0 + size: 91283 + timestamp: 1736601509593 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.11.15-h0c9c016_1_cpython.conda + build_number: 1 + sha256: a44be5222fe8d3c072ecd22491d37316724b70be6b8e8dabdc1a25e6d293fba8 + md5: 91607d75cdf9fafc95061e3763582657 depends: - - python - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.3,<6.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata constrains: - - __glibc >=2.17 + - python_abi 3.11.* *_cp311 + license: Python-2.0 + purls: [] + run_exports: + weak: + - python_abi 3.11.* *_cp311 + noarch: + - python + size: 15389700 + timestamp: 1781148926804 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py311hc290fe0_1.conda + sha256: 984e73d7957460689e10533059de8adb38a308853d298900a37acc58edd84cec + md5: e4b908da7cd496b3fa6798c0f60a2a19 + depends: + - __osx >=11.0 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT purls: - - pkg:pypi/ruff?source=hash-mapping - size: 9220190 - timestamp: 1774012576023 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruff-0.15.7-h9f438e6_1.conda - noarch: python - sha256: 12f3e09ad65e1b90ea9f9364198ceec9181bb812a4b36dece3d7b3f1f9259a84 - md5: b46ef22af5048a38a3051707e5db6ee1 + - pkg:pypi/pyyaml?source=hash-mapping + run_exports: {} + size: 192948 + timestamp: 1770223655988 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 depends: - - python - - libgcc >=14 - constrains: - - __glibc >=2.17 + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + run_exports: + weak: + - readline >=8.3,<9.0a0 + size: 313930 + timestamp: 1765813902568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rhash-1.4.6-h5505292_1.conda + sha256: f4957c05f4fbcd99577de8838ca4b5b1ae4b400a44be647a0159c14f85b9bfc0 + md5: 029e812c8ae4e0d4cf6ff4f7d8dc9366 + depends: + - __osx >=11.0 license: MIT license_family: MIT - purls: - - pkg:pypi/ruff?source=compressed-mapping - size: 8811982 - timestamp: 1774012576944 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.15.7-h16586dd_1.conda - noarch: python - sha256: da9d7924d76798615c7919b7b3a77e40f848a89277fe996badb82fdc80d35ae7 - md5: 21c9b7a026f3f0244ab849aef6970138 + purls: [] + run_exports: + weak: + - rhash >=1.4.6,<2.0a0 + size: 185448 + timestamp: 1748645057503 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-2026.6.3-py311haff49d3_0.conda + sha256: 466a9df1864e3104ca95bb7292fb045fd320c0b40e71647162341329437eb23e + md5: f55ea3840a1a4c76fdb03dc8d95e7a76 depends: - python - __osx >=11.0 + - python_abi 3.11.* *_cp311 constrains: - - __osx >=10.13 + - __osx >=11.0 license: MIT license_family: MIT purls: - - pkg:pypi/ruff?source=hash-mapping - size: 9184114 - timestamp: 1774012818963 + - pkg:pypi/rpds-py?source=hash-mapping + run_exports: {} + size: 286691 + timestamp: 1782831311985 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.15.7-hc5c3a1d_1.conda noarch: python sha256: b8e869469607bf00dc80ad920bffe96adc9b21d22e940ffeff71a664d35ef9b7 @@ -12799,1600 +15195,2141 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/ruff?source=compressed-mapping + - pkg:pypi/ruff?source=hash-mapping + run_exports: {} size: 8432798 timestamp: 1774012820989 -- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.7-h02f8532_1.conda - noarch: python - sha256: 998087b7aef322be09d8e4db7726012ec54d67d12f0622c0001c68660cfa6012 - md5: bff0dba1b6297c0c35169c9b85809b3c - depends: - - python - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruff?source=compressed-mapping - size: 9696461 - timestamp: 1774012631179 -- conda: https://conda.anaconda.org/conda-forge/linux-64/sdl2-2.32.54-h3f2d84a_0.conda - sha256: 7cd82ca1d1989de6ac28e72ba0bfaae1c055278f931b0c7ef51bb1abba3ddd2f - md5: 91f8537d64c4d52cbbb2910e8bd61bd2 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl2-2.32.56-h784d473_0.conda + sha256: 595db3f62eec1b86aad03ad8c7e4943e78a18e112228c91adf3f3ada4e959a5c + md5: 81a4c982e9ac52e620eb810f463de9ad depends: - - libgcc >=13 - - __glibc >=2.17,<3.0.a0 - - libstdcxx >=13 - - libgcc >=13 - - sdl3 >=3.2.10,<4.0a0 - - libgl >=1.7.0,<2.0a0 - - libegl >=1.7.0,<2.0a0 + - __osx >=11.0 + - libcxx >=19 + - sdl3 >=3.4.12,<4.0a0 license: Zlib purls: [] - size: 587053 - timestamp: 1745799881584 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl2-2.32.54-h5ad3122_0.conda - sha256: d83c13fc35ed447d186150d32b8bc48bdd73a047280ba6e06f151d4cce52639d - md5: 6b38021cb802b4e5bede7fe38c547883 + run_exports: + weak: + - sdl2 >=2.32.56,<3.0a0 + size: 543557 + timestamp: 1783451926011 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl3-3.4.12-h6fa9c73_0.conda + sha256: 33d7ed63db0b2242d004f2cb86812a993f6129f5857caaf97c26d72165d053a5 + md5: adc908ce654d6bbda08851bb1457e4c7 depends: - - libstdcxx >=13 - - libgcc >=13 - - libegl >=1.7.0,<2.0a0 - - libgl >=1.7.0,<2.0a0 - - sdl3 >=3.2.10,<4.0a0 + - libcxx >=19 + - __osx >=11.0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - libusb >=1.0.29,<2.0a0 + - dbus >=1.16.2,<2.0a0 license: Zlib purls: [] - size: 597383 - timestamp: 1745799910298 -- conda: https://conda.anaconda.org/conda-forge/osx-64/sdl2-2.32.54-h92383a6_0.conda - sha256: 99b750dbdd6137cf7131813cfc23a30e4fee5aed76cf44482ecf197e47f71246 - md5: 20cba443d3a3b5da52bd8ba52a7c3bda + run_exports: + weak: + - sdl3 >=3.4.12,<4.0a0 + size: 1567749 + timestamp: 1782948095075 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-0.1.3-h98dc951_0.conda + sha256: aa8161f76fa1f1cfdd9371319dcccfc1884e790dabe2a284fe494ee6ae14a99c + md5: b7349cda16aa098a67c87bf9581faf22 depends: - - libcxx >=18 - - __osx >=10.13 - - sdl3 >=3.2.10,<4.0a0 - license: Zlib + - __osx >=11.0 + - libsigtool 0.1.3 h98dc951_0 + - openssl >=3.5.4,<4.0a0 + - sigtool-codesign 0.1.3 h98dc951_0 + license: MIT + license_family: MIT purls: [] - size: 739288 - timestamp: 1745799864136 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl2-2.32.54-ha1acc90_0.conda - sha256: ba0ba41b3f7404ddc5421885ad9efe346c4bdc2ec88bc43edd271d9f25f6f0e4 - md5: 71364ba4c5f333860c4431cb46cb9b6c + run_exports: {} + size: 117579 + timestamp: 1767045110047 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-codesign-0.1.3-h98dc951_0.conda + sha256: f3d006e2441f110160a684744d90921bbedbffa247d7599d7e76b5cd048116dc + md5: ade77ad7513177297b1d75e351e136ce depends: - - libcxx >=18 - __osx >=11.0 - - sdl3 >=3.2.10,<4.0a0 - license: Zlib + - libsigtool 0.1.3 h98dc951_0 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT purls: [] - size: 546209 - timestamp: 1745799899902 -- conda: https://conda.anaconda.org/conda-forge/win-64/sdl2-2.32.54-he0c23c2_0.conda - sha256: 477781545f317cd9f0a35cc39e22976ee374f9c98b5cbb083812f6d33cf47c08 - md5: b1a715daa818f0ffcd23bb02b7fcf861 + run_exports: {} + size: 114331 + timestamp: 1767045086274 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda + sha256: cb9305ede19584115f43baecdf09a3866bfcd5bcca0d9e527bd76d9a1dbe2d8d + md5: fca4a2222994acd7f691e57f94b750c5 depends: - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - ucrt >=10.0.20348.0 - - sdl3 >=3.2.10,<4.0a0 - license: Zlib + - libcxx >=19 + - __osx >=11.0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 572859 - timestamp: 1745799945033 -- conda: https://conda.anaconda.org/conda-forge/linux-64/sdl3-3.2.14-he3e324a_0.conda - sha256: b55edbcbcbfc7cff671ef15b6a663b91cb2ca59ab285c283d02f29c51de59e9e - md5: a750ab1e94750185033ea96eadfc925d + run_exports: + weak: + - snappy >=1.2.2,<1.3.0a0 + size: 38883 + timestamp: 1762948066818 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/svt-av1-3.1.2-h12ba402_0.conda + sha256: 3b0f4f2a6697f0cdbbe0c0b5f5c7fa8064483d58b4d9674d5babda7f7146af7a + md5: cb56c114b25f20bd09ef1c66a21136ff depends: - - libstdcxx >=13 - - libgcc >=13 - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libgl >=1.7.0,<2.0a0 - - dbus >=1.13.6,<2.0a0 - - libxkbcommon >=1.9.2,<2.0a0 - - xorg-libxext >=1.3.6,<2.0a0 - - pulseaudio-client >=17.0,<17.1.0a0 - - xorg-libx11 >=1.8.12,<2.0a0 - - libudev1 >=257.4 - - libunwind >=1.6.2,<1.7.0a0 - - wayland >=1.23.1,<2.0a0 - - xorg-libxcursor >=1.2.3,<2.0a0 - - libusb >=1.0.28,<2.0a0 - - xorg-libxfixes >=6.0.1,<7.0a0 - - libdrm >=2.4.124,<2.5.0a0 - - xorg-libxscrnsaver >=1.2.4,<2.0a0 - - liburing >=2.9,<2.10.0a0 - - libegl >=1.7.0,<2.0a0 - license: Zlib + - __osx >=11.0 + - libcxx >=19 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 1939690 - timestamp: 1747327532502 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sdl3-3.2.14-h7e2c5d6_0.conda - sha256: 83e07e24de6018133139d21e33cc61623864144cc1bc279d4affaf8d773fa52b - md5: ffe115848f7f2406decbe70ff4530c06 - depends: - - libstdcxx >=13 - - libgcc >=13 - - libxkbcommon >=1.9.2,<2.0a0 - - xorg-libxext >=1.3.6,<2.0a0 - - libgl >=1.7.0,<2.0a0 - - libusb >=1.0.28,<2.0a0 - - pulseaudio-client >=17.0,<17.1.0a0 - - liburing >=2.9,<2.10.0a0 - - xorg-libxcursor >=1.2.3,<2.0a0 - - libudev1 >=257.4 - - libegl >=1.7.0,<2.0a0 - - libdrm >=2.4.124,<2.5.0a0 - - libunwind >=1.6.2,<1.7.0a0 - - dbus >=1.13.6,<2.0a0 - - xorg-libxfixes >=6.0.1,<7.0a0 - - xorg-libx11 >=1.8.12,<2.0a0 - - wayland >=1.23.1,<2.0a0 - license: Zlib + run_exports: + weak: + - svt-av1 >=3.1.2,<3.1.3.0a0 + size: 1474592 + timestamp: 1756086729326 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1300.6.5-h03f4b80_0.conda + sha256: 37cd4f62ec023df8a6c6f9f6ffddde3d6620a83cbcab170a8fff31ef944402e5 + md5: b703bc3e6cba5943acf0e5f987b5d0e2 + depends: + - __osx >=11.0 + - libcxx >=17.0.0.a0 + - ncurses >=6.5,<7.0a0 + license: NCSA + license_family: MIT purls: [] - size: 1897812 - timestamp: 1747327559219 -- conda: https://conda.anaconda.org/conda-forge/osx-64/sdl3-3.2.22-hc0b302d_0.conda - sha256: bc4b35801d55600deba29da19b8d1707db23d165b06fe900ff0ba07d628161e2 - md5: dcaf060cee2fb96259b989c44505d4bf + run_exports: {} + size: 207679 + timestamp: 1725491499758 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/taplo-0.9.1-h16c8c8b_0.conda + sha256: 3a387ea7779d061d28af0426d1249fe81f798f35a2d0cb979a6ff84525187667 + md5: 8171587b7a366dbbaab309ae1c45bd93 depends: - - libcxx >=19 - - __osx >=10.13 - - libusb >=1.0.29,<2.0a0 - - dbus >=1.16.2,<2.0a0 - license: Zlib + - openssl >=3.2.1,<4.0a0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT purls: [] - size: 1548166 - timestamp: 1756780255681 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sdl3-3.2.22-he22eeb8_0.conda - sha256: f4bebfe966e4df667887b06bea6539f2fde23bf3a89649f5b57b53716f1cc2d5 - md5: cd2b01e16daf07b77c3754bfdeb8095d + run_exports: {} + size: 3560280 + timestamp: 1710793219601 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tbb-2022.3.0-h66ce52b_1.conda + sha256: 06de2fb5bdd4e51893d651165c3dc2679c4c84b056d962432f31cd9f2ccb1304 + md5: 6f026b94077bed22c27ad8365e024e18 depends: - __osx >=11.0 - libcxx >=19 - - libusb >=1.0.29,<2.0a0 - - dbus >=1.16.2,<2.0a0 - license: Zlib + - libhwloc >=2.12.1,<2.12.2.0a0 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 1416196 - timestamp: 1756780255242 -- conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.2.22-h5112557_0.conda - sha256: 01d040f2ebe976a0b9cafc13e8b6fd2cf297afbcdec462a5e254cc8c261f70c5 - md5: ce2d3317d46b92ea361dd9178bc7df91 + run_exports: {} + size: 121436 + timestamp: 1762510628662 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-hd3d0363_3.conda + sha256: 47186bc7ab8d7e8bee86bbd1a917196f8c21cf63f081fc33cd6d1221af087580 + md5: 8e3cf0e455e6b54519f0b1c72c61780a depends: - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - libusb >=1.0.29,<2.0a0 - license: Zlib + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: TCL purls: [] - size: 1521753 - timestamp: 1756780243694 -- pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - name: secretstorage - version: 3.5.0 - sha256: 0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 - requires_dist: - - cryptography>=2.0 - - jeepney>=0.6 - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/semver-3.0.4-pyhd8ed1ab_0.conda - sha256: 7d3f5531269e15cb533b60009aa2a950f9844acf31f38c1b55c8000dbb316676 - md5: 982aa48accc06494cbd2b51af69e17c7 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/semver?source=hash-mapping - size: 21110 - timestamp: 1737841666447 -- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - sha256: 972560fcf9657058e3e1f97186cc94389144b46dbdf58c807ce62e83f977e863 - md5: 4de79c071274a53dcaf2a8c749d1499e + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3338712 + timestamp: 1784229090530 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ty-0.0.31-hdfcc030_0.conda + noarch: python + sha256: b76630c21ab40c7a51d2bba12c8d5e5d8beb12cb6e9039e478d3fa8eee4db8ea + md5: 0823cec0cbdf0d23ac3269e83744def7 depends: - - python >=3.9 + - python + - __osx >=11.0 + - _python_abi3_support 1.* + - cpython >=3.10 + constrains: + - __osx >=11.0 license: MIT license_family: MIT purls: - - pkg:pypi/setuptools?source=hash-mapping - size: 748788 - timestamp: 1748804951958 -- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - name: shellingham - version: 1.5.4 - sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 - requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/osx-64/sigtool-0.1.3-h88f4db0_0.tar.bz2 - sha256: 46fdeadf8f8d725819c4306838cdfd1099cd8fe3e17bd78862a5dfdcd6de61cf - md5: fbfb84b9de9a6939cb165c02c69b1865 + - pkg:pypi/ty?source=hash-mapping + run_exports: {} + size: 8603653 + timestamp: 1776273658703 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/typos-1.48.0-h6fdd925_0.conda + sha256: 1d9e347d6ce66bbb0486d97f1799ffde92c068b9b7bbd0f249941c736f5db12b + md5: c679568be20dfa9f4bd979d010636be2 depends: - - openssl >=3.0.0,<4.0a0 - license: MIT - license_family: MIT + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT OR Apache-2.0 purls: [] - size: 213817 - timestamp: 1643442169866 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sigtool-0.1.3-h44b9a77_0.tar.bz2 - sha256: 70791ae00a3756830cb50451db55f63e2a42a2fa2a8f1bab1ebd36bbb7d55bff - md5: 4a2cac04f86a4540b8c9b8d8f597848f + run_exports: {} + size: 2712152 + timestamp: 1782859631187 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wasm-pack-0.15.0-h6fdd925_0.conda + sha256: da3268c99f9ba928e6c8e7b5ccc80d615686fe926179cab5a76703d6ee03816f + md5: acbf02604fb4e6881967c355164f6b74 depends: - - openssl >=3.0.0,<4.0a0 - license: MIT - license_family: MIT + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT OR Apache-2.0 purls: [] - size: 210264 - timestamp: 1643442231687 -- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - name: six - version: 1.17.0 - sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' -- conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - sha256: eb92d0ad94b65af16c73071cc00cc0e10f2532be807beb52758aab2b06eb21e2 - md5: 87f47a78808baf2fa1ea9c315a1e48f1 + run_exports: {} + size: 2011074 + timestamp: 1780752614144 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/x264-1!164.3095-h57fd34a_2.tar.bz2 + sha256: debdf60bbcfa6a60201b12a1d53f36736821db281a28223a09e0685edcce105a + md5: b1f6dccde5d3a1f911960b6e567113ff + license: GPL-2.0-or-later + license_family: GPL + purls: [] + run_exports: + weak: + - x264 >=1!164.3095,<1!165 + size: 717038 + timestamp: 1660323292329 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/x265-3.5-hbc6ce65_3.tar.bz2 + sha256: 2fed6987dba7dee07bd9adc1a6f8e6c699efb851431bcb6ebad7de196e87841d + md5: b1f7f2780feffe310b068c021e8ff9b2 depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/smmap?source=hash-mapping - size: 26051 - timestamp: 1739781801801 -- conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_0.conda - sha256: 8b8acbde6814d1643da509e11afeb6bb30eb1e3004cf04a7c9ae43e9b097f063 - md5: 3d8da0248bdae970b4ade636a104b7f5 + - libcxx >=12.0.1 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + run_exports: + weak: + - x265 >=3.5,<3.6.0a0 + size: 1832744 + timestamp: 1646609481185 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.3-hd0f0c4f_0.conda + sha256: d5bb880876336f625523c022aa9bdc6be76a85df721d9e7b33c352f528619185 + md5: 4df12be699991b97b66a85cc46ea75b5 depends: - - libgcc >=14 - - libstdcxx >=14 - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - liblzma 5.8.3 h8088a28_0 + - liblzma-devel 5.8.3 h8088a28_0 + - xz-gpl-tools 5.8.3 hd0f0c4f_0 + - xz-tools 5.8.3 h8088a28_0 + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later purls: [] - size: 45805 - timestamp: 1753083455352 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.2.2-he774c54_0.conda - sha256: 06648e1c2fd7c5426b2611d4e480768aea934b54fe8034a8f7a6378a40b20695 - md5: b80bb2997c2139b3659edfca69b72dae + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 24348 + timestamp: 1775825911109 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.3-hd0f0c4f_0.conda + sha256: 0864d53202f618b4c8a5f2c38b7029f14b900b852e99b6248813303f69ccfdee + md5: 43d168a8c95a8fcdcc44c2a0a7887653 depends: - - libstdcxx >=14 - - libgcc >=14 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - liblzma 5.8.3 h8088a28_0 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later purls: [] - size: 47059 - timestamp: 1753083509250 -- conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.2-h25c286d_0.conda - sha256: e9ccbdbfaa9abd21636decd524d9845dee5a67af593b1d54525a48f2b03d3d76 - md5: e6544ab8824f58ca155a5b8225f0c780 + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 34224 + timestamp: 1775825884830 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.3-h8088a28_0.conda + sha256: beb7fb34d5517cce06f5f89710de7108a8ca65ed9c0324269fc0446807bcbd91 + md5: b8c47eab4e58fe7015a12ceb9d5b114c depends: - - libcxx >=19 - - __osx >=10.13 - license: BSD-3-Clause - license_family: BSD + - __osx >=11.0 + - liblzma 5.8.3 h8088a28_0 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later purls: [] - size: 39975 - timestamp: 1753083485577 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hd121638_0.conda - sha256: b3d447d72d2af824006f4ba78ae4188747886d6d95f2f165fe67b95541f02b05 - md5: ba9ca3813f4db8c0d85d3c84404e02ba + run_exports: {} + size: 85931 + timestamp: 1775825857949 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + sha256: b03433b13d89f5567e828ea9f1a7d5c5d697bf374c28a4168d71e9464f5dafac + md5: 78a0fe9e9c50d2c381e8ee47e3ea437d depends: - - libcxx >=19 - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - yaml >=0.2.5,<0.3.0a0 + size: 83386 + timestamp: 1753484079473 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.24.5-py311hc290fe0_0.conda + sha256: a383ca97e08312dbb7ca917fff7c6aa054f383aa0fdb1aafc87a35303243ae7e + md5: 9241589df3b7c8fc43d6d329b39c790e + depends: + - __osx >=11.0 + - idna >=2.0 + - multidict >=4.0 + - propcache >=0.2.1 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/yarl?source=hash-mapping + run_exports: {} + size: 165495 + timestamp: 1784526908831 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py311h5bb9006_1.conda + sha256: 2ee455765fe831cca8fe127c56ae99938e353797135fe33140b28abb4fbe1049 + md5: 651594b8f9b9cccc5948287a18903c34 + depends: + - python + - cffi >=1.11 + - zstd >=1.5.7,<1.5.8.0a0 + - python 3.11.* *_cpython + - __osx >=11.0 + - python_abi 3.11.* *_cp311 + - zstd >=1.5.7,<1.6.0a0 license: BSD-3-Clause license_family: BSD - purls: [] - size: 38824 - timestamp: 1753083462800 -- conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-3.1.2-hecca717_0.conda - sha256: 34e2e9c505cd25dba0a9311eb332381b15147cf599d972322a7c197aedfc8ce2 - md5: 9859766c658e78fec9afa4a54891d920 + purls: + - pkg:pypi/zstandard?source=hash-mapping + run_exports: {} + size: 390026 + timestamp: 1762512731928 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9 + md5: ab136e4c34e97f34fb621d2592a393d8 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - license: BSD-2-Clause + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause license_family: BSD purls: [] - size: 2741200 - timestamp: 1756086702093 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/svt-av1-3.1.2-hfae3067_0.conda - sha256: e4b482062da7cf259f21465274a0f3613d1dbd8ea649aca6072625f5038ac40d - md5: 7602d3004ed53b3f8e5e0e04e5de4de7 + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 + size: 433413 + timestamp: 1764777166076 +- conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.9.5-py311ha68e1ae_0.conda + sha256: 03e161ef1e710089630276964921bb6de9c9852d0b04a59e3fe528c608327767 + md5: 9c350d73bdc0e3c68fd1d20afa9466a1 depends: - - libgcc >=14 - - libstdcxx >=14 + - aiosignal >=1.1.2 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - multidict >=4.5,<7.0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - yarl >=1.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/aiohttp?source=hash-mapping + run_exports: {} + size: 769123 + timestamp: 1713965512225 +- conda: https://conda.anaconda.org/conda-forge/win-64/aom-3.9.1-he0c23c2_0.conda + sha256: 0524d0c0b61dacd0c22ac7a8067f977b1d52380210933b04141f5099c5b6fec7 + md5: 3d7c14285d3eb3239a76ff79063f27a5 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 license: BSD-2-Clause license_family: BSD purls: [] - size: 2106252 - timestamp: 1756090698097 -- conda: https://conda.anaconda.org/conda-forge/osx-64/svt-av1-3.1.2-h21dd04a_0.conda - sha256: e6fa8309eadc275aae8c456b9473be5b2b9413b43c6ef2fdbebe21fb3818dd55 - md5: c11ebe332911d9642f0678da49bedf44 + run_exports: + weak: + - aom >=3.9.1,<3.10.0a0 + size: 1958151 + timestamp: 1718551737234 +- conda: https://conda.anaconda.org/conda-forge/win-64/binaryen-117-h63175ca_0.conda + sha256: 2cc0e433360f7c4a5ce8e2b5f8960cfba8675b6b3232830da7e6f8403c6b4186 + md5: b0028cf00bb7d8f3fd8075de8165b1a8 depends: - - __osx >=10.13 - - libcxx >=19 - license: BSD-2-Clause - license_family: BSD + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 2390115 - timestamp: 1756086715447 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/svt-av1-3.1.2-h12ba402_0.conda - sha256: 3b0f4f2a6697f0cdbbe0c0b5f5c7fa8064483d58b4d9674d5babda7f7146af7a - md5: cb56c114b25f20bd09ef1c66a21136ff + run_exports: {} + size: 40046563 + timestamp: 1709093094826 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py311hc5da9e4_1.conda + sha256: 1803c838946d79ef6485ae8c7dafc93e28722c5999b059a34118ef758387a4c9 + md5: b0c459f98ac5ea504a9d9df6242f7ee1 depends: - - __osx >=11.0 - - libcxx >=19 - license: BSD-2-Clause - license_family: BSD + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libbrotlicommon 1.2.0 hfd05255_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + run_exports: {} + size: 335333 + timestamp: 1764018370925 +- conda: https://conda.anaconda.org/conda-forge/win-64/buf-1.66.0-hd02998f_0.conda + sha256: 5544204629d316a4494aa3c1b95f4d459909820b279a791fde31d70ad602ad1f + md5: 60fbc2d921bdd160416678ebbe61801a + license: Apache-2.0 + license_family: APACHE purls: [] - size: 1474592 - timestamp: 1756086729326 -- conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-3.1.2-hac47afa_0.conda - sha256: 444c94a9c1fcb2cdf78b260472451990257733bcf89ed80c73db36b5047d3134 - md5: 91866412570c922f55178855deb0f952 + run_exports: {} + size: 60121611 + timestamp: 1771990361216 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + sha256: 76dfb71df5e8d1c4eded2dbb5ba15bb8fb2e2b0fe42d94145d5eed4c75c35902 + md5: 4cb8e6b48f67de0b018719cdf1136306 depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - license: BSD-2-Clause + license: bzip2-1.0.6 license_family: BSD purls: [] - size: 1862756 - timestamp: 1756086862067 -- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda - sha256: 0053c17ffbd9f8af1a7f864995d70121c292e317804120be4667f37c92805426 - md5: 1bad93f0aa428d618875ef3a588a889e + run_exports: + weak: + - bzip2 >=1.0.8,<2.0a0 + size: 56115 + timestamp: 1771350256444 +- conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda + sha256: 9ee4ad706c5d3e1c6c469785d60e3c2b263eec569be0eac7be33fbaef978bccc + md5: 52ea1beba35b69852d210242dd20f97d depends: - - __glibc >=2.28 - - kernel-headers_linux-64 4.18.0 he073ed8_8 - - tzdata - license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later - license_family: GPL + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - icu >=78.1,<79.0a0 + - libexpat >=2.7.3,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libglib >=2.86.3,<3.0a0 + - libpng >=1.6.53,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.46.4,<1.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-only or MPL-1.1 purls: [] - size: 24210909 - timestamp: 1752669140965 -- conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_8.conda - sha256: 8ab275b5c5fbe36416c7d3fb8b71241eca2d024e222361f8e15c479f17050c0e - md5: 1263d6ac8dadaea7c60b29f1b4af45b8 + run_exports: + weak: + - cairo >=1.18.4,<2.0a0 + size: 1537783 + timestamp: 1766416059188 +- conda: https://conda.anaconda.org/conda-forge/win-64/cargo-llvm-cov-0.8.7-h77a83cd_0.conda + sha256: 1fcae7d44cfe1d8b5e55e5e2f8a4147966bd695e8da7b64a35e5965eae15e95d + md5: ca4f66f0876f063bf905efdd1e5b51a0 depends: - - __glibc >=2.28 - - kernel-headers_linux-aarch64 4.18.0 h05a177a_8 - - tzdata - license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later - license_family: GPL + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache purls: [] - size: 23863575 - timestamp: 1752669129101 -- conda: https://conda.anaconda.org/conda-forge/osx-64/tapi-1300.6.5-h390ca13_0.conda - sha256: f97372a1c75b749298cb990405a690527e8004ff97e452ed2c59e4bc6a35d132 - md5: c6ee25eb54accb3f1c8fc39203acfaf1 + run_exports: {} + size: 1295216 + timestamp: 1778642257194 +- conda: https://conda.anaconda.org/conda-forge/win-64/cargo-nextest-0.9.140-h18a1a76_0.conda + sha256: 47ad2cdb67cbfab8c564793b0e3a06058157e107614b4547da36eb2c97c189c8 + md5: 2850f7c932d93e0c876c7b395f8be570 depends: - - __osx >=10.13 - - libcxx >=17.0.0.a0 - - ncurses >=6.5,<7.0a0 - license: NCSA + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT license_family: MIT purls: [] - size: 221236 - timestamp: 1725491044729 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tapi-1300.6.5-h03f4b80_0.conda - sha256: 37cd4f62ec023df8a6c6f9f6ffddde3d6620a83cbcab170a8fff31ef944402e5 - md5: b703bc3e6cba5943acf0e5f987b5d0e2 + run_exports: {} + size: 7121568 + timestamp: 1783308125842 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.1.0-py311h3485c13_0.conda + sha256: 2678d367277f063cea235be77cc9e950354fa6dfe4d57326840a90d11665aef2 + md5: bcec9706d41771c6df9cf29ca7ae873b depends: - - __osx >=11.0 - - libcxx >=17.0.0.a0 - - ncurses >=6.5,<7.0a0 - license: NCSA + - pycparser + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + run_exports: {} + size: 352297 + timestamp: 1783424271538 +- conda: https://conda.anaconda.org/conda-forge/win-64/clang-16-16.0.6-default_h7df9e1c_15.conda + sha256: 4fd4b39552367bcc94476810a32013f7495b473851dfcac0089cc499d467943f + md5: 3eeb79ed453b1c3b87b0dc60ac092763 + depends: + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - clangxx 16.0.6 + - clang-tools 16.0.6 + - llvm-tools 16.0.6 + - clangdev 16.0.6 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 207679 - timestamp: 1725491499758 -- conda: https://conda.anaconda.org/conda-forge/linux-64/taplo-0.9.1-h1ff36dd_0.conda - sha256: 82b3528f63ae71e0158fdbf8b66e66f619cb70584c471f3d89a2ee6fd44ef20b - md5: 29207c9b716932300221e5acd0b310f7 + run_exports: {} + size: 30820305 + timestamp: 1756194440748 +- conda: https://conda.anaconda.org/conda-forge/win-64/clang-16.0.6-default_h5a21124_15.conda + sha256: b6ce3d51658f230f93058bc1a6ac03fb410b876a9ee5c1ca9cff3886654f4352 + md5: 40fc30ac75e2efce770e636bc42c928a depends: - - libgcc-ng >=12 - - openssl >=3.2.1,<4.0a0 - license: MIT - license_family: MIT + - clang-16 16.0.6 default_h7df9e1c_15 + - libzlib >=1.3.1,<2.0a0 + - ucrt + - vc14_runtime + - zstd >=1.5.7,<1.6.0a0 + constrains: + - clang-tools 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 3877123 - timestamp: 1710792099600 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/taplo-0.9.1-hb8f9562_0.conda - sha256: dbcd4fa63270cef1c777cdbba2b697845704470bb7f3011e2b1b318fb9eb59b7 - md5: 0cf5ee26646e7780a0f89e0fbeac329e + run_exports: {} + size: 90373728 + timestamp: 1756194582485 +- conda: https://conda.anaconda.org/conda-forge/win-64/clang-format-16.0.6-default_h7df9e1c_15.conda + sha256: 958dabce7477e2ed377e415bc2aca29e43f5003faf9efee62373b656a444e4cb + md5: c68cbb230d69b2343c9e96878643eeb6 depends: - - libgcc-ng >=12 - - openssl >=3.2.1,<4.0a0 - license: MIT - license_family: MIT + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 3717546 - timestamp: 1710801928738 -- conda: https://conda.anaconda.org/conda-forge/osx-64/taplo-0.9.1-h236d3af_0.conda - sha256: 3e9032084b3f8d686b15f67500323ae2cae5637dc427b309b661a30026d8f00c - md5: 02c8d9c54b2887c5456fb7a0ecec62f3 + run_exports: {} + size: 1184074 + timestamp: 1756195538812 +- conda: https://conda.anaconda.org/conda-forge/win-64/clang-tools-16.0.6-default_h7df9e1c_15.conda + sha256: 54f20f43fb6719d01508fc43595d547e696cef7635fda1b4203911c26ab9bbac + md5: b7e6316f5f7d8ecba9d209d70bc83ffe depends: - - openssl >=3.2.1,<4.0a0 + - clang-format 16.0.6 default_h7df9e1c_15 + - libclang13 >=16.0.6 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 constrains: - - __osx >=10.12 - license: MIT - license_family: MIT + - clangdev 16.0.6 + - clang 16.0.6.* + - llvm 16.0.6.* + - llvm-tools 16.0.6.* + - llvmdev 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 3773670 - timestamp: 1710793055293 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/taplo-0.9.1-h16c8c8b_0.conda - sha256: 3a387ea7779d061d28af0426d1249fe81f798f35a2d0cb979a6ff84525187667 - md5: 8171587b7a366dbbaab309ae1c45bd93 + run_exports: {} + size: 226347785 + timestamp: 1756195760295 +- conda: https://conda.anaconda.org/conda-forge/win-64/cmake-3.27.6-hf0feee3_0.conda + sha256: 12b94bce6d7c76ff408f8ea240c7d78987b0bc3cb4f632f381c4b0efd30ebfe0 + md5: 4dc81f3bf26f0949fedd4e31cecea1d1 depends: - - openssl >=3.2.1,<4.0a0 - constrains: - - __osx >=11.0 - license: MIT - license_family: MIT + - bzip2 >=1.0.8,<2.0a0 + - libcurl >=8.3.0,<9.0a0 + - libexpat >=2.5.0,<3.0a0 + - libuv >=1.44.2,<2.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - ucrt >=10.0.20348.0 + - vc14_runtime >=14.29.30139 + - xz >=5.2.6,<6.0a0 + - zstd >=1.5.5,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 3560280 - timestamp: 1710793219601 -- conda: https://conda.anaconda.org/conda-forge/win-64/taplo-0.9.1-h7f3b576_0.conda - sha256: 7ef6b5f23fd749fde17628793e4e76e36395b9645a3d3b8b0fa5a4d9b2b9ccfb - md5: 0a798b7bf999885c00e40fcb0cfe7136 + run_exports: {} + size: 13777396 + timestamp: 1695270971791 +- conda: https://conda.anaconda.org/conda-forge/win-64/dav1d-1.2.1-hcfcfb64_0.conda + sha256: 2aa2083c9c186da7d6f975ccfbef654ed54fff27f4bc321dbcd12cee932ec2c4 + md5: ed2c27bda330e3f0ab41577cf8b9b585 depends: - - m2w64-gcc-libs - - m2w64-gcc-libs-core + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-2-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - dav1d >=1.2.1,<1.2.2.0a0 + size: 618643 + timestamp: 1685696352968 +- conda: https://conda.anaconda.org/conda-forge/win-64/doxygen-1.9.7-h849606c_1.conda + sha256: b78b504b6c61a7a6252be49f2838c4788332332616fdd427f81adddc650b2520 + md5: 7c9a71d497a45a053fa85eeef616f936 + depends: + - libiconv >=1.17,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: GPL-2.0-only + license_family: GPL + purls: [] + run_exports: {} + size: 4861033 + timestamp: 1687333355663 +- conda: https://conda.anaconda.org/conda-forge/win-64/fd-find-10.4.2-h77a83cd_0.conda + sha256: e31b3cc7ed5cb9597a7eabed3bb3d425a634fe0ebd3d66b67cf36b2d73f448a1 + md5: 8ad83594e66dad95d3cb583a373823bc + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: [] - size: 3924159 - timestamp: 1710794002174 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2022.2.0-hb60516a_1.conda - sha256: 105a12b00e407aaaf04d811d3e737d470fd9e9328bc9a6a57f0f3fea5a486e84 - md5: 29ed2be4b47b5aa1b07689e12407fbfd + run_exports: {} + size: 1245700 + timestamp: 1773352952869 +- conda: https://conda.anaconda.org/conda-forge/win-64/ffmpeg-7.1.1-gpl_h70aa942_910.conda + sha256: 49d38240ff7bfde5c53d6ae20c98ee65b82b1d0d8e1dcb5e2515de839b8678f3 + md5: 35d77007b30682debfbf97ad6cebbbda + depends: + - aom >=3.9.1,<3.10.0a0 + - bzip2 >=1.0.8,<2.0a0 + - dav1d >=1.2.1,<1.2.2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - harfbuzz >=11.4.5 + - lame >=3.100,<3.101.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libopus >=1.5.2,<2.0a0 + - librsvg >=2.58.4,<3.0a0 + - libvorbis >=1.3.7,<1.4.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - openh264 >=2.6.0,<2.6.1.0a0 + - openssl >=3.5.2,<4.0a0 + - sdl2 >=2.32.54,<3.0a0 + - svt-av1 >=3.1.2,<3.1.3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - x264 >=1!164.3095,<1!165 + - x265 >=3.5,<3.6.0a0 + constrains: + - __cuda >=12.8 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + run_exports: + weak: + - ffmpeg >=7.1.1,<8.0a0 + size: 10027541 + timestamp: 1757216486092 +- conda: https://conda.anaconda.org/conda-forge/win-64/flatbuffers-25.12.19-h5112557_0.conda + sha256: e155e1d61041e0f4391bfbbbf8b7331afbbea364823525cc421c8bd5c3b887fc + md5: 6070d5165220ed7f30c8511917c77cf9 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libhwloc >=2.12.1,<2.12.2.0a0 - - libstdcxx >=14 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 license: Apache-2.0 license_family: APACHE purls: [] - size: 183204 - timestamp: 1755775909376 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tbb-2022.2.0-h8f856e4_1.conda - sha256: e706f8216b4f0e1bb363c1940c415ce96483889bd24248ac99284a7fcb9eaf9b - md5: e506cac9e67b6d6e6d1f9bc17db721ee + run_exports: + weak: + - flatbuffers >=25.12.19,<25.12.20.0a0 + size: 2094000 + timestamp: 1766388891562 +- conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.18.1-hd47e2ca_0.conda + sha256: 9217184c4a8e82101b0e512b059ae3ff67e3913133b9031edad89ab5341284e4 + md5: abd79bad98c99c1a116154d6de74ea89 + depends: + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libiconv >=1.18,<2.0a0 + - libintl >=0.22.5,<1.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - fontconfig >=2.18.1,<3.0a0 + - fonts-conda-ecosystem + size: 202630 + timestamp: 1780450217840 +- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_1.conda + sha256: a0e419e96146159f12344c870dca608d11bca36841f228092b986ffc2e1e0f02 + md5: e77293b32225b136a8be300f93d0e89f + depends: + - libfreetype 2.14.3 h57928b3_1 + - libfreetype6 2.14.3 hdbac1cb_1 + - zlib + license: GPL-2.0-only OR FTL + purls: [] + run_exports: + weak: + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + size: 185584 + timestamp: 1780934817461 +- conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda + sha256: 15011071ee56c216ffe276c8d734427f1f893f275ef733f728d13f610ed89e6e + md5: c27bd87e70f970010c1c6db104b88b18 depends: - - libgcc >=14 - - libhwloc >=2.12.1,<2.12.2.0a0 - - libstdcxx >=14 - license: Apache-2.0 - license_family: APACHE + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-or-later purls: [] - size: 146718 - timestamp: 1755777414300 -- conda: https://conda.anaconda.org/conda-forge/osx-64/tbb-2022.2.0-hc025b3e_1.conda - sha256: 44d9b5795d8c72da1002ef504c16eadcb8615c9c8098c830c12ebacae31149ed - md5: 796b8d4a40afd4951d87ffd939c6a206 + run_exports: + weak: + - fribidi >=1.0.16,<2.0a0 + size: 64394 + timestamp: 1757438741305 +- conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.8.0-py311hdf60d3a_0.conda + sha256: 16db4b5c343de93761b2547e8d2e293b47a0e6db4935ac00987ff2c03213df39 + md5: 3483aab7716ce942bb99efffdb5a99b5 depends: - - __osx >=10.13 - - libcxx >=19 - - libhwloc >=2.12.1,<2.12.2.0a0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: Apache-2.0 license_family: APACHE + purls: + - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} + size: 50366 + timestamp: 1779999906989 +- conda: https://conda.anaconda.org/conda-forge/win-64/gdk-pixbuf-2.44.7-h1f5b9c4_0.conda + sha256: 4965bf7c84c9b7c970f1f7bc475962e43441018cf9551682a4a22960c5090588 + md5: 5daba86dbe8072c7e49f74013ef308cd + depends: + - libglib >=2.88.2,<3.0a0 + - libintl >=0.22.5,<1.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-or-later + license_family: LGPL purls: [] - size: 164273 - timestamp: 1755776307318 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tbb-2022.2.0-h5b2e6d4_1.conda - sha256: 561cc8c407880ff6f3965778f78c860d93d3b9c5bd206ba9aac7c437794d4155 - md5: 1cdd70110585806da18f400d30d9b497 - depends: - - __osx >=11.0 - - libcxx >=19 - - libhwloc >=2.12.1,<2.12.2.0a0 + run_exports: + weak: + - gdk-pixbuf >=2.44.7,<3.0a0 + size: 579329 + timestamp: 1782591520147 +- conda: https://conda.anaconda.org/conda-forge/win-64/gh-2.96.0-h11686cb_0.conda + sha256: 75361364f5f410c34ad882914da203c792a30cd00c1a2cbac66a331c87e179de + md5: 4ac318bd7d3fe1a3b6168dad0b0546b1 license: Apache-2.0 license_family: APACHE purls: [] - size: 119970 - timestamp: 1755776161308 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - sha256: a84ff687119e6d8752346d1d408d5cf360dee0badd487a472aa8ddedfdc219e1 - md5: a0116df4f4ed05c303811a837d5b39d8 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3285204 - timestamp: 1748387766691 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5688188_102.conda - sha256: 46e10488e9254092c655257c18fcec0a9864043bdfbe935a9fbf4fb2028b8514 - md5: 2562c9bfd1de3f9c590f0fe53858d85c - depends: - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - license: TCL - license_family: BSD - purls: [] - size: 3342845 - timestamp: 1748393219221 -- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-hf689a15_2.conda - sha256: b24468006a96b71a5f4372205ea7ec4b399b0f2a543541e86f883de54cd623fc - md5: 9864891a6946c2fe037c02fca7392ab4 - depends: - - __osx >=10.13 - - libzlib >=1.3.1,<2.0a0 - license: TCL - license_family: BSD + run_exports: {} + size: 12894463 + timestamp: 1783039160192 +- conda: https://conda.anaconda.org/conda-forge/win-64/glib-2.88.2-h395db07_0.conda + sha256: b67107959a71dbf9f5c2e95511f18095d9ac6f0001f0de4a1b6e14282162989e + md5: 7d203837b88a2255b32dca555d37ca50 + depends: + - python * + - packaging + - libglib ==2.88.2 h7ce1215_0 + - glib-tools ==2.88.2 h74ecf4c_0 + - libintl-devel + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libintl >=0.22.5,<1.0a0 + license: LGPL-2.1-or-later purls: [] - size: 3259809 - timestamp: 1748387843735 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - sha256: cb86c522576fa95c6db4c878849af0bccfd3264daf0cc40dd18e7f4a7bfced0e - md5: 7362396c170252e7b7b0c8fb37fe9c78 - depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - license: TCL - license_family: BSD + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 75973 + timestamp: 1782463965040 +- conda: https://conda.anaconda.org/conda-forge/win-64/glib-tools-2.88.2-h74ecf4c_0.conda + sha256: c604b6ca42c6271f281b1c0616e0d50bd800f8fc913a91a2753c2551b3b6b16c + md5: 63c615f0c525ee64f72e557e583b6257 + depends: + - libglib ==2.88.2 h7ce1215_0 + - libffi + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libintl >=0.22.5,<1.0a0 + license: LGPL-2.1-or-later purls: [] - size: 3125538 - timestamp: 1748388189063 -- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda - sha256: e3614b0eb4abcc70d98eae159db59d9b4059ed743ef402081151a948dce95896 - md5: ebd0e761de9aa879a51d22cc721bd095 + run_exports: {} + size: 251644 + timestamp: 1782463965040 +- conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.15-hac47afa_0.conda + sha256: 88b6601f8edae59834b59b521e293ff3b58361dc1603240f5a8328c24e6936ad + md5: ff9a9bfe791f56b0227597a7651a6af0 depends: - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: TCL - license_family: BSD + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.0-or-later + license_family: LGPL purls: [] - size: 3466348 - timestamp: 1748388121356 -- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - sha256: cb77c660b646c00a48ef942a9e1721ee46e90230c7c570cdeb5a893b5cce9bff - md5: d2732eb636c264dc9aa4cbee404b1a53 + run_exports: + weak: + - graphite2 >=1.3.15,<2.0a0 + size: 97308 + timestamp: 1780454389458 +- conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-14.2.1-h57928b3_1.conda + sha256: c8bf564f2c415b82f056eff98b8967fb75c86821398998f20289397b5acf1ef7 + md5: e706de885f817f9832c56f1c9ad7ce71 depends: - - python >=3.10 - - python + - libharfbuzz-devel 14.2.1 h03b5201_1 license: MIT license_family: MIT - purls: - - pkg:pypi/tomli?source=compressed-mapping - size: 20973 - timestamp: 1760014679845 -- pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - name: tomli-w - version: 1.2.0 - sha256: 188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.12.3-pyha770c72_0.conda - sha256: 53cc436ab92d38683df1320e4468a8b978428e800195bf1c8c2460e90b0bc117 - md5: 074d0ce7a6261ab8b497c3518796ef3e + purls: [] + run_exports: + weak: + - libharfbuzz >=14.2.1 + size: 11542 + timestamp: 1782801047786 +- conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.3-h637d24d_0.conda + sha256: 1bda728d70a619731b278c859eda364146cb5b4b8c739a64da8128353d81d1c4 + md5: 0097b24800cb696915c3dbd1f5335d3f depends: - - python >=3.7 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT - purls: - - pkg:pypi/tomlkit?source=hash-mapping - size: 37132 - timestamp: 1700046842169 -- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.6-pyhd8ed1ab_0.conda - sha256: 32c39424090a8cafe7994891a816580b3bd253eb4d4f5473bdefcf6a81ebc061 - md5: 92718e1f892e1e4623dcc59b9f9c4e55 - depends: - - colorama - - python >=3.7 - license: MPL-2.0 or MIT - purls: - - pkg:pypi/tqdm?source=hash-mapping - size: 89367 - timestamp: 1730145312554 -- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 - md5: 019a7385be9af33791c989871317e1ed - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/traitlets?source=hash-mapping - size: 110051 - timestamp: 1733367480074 -- pypi: https://files.pythonhosted.org/packages/4f/7e/bc19996fa86cad8801e8ffe6f1bba5836ca0160df76d0410d27432193712/trove_classifiers-2025.12.1.14-py3-none-any.whl - name: trove-classifiers - version: 2025.12.1.14 - sha256: a8206978ede95937b9959c3aff3eb258bbf7b07dff391ddd4ea7e61f316635ab -- conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.31-h4e94fc0_0.conda - noarch: python - sha256: 8af5eb756191050f9516b1db6600303628ddb674b8629ed06dd79065f5dc3046 - md5: 8664e2153bea060af6d021e91a4c057b + purls: [] + run_exports: + weak: + - icu >=78.3,<79.0a0 + size: 14954024 + timestamp: 1773822508646 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda + sha256: c55745796e762ba9e817ab1fc0f21f1a049e202f90fa762df39578f37923f6c2 + md5: 00335c2c4a98656554771aaf6f1a7400 depends: - - python - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - _python_abi3_support 1.* - - cpython >=3.10 - constrains: - - __glibc >=2.17 + - openssl >=3.5.7,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT - purls: - - pkg:pypi/ty?source=compressed-mapping - size: 9529492 - timestamp: 1776273647001 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ty-0.0.31-h47ce4e6_0.conda - noarch: python - sha256: 95cc44da5bff86fdfcd5db03f79424c9a8e90cce961ab553348f2a46260ff671 - md5: 5a932a9579df431f51b59e3587206a84 + purls: [] + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 750320 + timestamp: 1781859644591 +- conda: https://conda.anaconda.org/conda-forge/win-64/lame-3.100-hcfcfb64_1003.tar.bz2 + sha256: 824988a396b97bb9138823a1b3aabd8326e06da5834b3011253d72bb45fd3a88 + md5: d92e64077c44c9e32c72d4b5799d47e4 depends: - - python - - libgcc >=14 - - _python_abi3_support 1.* - - cpython >=3.10 - constrains: - - __glibc >=2.17 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ty?source=compressed-mapping - size: 9121062 - timestamp: 1776273658766 -- conda: https://conda.anaconda.org/conda-forge/osx-64/ty-0.0.31-h479939e_0.conda - noarch: python - sha256: aaf3c72e0ad337dc1c57f535cb77aacef03817ba8eb243888ebebe7cb4cb4968 - md5: 8c1e65a7ce27587a4398abbb4a65613b + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vs2015_runtime >=14.29.30139 + license: LGPL-2.0-only + license_family: LGPL + purls: [] + run_exports: + weak: + - lame >=3.100,<3.101.0a0 + size: 570583 + timestamp: 1664996824680 +- conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.1.0-hd936e49_0.conda + sha256: 45df58fca800b552b17c3914cc9ab0d55a82c5172d72b5c44a59c710c06c5473 + md5: 54b231d595bc1ff9bff668dd443ee012 depends: - - python - - __osx >=11.0 - - _python_abi3_support 1.* - - cpython >=3.10 - constrains: - - __osx >=10.13 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ty?source=compressed-mapping - size: 9197176 - timestamp: 1776273727251 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ty-0.0.31-hdfcc030_0.conda - noarch: python - sha256: b76630c21ab40c7a51d2bba12c8d5e5d8beb12cb6e9039e478d3fa8eee4db8ea - md5: 0823cec0cbdf0d23ac3269e83744def7 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - lerc >=4.1.0,<5.0a0 + size: 172395 + timestamp: 1773113455582 +- conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20260107.1-cxx17_h0eb2380_0.conda + sha256: 7e7f3754f8afaabd946dc11d7c00fd1dc93f0388a2d226a7abf1bf07deab0e2b + md5: 60da39dd5fd93b2a4a0f986f3acc2520 depends: - - python - - __osx >=11.0 - - _python_abi3_support 1.* - - cpython >=3.10 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - __osx >=11.0 - license: MIT + - libabseil-static =20260107.1=cxx17* + - abseil-cpp =20260107.1 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libabseil >=20260107.1,<20260108.0a0 + - libabseil =*=cxx17* + size: 1884784 + timestamp: 1770863303486 +- conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-22.1.8-default_ha2db4b5_2.conda + sha256: 1a2ead8255567347e6f9b2e941e07a1bcf3ce061d94d51f1b7d88089cd1a9552 + md5: 45a2834578a5f167258aa109af48b036 + depends: + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: + weak: + - libclang13 >=22.1.8 + size: 30487117 + timestamp: 1781848941933 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h51a1c48_2.conda + sha256: e9a9231e6c04b82979a6a1a4f90b8a697dc3a42884e709dee8ba5d0355b45296 + md5: 88c875a8f34785d6f6185ceb646154fa + depends: + - krb5 >=1.22.2,<1.23.0a0 + - libpsl >=0.22.0,<0.23.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: curl license_family: MIT - purls: - - pkg:pypi/ty?source=compressed-mapping - size: 8603653 - timestamp: 1776273658703 -- conda: https://conda.anaconda.org/conda-forge/win-64/ty-0.0.31-hc21aad4_0.conda - noarch: python - sha256: 617dadab217a0b516a377577c090b96fc16eb866451d552ff517781a74709266 - md5: ba95660274577c41d912f7987588b35a + purls: [] + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 404786 + timestamp: 1782911887650 +- conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda + sha256: 834e4881a18b690d5ec36f44852facd38e13afe599e369be62d29bd675f107ee + md5: e77030e67343e28b084fabd7db0ce43e depends: - - python + - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - _python_abi3_support 1.* - - cpython >=3.10 license: MIT license_family: MIT - purls: - - pkg:pypi/ty?source=compressed-mapping - size: 9582179 - timestamp: 1776273731957 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda - sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 - md5: 0caa1af407ecff61170c9437a808404d - depends: - - python >=3.10 - - python - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/typing-extensions?source=hash-mapping - size: 51692 - timestamp: 1756220668932 -- conda: https://conda.anaconda.org/conda-forge/linux-64/typos-1.45.1-hb17b654_0.conda - sha256: 51c8fda53a6b9ed6d9379d3f5c61038b988f8c3142655bdf0781b2e93e8b0293 - md5: 9b5b19ac1630e7190ca35e361456badd + purls: [] + run_exports: + weak: + - libdeflate >=1.25,<1.26.0a0 + size: 156818 + timestamp: 1761979842440 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda + sha256: 1a54d874addda73b6f7164d5f3905821277a1831bcc05edd74b3085391688571 + md5: ccc490c81ffe14181861beac0e8f3169 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - __glibc >=2.17 - license: MIT OR Apache-2.0 + - expat 2.8.1.* + license: MIT + license_family: MIT purls: [] - size: 3359580 - timestamp: 1776192165109 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/typos-1.45.1-h069e38c_0.conda - sha256: e7b4997f508d1826960fa060f0cbc9955afcc96ed103f9589fbeb7706d00647e - md5: b28fbf30cd2dcbe74ac87140230b8c39 + run_exports: {} + size: 71631 + timestamp: 1781203724164 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 + md5: 720b39f5ec0610457b725eb3f396219a depends: - - libgcc >=14 - constrains: - - __glibc >=2.17 - license: MIT OR Apache-2.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT purls: [] - size: 3870189 - timestamp: 1776192184466 -- conda: https://conda.anaconda.org/conda-forge/osx-64/typos-1.45.1-h19f9e61_0.conda - sha256: 7baf9ce2abb1f2c0de15facf5e10dfc051b2b5aa85b66df74b521fbda23392d2 - md5: f47fcf20376ee0a10d4f34188f1df9eb + run_exports: + weak: + - libffi >=3.5.2,<3.6.0a0 + size: 45831 + timestamp: 1769456418774 +- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_1.conda + sha256: 035d0c67bf9f7a16f4a1764f420c120f1a995d071bb265fcc66ef688ef709d7b + md5: e45b52fb9a81c9e2708465a706e05952 depends: - - __osx >=11.0 - constrains: - - __osx >=10.13 - license: MIT OR Apache-2.0 + - libfreetype6 >=2.14.3 + license: GPL-2.0-only OR FTL purls: [] - size: 2853099 - timestamp: 1776192285013 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/typos-1.45.1-h6fdd925_0.conda - sha256: e79c88dac056f5739af57af0b2b0de9ef5da1d47760c3bda6b11552a9b70c721 - md5: 230a9d1aa44a555901b91fbdfec3d5d0 + run_exports: {} + size: 8711 + timestamp: 1780934891782 +- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_1.conda + sha256: 0bbd19c9f7c4d0232b31892e6a4d1f82b8d19d1b84d89725f1f491b336447758 + md5: 4e4d54f9f98383d977ba56ef39ebf46d depends: - - __osx >=11.0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - __osx >=11.0 - license: MIT OR Apache-2.0 + - freetype >=2.14.3 + license: GPL-2.0-only OR FTL purls: [] - size: 2713250 - timestamp: 1776192204549 -- conda: https://conda.anaconda.org/conda-forge/win-64/typos-1.45.1-h18a1a76_0.conda - sha256: 502f80f4e3ecf6d61f76ca86d1145d6028631b6c4f02ec382341dff6abb37ddd - md5: c0a89a1666cd6ae11553dd5da0b0049a + run_exports: {} + size: 340411 + timestamp: 1780934813224 +- conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.88.2-h7ce1215_0.conda + sha256: 20d4a182b8aa1d71b331579fae281bb3ccb1a199257ce15fadc53786031a7408 + md5: 5be116480ef34a5646894d7f7cd7ae41 depends: - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - license: MIT OR Apache-2.0 - purls: [] - size: 2913504 - timestamp: 1776192249270 -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 - md5: 4222072737ccff51314b5ece9c7d6f5a - license: LicenseRef-Public-Domain - purls: [] - size: 122968 - timestamp: 1742727099393 -- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 - md5: 71b24316859acd00bdb8b38f5e2ce328 + - libintl >=0.22.5,<1.0a0 + - libzlib >=1.3.2,<2.0a0 + - libiconv >=1.18,<2.0a0 + - pcre2 >=10.47,<10.48.0a0 + - libffi >=3.5.2,<3.6.0a0 constrains: - - vc14_runtime >=14.29.30037 - - vs2015_runtime >=14.29.30037 - license: LicenseRef-MicrosoftWindowsSDK10 + - glib >2.66 + license: LGPL-2.1-or-later purls: [] - size: 694692 - timestamp: 1756385147981 -- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda - sha256: 4fb9789154bd666ca74e428d973df81087a697dbb987775bc3198d2215f240f8 - md5: 436c165519e140cb08d246a4472a9d6a + run_exports: + weak: + - libglib >=2.88.2,<3.0a0 + size: 4518265 + timestamp: 1782463965040 +- conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-14.2.1-h03b5201_1.conda + sha256: 634a64cf43f1ce5a4139334bcdbde54d6854ae33d881ae1774377965e21051a5 + md5: 005469a341088900ca235892d3154c24 depends: - - brotli-python >=1.0.9 - - h2 >=4,<5 - - pysocks >=1.5.6,<2.0,!=1.5.7 - - python >=3.9 - - zstandard >=0.18.0 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.88.2,<3.0a0 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT - purls: - - pkg:pypi/urllib3?source=hash-mapping - size: 101735 - timestamp: 1750271478254 -- pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - name: userpath - version: 1.9.2 - sha256: 2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d - requires_dist: - - click - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl - name: uv - version: 0.9.17 - sha256: 22fcc26755abebdf366becc529b2872a831ce8bb14b36b6a80d443a1d7f84d3b - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl - name: uv - version: 0.9.17 - sha256: 330e7085857e4205c5196a417aca81cfbfa936a97dd2a0871f6560a88424ebf2 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: uv - version: 0.9.17 - sha256: cd2c3d25fbd8f91b30d0fac69a13b8e2c2cd8e606d7e6e924c1423e4ff84e616 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl - name: uv - version: 0.9.17 - sha256: 233b3d90f104c59d602abf434898057876b87f64df67a37129877d6dab6e5e10 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl - name: uv - version: 0.9.17 - sha256: 4b8e5513d48a267bfa180ca7fefaf6f27b1267e191573b3dba059981143e88ef - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda - sha256: cb357591d069a1e6cb74199a8a43a7e3611f72a6caed9faa49dbb3d7a0a98e0b - md5: 28f4ca1e0337d0f27afb8602663c5723 + purls: [] + run_exports: {} + size: 1008194 + timestamp: 1782801000396 +- conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-devel-14.2.1-h03b5201_1.conda + sha256: d04bac65245bf76ae23a18148b941d8215085d38a6d391971966ccda8a996b99 + md5: de077ebf9cbc0c1da6510fdf1bbc6baa depends: + - cairo >=1.18.4,<2.0a0 + - freetype + - glib + - graphite2 >=1.3.15,<2.0a0 + - icu >=78.3,<79.0a0 + - libexpat >=2.8.1,<3.0a0 + - libfreetype >=2.14.3 + - libfreetype6 >=2.14.3 + - libglib >=2.88.2,<3.0a0 + - libharfbuzz 14.2.1 h03b5201_1 + - libpng >=1.6.58,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - track_features: - - vc14 - license: BSD-3-Clause - license_family: BSD + license: MIT + license_family: MIT purls: [] - size: 18249 - timestamp: 1753739241465 -- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda - sha256: af4b4b354b87a9a8d05b8064ff1ea0b47083274f7c30b4eb96bc2312c9b5f08f - md5: 603e41da40a765fd47995faa021da946 + run_exports: + weak: + - libharfbuzz >=14.2.1 + size: 321750 + timestamp: 1782801035822 +- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 + md5: 64571d1dd6cdcfa25d0664a5950fdaa2 depends: - ucrt >=10.0.20348.0 - - vcomp14 14.44.35208 h818238b_31 - constrains: - - vs2015_runtime 14.44.35208.* *_31 - license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime - license_family: Proprietary + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: LGPL-2.1-only purls: [] - size: 682424 - timestamp: 1753739239305 -- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda - sha256: 67b317b64f47635415776718d25170a9a6f9a1218c0f5a6202bfd687e07b6ea4 - md5: a6b1d5c1fc3cb89f88f7179ee6a9afe3 + run_exports: + weak: + - libiconv >=1.18,<2.0a0 + size: 696926 + timestamp: 1754909290005 +- conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + sha256: c7e4600f28bcada8ea81456a6530c2329312519efcf0c886030ada38976b0511 + md5: 2cf0cf76cc15d360dfa2f17fd6cf9772 depends: - - ucrt >=10.0.20348.0 - constrains: - - vs2015_runtime 14.44.35208.* *_31 - license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime - license_family: Proprietary + - libiconv >=1.17,<2.0a0 + license: LGPL-2.1-or-later purls: [] - size: 113963 - timestamp: 1753739198723 -- pypi: https://files.pythonhosted.org/packages/eb/6a/0af36875e0023a1f2d0b66b4051721fc26740e947696922df1665b75e5d3/virtualenv-20.36.0-py3-none-any.whl - name: virtualenv - version: 20.36.0 - sha256: e7ded577f3af534fd0886d4ca03277f5542053bedb98a70a989d3c22cfa5c9ac - requires_dist: - - distlib>=0.3.7,<1 - - filelock>=3.16.1,<4 ; python_full_version < '3.10' - - filelock>=3.20.1,<4 ; python_full_version >= '3.10' - - importlib-metadata>=6.6 ; python_full_version < '3.8' - - platformdirs>=3.9.1,<5 - - typing-extensions>=4.13.2 ; python_full_version < '3.11' - - furo>=2023.7.26 ; extra == 'docs' - - proselint>=0.13 ; extra == 'docs' - - sphinx>=7.1.2,!=7.3 ; extra == 'docs' - - sphinx-argparse>=0.4 ; extra == 'docs' - - sphinxcontrib-towncrier>=0.2.1a0 ; extra == 'docs' - - towncrier>=23.6 ; extra == 'docs' - - covdefaults>=2.3 ; extra == 'test' - - coverage-enable-subprocess>=1 ; extra == 'test' - - coverage>=7.2.7 ; extra == 'test' - - flaky>=3.7 ; extra == 'test' - - packaging>=23.1 ; extra == 'test' - - pytest-env>=0.8.2 ; extra == 'test' - - pytest-freezer>=0.4.8 ; (python_full_version >= '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and extra == 'test') or (platform_python_implementation == 'GraalVM' and extra == 'test') or (platform_python_implementation == 'PyPy' and extra == 'test') - - pytest-mock>=3.11.1 ; extra == 'test' - - pytest-randomly>=3.12 ; extra == 'test' - - pytest-timeout>=2.1 ; extra == 'test' - - pytest>=7.4 ; extra == 'test' - - setuptools>=68 ; extra == 'test' - - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_31.conda - sha256: 8b20152d00e1153ccb1ed377a160110482f286a6d85a82b57ffcd60517d523a7 - md5: d75abcfbc522ccd98082a8c603fce34c + run_exports: + weak: + - libintl >=0.22.5,<1.0a0 + size: 95568 + timestamp: 1723629479451 +- conda: https://conda.anaconda.org/conda-forge/win-64/libintl-devel-0.22.5-h5728263_3.conda + sha256: be1f3c48bc750bca7e68955d57180dfd826d6f9fa7eb32994f6cb61b813f9a6a + md5: 7537784e9e35399234d4007f45cdb744 depends: - - vc14_runtime >=14.44.35208 - license: BSD-3-Clause - license_family: BSD + - libiconv >=1.17,<2.0a0 + - libintl 0.22.5 h5728263_3 + license: LGPL-2.1-or-later purls: [] - size: 18249 - timestamp: 1753739241918 -- conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.37.32822-h0123c8e_17.conda - sha256: 259b5d4ac07b131bf15bf1a2d101eb9eb039e32cfef57de79061cb4c8f1889fe - md5: 8b02594cf497f7516a3ed20a164de75e + run_exports: + weak: + - libintl >=0.22.5,<1.0a0 + size: 40746 + timestamp: 1723629745649 +- conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_0.conda + sha256: 3d6635efa9497b9c5ba7957df8067c0a980d5cb10a6ea136931809eb410f8a99 + md5: cf219146d5bf2fee5907409ff9f5ac89 depends: - - vswhere + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - vs_win-64 2022.* - track_features: - - vc14 - license: BSD-3-Clause - license_family: BSD + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib purls: [] - size: 19405 - timestamp: 1694292390059 -- conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda - sha256: b72270395326dc56de9bd6ca82f63791b3c8c9e2b98e25242a9869a4ca821895 - md5: f622897afff347b715d046178ad745a5 + run_exports: + weak: + - libjpeg-turbo >=3.2.0,<4.0a0 + size: 991057 + timestamp: 1783731990693 +- conda: https://conda.anaconda.org/conda-forge/win-64/libllvm16-16.0.6-h2a44499_4.conda + sha256: 4daf0c7c9c44fab479448af21a2b1a122341a6aae646b4f16baa352bc3fa7549 + md5: 59154fa392913f71c729aaee71d9222b depends: - - __win - license: MIT - license_family: MIT + - libxml2 >=2.13.5,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - zstd >=1.5.6,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 238764 - timestamp: 1745560912727 -- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda - sha256: ba673427dcd480cfa9bbc262fd04a9b1ad2ed59a159bd8f7e750d4c52282f34c - md5: 0f2ca7906bf166247d1d760c3422cb8a + run_exports: {} + size: 55022 + timestamp: 1739802154215 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + sha256: d636d1a25234063642f9c531a7bb58d84c1c496411280a36ea000bd122f078f1 + md5: 8f83619ab1588b98dd99c90b0bfc5c6d depends: - - __glibc >=2.17,<3.0.a0 - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - libgcc >=13 - - libstdcxx >=13 - license: MIT - license_family: MIT + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.3.* + license: 0BSD purls: [] - size: 330474 - timestamp: 1751817998141 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.24.0-h698ed42_0.conda - sha256: 2a58c43ae7a618a329705df8406420ac89c9093386c5ca356ae7f2291f012e58 - md5: 2a57237cee70cb13c402af1ef6f8e5f6 + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 106486 + timestamp: 1775825663227 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-devel-5.8.3-hfd05255_0.conda + sha256: 875f0535e135b9949720b80057508e14a9cb9351d4117760dacc6296f5b5704a + md5: 7845201435d0c7c0a02269c2742da1cc depends: - - libexpat >=2.7.0,<3.0a0 - - libffi >=3.4.6,<3.5.0a0 - - libgcc >=13 - - libstdcxx >=13 - license: MIT - license_family: MIT - purls: [] - size: 332236 - timestamp: 1751818023302 -- conda: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.45-hd8ed1ab_0.conda - sha256: 37b0e03a943c048e143f624c51b329778f36923052092fd938827f8c19a4941d - md5: 6db9be3b67190229479780eeeee1b35b - license: MIT - license_family: MIT + - liblzma 5.8.3 hfd05255_0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: 0BSD purls: [] - size: 138011 - timestamp: 1749836220507 -- conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.38.4-pyhd8ed1ab_0.tar.bz2 - sha256: bd4f11ff075ff251ade9f57686f31473e25be46ab282d9603f551401250f9f44 - md5: c829cfb8cb826acb9de0ac1a2df0a940 + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 130960 + timestamp: 1775825690054 +- conda: https://conda.anaconda.org/conda-forge/win-64/libogg-1.3.5-h2466b09_1.conda + sha256: c63e5fb169dbd192aacdcee6e37235407f106b8ca9c9036942a25e0366cbc73c + md5: b67ed8c9ca072695ff482e50d888a523 depends: - - python >=3.7 - license: MIT - license_family: MIT - purls: - - pkg:pypi/wheel?source=hash-mapping - size: 32521 - timestamp: 1668051714265 -- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f - md5: 46e441ba871f524e2b067929da3051c2 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libogg >=1.3.5,<1.4.0a0 + size: 35040 + timestamp: 1745826086628 +- conda: https://conda.anaconda.org/conda-forge/win-64/libopus-1.6.1-h6a83c73_0.conda + sha256: c3678f111866235b44fa65265966abae7d90b6387178f1459afaedcee8b4a997 + md5: 0ed21da5b6e3a0393e05762b3cce2878 depends: - - __win - - python >=3.9 - license: LicenseRef-Public-Domain - purls: - - pkg:pypi/win-inet-pton?source=hash-mapping - size: 9555 - timestamp: 1733130678956 -- pypi: https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl - name: wrapt - version: 2.0.1 - sha256: 85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6 - requires_dist: - - pytest ; extra == 'dev' - - setuptools ; extra == 'dev' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - name: wrapt - version: 2.0.1 - sha256: df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28 - requires_dist: - - pytest ; extra == 'dev' - - setuptools ; extra == 'dev' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl - name: wrapt - version: 2.0.1 - sha256: 47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b - requires_dist: - - pytest ; extra == 'dev' - - setuptools ; extra == 'dev' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - name: wrapt - version: 2.0.1 - sha256: 4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb - requires_dist: - - pytest ; extra == 'dev' - - setuptools ; extra == 'dev' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl - name: wrapt - version: 2.0.1 - sha256: c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7 - requires_dist: - - pytest ; extra == 'dev' - - setuptools ; extra == 'dev' - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/x264-1!164.3095-h166bdaf_2.tar.bz2 - sha256: 175315eb3d6ea1f64a6ce470be00fa2ee59980108f246d3072ab8b977cb048a5 - md5: 6c99772d483f566d59e25037fea2c4b1 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libopus >=1.6.1,<2.0a0 + size: 307373 + timestamp: 1768497136248 +- conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda + sha256: 218913aeee391460bd0e341b834dbd9c6fa6ae0a4276c0c300266cc99a816a28 + md5: 52f1280563f3b48b5f75414cd2d15dd1 depends: - - libgcc-ng >=12 - license: GPL-2.0-or-later - license_family: GPL + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.2,<2.0a0 + license: zlib-acknowledgement purls: [] - size: 897548 - timestamp: 1660323080555 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x264-1!164.3095-h4e544f5_2.tar.bz2 - sha256: b48f150db8c052c197691c9d76f59e252d3a7f01de123753d51ebf2eed1cf057 - md5: 0efaf807a0b5844ce5f605bd9b668281 + run_exports: + weak: + - libpng >=1.6.58,<1.7.0a0 + size: 385227 + timestamp: 1776315248638 +- conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-6.33.5-h637c107_2.conda + sha256: b5ad018d96953534c4f1b513af3f73581e7e8fbfc7b54b3d67d40c05fb42885d + md5: 4493b928107c306fa19455841de1b4bb depends: - - libgcc-ng >=12 - license: GPL-2.0-or-later - license_family: GPL + - libabseil * cxx17* + - libabseil >=20260107.1,<20260108.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1000661 - timestamp: 1660324722559 -- conda: https://conda.anaconda.org/conda-forge/osx-64/x264-1!164.3095-h775f41a_2.tar.bz2 - sha256: de611da29f4ed0733a330402e163f9260218e6ba6eae593a5f945827d0ee1069 - md5: 23e9c3180e2c0f9449bb042914ec2200 - license: GPL-2.0-or-later - license_family: GPL + run_exports: + weak: + - libprotobuf >=6.33.5,<6.33.6.0a0 + size: 6950375 + timestamp: 1783169980984 +- conda: https://conda.anaconda.org/conda-forge/win-64/libpsl-0.22.0-h25e0afd_1.conda + sha256: 040d4adabae2634b13183203e383c21f74ed98028b5d50bf9bae4968dd05aaa5 + md5: ba200e33e10ccf0d730c4ce87badbdf1 + depends: + - icu >=78.3,<79.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT purls: [] - size: 937077 - timestamp: 1660323305349 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/x264-1!164.3095-h57fd34a_2.tar.bz2 - sha256: debdf60bbcfa6a60201b12a1d53f36736821db281a28223a09e0685edcce105a - md5: b1f6dccde5d3a1f911960b6e567113ff - license: GPL-2.0-or-later - license_family: GPL + run_exports: + weak: + - libpsl >=0.22.0,<0.23.0a0 + size: 72405 + timestamp: 1783937048984 +- conda: https://conda.anaconda.org/conda-forge/win-64/librsvg-2.58.4-h5ce5fed_3.conda + sha256: 8910bc40a52f2b979ced95137f09b8faf0113e14c430ca8fa7dd94dc88dafb83 + md5: 34fefcb3aed33ea39f1b040f5b9849e3 + depends: + - cairo >=1.18.4,<2.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - libglib >=2.84.0,<3.0a0 + - libxml2 >=2.13.7,<2.14.0a0 + - pango >=1.56.3,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.42.34438 + license: LGPL-2.1-or-later purls: [] - size: 717038 - timestamp: 1660323292329 -- conda: https://conda.anaconda.org/conda-forge/win-64/x264-1!164.3095-h8ffe710_2.tar.bz2 - sha256: 97166b318f8c68ffe4d50b2f4bd36e415219eeaef233e7d41c54244dc6108249 - md5: 19e39905184459760ccb8cf5c75f148b + run_exports: + weak: + - librsvg >=2.58.4,<3.0a0 + size: 3919170 + timestamp: 1743369262131 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.3-hf5d6505_0.conda + sha256: 692dfb73a22c873656d5e393b8f1e2b019a3c8a6486c97cb6900552e64e38c25 + md5: 051f1b2228e7517a2ef8cca5146c8967 depends: - - vc >=14.1,<15 - - vs2015_runtime >=14.16.27033 - license: GPL-2.0-or-later - license_family: GPL + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing purls: [] - size: 1041889 - timestamp: 1660323726084 -- conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 - sha256: 76c7405bcf2af639971150f342550484efac18219c0203c5ee2e38b8956fe2a0 - md5: e7f6ed84d4623d52ee581325c1587a6b + run_exports: + weak: + - libsqlite >=3.53.3,<4.0a0 + size: 1315909 + timestamp: 1782519131898 +- conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + sha256: cbdf93898f2e27cefca5f3fe46519335d1fab25c4ea2a11b11502ff63e602c09 + md5: 9dce2f112bfd3400f4f432b3d0ac07b2 depends: - - libgcc-ng >=10.3.0 - - libstdcxx-ng >=10.3.0 - license: GPL-2.0-or-later - license_family: GPL + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 3357188 - timestamp: 1646609687141 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/x265-3.5-hdd96247_3.tar.bz2 - sha256: cb2227f2441499900bdc0168eb423d7b2056c8fd5a3541df4e2d05509a88c668 - md5: 786853760099c74a1d4f0da98dd67aea + run_exports: + weak: + - libssh2 >=1.11.1,<2.0a0 + size: 292785 + timestamp: 1745608759342 +- conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.2-h8f73337_0.conda + sha256: ec6d66308a6d6abaf3225f2f185113e6172e77eb0fa8622af982d7a5d6d47a2c + md5: e83f459471905a04ebe15e21d063c49d + depends: + - lerc >=4.1.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libjpeg-turbo >=3.1.4.1,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + run_exports: + weak: + - libtiff >=4.7.2,<4.8.0a0 + size: 1014598 + timestamp: 1783085017197 +- conda: https://conda.anaconda.org/conda-forge/win-64/libusb-1.0.29-h1839187_0.conda + sha256: 9837f8e8de20b6c9c033561cd33b4554cd551b217e3b8d2862b353ed2c23d8b8 + md5: a656b2c367405cd24988cf67ff2675aa depends: - - libgcc-ng >=10.3.0 - - libstdcxx-ng >=10.3.0 - license: GPL-2.0-or-later - license_family: GPL + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + license: LGPL-2.1-or-later purls: [] - size: 1018181 - timestamp: 1646610147365 -- conda: https://conda.anaconda.org/conda-forge/osx-64/x265-3.5-hbb4e6a2_3.tar.bz2 - sha256: 6b6a57710192764d0538f72ea1ccecf2c6174a092e0bc76d790f8ca36bbe90e4 - md5: a3bf3e95b7795871a6734a784400fcea + run_exports: + weak: + - libusb >=1.0.29,<2.0a0 + size: 118204 + timestamp: 1748856290542 +- conda: https://conda.anaconda.org/conda-forge/win-64/libuv-1.52.1-h6a83c73_0.conda + sha256: ca55710ece8736785ffa0fad4d45402dd40992a81a045d69eda5d40bc1a288f9 + md5: 741d96e586ac833409e5d27cdae08d15 depends: - - libcxx >=12.0.1 - license: GPL-2.0-or-later - license_family: GPL + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT purls: [] - size: 3433205 - timestamp: 1646610148268 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/x265-3.5-hbc6ce65_3.tar.bz2 - sha256: 2fed6987dba7dee07bd9adc1a6f8e6c699efb851431bcb6ebad7de196e87841d - md5: b1f7f2780feffe310b068c021e8ff9b2 + run_exports: + weak: + - libuv >=1.52.1,<2.0a0 + size: 331213 + timestamp: 1779396042250 +- conda: https://conda.anaconda.org/conda-forge/win-64/libvorbis-1.3.7-h5112557_2.conda + sha256: 429124709c73b2e8fae5570bdc6b42f5418a7551ba72e591bb960b752e87b365 + md5: 42a8a56c60882da5d451aa95b8455111 depends: - - libcxx >=12.0.1 - license: GPL-2.0-or-later - license_family: GPL + - libogg + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libogg >=1.3.5,<1.4.0a0 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 1832744 - timestamp: 1646609481185 -- conda: https://conda.anaconda.org/conda-forge/win-64/x265-3.5-h2d74725_3.tar.bz2 - sha256: 02b9874049112f2b7335c9a3e880ac05d99a08d9a98160c5a98898b2b3ac42b2 - md5: ca7129a334198f08347fb19ac98a2de9 + run_exports: + weak: + - libvorbis >=1.3.7,<1.4.0a0 + size: 243401 + timestamp: 1753879416570 +- conda: https://conda.anaconda.org/conda-forge/win-64/libvulkan-loader-1.4.341.0-h477610d_0.conda + sha256: 0f0965edca8b255187604fc7712c53fe9064b31a1845a7dfb2b63bf660de84a7 + md5: 804880b2674119b84277d6c16b01677d depends: - - vc >=14.1,<15 - - vs2015_runtime >=14.16.27033 - license: GPL-2.0-or-later - license_family: GPL + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + constrains: + - libvulkan-headers 1.4.341.0.* + license: Apache-2.0 + license_family: APACHE purls: [] - size: 5517425 - timestamp: 1646611941216 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda - sha256: a5d4af601f71805ec67403406e147c48d6bad7aaeae92b0622b7e2396842d3fe - md5: 397a013c2dc5145a70737871aaa87e98 + run_exports: + weak: + - libvulkan-loader >=1.4.341.0,<2.0a0 + size: 282251 + timestamp: 1770077165680 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.9-h741aa76_0.conda + sha256: 28ac5bbed11644b9e06241ba1dfdac7e3a99e74b69915d45f646717ad9645ca5 + md5: 333d21ab129d5fa5742225bf1d7557a5 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - xorg-libx11 >=1.8.12,<2.0a0 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: [] - size: 392406 - timestamp: 1749375847832 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.45-h86ecc28_0.conda - sha256: 730ff2f6fbfecce94db54bbf3f1ae0ce79c54b6abc089f8a65a041525228d454 - md5: 01251d1503a253e39be4fa9bcf447d63 + run_exports: + weak: + - libxml2 >=2.13.9,<2.14.0a0 + size: 1521446 + timestamp: 1761766307746 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + sha256: 88609816e0cc7452bac637aaf65783e5edf4fee8a9f8e22bdc3a75882c536061 + md5: dbabbd6234dea34040e631f87676292f depends: - - libgcc >=13 - - xorg-libx11 >=1.8.12,<2.0a0 - license: MIT - license_family: MIT + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other purls: [] - size: 392754 - timestamp: 1749375869926 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda - sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b - md5: fb901ff28063514abb6046c9ec2c4a45 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 58347 + timestamp: 1774072851498 +- conda: https://conda.anaconda.org/conda-forge/win-64/llvm-tools-16.0.6-h2a44499_4.conda + sha256: 52391e45844178e99bceb8ea2e5e94c6ce52aef4cec6680cb4e14dc7935b1d3c + md5: 2e294d91c3e7d972e07fbfa7d6d95bf5 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT + - libllvm16 16.0.6 h2a44499_4 + - libxml2 >=2.13.5,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - zstd >=1.5.6,<1.6.0a0 + constrains: + - llvmdev 16.0.6 + - clang 16.0.6.* + - clang-tools 16.0.6.* + - llvm 16.0.6.* + license: Apache-2.0 WITH LLVM-exception + license_family: Apache purls: [] - size: 58628 - timestamp: 1734227592886 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda - sha256: a2ba1864403c7eb4194dacbfe2777acf3d596feae43aada8d1b478617ce45031 - md5: c8d8ec3e00cd0fd8a231789b91a7c5b7 + run_exports: {} + size: 348508595 + timestamp: 1739802687257 +- conda: https://conda.anaconda.org/conda-forge/win-64/lychee-0.23.0-hb3eb754_0.conda + sha256: 28ee5a30bea795d00df2151fa000f32c16a605e6e5743600f674cd2234864f67 + md5: cbe8bd414d963d87749cfd47659d55c4 depends: - - libgcc >=13 - license: MIT - license_family: MIT + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - openssl >=3.5.5,<4.0a0 + license: Apache-2.0 OR MIT purls: [] - size: 60433 - timestamp: 1734229908988 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda - sha256: 277841c43a39f738927145930ff963c5ce4c4dacf66637a3d95d802a64173250 - md5: 1c74ff8c35dcadf952a16f752ca5aa49 + run_exports: {} + size: 5763830 + timestamp: 1771270396939 +- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2 + sha256: 9de95a7996d5366ae0808eef2acbc63f9b11b874aa42375f55379e6715845dc6 + md5: 066552ac6b907ec6d72c0ddab29050dc depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libuuid >=2.38.1,<3.0a0 - - xorg-libice >=1.1.2,<2.0a0 - license: MIT - license_family: MIT + - m2w64-gcc-libs-core + - msys2-conda-epoch ==20160418 + license: GPL, LGPL, FDL, custom purls: [] - size: 27590 - timestamp: 1741896361728 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda - sha256: b86a819cd16f90c01d9d81892155126d01555a20dabd5f3091da59d6309afd0a - md5: 2d1409c50882819cb1af2de82e2b7208 + run_exports: {} + size: 350687 + timestamp: 1608163451316 +- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2 + sha256: 3bd1ab02b7c89a5b153a17be03b36d833f1517ff2a6a77ead7c4a808b88196aa + md5: fe759119b8b3bfa720b8762c6fdc35de depends: - - libgcc >=13 - - libuuid >=2.38.1,<3.0a0 - - xorg-libice >=1.1.2,<2.0a0 - license: MIT - license_family: MIT + - m2w64-gcc-libgfortran + - m2w64-gcc-libs-core + - m2w64-gmp + - m2w64-libwinpthread-git + - msys2-conda-epoch ==20160418 + license: GPL3+, partial:GCCRLE, partial:LGPL2+ purls: [] - size: 28701 - timestamp: 1741897678254 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda - sha256: 51909270b1a6c5474ed3978628b341b4d4472cd22610e5f22b506855a5e20f67 - md5: db038ce880f100acc74dba10302b5630 + run_exports: {} + size: 532390 + timestamp: 1608163512830 +- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-core-5.3.0-7.tar.bz2 + sha256: 58afdfe859ed2e9a9b1cc06bc408720cb2c3a6a132e59d4805b090d7574f4ee0 + md5: 4289d80fb4d272f1f3b56cfe87ac90bd depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libxcb >=1.17.0,<2.0a0 - license: MIT - license_family: MIT + - m2w64-gmp + - m2w64-libwinpthread-git + - msys2-conda-epoch ==20160418 + license: GPL3+, partial:GCCRLE, partial:LGPL2+ purls: [] - size: 835896 - timestamp: 1741901112627 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.12-hca56bd8_0.conda - sha256: 452977d8ad96f04ec668ba74f46e70a53e00f99c0e0307956aeca75894c8131d - md5: 3df132f0048b9639bc091ef22937c111 + run_exports: {} + size: 219240 + timestamp: 1608163481341 +- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2 + sha256: 7e3cd95f554660de45f8323fca359e904e8d203efaf07a4d311e46d611481ed1 + md5: 53a1c73e1e3d185516d7e3af177596d9 depends: - - libgcc >=13 - - libxcb >=1.17.0,<2.0a0 - license: MIT - license_family: MIT + - msys2-conda-epoch ==20160418 + license: LGPL3 purls: [] - size: 864850 - timestamp: 1741901264068 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda - sha256: ed10c9283974d311855ae08a16dfd7e56241fac632aec3b92e3cfe73cff31038 - md5: f6ebe2cb3f82ba6c057dde5d9debe4f7 + run_exports: {} + size: 743501 + timestamp: 1608163782057 +- conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 + sha256: f63a09b2cae7defae0480f1740015d6235f1861afa6fe2e2d3e10bd0d1314ee0 + md5: 774130a326dee16f1ceb05cc687ee4f0 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT + - msys2-conda-epoch ==20160418 + license: MIT, BSD purls: [] - size: 14780 - timestamp: 1734229004433 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda - sha256: 7829a0019b99ba462aece7592d2d7f42e12d12ccd3b9614e529de6ddba453685 - md5: d5397424399a66d33c80b1f2345a36a6 + run_exports: {} + size: 31928 + timestamp: 1608166099896 +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py311h3f79411_1.conda + sha256: 3d37fb1900e31131f84549560e7a4bfea5f39aa3ecd73345fef1f33975cf0baa + md5: f55de41c947bdd2ff9bbeffedf8089f7 depends: - - libgcc >=13 - license: MIT - license_family: MIT + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + run_exports: {} + size: 29362 + timestamp: 1772445178723 +- conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2 + sha256: 99358d58d778abee4dca82ad29fb58058571f19b0f86138363c260049d4ac7f1 + md5: b0309b72560df66f71a9d5e34a5efdfa purls: [] - size: 15873 - timestamp: 1734230458294 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda - sha256: 832f538ade441b1eee863c8c91af9e69b356cd3e9e1350fff4fe36cc573fc91a - md5: 2ccd714aa2242315acaf0a67faea780b + run_exports: {} + size: 3227 + timestamp: 1608166968312 +- conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py311h3f79411_0.conda + sha256: b161957677bc3f7e98615d1a4d9e95e8bdf42763e7934365f9e61bb93301163b + md5: a9a3bce78a5f5b7f2be14c11984a3cf2 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - xorg-libx11 >=1.8.10,<2.0a0 - - xorg-libxfixes >=6.0.1,<7.0a0 - - xorg-libxrender >=0.9.11,<0.10.0a0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/multidict?source=hash-mapping + run_exports: {} + size: 92622 + timestamp: 1771610838436 +- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.14.1-py311he736701_0.conda + sha256: 12a90fb2507dd5c56a0e846bf828fe8b3197aa79ec8d655934d455d20101a640 + md5: a3f3aebd6fbdbdec85098e24d14f89aa + depends: + - mypy_extensions >=1.0.0 + - psutil >=4.0 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - typing_extensions >=4.1.0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 license: MIT license_family: MIT + purls: + - pkg:pypi/mypy?source=hash-mapping + run_exports: {} + size: 10553025 + timestamp: 1735600107955 +- conda: https://conda.anaconda.org/conda-forge/win-64/nasm-2.16.03-hfd05255_1.conda + sha256: cce00ed17e684bf84c8cc592de578fedfb93b2d2357256c41c262b67ceacf6e7 + md5: ead716d50b01f09d327c781c05b25882 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.40.33810 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 32533 - timestamp: 1730908305254 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda - sha256: c5d3692520762322a9598e7448492309f5ee9d8f3aff72d787cf06e77c42507f - md5: f2054759c2203d12d0007005e1f1296d + run_exports: {} + size: 450395 + timestamp: 1721653214123 +- conda: https://conda.anaconda.org/conda-forge/win-64/ninja-1.11.1-h91493d7_0.conda + sha256: 0ffb1912768af8354a930f482368ef170bf3d8217db328dfea1c8b09772c8c71 + md5: 44a99ef26178ea98626ff8e027702795 depends: - - libgcc >=13 - - xorg-libx11 >=1.8.9,<2.0a0 - - xorg-libxfixes >=6.0.1,<7.0a0 - - xorg-libxrender >=0.9.11,<0.10.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vs2015_runtime >=14.29.30139 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: {} + size: 279200 + timestamp: 1676838681615 +- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-24.18.0-h80d1838_0.conda + sha256: 2c0661b2f77e1ca912da9038416ed886dd66a4ab47e2105d8e395c5185157dd2 + md5: 634f517a73580675d03145b7bf802b14 license: MIT license_family: MIT purls: [] - size: 34596 - timestamp: 1730908388714 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda - sha256: 6b250f3e59db07c2514057944a3ea2044d6a8cdde8a47b6497c254520fade1ee - md5: 8035c64cb77ed555e3f150b7b3972480 + run_exports: + weak: + - nodejs >=24.18.0,<25.0a0 + size: 30836274 + timestamp: 1782396052125 +- conda: https://conda.anaconda.org/conda-forge/win-64/openh264-2.6.0-h1eab103_1.conda + sha256: 8d7e4a2dcd68afcc87c1e875a19600d980ca8f792f00105a622efd5faed6b05a + md5: 91c186a483e5491170156399b2850804 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 19901 - timestamp: 1727794976192 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda - sha256: efcc150da5926cf244f757b8376d96a4db78bc15b8d90ca9f56ac6e75755971f - md5: 25a5a7b797fe6e084e04ffe2db02fc62 + run_exports: + weak: + - openh264 >=2.6.0,<2.6.1.0a0 + size: 422904 + timestamp: 1782686043511 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + sha256: cb6e7ba0d010ee0d3249ce9886de3d7613d26d9965d4c95666fa66b9c4c31001 + md5: e99f95734a326c0fd4d02bbd995150d4 depends: - - libgcc >=13 - license: MIT - license_family: MIT + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache purls: [] - size: 20615 - timestamp: 1727796660574 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda - sha256: da5dc921c017c05f38a38bd75245017463104457b63a1ce633ed41f214159c14 - md5: febbab7d15033c913d53c7a2c102309d + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 9414790 + timestamp: 1781071745579 +- conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h13911b6_1.conda + sha256: 3d4e6e541e633f6fd22fc2c1d79ad5ec39503dea3ba04fc3e01d5be904ec7cea + md5: 1f1cf3772ba7d4eef989e4679ddf97f7 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - xorg-libx11 >=1.8.10,<2.0a0 - license: MIT - license_family: MIT + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.17.1,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.16,<2.0a0 + - harfbuzz >=13.2.1 + - libexpat >=2.7.4,<3.0a0 + - libfreetype >=2.14.2 + - libfreetype6 >=2.14.2 + - libglib >=2.86.4,<3.0a0 + - libpng >=1.6.55,<1.7.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-2.1-or-later purls: [] - size: 50060 - timestamp: 1727752228921 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.6-h57736b2_0.conda - sha256: 8e216b024f52e367463b4173f237af97cf7053c77d9ce3e958bc62473a053f71 - md5: bd1e86dd8aa3afd78a4bfdb4ef918165 + run_exports: + weak: + - pango >=1.56.4,<2.0a0 + size: 454919 + timestamp: 1774282149607 +- conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.47-hd2b5f0e_0.conda + sha256: 3e9e02174edf02cb4bcdd75668ad7b74b8061791a3bc8bdb8a52ae336761ba3e + md5: 77eaf2336f3ae749e712f63e36b0f0a1 depends: - - libgcc >=13 - - xorg-libx11 >=1.8.9,<2.0a0 - license: MIT - license_family: MIT + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 50746 - timestamp: 1727754268156 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda - sha256: 2fef37e660985794617716eb915865ce157004a4d567ed35ec16514960ae9271 - md5: 4bdb303603e9821baf5fe5fdff1dc8f8 + run_exports: + weak: + - pcre2 >=10.47,<10.48.0a0 + size: 995992 + timestamp: 1763655708300 +- conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_2.conda + sha256: f37fb21952bd4297f8c7d78e2f256647da2b4ad4e1df097d49ebff8a40275cab + md5: df8da7fe89bdc91b880df69a8eb1c37b depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - xorg-libx11 >=1.8.10,<2.0a0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 license: MIT license_family: MIT purls: [] - size: 19575 - timestamp: 1727794961233 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.1-h57736b2_0.conda - sha256: f5c71e0555681a82a65c483374b91d91b2cb9a9903b3a22ddc00f36719fce549 - md5: 78f8715c002cc66991d7c11e3cf66039 + run_exports: + weak: + - pixman >=0.46.4,<1.0a0 + size: 257105 + timestamp: 1784286884376 +- conda: https://conda.anaconda.org/conda-forge/win-64/prettier-3.9.3-hc21fffc_0.conda + sha256: 4dc409cfb7d9bb40f7739814b7314c616ee5ad228996e7126d5cfab70815d947 + md5: eb38604a838e8ce2f8b42ee5633c5ee6 depends: - - libgcc >=13 - - xorg-libx11 >=1.8.9,<2.0a0 + - nodejs + - vc >=14.5,<15 + - vc14_runtime >=14.51.36231 + - ucrt >=10.0.20348.0 + - nodejs >=24.18.0,<25.0a0 license: MIT license_family: MIT purls: [] - size: 20289 - timestamp: 1727796500830 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - sha256: 044c7b3153c224c6cedd4484dd91b389d2d7fd9c776ad0f4a34f099b3389f4a1 - md5: 96d57aba173e878a2089d5638016dc5e + run_exports: {} + size: 1372486 + timestamp: 1782740672849 +- conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.5.2-py311h3f79411_0.conda + sha256: f9ea426edb6372afd7cb626adea0f214512181aa6707eb65a4d9153566b13e72 + md5: 2d4a3e8b0a30b7b1e96a3a576ade3497 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - xorg-libx11 >=1.8.10,<2.0a0 - license: MIT - license_family: MIT + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/propcache?source=hash-mapping + run_exports: {} + size: 49165 + timestamp: 1780037808046 +- conda: https://conda.anaconda.org/conda-forge/win-64/protobuf-6.33.5-py311heca59f8_2.conda + sha256: 681faeae29b838ee42253f04ed78ab616a73ec1ed544e27f3ba5625fceda73bf + md5: c13b999290f335112ff0cad8586ac765 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libprotobuf 6.33.5 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/protobuf?source=hash-mapping + run_exports: {} + size: 495980 + timestamp: 1773266600716 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.2.2-py311hf893f09_0.conda + sha256: 32da17824abadd1f5b46faedfa4964c7b1817b11887c2e8bb4e48628da51b93a + md5: fd968cdacc7967efd0ff5ef1805b812c + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + run_exports: {} + size: 249478 + timestamp: 1769678166841 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.11.15-h0159041_1_cpython.conda + build_number: 1 + sha256: 32716d8df907696e856cbd4cdcc5fe89ddae01c7c9a8cc99bd42260bf6d9a4a2 + md5: 06b84fcf19e4d5101a1d105d15dcfc88 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.3,<6.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 purls: [] - size: 33005 - timestamp: 1734229037766 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda - sha256: ffd77ee860c9635a28cfda46163dcfe9224dc6248c62404c544ae6b564a0be1f - md5: ae2c2dd0e2d38d249887727db2af960e + run_exports: + weak: + - python_abi 3.11.* *_cp311 + noarch: + - python + size: 18439395 + timestamp: 1781148714198 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-312-py311hefeebc8_0.conda + sha256: f77bdcffb3def47a4747829939fd3c9f7a5b4ce1a1a7019039371b665e98416f + md5: 233e99d07b434a27c39b43b559252ef6 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/pywin32?source=hash-mapping + run_exports: {} + size: 4482214 + timestamp: 1781362876562 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_1.conda + sha256: 301c3ba100d25cd5ae37895988ee3ab986210d4d972aa58efed948fbe857773d + md5: a0153c033dc55203e11d1cac8f6a9cf2 + depends: + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + run_exports: {} + size: 187108 + timestamp: 1770223467913 +- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-2026.6.3-py311h7337c20_0.conda + sha256: 3d3642bd56d248c6cdcd31831fc0cafaa5a9a3376e2d760d781f930ab0caa81e + md5: 1ae11d687fc860a2cd3b68e9c50c8dcc depends: - - libgcc >=13 - - xorg-libx11 >=1.8.10,<2.0a0 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.11.* *_cp311 license: MIT license_family: MIT - purls: [] - size: 33649 - timestamp: 1734229123157 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxscrnsaver-1.2.4-hb9d3cd8_0.conda - sha256: 58e8fc1687534124832d22e102f098b5401173212ac69eb9fd96b16a3e2c8cb2 - md5: 303f7a0e9e0cd7d250bb6b952cecda90 + purls: + - pkg:pypi/rpds-py?source=hash-mapping + run_exports: {} + size: 222540 + timestamp: 1782831456252 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.15.7-h02f8532_1.conda + noarch: python + sha256: 998087b7aef322be09d8e4db7726012ec54d67d12f0622c0001c68660cfa6012 + md5: bff0dba1b6297c0c35169c9b85809b3c depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - xorg-libx11 >=1.8.10,<2.0a0 - - xorg-libxext >=1.3.6,<2.0a0 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 license: MIT license_family: MIT - purls: [] - size: 14412 - timestamp: 1727899730073 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.1-hbcc6ac9_2.conda - sha256: 802725371682ea06053971db5b4fb7fbbcaee9cb1804ec688f55e51d74660617 - md5: 68eae977d7d1196d32b636a026dc015d + purls: + - pkg:pypi/ruff?source=hash-mapping + run_exports: {} + size: 9696461 + timestamp: 1774012631179 +- conda: https://conda.anaconda.org/conda-forge/win-64/sdl2-2.32.56-h5112557_0.conda + sha256: d17da21386bdbf32bce5daba5142916feb95eed63ef92b285808c765705bbfd2 + md5: 4cffbfebb6614a1bff3fc666527c25c7 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - liblzma 5.8.1 hb9d3cd8_2 - - liblzma-devel 5.8.1 hb9d3cd8_2 - - xz-gpl-tools 5.8.1 hbcc6ac9_2 - - xz-tools 5.8.1 hb9d3cd8_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - sdl3 >=3.2.22,<4.0a0 + license: Zlib purls: [] - size: 23987 - timestamp: 1749230104359 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.8.1-h2dbfc1b_2.conda - sha256: f8b2b55a672402bf6c870529c20a1a006f102e1604682d83c30a57ec9f3de55a - md5: 176b552740e8836d92c4935244d6756b + run_exports: + weak: + - sdl2 >=2.32.56,<3.0a0 + size: 572101 + timestamp: 1757842925694 +- conda: https://conda.anaconda.org/conda-forge/win-64/sdl3-3.4.12-h5112557_0.conda + sha256: cd70a95559fdcaa9809d7c697b1d2e283c2d61eb184f91f7b03f8c2291e206a8 + md5: 5f80121d90de6623ae8e0eee34da16ff depends: - - libgcc >=13 - - liblzma 5.8.1 h86ecc28_2 - - liblzma-devel 5.8.1 h86ecc28_2 - - xz-gpl-tools 5.8.1 h2dbfc1b_2 - - xz-tools 5.8.1 h86ecc28_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libvulkan-loader >=1.4.341.0,<2.0a0 + - libusb >=1.0.29,<2.0a0 + license: Zlib purls: [] - size: 23963 - timestamp: 1749232914469 -- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.8.1-h357f2ed_2.conda - sha256: 89248de6c9417522b6fec011dc26b81c25af731a31ba91e668f72f1b9aab05d7 - md5: 7eee908c7df8478c1f35b28efa2e42b1 + run_exports: + weak: + - sdl3 >=3.4.12,<4.0a0 + size: 1680362 + timestamp: 1782948074728 +- conda: https://conda.anaconda.org/conda-forge/win-64/svt-av1-3.1.2-hac47afa_0.conda + sha256: 444c94a9c1fcb2cdf78b260472451990257733bcf89ed80c73db36b5047d3134 + md5: 91866412570c922f55178855deb0f952 depends: - - __osx >=10.13 - - liblzma 5.8.1 hd471939_2 - - liblzma-devel 5.8.1 hd471939_2 - - xz-gpl-tools 5.8.1 h357f2ed_2 - - xz-tools 5.8.1 hd471939_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD purls: [] - size: 24033 - timestamp: 1749230223096 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.8.1-h9a6d368_2.conda - sha256: afb747cf017b67cc31d54c6e6c4bd1b1e179fe487a3d23a856232ed7fd0b099b - md5: 39435c82e5a007ef64cbb153ecc40cfd + run_exports: + weak: + - svt-av1 >=3.1.2,<3.1.3.0a0 + size: 1862756 + timestamp: 1756086862067 +- conda: https://conda.anaconda.org/conda-forge/win-64/taplo-0.9.1-h7f3b576_0.conda + sha256: 7ef6b5f23fd749fde17628793e4e76e36395b9645a3d3b8b0fa5a4d9b2b9ccfb + md5: 0a798b7bf999885c00e40fcb0cfe7136 depends: - - __osx >=11.0 - - liblzma 5.8.1 h39f12f2_2 - - liblzma-devel 5.8.1 h39f12f2_2 - - xz-gpl-tools 5.8.1 h9a6d368_2 - - xz-tools 5.8.1 h39f12f2_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + - m2w64-gcc-libs + - m2w64-gcc-libs-core + license: MIT + license_family: MIT purls: [] - size: 23995 - timestamp: 1749230346887 -- conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.1-h208afaa_2.conda - sha256: 22289a81da4698bb8d13ac032a88a4a1f49505b2303885e1add3d8bd1a7b56e6 - md5: fb3fa84ea37de9f12cc8ba730cec0bdc + run_exports: {} + size: 3924159 + timestamp: 1710794002174 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h967ab96_3.conda + sha256: 13fa29257d43f8e630a1e591ed77fae9bbbb236b011432f01e2034cf36e6bf03 + md5: aaf79e2af50a151fb5b5a3e3f38b7a69 depends: - - liblzma 5.8.1 h2466b09_2 - - liblzma-devel 5.8.1 h2466b09_2 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - xz-tools 5.8.1 h2466b09_2 - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + license: TCL purls: [] - size: 24430 - timestamp: 1749230691276 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.1-hbcc6ac9_2.conda - sha256: 840838dca829ec53f1160f3fca6dbfc43f2388b85f15d3e867e69109b168b87b - md5: bf627c16aa26231720af037a2709ab09 + run_exports: + weak: + - tk >=8.6.13,<8.7.0a0 + size: 3782314 + timestamp: 1784229072899 +- conda: https://conda.anaconda.org/conda-forge/win-64/ty-0.0.31-hc21aad4_0.conda + noarch: python + sha256: 617dadab217a0b516a377577c090b96fc16eb866451d552ff517781a74709266 + md5: ba95660274577c41d912f7987588b35a depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - liblzma 5.8.1 hb9d3cd8_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later - purls: [] - size: 33911 - timestamp: 1749230090353 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-gpl-tools-5.8.1-h2dbfc1b_2.conda - sha256: 1e328310210b507064d6b5916c66ce49d4e1ba2fba5a710a5371e6e0432a4731 - md5: 0d5f95b450e75655b5e76ae626197383 + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - _python_abi3_support 1.* + - cpython >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ty?source=hash-mapping + run_exports: {} + size: 9582179 + timestamp: 1776273731957 +- conda: https://conda.anaconda.org/conda-forge/win-64/typos-1.48.0-h18a1a76_0.conda + sha256: 10f973b1a21082f06ef9c8388bb3b95ff9fc37054b18879c16fa443339e44633 + md5: 36422364f71f8110de1829127d0e99fc depends: - - libgcc >=13 - - liblzma 5.8.1 h86ecc28_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT OR Apache-2.0 purls: [] - size: 33948 - timestamp: 1749232746339 -- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-gpl-tools-5.8.1-h357f2ed_2.conda - sha256: 5cdadfff31de7f50d1b2f919dd80697c0a08d90f8d6fb89f00c93751ec135c3c - md5: d4044359fad6af47224e9ef483118378 - depends: - - __osx >=10.13 - - liblzma 5.8.1 hd471939_2 + run_exports: {} + size: 2919424 + timestamp: 1782859645300 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 purls: [] - size: 33890 - timestamp: 1749230206830 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-gpl-tools-5.8.1-h9a6d368_2.conda - sha256: a0790cfb48d240e7b655b0d797a00040219cf39e3ee38e2104e548515df4f9c2 - md5: 09b1442c1d49ac7c5f758c44695e77d1 + run_exports: {} + size: 694692 + timestamp: 1756385147981 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_39.conda + sha256: 17693b60cb54f80c60275f003f3bfc1b128af56dbfd65c4fae37c64eeb755ce1 + md5: 2eacea63f545b97342da520df6854276 depends: - - __osx >=11.0 - - liblzma 5.8.1 h39f12f2_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later + - vc14_runtime >=14.51.36231 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 34103 - timestamp: 1749230329933 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.1-hb9d3cd8_2.conda - sha256: 58034f3fca491075c14e61568ad8b25de00cb3ae479de3e69be6d7ee5d3ace28 - md5: 1bad2995c8f1c8075c6c331bf96e46fb + run_exports: {} + size: 20362 + timestamp: 1781320968457 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_39.conda + sha256: 8153ed849c92e891eacac0f2f8d7ecb79f9b5fd7f7917fbb896f252a60a40390 + md5: 06a5bf5a1ca16cce0df6eaa91fc42bc2 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - liblzma 5.8.1 hb9d3cd8_2 + - ucrt >=10.0.20348.0 + - vcomp14 14.51.36231 h1b9f54f_39 constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later + - vs2015_runtime 14.51.36231.* *_39 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary purls: [] - size: 96433 - timestamp: 1749230076687 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-tools-5.8.1-h86ecc28_2.conda - sha256: b8653678a9954303b948a4be79092101b926087c41fc06bd26573876bb6d3e2a - md5: 04a5f1734c23daf8c7fe59045f755efb + run_exports: {} + size: 737434 + timestamp: 1781320964561 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_39.conda + sha256: 07fb14713c4bc62e2533a2e23a363abfb0e65650681fba0ae4c840e2219350f3 + md5: 8b53a83fda40ec679e4d63fa32fae989 depends: - - libgcc >=13 - - liblzma 5.8.1 h86ecc28_2 + - ucrt >=10.0.20348.0 constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later + - vs2015_runtime 14.51.36231.* *_39 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary purls: [] - size: 101611 - timestamp: 1749232578309 -- conda: https://conda.anaconda.org/conda-forge/osx-64/xz-tools-5.8.1-hd471939_2.conda - sha256: 3b1d8958f8dceaa4442100d5326b2ec9bcc2e8d7ee55345bf7101dc362fb9868 - md5: 349148960ad74aece88028f2b5c62c51 + run_exports: + strong: + - vcomp14 >=14.51.36231 + size: 120684 + timestamp: 1781320948530 +- conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.51.36231-h84cd919_39.conda + sha256: 6de6c2cf008fc2dce61060b583f2d8494c83883106952b201381b6b0505f03d7 + md5: 2ccc63d7b7d066a814ed9f99072832d7 depends: - - __osx >=10.13 - - liblzma 5.8.1 hd471939_2 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later + - vc14_runtime >=14.51.36231 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 85777 - timestamp: 1749230191007 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-tools-5.8.1-h39f12f2_2.conda - sha256: 9d1232705e3d175f600dc8e344af9182d0341cdaa73d25330591a28532951063 - md5: 37996935aa33138fca43e4b4563b6a28 + run_exports: {} + size: 20355 + timestamp: 1781320968804 +- conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.37.32822-h0123c8e_17.conda + sha256: 259b5d4ac07b131bf15bf1a2d101eb9eb039e32cfef57de79061cb4c8f1889fe + md5: 8b02594cf497f7516a3ed20a164de75e depends: - - __osx >=11.0 - - liblzma 5.8.1 h39f12f2_2 + - vswhere constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later + - vs_win-64 2022.* + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD purls: [] - size: 86425 - timestamp: 1749230316106 -- conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.1-h2466b09_2.conda - sha256: 38712f0e62f61741ab69d7551fa863099f5be769bdf9fdbc28542134874b4e88 - md5: e1b62ec0457e6ba10287a49854108fdb + run_exports: + strong: + - vc >=14.3,<15 + - vc14_runtime >=14.36.32532 + - ucrt >=10.0.20348.0 + size: 19405 + timestamp: 1694292390059 +- conda: https://conda.anaconda.org/conda-forge/win-64/wasm-pack-0.15.0-h18a1a76_0.conda + sha256: 706828365c117cfcc8c4c0e28e2d41d14061152c89a47b4e60dff4319b390651 + md5: c1d45f169e2717a1c22d5d275474df8a depends: - - liblzma 5.8.1 h2466b09_2 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - xz 5.8.1.* - license: 0BSD AND LGPL-2.1-or-later + license: MIT OR Apache-2.0 purls: [] - size: 67419 - timestamp: 1749230666460 -- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad - md5: a77f85f77be52ff59391544bfe73390a + run_exports: {} + size: 2095862 + timestamp: 1780752567368 +- conda: https://conda.anaconda.org/conda-forge/win-64/x264-1!164.3095-h8ffe710_2.tar.bz2 + sha256: 97166b318f8c68ffe4d50b2f4bd36e415219eeaef233e7d41c54244dc6108249 + md5: 19e39905184459760ccb8cf5c75f148b depends: - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - license: MIT - license_family: MIT + - vc >=14.1,<15 + - vs2015_runtime >=14.16.27033 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 85189 - timestamp: 1753484064210 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda - sha256: 66265e943f32ce02396ad214e27cb35f5b0490b3bd4f064446390f9d67fa5d88 - md5: 032d8030e4a24fe1f72c74423a46fb88 + run_exports: + weak: + - x264 >=1!164.3095,<1!165 + size: 1041889 + timestamp: 1660323726084 +- conda: https://conda.anaconda.org/conda-forge/win-64/x265-3.5-h2d74725_3.tar.bz2 + sha256: 02b9874049112f2b7335c9a3e880ac05d99a08d9a98160c5a98898b2b3ac42b2 + md5: ca7129a334198f08347fb19ac98a2de9 depends: - - libgcc >=14 - license: MIT - license_family: MIT + - vc >=14.1,<15 + - vs2015_runtime >=14.16.27033 + license: GPL-2.0-or-later + license_family: GPL purls: [] - size: 88088 - timestamp: 1753484092643 -- conda: https://conda.anaconda.org/conda-forge/osx-64/yaml-0.2.5-h4132b18_3.conda - sha256: a335161bfa57b64e6794c3c354e7d49449b28b8d8a7c4ed02bf04c3f009953f9 - md5: a645bb90997d3fc2aea0adf6517059bd + run_exports: + weak: + - x265 >=3.5,<3.6.0a0 + size: 5517425 + timestamp: 1646611941216 +- conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.8.3-hb6c8415_0.conda + sha256: 2c34f766619921fd543095f26a2688b845fe2cb372b71488d232ad3612630bb7 + md5: 46f70d503d68c55c104a564928a39852 depends: - - __osx >=10.13 - license: MIT - license_family: MIT + - liblzma 5.8.3 hfd05255_0 + - liblzma-devel 5.8.3 hfd05255_0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - xz-tools 5.8.3 hfd05255_0 + license: 0BSD AND LGPL-2.1-or-later AND GPL-2.0-or-later purls: [] - size: 79419 - timestamp: 1753484072608 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - sha256: b03433b13d89f5567e828ea9f1a7d5c5d697bf374c28a4168d71e9464f5dafac - md5: 78a0fe9e9c50d2c381e8ee47e3ea437d + run_exports: + weak: + - liblzma >=5.8.3,<6.0a0 + size: 24697 + timestamp: 1775825746585 +- conda: https://conda.anaconda.org/conda-forge/win-64/xz-tools-5.8.3-hfd05255_0.conda + sha256: 96b8212ff33dd8a1df8c0f69c0ad7b3551496c3068576408c0bedf9260febae2 + md5: 01b20ff45704b888d218f31a3aa3ea2a depends: - - __osx >=11.0 - license: MIT - license_family: MIT + - liblzma 5.8.3 hfd05255_0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.3.* + license: 0BSD AND LGPL-2.1-or-later purls: [] - size: 83386 - timestamp: 1753484079473 + run_exports: {} + size: 68149 + timestamp: 1775825719953 - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda sha256: 80ee68c1e7683a35295232ea79bcc87279d31ffeda04a1665efdb43cbd50a309 md5: 433699cba6602098ae8957a323da2664 @@ -14401,267 +17338,1174 @@ packages: - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - license: MIT - license_family: MIT - purls: [] - size: 63944 - timestamp: 1753484092156 -- conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.20.1-py311h2dc5d0c_0.conda - sha256: 9b6cce2794e836a43679d733b8bdbafeed45ff534c338b84d439ed55cd7b2170 - md5: 18c288aa6aae90e2fd8d1cf01d655e4f - depends: - - __glibc >=2.17,<3.0.a0 - - idna >=2.0 - - libgcc >=13 - - multidict >=4.0 - - propcache >=0.2.1 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/yarl?source=hash-mapping - size: 151355 - timestamp: 1749555157521 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.20.1-py311h58d527c_0.conda - sha256: 6a6d6b85422e8bf9e1b0ca09e414ae57e4aafe8797e990883f1d4b18c6fb6ff6 - md5: deff36ad6ad1800ab8aaaa9e587b7c37 - depends: - - idna >=2.0 - - libgcc >=13 - - multidict >=4.0 - - propcache >=0.2.1 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython - - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/yarl?source=hash-mapping - size: 151456 - timestamp: 1749555022085 -- conda: https://conda.anaconda.org/conda-forge/osx-64/yarl-1.20.1-py311ha3cf9ac_0.conda - sha256: 4873b587060f035d09dbbe0b227acba11d99e603ce9aea0a8b5b48453a3f0518 - md5: 2e33aec1ba23ef3ec45da91584972bc5 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - yaml >=0.2.5,<0.3.0a0 + size: 63944 + timestamp: 1753484092156 +- conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.24.5-py311h3f79411_0.conda + sha256: ee8c3fdbf5953b05207d0afa1d167da38952fe7ffb5acecc011fceae5f211a92 + md5: e5518a77be7ce826014ecbe729a941e3 depends: - - __osx >=10.13 - idna >=2.0 - multidict >=4.0 - propcache >=0.2.1 - python >=3.11,<3.12.0a0 - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/yarl?source=hash-mapping - size: 144813 - timestamp: 1749555109713 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.20.1-py311h4921393_0.conda - sha256: dd971901aabc65c20ae9e784ffa6c492b99c953a60e79f9c7f07338934dafc92 - md5: 2e3830e9460b7801d8926ab1a13cce85 + run_exports: {} + size: 168557 + timestamp: 1784526543484 +- conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + sha256: ef408f85f664a4b9c9dac3cb2e36154d9baa15a88984ea800e11060e0f2394a1 + md5: 5187ecf958be3c39110fe691cbd6873e + depends: + - libzlib 1.3.2 hfd05255_2 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Zlib + license_family: Other + purls: [] + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 850351 + timestamp: 1774072891049 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py311hf893f09_1.conda + sha256: 10f089bedef1a28c663ef575fb9cec66b2058e342c4cf4a753083ab07591008f + md5: b2d90bca78b57c17205ce3ca1c427813 depends: - - __osx >=11.0 - - idna >=2.0 - - multidict >=4.0 - - propcache >=0.2.1 - - python >=3.11,<3.12.0a0 - - python >=3.11,<3.12.0a0 *_cpython + - python + - cffi >=1.11 + - zstd >=1.5.7,<1.5.8.0a0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 - python_abi 3.11.* *_cp311 - license: Apache-2.0 - license_family: Apache + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause + license_family: BSD purls: - - pkg:pypi/yarl?source=hash-mapping - size: 144349 - timestamp: 1749555186043 -- conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.20.1-py311h5082efb_0.conda - sha256: f728006d9661123c6f28aa6044cdc7e5355b3b0ee20174897a9058ab8e660bcb - md5: f4f14f9f2092ace016e8e52822cb20da + - pkg:pypi/zstandard?source=hash-mapping + run_exports: {} + size: 375869 + timestamp: 1762512737575 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + sha256: 368d8628424966fd8f9c8018326a9c779e06913dd39e646cf331226acc90e5b2 + md5: 053b84beec00b71ea8ff7a4f84b55207 depends: - - idna >=2.0 - - multidict >=4.0 - - propcache >=0.2.1 - - python >=3.11,<3.12.0a0 - - python_abi 3.11.* *_cp311 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/yarl?source=hash-mapping - size: 143096 - timestamp: 1749555366270 -- pypi: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - zstd >=1.5.7,<1.6.0a0 + size: 388453 + timestamp: 1764777142545 +- pypi: ./rerun_pixi_env + name: rerun-pixi-env + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl + name: distlib + version: 0.4.3 + sha256: 4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b +- pypi: https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl + name: jaraco-functools + version: 4.6.0 + sha256: 99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30 + requires_dist: + - more-itertools + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-classes ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.14 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz + name: google-crc32c + version: 1.8.0 + sha256: a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + name: h11 + version: 0.16.0 + sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl + name: uv + version: 0.9.17 + sha256: 22fcc26755abebdf366becc529b2872a831ce8bb14b36b6a80d443a1d7f84d3b + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl + name: charset-normalizer + version: 3.4.9 + sha256: 0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/0d/0a/2b4e653186fc85061f0dfde43d602e7e93c08c0d75b23fa3577f9b3f83fd/hatch-1.17.1-py3-none-any.whl + name: hatch + version: 1.17.1 + sha256: cc6c06d302cfa785c35586ab4285c8c28a24b1744addb66344192302f6958083 + requires_dist: + - backports-zstd>=1.0.0 ; python_full_version < '3.14' + - click>=8.0.6 + - distro>=1.0.0 ; sys_platform == 'linux' + - hatchling>=1.27.0 + - httpx2>=0.22.0 + - hyperlink>=21.0.0 + - keyring>=23.5.0 + - packaging>=24.2 + - pexpect~=4.8 + - platformdirs>=2.5.0 + - pyproject-hooks + - python-discovery>=1.1 + - rich>=11.2.0 + - shellingham>=1.4.0 + - tomli-w>=1.0 + - tomlkit>=0.11.1 + - userpath~=1.7 + - uv>=0.5.23 + - virtualenv>=21 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl + name: distro + version: 1.9.0 + sha256: 7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl + name: uv + version: 0.9.17 + sha256: 330e7085857e4205c5196a417aca81cfbfa936a97dd2a0871f6560a88424ebf2 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl + name: truststore + version: 0.10.4 + sha256: adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl + name: httpx2 + version: 2.7.0 + sha256: ed2a2719c696789e09493bd8e2bec3d8bd925cc6e26b68389ec25ade132f7bf4 + requires_dist: + - anyio>=4.10 + - httpcore2==2.7.0 + - idna>=3.18 + - truststore>=0.10 + - typing-extensions>=4.5.0 ; python_full_version < '3.13' + - brotli ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi ; platform_python_implementation != 'CPython' and extra == 'brotli' + - click>=8.4 ; extra == 'cli' + - pygments==2.* ; extra == 'cli' + - rich>=10,<16 ; extra == 'cli' + - h2>=3,<5 ; extra == 'http2' + - socksio==1.* ; extra == 'socks' + - wsproto>=1.2 ; extra == 'ws' + - zstandard>=0.18.0 ; python_full_version < '3.14' and extra == 'zstd' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + name: ptyprocess + version: 0.7.0 + sha256: 4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 +- pypi: https://files.pythonhosted.org/packages/26/f8/a81170a816679fca9ccd907b801992acfc03c33f952440421c921af2cc57/cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl + name: cryptography + version: 38.0.4 + sha256: ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c + requires_dist: + - cffi>=1.12 + - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pyenchant>=1.6.11 ; extra == 'docstest' + - twine>=1.12.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' + - black ; extra == 'pep8test' + - flake8 ; extra == 'pep8test' + - flake8-import-order ; extra == 'pep8test' + - pep8-naming ; extra == 'pep8test' + - setuptools-rust>=0.11.4 ; extra == 'sdist' + - bcrypt>=3.1.5 ; extra == 'ssh' + - pytest>=6.2.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-subtests ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pretend ; extra == 'test' + - iso8601 ; extra == 'test' + - pytz ; extra == 'test' + - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl + name: wrapt + version: 2.2.2 + sha256: f1a2ff355ece6a111ca7a20dc86df6659c9205d3fcee674ca34f2a2854fd4e73 + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2a/f2/f22c19b4cdde429805ff5ac8dd77a95569a7c4cb8991741b2ff0d538f220/backports_zstd-1.6.0-cp311-cp311-win_amd64.whl + name: backports-zstd + version: 1.6.0 + sha256: 10b61850c4112952e05aa6e6cce8c9a5936fbeadb321e154216705cc76a14afa + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: uv + version: 0.9.17 + sha256: cd2c3d25fbd8f91b30d0fac69a13b8e2c2cd8e606d7e6e924c1423e4ff84e616 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: wrapt + version: 2.2.2 + sha256: a2d78c363f97d8bd718ee40432c66395685e9e98528ccaa423c3355d1715a26d + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + name: importlib-metadata + version: 9.0.0 + sha256: 2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 + requires_dist: + - zipp>=3.20 + - pytest>=6,!=8.1.* ; extra == 'test' + - packaging ; extra == 'test' + - pyfakefs ; extra == 'test' + - pytest-perf>=0.9.2 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - ipython ; extra == 'perf' + - pytest-checkdocs>=2.14 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl name: zipp - version: 3.23.0 - sha256: 071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e + version: 4.1.0 + sha256: 25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-itertools ; extra == 'test' + - jaraco-functools ; extra == 'test' + - more-itertools ; extra == 'test' + - big-o ; extra == 'test' + - pytest-ignore-flaky ; extra == 'test' + - jaraco-test ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.14 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 12.3.0 + sha256: 23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - setuptools ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: pynacl + version: 1.6.2 + sha256: 8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c + requires_dist: + - cffi>=1.4.1 ; python_full_version < '3.9' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - pytest>=7.4.0 ; extra == 'tests' + - pytest-cov>=2.10.1 ; extra == 'tests' + - pytest-xdist>=3.5.0 ; extra == 'tests' + - hypothesis>=3.27.0 ; extra == 'tests' + - sphinx<7 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl + name: pynacl + version: 1.6.2 + sha256: 62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0 + requires_dist: + - cffi>=1.4.1 ; python_full_version < '3.9' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - pytest>=7.4.0 ; extra == 'tests' + - pytest-cov>=2.10.1 ; extra == 'tests' + - pytest-xdist>=3.5.0 ; extra == 'tests' + - hypothesis>=3.27.0 ; extra == 'tests' + - sphinx<7 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + name: userpath + version: 1.9.2 + sha256: 2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d + requires_dist: + - click + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + name: pyasn1-modules + version: 0.4.2 + sha256: 29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a + requires_dist: + - pyasn1>=0.6.1,<0.7.0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/4d/25/a9e37dd035027565fa0b7e367da50e88a6ab26e7fd413269aa118e25258b/backports_zstd-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: backports-zstd + version: 1.6.0 + sha256: 7293fefe15f0e5852bdb4ad1e0e26f3cbd4d3e61c19f751ecc4ff34bc1eb237d + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/52/1b/49ebc2b59e9126f1f378ae910e98704d54a3f48b78e2d6d6c8cfe6fbe06f/cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl + name: cryptography + version: 38.0.4 + sha256: 1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb + requires_dist: + - cffi>=1.12 + - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pyenchant>=1.6.11 ; extra == 'docstest' + - twine>=1.12.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' + - black ; extra == 'pep8test' + - flake8 ; extra == 'pep8test' + - flake8-import-order ; extra == 'pep8test' + - pep8-naming ; extra == 'pep8test' + - setuptools-rust>=0.11.4 ; extra == 'sdist' + - bcrypt>=3.1.5 ; extra == 'ssh' + - pytest>=6.2.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-subtests ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pretend ; extra == 'test' + - iso8601 ; extra == 'test' + - pytz ; extra == 'test' + - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + name: pluggy + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + requires_dist: + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + - pytest ; extra == 'testing' + - pytest-benchmark ; extra == 'testing' + - coverage ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl + name: pillow + version: 12.3.0 + sha256: 8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - setuptools ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/64/e2/2c0af0a52d16be74a4f194564fcdc417521ed863e9b65e4bc9052dacba6f/hatchling-1.31.0-py3-none-any.whl + name: hatchling + version: 1.31.0 + sha256: aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544 + requires_dist: + - packaging>=24.2 + - pathspec>=0.10.1 + - pluggy>=1.0.0 + - tomli>=1.2.2 ; python_full_version < '3.11' + - trove-classifiers + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: pynacl + version: 1.6.2 + sha256: 46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87 + requires_dist: + - cffi>=1.4.1 ; python_full_version < '3.9' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - pytest>=7.4.0 ; extra == 'tests' + - pytest-cov>=2.10.1 ; extra == 'tests' + - pytest-xdist>=3.5.0 ; extra == 'tests' + - hypothesis>=3.27.0 ; extra == 'tests' + - sphinx<7 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6a/34/2f2b57dbfd145b995a29847a16b0903fce5ef6ad3c7aad740a609c5d3678/proto_plus-1.28.1-py3-none-any.whl + name: proto-plus + version: 1.28.1 + sha256: 6660f5f1970874bdcfc3088b435188a36a37bd3596668f7d726417c4ae8cfbed + requires_dist: + - protobuf>=4.25.8,<8.0.0 + - google-api-core>=1.31.5 ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + name: hyperlink + version: 21.0.0 + sha256: e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4 + requires_dist: + - idna>=2.5 + - typing ; python_full_version < '3.5' + requires_python: '>=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl + name: httpcore2 + version: 2.7.0 + sha256: 1452f589fe23f55b44546cd884294c41a29330af902bc0b71a761fd52d18f92b + requires_dist: + - h11>=0.16 + - truststore>=0.10 + - anyio>=4.5.0,<5.0 ; extra == 'asyncio' + - h2>=3,<5 ; extra == 'http2' + - socksio==1.* ; extra == 'socks' + - trio>=0.22.0,<1.0 ; extra == 'trio' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6f/ed/1409cdb557e558a6c98003ab12fdd4284699e158c167c187cb0f124eea4c/py_spy-0.4.2-py2.py3-none-win_amd64.whl + name: py-spy + version: 0.4.2 + sha256: 8b06a353c177677e4e1701b288d8c58e2f8d4208ee81a8048d9f72ba800918f8 + requires_dist: + - numpy ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: google-crc32c + version: 1.8.0 + sha256: 17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl + name: google-cloud-storage + version: 2.9.0 + sha256: 83a90447f23d5edd045e0037982c270302e3aeb45fc1288d2c2ca713d27bad94 + requires_dist: + - google-auth>=1.25.0,<3.0.dev0 + - google-api-core>=1.31.5,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0.dev0 + - google-cloud-core>=2.3.0,<3.0.dev0 + - google-resumable-media>=2.3.2 + - requests>=2.18.0,<3.0.0.dev0 + - protobuf<5.0.0.dev0 ; extra == 'protobuf' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/75/7a/2ea7dd2202638cf1053aaa8fbbaddded0b78c78832b3d03cafa0416a6c84/cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl + name: cryptography + version: 38.0.4 + sha256: 2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70 + requires_dist: + - cffi>=1.12 + - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pyenchant>=1.6.11 ; extra == 'docstest' + - twine>=1.12.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' + - black ; extra == 'pep8test' + - flake8 ; extra == 'pep8test' + - flake8-import-order ; extra == 'pep8test' + - pep8-naming ; extra == 'pep8test' + - setuptools-rust>=0.11.4 ; extra == 'sdist' + - bcrypt>=3.1.5 ; extra == 'ssh' + - pytest>=6.2.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-subtests ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pretend ; extra == 'test' + - iso8601 ; extra == 'test' + - pytz ; extra == 'test' + - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl + name: google-crc32c + version: 1.8.0 + sha256: 71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl + name: trove-classifiers + version: 2026.6.1.19 + sha256: ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 +- pypi: https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl + name: wrapt + version: 2.2.2 + sha256: 55b9a899e6fff5444f229d30aa6e9ac92d2216d9d60f33c771b5d76a760d5f8e + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl + name: wrapt + version: 2.2.2 + sha256: 8636809939152be6ae20a6cef0fed9fe60f411b47847d0426a826884b469e971 + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + name: jaraco-classes + version: 3.4.0 + sha256: f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 + requires_dist: + - more-itertools + - sphinx>=3.5 ; extra == 'docs' + - jaraco-packaging>=9.3 ; extra == 'docs' + - rst-linker>=1.9 ; extra == 'docs' + - furo ; extra == 'docs' + - sphinx-lint ; extra == 'docs' + - jaraco-tidelift>=1.4 ; extra == 'docs' + - pytest>=6 ; extra == 'testing' + - pytest-checkdocs>=2.4 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-mypy ; extra == 'testing' + - pytest-enabler>=2.2 ; extra == 'testing' + - pytest-ruff>=0.2.1 ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/81/44/5018c5ac1526c98169db98d87a6ff7d5508f5246621c3ee1a046fdd5e0a6/google_api_core-2.32.0-py3-none-any.whl + name: google-api-core + version: 2.32.0 + sha256: ae1f0d58a6c8869350bf469f8eb3092e7f8c494a942d9525494afb6c162b0904 + requires_dist: + - googleapis-common-protos>=1.63.2,<2.0.0 + - protobuf>=5.29.6,<8.0.0 + - proto-plus>=1.24.0,<2.0.0 + - proto-plus>=1.25.0,<2.0.0 ; python_full_version >= '3.13' + - google-auth>=2.14.1,<3.0.0 + - requests>=2.33.0,<3.0.0 + - google-auth[aiohttp]>=2.14.1,<3.0.0 ; extra == 'async-rest' + - aiohttp>=3.13.4 ; extra == 'async-rest' + - grpcio>=1.41.0,<2.0.0 ; extra == 'grpc' + - grpcio>=1.49.1,<2.0.0 ; python_full_version >= '3.11' and extra == 'grpc' + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' + - grpcio-status>=1.41.0,<2.0.0 ; extra == 'grpc' + - grpcio-status>=1.49.1,<2.0.0 ; python_full_version >= '3.11' and extra == 'grpc' + - grpcio-status>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + name: keyring + version: 25.7.0 + sha256: be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f + requires_dist: + - pywin32-ctypes>=0.2.0 ; sys_platform == 'win32' + - secretstorage>=3.2 ; sys_platform == 'linux' + - jeepney>=0.4.2 ; sys_platform == 'linux' + - importlib-metadata>=4.11.4 ; python_full_version < '3.12' + - jaraco-classes + - jaraco-functools + - jaraco-context + - pytest>=6,!=8.1.* ; extra == 'test' + - pyfakefs ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; extra == 'type' + - pygobject-stubs ; extra == 'type' + - shtab ; extra == 'type' + - types-pywin32 ; extra == 'type' + - shtab>=1.1.0 ; extra == 'completion' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + name: google-cloud-core + version: 2.6.0 + sha256: 6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e + requires_dist: + - google-api-core>=2.11.0,<3.0.0 + - google-auth>=2.14.1,!=2.24.0,!=2.25.0,<3.0.0 + - grpcio>=1.47.0,<2.0.0 ; python_full_version < '3.14' and extra == 'grpc' + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' + - grpcio-status>=1.47.0,<2.0.0 ; extra == 'grpc' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl + name: deprecated + version: 1.3.1 + sha256: 597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f + requires_dist: + - wrapt>=1.10,<3 + - inspect2 ; python_full_version < '3' + - tox ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - bump2version<1 ; extra == 'dev' + - setuptools ; python_full_version >= '3.12' and extra == 'dev' + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/88/61/df1d9db18f188d0ae648956a1decadc0e3b77d0571474370fd01f28a82b1/filelock-3.31.1-py3-none-any.whl + name: filelock + version: 3.31.1 + sha256: 9ea33146c780161bf67cb20c7cb26b651566820d65ad8dfdd79422602a2dcfc0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/89/23/3eb4c23c684ebd667674ce1d076ae855e0621d1d9bd5e052aa3f7982f757/py_spy-0.4.2-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: py-spy + version: 0.4.2 + sha256: 142887e984a4e541071c99a4401ff8c3770f255d329dbd0f64e8c1dd51882cce + requires_dist: + - numpy ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl + name: pyasn1 + version: 0.6.4 + sha256: deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl + name: python-discovery + version: 1.4.4 + sha256: abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe + requires_dist: + - filelock>=3.15.4 + - platformdirs>=4.3.6,<5 + - furo>=2025.12.19 ; extra == 'docs' + - sphinx-autodoc-typehints>=3.6.3 ; extra == 'docs' + - sphinx>=9.1 ; extra == 'docs' + - sphinxcontrib-mermaid>=2 ; extra == 'docs' + - sphinxcontrib-towncrier>=0.4 ; extra == 'docs' + - towncrier>=25.8 ; extra == 'docs' + - covdefaults>=2.3 ; extra == 'testing' + - coverage>=7.5.4 ; extra == 'testing' + - pytest-mock>=3.14 ; extra == 'testing' + - pytest>=8.3.5 ; extra == 'testing' + - setuptools>=75.1 ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + name: pexpect + version: 4.9.0 + sha256: 7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 + requires_dist: + - ptyprocess>=0.5 +- pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + name: requests + version: 2.34.2 + sha256: 2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.26,<3 + - certifi>=2023.5.7 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<8 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a2/8f/6c52b1f9d650863e8f67edbe062c04f1c8455579eaace1593d8fe469319a/cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl + name: cryptography + version: 38.0.4 + sha256: bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b + requires_dist: + - cffi>=1.12 + - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pyenchant>=1.6.11 ; extra == 'docstest' + - twine>=1.12.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' + - black ; extra == 'pep8test' + - flake8 ; extra == 'pep8test' + - flake8-import-order ; extra == 'pep8test' + - pep8-naming ; extra == 'pep8test' + - setuptools-rust>=0.11.4 ; extra == 'sdist' + - bcrypt>=3.1.5 ; extra == 'ssh' + - pytest>=6.2.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-subtests ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pretend ; extra == 'test' + - iso8601 ; extra == 'test' + - pytz ; extra == 'test' + - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + name: pyjwt + version: 2.13.0 + sha256: 66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + requires_dist: + - typing-extensions>=4.0 ; python_full_version < '3.11' + - cryptography>=3.4.0 ; extra == 'crypto' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.9 + sha256: 04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: wrapt + version: 2.2.2 + sha256: d619e1eed9bd4f6ed9f24cd61971aa086fa86505289628d464bcf8a2c2e3f328 + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a8/7d/cd3e187f14ce832e419e70709bfcc40cb0dc11517d5d03c9d3919bcc3101/google_auth-2.56.0-py3-none-any.whl + name: google-auth + version: 2.56.0 + sha256: 6e88c10217e07a92bfd01cac8ee99e32ccfb08414c3102e6c5b8d58f37a0d1e0 + requires_dist: + - pyasn1-modules>=0.2.1 + - cryptography>=38.0.3 ; python_full_version < '3.14' + - cryptography>=41.0.5 ; python_full_version >= '3.14' + - cryptography>=38.0.3 ; python_full_version < '3.14' and extra == 'cryptography' + - cryptography>=41.0.5 ; python_full_version >= '3.14' and extra == 'cryptography' + - cryptography>=38.0.3 ; python_full_version < '3.14' and extra == 'pyopenssl' + - cryptography>=41.0.5 ; python_full_version >= '3.14' and extra == 'pyopenssl' + - aiohttp>=3.8.0,<4.0.0 ; python_full_version < '3.14' and extra == 'aiohttp' + - aiohttp>=3.9.0,<4.0.0 ; python_full_version >= '3.14' and extra == 'aiohttp' + - requests>=2.30.0,<3.0.0 ; extra == 'aiohttp' + - cryptography>=38.0.3 ; python_full_version < '3.14' and extra == 'enterprise-cert' + - cryptography>=41.0.5 ; python_full_version >= '3.14' and extra == 'enterprise-cert' + - pyjwt>=2.0 ; extra == 'pyjwt' + - pyu2f>=0.1.5 ; extra == 'reauth' + - requests>=2.30.0,<3.0.0 ; extra == 'requests' + - grpcio>=1.59.0,<2.0.0 ; python_full_version < '3.14' and extra == 'testing' + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'testing' + - flask ; extra == 'testing' + - freezegun ; extra == 'testing' + - pyjwt>=2.0 ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-localserver ; extra == 'testing' + - pyu2f>=0.1.5 ; extra == 'testing' + - responses ; extra == 'testing' + - urllib3>=1.26.15,<3.0.0 ; extra == 'testing' + - packaging>=20.0 ; extra == 'testing' + - aiohttp>=3.8.0,<4.0.0 ; python_full_version < '3.14' and extra == 'testing' + - aiohttp>=3.9.0,<4.0.0 ; python_full_version >= '3.14' and extra == 'testing' + - requests>=2.30.0,<3.0.0 ; extra == 'testing' + - aioresponses ; extra == 'testing' + - pytest-asyncio ; extra == 'testing' + - urllib3>=1.26.15,<3.0.0 ; extra == 'urllib3' + - packaging>=20.0 ; extra == 'urllib3' + - rsa>=4.0.0,<5 ; extra == 'rsa' + - grpcio>=1.59.0,<2.0.0 ; python_full_version < '3.14' and extra == 'grpc' + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ac/fc/a444cd19ccc8c4946a512f3827ed0b3565c88488719d800d54a75d541c0b/PyGithub-2.6.1-py3-none-any.whl + name: pygithub + version: 2.6.1 + sha256: 6f2fa6d076ccae475f9fc392cc6cdbd54db985d4f69b8833a28397de75ed6ca3 + requires_dist: + - pynacl>=1.4.0 + - requests>=2.14.0 + - pyjwt[crypto]>=2.4.0 + - typing-extensions>=4.0.0 + - urllib3>=1.26.0 + - deprecated + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + name: google-resumable-media + version: 2.10.0 + sha256: 88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c + requires_dist: + - google-crc32c>=1.0.0,<2.0.0 + - requests>=2.18.0,<3.0.0 ; extra == 'requests' + - aiohttp>=3.6.2,<4.0.0 ; extra == 'aiohttp' + - google-auth>=1.22.0,<2.0.0 ; extra == 'aiohttp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl + name: pillow + version: 12.3.0 + sha256: 37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - setuptools ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + name: jeepney + version: 0.9.0 + sha256: 97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 + requires_dist: + - pytest ; extra == 'test' + - pytest-trio ; extra == 'test' + - pytest-asyncio>=0.17 ; extra == 'test' + - testpath ; extra == 'test' + - trio ; extra == 'test' + - async-timeout ; python_full_version < '3.11' and extra == 'test' + - trio ; extra == 'trio' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + name: secretstorage + version: 3.5.0 + sha256: 0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 + requires_dist: + - cryptography>=2.0 + - jeepney>=0.6 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + name: six + version: 1.17.0 + sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/b9/cb/af58363b0dd0b497282ecef1fa99789b03cc1885a01a41394cad42ceeff6/backports_zstd-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: backports-zstd + version: 1.6.0 + sha256: 0308990ffc998df3c7ed35276bde049728b5c3956203cae40d80893576a41459 + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + name: backports-tarfile + version: 1.2.0 + sha256: 77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 + requires_dist: + - sphinx>=3.5 ; extra == 'docs' + - jaraco-packaging>=9.3 ; extra == 'docs' + - rst-linker>=1.9 ; extra == 'docs' + - furo ; extra == 'docs' + - sphinx-lint ; extra == 'docs' + - pytest>=6,!=8.1.* ; extra == 'testing' + - pytest-checkdocs>=2.4 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-enabler>=2.2 ; extra == 'testing' + - jaraco-test ; extra == 'testing' + - pytest!=8.0.* ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + name: pyproject-hooks + version: 1.2.0 + sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl + name: pynacl + version: 1.6.2 + sha256: c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465 + requires_dist: + - cffi>=1.4.1 ; python_full_version < '3.9' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - pytest>=7.4.0 ; extra == 'tests' + - pytest-cov>=2.10.1 ; extra == 'tests' + - pytest-xdist>=3.5.0 ; extra == 'tests' + - hypothesis>=3.27.0 ; extra == 'tests' + - sphinx<7 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c0/eb/f52b165db2abd662cda0a76efb7579a291fed1a7979cf41146cdc19e0d7a/cryptography-38.0.4-cp36-abi3-win_amd64.whl + name: cryptography + version: 38.0.4 + sha256: 8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d + requires_dist: + - cffi>=1.12 + - sphinx>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pyenchant>=1.6.11 ; extra == 'docstest' + - twine>=1.12.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=4.0.1 ; extra == 'docstest' + - black ; extra == 'pep8test' + - flake8 ; extra == 'pep8test' + - flake8-import-order ; extra == 'pep8test' + - pep8-naming ; extra == 'pep8test' + - setuptools-rust>=0.11.4 ; extra == 'sdist' + - bcrypt>=3.1.5 ; extra == 'ssh' + - pytest>=6.2.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-subtests ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pretend ; extra == 'test' + - iso8601 ; extra == 'test' + - pytz ; extra == 'test' + - hypothesis>=1.11.4,!=3.79.2 ; extra == 'test' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl + name: virtualenv + version: 21.6.1 + sha256: afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b + requires_dist: + - distlib>=0.3.7,<1 + - filelock>=3.24.2,<4 ; python_full_version >= '3.10' + - filelock>=3.16.1,<=3.19.1 ; python_full_version < '3.10' + - platformdirs>=3.9.1,<5 + - python-discovery>=1.4.2 + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: charset-normalizer + version: 3.4.9 + sha256: 2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/c5/90/428dd82228b1b6d62d5a1bf312c29e6c125af6a182fcfd82768ca179dcc7/backports_zstd-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl + name: backports-zstd + version: 1.6.0 + sha256: c4fc41b2df5529cad5ceb230319e82728096d4b353ce8d4df68a2ec37e291bb8 + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + name: tomli-w + version: 1.2.0 + sha256: 188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: pillow + version: 12.3.0 + sha256: bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - setuptools ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl + name: uv + version: 0.9.17 + sha256: 233b3d90f104c59d602abf434898057876b87f64df67a37129877d6dab6e5e10 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl + name: anyio + version: 4.14.2 + sha256: 9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 + requires_dist: + - exceptiongroup>=1.0.2 ; python_full_version < '3.11' + - idna>=2.8 + - typing-extensions>=4.5 ; python_full_version < '3.13' + - trio>=0.32.0 ; extra == 'trio' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl + name: uv + version: 0.9.17 + sha256: 4b8e5513d48a267bfa180ca7fefaf6f27b1267e191573b3dba059981143e88ef + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl + name: pywin32-ctypes + version: 0.2.3 + sha256: 8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + name: shellingham + version: 1.5.4 + sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + name: googleapis-common-protos + version: 1.75.0 + sha256: 961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed + requires_dist: + - protobuf>=4.25.8,<8.0.0 + - grpcio>=1.44.0,<2.0.0 ; extra == 'grpc' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl + name: more-itertools + version: 11.1.0 + sha256: 4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + name: python-dateutil + version: 2.9.0.post0 + sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + requires_dist: + - six>=1.5 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl + name: charset-normalizer + version: 3.4.9 + sha256: 6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/ef/21/ec030145a0c7992bd4b9eafb2f06f56358b3a5339eab4a16534baf3c69aa/py_spy-0.4.2-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + name: py-spy + version: 0.4.2 + sha256: 1ccf688393105111684435f035bc14ec3f22117dd2b85b2414612cf27a22755a + requires_dist: + - numpy ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + name: certifi + version: 2026.6.17 + sha256: 2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/ef/48/768edf21fe33bae8d874470b1be136681d4d32eb820a32e1c98262ebe39b/backports_zstd-1.6.0-cp311-cp311-macosx_11_0_arm64.whl + name: backports-zstd + version: 1.6.0 + sha256: 83391ef5935cc0f329b1abca414ae20ffe40d335fc21a4b5e664f08a74317d5f + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + name: pathspec + version: 1.1.1 + sha256: a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + requires_dist: + - hyperscan>=0.7 ; extra == 'hyperscan' + - typing-extensions>=4 ; extra == 'optional' + - google-re2>=1.1 ; extra == 're2' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + name: jaraco-context + version: 6.1.2 + sha256: bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 requires_dist: + - backports-tarfile ; python_full_version < '3.12' - pytest>=6,!=8.1.* ; extra == 'test' - - jaraco-itertools ; extra == 'test' - - jaraco-functools ; extra == 'test' - - more-itertools ; extra == 'test' - - big-o ; extra == 'test' - - pytest-ignore-flaky ; extra == 'test' - - jaraco-test ; extra == 'test' + - jaraco-test>=5.6.0 ; extra == 'test' + - portend ; extra == 'test' - sphinx>=3.5 ; extra == 'doc' - jaraco-packaging>=9.3 ; extra == 'doc' - rst-linker>=1.9 ; extra == 'doc' - furo ; extra == 'doc' - sphinx-lint ; extra == 'doc' - jaraco-tidelift>=1.4 ; extra == 'doc' - - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-checkdocs>=2.14 ; extra == 'check' - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - pytest-cov ; extra == 'cover' - - pytest-enabler>=2.2 ; extra == 'enabler' - - pytest-mypy ; extra == 'type' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + name: google-crc32c + version: 1.8.0 + sha256: 19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py311haee01d2_1.conda - sha256: d534a6518c2d8eccfa6579d75f665261484f0f2f7377b50402446a9433d46234 - md5: ca45bfd4871af957aaa5035593d5efd2 - depends: - - python - - cffi >=1.11 - - zstd >=1.5.7,<1.5.8.0a0 - - libgcc >=14 - - __glibc >=2.17,<3.0.a0 - - zstd >=1.5.7,<1.6.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 466893 - timestamp: 1762512695614 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.25.0-py311h51cfe5d_1.conda - sha256: ddeec193065b235166fb9f8ca4e5cbb931215ab90cbd17e9f9d753c8966b57b1 - md5: c8b3365fe290eeee3084274948012394 - depends: - - python - - cffi >=1.11 - - zstd >=1.5.7,<1.5.8.0a0 - - python 3.11.* *_cpython - - libgcc >=14 - - zstd >=1.5.7,<1.6.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 459426 - timestamp: 1762512724303 -- conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py311h62e9434_1.conda - sha256: 8b4e61e45260fdcdedd36192a225de724a0491548fd84a69679e872f467e55fd - md5: 9e05cc70a6656c2718783f011128991f - depends: - - python - - cffi >=1.11 - - zstd >=1.5.7,<1.5.8.0a0 - - __osx >=10.13 - - zstd >=1.5.7,<1.6.0a0 - - python_abi 3.11.* *_cp311 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 462796 - timestamp: 1762512690757 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py311h5bb9006_1.conda - sha256: 2ee455765fe831cca8fe127c56ae99938e353797135fe33140b28abb4fbe1049 - md5: 651594b8f9b9cccc5948287a18903c34 - depends: - - python - - cffi >=1.11 - - zstd >=1.5.7,<1.5.8.0a0 - - python 3.11.* *_cpython - - __osx >=11.0 - - python_abi 3.11.* *_cp311 - - zstd >=1.5.7,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 390026 - timestamp: 1762512731928 -- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py311hf893f09_1.conda - sha256: 10f089bedef1a28c663ef575fb9cec66b2058e342c4cf4a753083ab07591008f - md5: b2d90bca78b57c17205ce3ca1c427813 - depends: - - python - - cffi >=1.11 - - zstd >=1.5.7,<1.5.8.0a0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.11.* *_cp311 - - zstd >=1.5.7,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 375869 - timestamp: 1762512737575 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - sha256: a4166e3d8ff4e35932510aaff7aa90772f84b4d07e9f6f83c614cba7ceefe0eb - md5: 6432cb5d4ac0046c3ac0a8a0f95842f9 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 567578 - timestamp: 1742433379869 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda - sha256: 0812e7b45f087cfdd288690ada718ce5e13e8263312e03b643dd7aa50d08b51b - md5: 5be90c5a3e4b43c53e38f50a85e11527 - depends: - - libgcc >=13 - - libstdcxx >=13 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 551176 - timestamp: 1742433378347 -- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h8210216_2.conda - sha256: c171c43d0c47eed45085112cb00c8c7d4f0caa5a32d47f2daca727e45fb98dca - md5: cd60a4a5a8d6a476b30d8aa4bb49251a - depends: - - __osx >=10.13 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 485754 - timestamp: 1742433356230 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - sha256: 0d02046f57f7a1a3feae3e9d1aa2113788311f3cf37a3244c71e61a93177ba67 - md5: e6f69c7bcccdefa417f056fa593b40f0 - depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 399979 - timestamp: 1742433432699 -- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - sha256: bc64864377d809b904e877a98d0584f43836c9f2ef27d3d2a1421fa6eae7ca04 - md5: 21f56217d6125fb30c3c3f10c786d751 - depends: - - libzlib >=1.3.1,<2.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 354697 - timestamp: 1742433568506 +- pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + name: ghp-import + version: 2.1.0 + sha256: 8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 + requires_dist: + - python-dateutil>=2.8.1 + - twine ; extra == 'dev' + - markdown ; extra == 'dev' + - flake8 ; extra == 'dev' + - wheel ; extra == 'dev' +- pypi: https://files.pythonhosted.org/packages/f9/34/dd7d3c763a00b7b965e25a5eab0acd1a345dbaf0f45fffe595278873a1c0/py_spy-0.4.2-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl + name: py-spy + version: 0.4.2 + sha256: aeb0323409199c785f730645e9f4bb7a7b9ca2c481f2c331a55642b5d13fa52f + requires_dist: + - numpy ; extra == 'test' +- pypi: https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl + name: pillow + version: 12.3.0 + sha256: 00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - setuptools ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + name: click + version: 8.4.2 + sha256: e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' diff --git a/pixi.toml b/pixi.toml index 1a5859d85a98..e84bd5f6fddb 100644 --- a/pixi.toml +++ b/pixi.toml @@ -27,15 +27,17 @@ channels = ["conda-forge"] description = "Log images, point clouds, etc, and visualize them effortlessly" homepage = "https://rerun.io" license = "MIT OR Apache-2.0" -platforms = ["linux-64", "linux-aarch64", "osx-arm64", "osx-64", "win-64"] +platforms = [ + { name = "linux-64-glibc-2-28", platform = "linux-64", glibc = "2.28" }, + { name = "linux-aarch64-glibc-2-28", platform = "linux-aarch64", glibc = "2.28" }, + { name = "osx-arm64-macos-11", platform = "osx-arm64", macos = "11.0" }, + { name = "osx-64-macos-11", platform = "osx-64", macos = "11.0" }, + "win-64", +] readme = "README.md" repository = "https://github.com/rerun-io/rerun" version = "0.1.0" # TODO(emilk): sync version with `Cargo.toml` with help from `crates.py` -requires-pixi = ">=0.55.0" # Make sure to keep this in sync with the version on our CI jobs! - -[system-requirements] -macos = "11.0" # needed for some reason otherwise fails to resolve mediapipe package -libc = "2.28" +requires-pixi = ">=0.71.3" # Make sure to keep this in sync with the version on our CI jobs! # These should be kept empty as their content is pulled in every single environment. For the "standard" stuff, @@ -63,6 +65,18 @@ EXECUTABLE_EXTENSION = "" # This ensures uv targets .venv instead of the pixi environment. PATH = "${PIXI_PROJECT_ROOT}/scripts/pixi:${PATH}" +[target.linux-64.activation.env] +# `gilrs` uses `pkg-config` to find libudev on Linux. Prefer Pixi's pkgconf and +# make the Pixi pkg-config files visible before falling back to host paths. +PKG_CONFIG = "pkgconf" +PKG_CONFIG_PATH = "${CONDA_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH}" + +[target.linux-aarch64.activation.env] +# `gilrs` uses `pkg-config` to find libudev on Linux. Prefer Pixi's pkgconf and +# make the Pixi pkg-config files visible before falling back to host paths. +PKG_CONFIG = "pkgconf" +PKG_CONFIG_PATH = "${CONDA_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH}" + [target.win-64.activation] # Run ensure-rerun-env to set up pyo3-build.cfg and uv shim. # This is a polyglot .bat that works in both cmd.exe and bash (Git Bash). @@ -71,6 +85,9 @@ scripts = ["scripts/pixi/activate.bat"] [target.win-64.activation.env] # The executable extension for binaries on the current platform. EXECUTABLE_EXTENSION = ".exe" +# cc-rs needs an archiver for wasm32 cross-compilation; MSVC's lib.exe isn't found +# for non-MSVC targets, so point AR at llvm-ar from the llvm-tools conda package. +AR = "llvm-ar" # Prepend scripts/pixi to PATH so our uv wrapper shadows the conda uv. # This ensures uv targets .venv instead of the pixi environment. PATH = "%PIXI_PROJECT_ROOT%\\scripts\\pixi;%PATH%" @@ -102,6 +119,11 @@ default = ["base"] # in which you want to do viewer builds. cpp = ["base", "cpp"] +# Test coverage tooling (cargo-llvm-cov + cargo-nextest). Kept out of the default +# environment so normal dev/CI installs don't pull these tools, and so CI's own pinned +# nextest (installed via taiki-e/install-action) isn't shadowed. Used by `rs-coverage`. +coverage = ["base", "coverage"] + ################################################################################ # TASKS ################################################################################ @@ -131,6 +153,13 @@ man = "RERUN_DISABLE_WEB_VIEWER_SERVER=1 cargo run --package rerun-cli --all-fea # You can also give an argument for what to view (e.g. an .rrd file). rerun = "cargo run --package rerun-cli --no-default-features --features release_no_web_viewer --" +# Compile and run the rerun cli tool, without any viewer. +# +# This avoid recompiling the viewer when you just want to run a quick command. +# +# You can also give an argument for what to view (e.g. an .rrd file). +rerun-cli = "cargo run --package rerun-cli --no-default-features --" + # Compile and run the rerun viewer, with performance telemetry including Tracy profiler. # # You can also give an argument for what to view (e.g. an .rrd file). @@ -164,6 +193,7 @@ rerun-build-native-and-web-release = { cmd = "cargo build --package rerun-cli -- ] } rerun-build-native-and-pending-web-release = { cmd = "cargo build --package rerun-cli --no-default-features --features release_full --release --", env = { RERUN_TRAILING_WEB_VIEWER = "1" } } +rerun-build-native-and-pending-web-release-zig = { cmd = "cargo zigbuild --locked --package rerun-cli --no-default-features --features release_full --release --target x86_64-unknown-linux-gnu.2.28", env = { RERUN_TRAILING_WEB_VIEWER = "1" } } # Compile and run the web-viewer via rerun-cli. # @@ -209,8 +239,17 @@ rs-check = "python scripts/ci/rust_checks.py" # See tests/assets/rrd/README.md for more. # Note that we run with `--check-footers=false`: it is expected that these RRDs do not have proper footers (they predate them). check-backwards-compatibility = { cmd = "find tests/assets/rrd -name '*.rrd' -type f -print0 | xargs -0 cargo run --package rerun-cli --no-default-features rrd verify --check-footers=false" } +web-test = { cmd = "wasm-pack test --headless --{{ browser }} crates/store/re_server --all-targets", args = [ + { arg = "browser", default = "firefox" }, +] } -rs-fmt = "cargo fmt --all" +# `cargo fmt --all` only sees files reachable from each crate's lib.rs/main.rs. +# Snippets in `docs/snippets/all/*.rs` are copied into the crate by `build.rs`, +# so the originals are never formatted by cargo fmt. Run rustfmt on them +# directly so `docs/snippets/rustfmt.toml` (max_width=80) is applied. +# We filter to files containing `fn main()` to skip partial fragments +# (they don't parse standalone). +rs-fmt = { cmd = "cargo fmt --all && grep -rl 'fn main()' --include='*.rs' docs/snippets/all | xargs rustfmt --edition 2024" } # Code formatting for all languages. format = { depends-on = ["cpp-fmt", "misc-fmt", "pb-fmt", "py-fmt", "rs-fmt", "toml-fmt"] } @@ -256,10 +295,10 @@ uv-lock-check = { depends-on = ["uv-lock-check-workspace", "uv-lock-check-isolat # * # * -pb-fmt-check = "buf format --exit-code --diff" -pb-fmt = "buf format --exit-code --write" +pb-fmt-check = "buf format --config scripts/ci/buf.yaml --exit-code --diff" +pb-fmt = "buf format --config scripts/ci/buf.yaml --exit-code --write" -pb-lint = "buf lint --error-format=json" +pb-lint = "buf lint --config scripts/ci/buf.yaml --error-format=json" # NOTE(cmc): I'm keeping all the snapshot machinery around if it turns out we need something more robust # than a pure git solution in the future. For now, convenience wins. @@ -276,7 +315,7 @@ pb-lint = "buf lint --error-format=json" # "pb-snapshot-main", # ]} -pb-breaking = "echo 'ℹ️ If this CI step is failing even though you did not modify any Protobuf files, that means that somebody purposefully published breaking Protobuf changes onto the main branch in the meantime.\nℹ️ For that reason, the Protobuf definitions on your local branch are now incompatible with those on main.\nℹ️ When that happens, simply rebase your branch on latest main, and the errors will go away.' ; buf breaking --error-format=json --against '.git#branch=origin/main'" +pb-breaking = "echo 'ℹ️ If this CI step is failing even though you did not modify any Protobuf files, that means that somebody purposefully published breaking Protobuf changes onto the main branch in the meantime.\nℹ️ For that reason, the Protobuf definitions on your local branch are now incompatible with those on main.\nℹ️ When that happens, simply rebase your branch on latest main, and the errors will go away.' ; buf breaking --config scripts/ci/buf.yaml --against-config scripts/ci/buf.yaml --error-format=json --against '.git#branch=origin/main'" pb-check = { depends-on = ["pb-fmt-check", "pb-lint", "pb-breaking"] } @@ -456,6 +495,7 @@ py-build-notebook-js = { cmd = "npm --prefix rerun_notebook run build", depends- # Build an installable SDK-only wheel. IMPORTANT: unlike the officially published wheels, the wheel produced by this command does NOT include the viewer. py-build-wheels-sdk-only = { cmd = "RERUN_ALLOW_MISSING_BIN=1 python scripts/ci/build_and_upload_wheels.py --mode pr --dir ''" } +py-build-wheels-sdk-only-zig = { cmd = "RERUN_ALLOW_MISSING_BIN=1 python scripts/ci/build_and_upload_wheels.py --mode pr --dir '' --compat manylinux_2_28 --zig" } # Helper alias to run the python interpreter in the context of the python environment rrpy = "uvpy" @@ -489,6 +529,23 @@ RUSTFLAGS = "-Z threads=0" cmd = "target/dev-fast/viewer" depends-on = ["rerun-build-fast"] +# ------------------------------------------------------------------------------------------ +# Test coverage (cargo-llvm-cov + nextest) — see scripts/rs_coverage.sh for details. +# +# Lives in the dedicated `coverage` environment (see [environments]) so the default env +# and CI stay lean. `pixi run rs-coverage` auto-selects that environment since the task +# exists only there. +# +# Runs the test suite under source-based coverage instrumentation and writes lcov.info +# (for the "Coverage Gutters" VS Code extension), a browsable HTML report, and a per-file +# summary table in the terminal. +# +# With no argument it covers the whole workspace; pass a crate to scope it, e.g. +# pixi run rs-coverage re_dataframe +[feature.coverage.tasks.rs-coverage] +args = [{ arg = "crate", default = "--workspace" }] +cmd = "bash scripts/rs_coverage.sh {{ crate }}" + [feature.cpp.tasks] # All the cpp-* tasks can be configured with environment variables, e.g.: RERUN_WERROR=ON CXX=clang++ # We export a compilation database (compile_commands.json) in the CMake configure step (-DCMAKE_EXPORT_COMPILE_COMMANDS=ON), @@ -588,6 +645,7 @@ tqdm = ">=4.66.2,<4.67" # For displaying progress in various utility scripts. ty = "==0.0.31" typing_extensions = ">4.5" typos = ">=1.45.1" +wasm-pack = "0.15.*" urllib3 = "<2.6.0" # TODO(googleapis/google-resumable-media-python#491) gh = ">=2.79.0,<3" binaryen = "117.*" # for `wasm-opt` @@ -607,10 +665,28 @@ requests = ">=2.31,<3" # For `thumbnails.py` & `upload_image.py` rerun-pixi-env = { path = "rerun_pixi_env", editable = true } uv = "==0.9.17" +# Test coverage tooling, isolated in the `coverage` environment (see [environments]). +# Available on all supported platforms. `cargo-nextest` lives here rather than in `base` +# on purpose: CI installs its own pinned nextest, and a second copy in the default env +# would shadow it. Used only by the `rs-coverage` task. +[feature.coverage.dependencies] +cargo-nextest = ">=0.9.140" # Test runner driven by cargo-llvm-cov +cargo-llvm-cov = ">=0.8.7" # Source-based Rust test coverage + [target.linux-64.dependencies] +cargo-zigbuild = "0.20.*" +libudev = "257.4.*" # for `gilrs` Linux backend patchelf = ">=0.17" +pkgconf = ">=2" # Provides `pkg-config` for `gilrs` Linux backend. meilisearch = "1.5.1.*" # not available for linux-aarch64 buf = "1.*" # not available for linux-aarch64 +zig = "0.13.0.*" + +[target.linux-aarch64.dependencies] +cargo-zigbuild = "0.20.*" +libudev = "257.4.*" # for `gilrs` Linux backend +pkgconf = ">=2" # Provides `pkg-config` for `gilrs` Linux backend. +zig = "0.13.0.*" [target.osx-arm64.dependencies] buf = "1.*" # not available for linux-aarch64 @@ -620,6 +696,7 @@ llvm-tools = "16.0.6.*" # Various Wasm targets fail on mac without llvm-ar [target.win-64.dependencies] buf = "1.*" # not available for linux-aarch64 +llvm-tools = "16.0.6.*" # Wasm targets need llvm-ar for cc-rs (same as osx-arm64) [feature.cpp.target.linux-64.dependencies] sysroot_linux-64 = ">=2.17,<3" # rustc 1.64+ requires glibc 2.17+, see https://blog.rust-lang.org/2022/08/01/Increasing-glibc-kernel-requirements.html diff --git a/pyproject.toml b/pyproject.toml index d026bd64c920..31ede0cb627a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ [project] name = "rerun-workspace" -version = "0.30.0a4" +version = "0.28.0-alpha.1+dev" description = "Rerun Python workspace" requires-python = ">=3.10,<3.13" @@ -25,7 +25,7 @@ dev = [ # with editable installs by adding rerun_py/rerun_sdk to the Python path. "rerun-dev-fixup", "opentelemetry-exporter-otlp-proto-grpc==1.39.0", - "maturin>=1.0.0", + "maturin>=1.14.1", "ruff==0.15.7", "mypy==1.19.1", "nbqa==1.9.1", @@ -40,6 +40,7 @@ dev = [ "colorama>=0.4", "types-colorama>=0.4.15", "Pillow>=12.2.0,<13", # has py.typed, no stubs needed + "types-psutil>=7.0", # Stubs for `psutil`, used by `rerun.tracing_session()`. # Typed packages needed for linting scripts/examples "attrs>=23.1.0", "semver>=3.0,<3.1", @@ -69,14 +70,13 @@ dev = [ "huggingface-hub<1.0", # Typed packages for dataframe/arrow operations (needed for linting examples/tests) "polars>=1.0", - "datafusion>=50.0", + "datafusion>=53.0", "jupyter>=1.0", - "types-protobuf", # Needed by mcap-protobuf-support # Needed for pretty-printing tables in `fetch_patch_candidates.py` "tabulate>=0.9.0", "types-tabulate>=0.9.0", ] -snippets = ["rerun-sdk", "av", "pandas", "mcap-protobuf-support"] +snippets = ["rerun-sdk[dataloader]", "av", "pandas"] examples = [ # External deps used by examples "polars>=0.12.0", @@ -84,9 +84,9 @@ examples = [ "segment-anything @ git+https://github.com/facebookresearch/segment-anything.git", "torch>=2.5.1", # require at least 2.5.1 for some examples "torchvision>=0.20.1", # require at least 0.20.1 for some examples - # lerobot and rerun_export are not listed here to avoid override-dependencies - # conflicts during normal uv sync. Use `uv sync --package rerun_export` to - # install rerun_export with its lerobot dependency. + # lerobot (used by the dataloader example) is not listed here because it + # requires Python >=3.12 while this workspace supports 3.10+. The dataloader + # is an isolated uv project — sync it from its own directory. # Example packages from workspace "air_traffic_data", "animated_urdf", @@ -123,6 +123,7 @@ examples = [ "plots", "raw_mesh", "rgbd", + "robot_data_preprocessing", "rrt_star", "segment_anything_model", "server_tables", @@ -137,16 +138,18 @@ examples = [ ] docs = [ # Documentation build dependencies (mkdocs and plugins) - "griffe==1.4.1", + "griffe>=1.14,<2", + "griffe-public-redundant-aliases==0.3.0", "griffe-warnings-deprecated==1.1.0", "mkdocs==1.6.1", "mkdocs-gen-files==0.5.0", "mkdocs-literate-nav==0.6.1", + "mkdocs-redirects==1.2.2", "mkdocs-material==9.4.7", "mkdocs-material-extensions==1.3", - "mkdocs-redirects @ git+https://github.com/rerun-io/mkdocs-redirects.git@fb6b074554975ba7729d68d04957ce7c7dfd5003", - "mkdocstrings==0.26.2", - "mkdocstrings-python==1.12.1", + "mkdocstrings>=0.28.2,<0.29", + "mkdocstrings-python>=1.16.2,<2", + "pygments", "setuptools>75", "sphobjinv==2.3.1", ] @@ -154,9 +157,9 @@ docs = [ [tool.uv] package = false -# Override lerobot's dependency on rerun-sdk to use the workspace version -# TODO(RR-3548): Figure out how to have lerobot as a workspace member without causing dependency issues. -# override-dependencies = ["rerun-sdk"] +# pyarrow 24.0.0 ships an incomplete `py.typed` that breaks mypy on `pyarrow.compute.*`. +# 23.x has no type stubs (so mypy leaves it untyped) and clears RUSTSEC/GHSA pyarrow alert >= 23.0.1. +constraint-dependencies = ["pyarrow>=23.0.1,<24"] [tool.uv.sources] rerun-sdk = { workspace = true } @@ -199,15 +202,16 @@ objectron = { workspace = true } open_photogrammetry_format = { workspace = true } openstreetmap_data = { workspace = true } # TODO(RR-3548): Figure out a way to include this, without lerobot causing dependency issues. -# rerun_export = { workspace = true } -# dataloader = { workspace = true } # Excluded: same RR-3548 reason (lerobot pins old rerun-sdk). +# dataloader = { workspace = true } # Excluded: lerobot requires Python >=3.12. plots = { workspace = true } raw_mesh = { workspace = true } rgbd = { workspace = true } +robot_data_preprocessing = { workspace = true } rrt_star = { workspace = true } segment_anything_model = { workspace = true } server_tables = { workspace = true } shared_recording = { workspace = true } +state_timeline = { workspace = true } stdio = { workspace = true } structure_from_motion = { workspace = true } using_index_values = { workspace = true } @@ -236,6 +240,7 @@ members = [ # "examples/python/gesture_detection", "examples/python/graph_lattice", "examples/python/graphs", + "examples/python/table_grid_with_flags", # "examples/python/human_pose_tracking", "examples/python/imu_signals", "examples/python/incremental_logging", @@ -255,17 +260,19 @@ members = [ # "examples/python/ocr", # Excluded: paddleclas has incompatible opencv requirements "examples/python/open_photogrammetry_format", "examples/python/openstreetmap_data", - # "examples/python/rerun_export" # Excluded: causes dependency issues, - # "examples/python/dataloader" # Excluded: same RR-3548 reason (lerobot). + # "examples/python/dataloader" # Excluded: RR-3548 (lerobot requires Python >=3.12). "examples/python/plots", "examples/python/raw_mesh", "examples/python/rgbd", + "examples/python/robot_data_preprocessing", "examples/python/rrt_star", "examples/python/segment_anything_model", "examples/python/server_tables", "examples/python/shared_recording", + "examples/python/state_timeline", "examples/python/stdio", "examples/python/structure_from_motion", + "examples/python/table_blueprints", "examples/python/table_zoo", "examples/python/template", "examples/python/using_index_values", diff --git a/rerun_cpp/README.md b/rerun_cpp/README.md index e7e16a9c4c21..11b8a09eb73a 100644 --- a/rerun_cpp/README.md +++ b/rerun_cpp/README.md @@ -138,6 +138,22 @@ find_package(rerun_sdk REQUIRED) target_link_libraries( PRIVATE rerun_sdk) ``` +### Install with vcpkg + +The Rerun C++ SDK is also available as the community-maintained [`rerun-sdk` vcpkg port](https://vcpkg.io/en/package/rerun-sdk). +You can install it with: + +```bash +vcpkg install rerun-sdk +``` + +Once installed, consume it from CMake like any other vcpkg-provided package: + +```cmake +find_package(rerun_sdk CONFIG REQUIRED) + +target_link_libraries( PRIVATE rerun_sdk) +``` ## Development in the Rerun repository diff --git a/rerun_cpp/src/rerun.hpp b/rerun_cpp/src/rerun.hpp index 04de9b291fa0..6a1695a287cb 100644 --- a/rerun_cpp/src/rerun.hpp +++ b/rerun_cpp/src/rerun.hpp @@ -38,17 +38,23 @@ namespace rerun { // Also import any component or datatype that has a unique name: using components::AlbedoFactor; using components::Color; + using components::Colormap; using components::FillMode; using components::GeoLineString; + using components::GraphType; using components::HalfSize2D; using components::HalfSize3D; + using components::ImageBuffer; + using components::KeyValuePairs; using components::LatLon; using components::LineStrip2D; using components::LineStrip3D; + using components::MarkerShape; using components::MediaType; using components::Position2D; using components::Position3D; using components::Radius; + using components::Scalar; using components::Text; using components::TextLogLevel; using components::TransformRelation; diff --git a/rerun_cpp/src/rerun/archetypes.hpp b/rerun_cpp/src/rerun/archetypes.hpp index f5939cba7cc8..2c9af71b9c52 100644 --- a/rerun_cpp/src/rerun/archetypes.hpp +++ b/rerun_cpp/src/rerun/archetypes.hpp @@ -15,6 +15,7 @@ #include "archetypes/coordinate_frame.hpp" #include "archetypes/cylinders3d.hpp" #include "archetypes/depth_image.hpp" +#include "archetypes/ellipses2d.hpp" #include "archetypes/ellipsoids3d.hpp" #include "archetypes/encoded_depth_image.hpp" #include "archetypes/encoded_image.hpp" @@ -40,7 +41,8 @@ #include "archetypes/segmentation_image.hpp" #include "archetypes/series_lines.hpp" #include "archetypes/series_points.hpp" -#include "archetypes/status.hpp" +#include "archetypes/state_change.hpp" +#include "archetypes/state_configuration.hpp" #include "archetypes/tensor.hpp" #include "archetypes/text_document.hpp" #include "archetypes/text_log.hpp" @@ -49,3 +51,4 @@ #include "archetypes/video_frame_reference.hpp" #include "archetypes/video_stream.hpp" #include "archetypes/view_coordinates.hpp" +#include "archetypes/voxel_grid_map.hpp" diff --git a/rerun_cpp/src/rerun/archetypes/.gitattributes b/rerun_cpp/src/rerun/archetypes/.gitattributes index e08f2af713d3..a4cbb4262ccd 100644 --- a/rerun_cpp/src/rerun/archetypes/.gitattributes +++ b/rerun_cpp/src/rerun/archetypes/.gitattributes @@ -27,6 +27,8 @@ cylinders3d.cpp linguist-generated=true cylinders3d.hpp linguist-generated=true depth_image.cpp linguist-generated=true depth_image.hpp linguist-generated=true +ellipses2d.cpp linguist-generated=true +ellipses2d.hpp linguist-generated=true ellipsoids3d.cpp linguist-generated=true ellipsoids3d.hpp linguist-generated=true encoded_depth_image.cpp linguist-generated=true @@ -77,8 +79,10 @@ series_lines.cpp linguist-generated=true series_lines.hpp linguist-generated=true series_points.cpp linguist-generated=true series_points.hpp linguist-generated=true -status.cpp linguist-generated=true -status.hpp linguist-generated=true +state_change.cpp linguist-generated=true +state_change.hpp linguist-generated=true +state_configuration.cpp linguist-generated=true +state_configuration.hpp linguist-generated=true tensor.cpp linguist-generated=true tensor.hpp linguist-generated=true text_document.cpp linguist-generated=true @@ -95,3 +99,5 @@ video_stream.cpp linguist-generated=true video_stream.hpp linguist-generated=true view_coordinates.cpp linguist-generated=true view_coordinates.hpp linguist-generated=true +voxel_grid_map.cpp linguist-generated=true +voxel_grid_map.hpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/archetypes/annotation_context.hpp b/rerun_cpp/src/rerun/archetypes/annotation_context.hpp index 52969014d011..ab084a370308 100644 --- a/rerun_cpp/src/rerun/archetypes/annotation_context.hpp +++ b/rerun_cpp/src/rerun/archetypes/annotation_context.hpp @@ -37,7 +37,8 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_annotation_context_segmentation"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_annotation_context_segmentation"); /// rec.spawn().exit_on_failure(); /// /// // create an annotation context to describe the classes @@ -57,10 +58,17 @@ namespace rerun::archetypes { /// std::fill_n(data.begin() + y * WIDTH + 50, 70, static_cast(1)); /// } /// for (auto y = 100; y <180; ++y) { - /// std::fill_n(data.begin() + y * WIDTH + 130, 150, static_cast(2)); + /// std::fill_n( + /// data.begin() + y * WIDTH + 130, + /// 150, + /// static_cast(2) + /// ); /// } /// - /// rec.log("segmentation/image", rerun::SegmentationImage(data.data(), {WIDTH, HEIGHT})); + /// rec.log( + /// "segmentation/image", + /// rerun::SegmentationImage(data.data(), {WIDTH, HEIGHT}) + /// ); /// } /// ``` /// diff --git a/rerun_cpp/src/rerun/archetypes/arrows2d.hpp b/rerun_cpp/src/rerun/archetypes/arrows2d.hpp index 51d2dccced9b..2e9cfb284af9 100644 --- a/rerun_cpp/src/rerun/archetypes/arrows2d.hpp +++ b/rerun_cpp/src/rerun/archetypes/arrows2d.hpp @@ -38,7 +38,9 @@ namespace rerun::archetypes { /// /// rec.log( /// "arrows", - /// rerun::Arrows2D::from_vectors({{1.0f, 0.0f}, {0.0f, -1.0f}, {-0.7f, 0.7f}}) + /// rerun::Arrows2D::from_vectors( + /// {{1.0f, 0.0f}, {0.0f, -1.0f}, {-0.7f, 0.7f}} + /// ) /// .with_radii(0.025f) /// .with_origins({{0.25f, 0.0f}, {0.25f, 0.0f}, {-0.1f, -0.1f}}) /// .with_colors({{255, 0, 0}, {0, 255, 0}, {127, 0, 255}}) diff --git a/rerun_cpp/src/rerun/archetypes/arrows3d.hpp b/rerun_cpp/src/rerun/archetypes/arrows3d.hpp index 0d08c0d7d3ae..fcc016922894 100644 --- a/rerun_cpp/src/rerun/archetypes/arrows3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/arrows3d.hpp @@ -57,7 +57,9 @@ namespace rerun::archetypes { /// /// rec.log( /// "arrows", - /// rerun::Arrows3D::from_vectors(vectors).with_origins(origins).with_colors(colors) + /// rerun::Arrows3D::from_vectors(vectors) + /// .with_origins(origins) + /// .with_colors(colors) /// ); /// } /// ``` diff --git a/rerun_cpp/src/rerun/archetypes/asset3d.hpp b/rerun_cpp/src/rerun/archetypes/asset3d.hpp index 8c8640ac64bc..8950d67131c4 100644 --- a/rerun_cpp/src/rerun/archetypes/asset3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/asset3d.hpp @@ -37,7 +37,8 @@ namespace rerun::archetypes { /// /// int main(int argc, char* argv[]) { /// if (argc <2) { - /// std::cerr <<"Usage: " <" <" <" <" + /// < frame_timestamps_ns = /// video_asset.read_frame_timestamps_nanos().value_or_throw(); /// // Note timeline values don't have to be the same as the video timestamps. - /// auto time_column = - /// rerun::TimeColumn::from_durations("video_time", rerun::borrow(frame_timestamps_ns)); + /// auto time_column = rerun::TimeColumn::from_durations( + /// "video_time", + /// rerun::borrow(frame_timestamps_ns) + /// ); /// - /// std::vector video_timestamps(frame_timestamps_ns.size()); + /// std::vector video_timestamps( + /// frame_timestamps_ns.size() + /// ); /// for (size_t i = 0; i " <" + /// <{0, 1, 3, 4, 7, 11}; - /// auto abscissa_data = rerun::TensorData(rerun::Collection{abscissa.size()}, abscissa); + /// auto abscissa_data = + /// rerun::TensorData(rerun::Collection{abscissa.size()}, abscissa); /// rec.log( /// "bar_chart_custom_abscissa", /// rerun::BarChart::i64({8, 4, 0, 9, 1, 4}).with_abscissa(abscissa_data) @@ -46,7 +47,9 @@ namespace rerun::archetypes { /// auto widths = std::vector{1, 2, 1, 3, 4, 1}; /// rec.log( /// "bar_chart_custom_abscissa_and_widths", - /// rerun::BarChart::i64({8, 4, 0, 9, 1, 4}).with_abscissa(abscissa_data).with_widths(widths) + /// rerun::BarChart::i64({8, 4, 0, 9, 1, 4}) + /// .with_abscissa(abscissa_data) + /// .with_widths(widths) /// ); /// } /// ``` diff --git a/rerun_cpp/src/rerun/archetypes/boxes2d.hpp b/rerun_cpp/src/rerun/archetypes/boxes2d.hpp index d72272486efa..336a7ef64c85 100644 --- a/rerun_cpp/src/rerun/archetypes/boxes2d.hpp +++ b/rerun_cpp/src/rerun/archetypes/boxes2d.hpp @@ -36,7 +36,10 @@ namespace rerun::archetypes { /// const auto rec = rerun::RecordingStream("rerun_example_box2d"); /// rec.spawn().exit_on_failure(); /// - /// rec.log("simple", rerun::Boxes2D::from_mins_and_sizes({{-1.f, -1.f}}, {{2.f, 2.f}})); + /// rec.log( + /// "simple", + /// rerun::Boxes2D::from_mins_and_sizes({{-1.f, -1.f}}, {{2.f, 2.f}}) + /// ); /// } /// ``` struct Boxes2D { diff --git a/rerun_cpp/src/rerun/archetypes/capsules3d.hpp b/rerun_cpp/src/rerun/archetypes/capsules3d.hpp index 44263b7136a6..4f6a411cda26 100644 --- a/rerun_cpp/src/rerun/archetypes/capsules3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/capsules3d.hpp @@ -67,11 +67,26 @@ namespace rerun::archetypes { /// {8.0f, 0.0f, 0.0f}, /// }) /// .with_rotation_axis_angles({ - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(0.0)), - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-22.5)), - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-45.0)), - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-67.5)), - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-90.0)), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(0.0) + /// ), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(-22.5) + /// ), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(-45.0) + /// ), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(-67.5) + /// ), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(-90.0) + /// ), /// }) /// ); /// } diff --git a/rerun_cpp/src/rerun/archetypes/coordinate_frame.hpp b/rerun_cpp/src/rerun/archetypes/coordinate_frame.hpp index 94d3b94145b0..67f52e5d3ee3 100644 --- a/rerun_cpp/src/rerun/archetypes/coordinate_frame.hpp +++ b/rerun_cpp/src/rerun/archetypes/coordinate_frame.hpp @@ -31,7 +31,8 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_transform3d_hierarchy"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_transform3d_hierarchy"); /// rec.spawn().exit_on_failure(); /// /// rec.set_time_sequence("time", 0); diff --git a/rerun_cpp/src/rerun/archetypes/cylinders3d.hpp b/rerun_cpp/src/rerun/archetypes/cylinders3d.hpp index 76054549ba66..3374be2314f4 100644 --- a/rerun_cpp/src/rerun/archetypes/cylinders3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/cylinders3d.hpp @@ -64,11 +64,26 @@ namespace rerun::archetypes { /// {8.0f, 0.0f, 0.0f}, /// }) /// .with_rotation_axis_angles({ - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(0.0)), - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-22.5)), - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-45.0)), - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-67.5)), - /// rerun::RotationAxisAngle({1.0f, 0.0f, 0.0f}, rerun::Angle::degrees(-90.0)), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(0.0) + /// ), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(-22.5) + /// ), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(-45.0) + /// ), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(-67.5) + /// ), + /// rerun::RotationAxisAngle( + /// {1.0f, 0.0f, 0.0f}, + /// rerun::Angle::degrees(-90.0) + /// ), /// }) /// ); /// } diff --git a/rerun_cpp/src/rerun/archetypes/depth_image.hpp b/rerun_cpp/src/rerun/archetypes/depth_image.hpp index 7d683bcf66a6..962aa2465d7a 100644 --- a/rerun_cpp/src/rerun/archetypes/depth_image.hpp +++ b/rerun_cpp/src/rerun/archetypes/depth_image.hpp @@ -51,10 +51,18 @@ namespace rerun::archetypes { /// const int WIDTH = 300; /// std::vector data(WIDTH * HEIGHT, 65535); /// for (auto y = 50; y <150; ++y) { - /// std::fill_n(data.begin() + y * WIDTH + 50, 100, static_cast(20000)); + /// std::fill_n( + /// data.begin() + y * WIDTH + 50, + /// 100, + /// static_cast(20000) + /// ); /// } /// for (auto y = 130; y <180; ++y) { - /// std::fill_n(data.begin() + y * WIDTH + 100, 180, static_cast(45000)); + /// std::fill_n( + /// data.begin() + y * WIDTH + 100, + /// 180, + /// static_cast(45000) + /// ); /// } /// /// // If we log a pinhole camera model, the depth gets automatically back-projected to 3D @@ -70,7 +78,7 @@ namespace rerun::archetypes { /// "world/camera/depth", /// rerun::DepthImage(data.data(), {WIDTH, HEIGHT}) /// .with_meter(10000.0) - /// .with_colormap(rerun::components::Colormap::Viridis) + /// .with_colormap(rerun::Colormap::Viridis) /// ); /// } /// ``` diff --git a/rerun_cpp/src/rerun/archetypes/ellipses2d.cpp b/rerun_cpp/src/rerun/archetypes/ellipses2d.cpp new file mode 100644 index 000000000000..44e42840fea2 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/ellipses2d.cpp @@ -0,0 +1,130 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/ellipses2d.fbs". + +#include "ellipses2d.hpp" + +#include "../collection_adapter_builtins.hpp" + +namespace rerun::archetypes { + Ellipses2D Ellipses2D::clear_fields() { + auto archetype = Ellipses2D(); + archetype.half_sizes = + ComponentBatch::empty(Descriptor_half_sizes) + .value_or_throw(); + archetype.centers = ComponentBatch::empty(Descriptor_centers) + .value_or_throw(); + archetype.colors = + ComponentBatch::empty(Descriptor_colors).value_or_throw(); + archetype.line_radii = + ComponentBatch::empty(Descriptor_line_radii) + .value_or_throw(); + archetype.labels = + ComponentBatch::empty(Descriptor_labels).value_or_throw(); + archetype.show_labels = + ComponentBatch::empty(Descriptor_show_labels) + .value_or_throw(); + archetype.draw_order = + ComponentBatch::empty(Descriptor_draw_order) + .value_or_throw(); + archetype.class_ids = + ComponentBatch::empty(Descriptor_class_ids) + .value_or_throw(); + return archetype; + } + + Collection Ellipses2D::columns(const Collection& lengths_) { + std::vector columns; + columns.reserve(8); + if (half_sizes.has_value()) { + columns.push_back(half_sizes.value().partitioned(lengths_).value_or_throw()); + } + if (centers.has_value()) { + columns.push_back(centers.value().partitioned(lengths_).value_or_throw()); + } + if (colors.has_value()) { + columns.push_back(colors.value().partitioned(lengths_).value_or_throw()); + } + if (line_radii.has_value()) { + columns.push_back(line_radii.value().partitioned(lengths_).value_or_throw()); + } + if (labels.has_value()) { + columns.push_back(labels.value().partitioned(lengths_).value_or_throw()); + } + if (show_labels.has_value()) { + columns.push_back(show_labels.value().partitioned(lengths_).value_or_throw()); + } + if (draw_order.has_value()) { + columns.push_back(draw_order.value().partitioned(lengths_).value_or_throw()); + } + if (class_ids.has_value()) { + columns.push_back(class_ids.value().partitioned(lengths_).value_or_throw()); + } + return columns; + } + + Collection Ellipses2D::columns() { + if (half_sizes.has_value()) { + return columns(std::vector(half_sizes.value().length(), 1)); + } + if (centers.has_value()) { + return columns(std::vector(centers.value().length(), 1)); + } + if (colors.has_value()) { + return columns(std::vector(colors.value().length(), 1)); + } + if (line_radii.has_value()) { + return columns(std::vector(line_radii.value().length(), 1)); + } + if (labels.has_value()) { + return columns(std::vector(labels.value().length(), 1)); + } + if (show_labels.has_value()) { + return columns(std::vector(show_labels.value().length(), 1)); + } + if (draw_order.has_value()) { + return columns(std::vector(draw_order.value().length(), 1)); + } + if (class_ids.has_value()) { + return columns(std::vector(class_ids.value().length(), 1)); + } + return Collection(); + } +} // namespace rerun::archetypes + +namespace rerun { + + Result> AsComponents::as_batches( + const archetypes::Ellipses2D& archetype + ) { + using namespace archetypes; + std::vector cells; + cells.reserve(8); + + if (archetype.half_sizes.has_value()) { + cells.push_back(archetype.half_sizes.value()); + } + if (archetype.centers.has_value()) { + cells.push_back(archetype.centers.value()); + } + if (archetype.colors.has_value()) { + cells.push_back(archetype.colors.value()); + } + if (archetype.line_radii.has_value()) { + cells.push_back(archetype.line_radii.value()); + } + if (archetype.labels.has_value()) { + cells.push_back(archetype.labels.value()); + } + if (archetype.show_labels.has_value()) { + cells.push_back(archetype.show_labels.value()); + } + if (archetype.draw_order.has_value()) { + cells.push_back(archetype.draw_order.value()); + } + if (archetype.class_ids.has_value()) { + cells.push_back(archetype.class_ids.value()); + } + + return rerun::take_ownership(std::move(cells)); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/ellipses2d.hpp b/rerun_cpp/src/rerun/archetypes/ellipses2d.hpp new file mode 100644 index 000000000000..bbe01e98518e --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/ellipses2d.hpp @@ -0,0 +1,305 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/ellipses2d.fbs". + +#pragma once + +#include "../collection.hpp" +#include "../component_batch.hpp" +#include "../component_column.hpp" +#include "../components/class_id.hpp" +#include "../components/color.hpp" +#include "../components/draw_order.hpp" +#include "../components/half_size2d.hpp" +#include "../components/position2d.hpp" +#include "../components/radius.hpp" +#include "../components/show_labels.hpp" +#include "../components/text.hpp" +#include "../result.hpp" + +#include +#include +#include +#include + +namespace rerun::archetypes { + /// **Archetype**: 2D ellipses with half-extents (semi-axes) and optional center, colors etc. + /// + /// The half-sizes specify the lengths of the ellipse's two axes along the local x and y directions. + /// If both half-sizes are equal, the ellipse is a circle. + /// + /// ## Examples + /// + /// ### Simple 2D ellipses + /// ```cpp + /// #include + /// + /// int main(int argc, char* argv[]) { + /// const auto rec = rerun::RecordingStream("rerun_example_ellipses2d"); + /// rec.spawn().exit_on_failure(); + /// + /// rec.log( + /// "simple", + /// rerun::Ellipses2D::from_centers_and_half_sizes( + /// {{0.0f, 0.0f}}, + /// {{2.0f, 1.0f}} + /// ) + /// ); + /// } + /// ``` + /// + /// ### Batch of 2D ellipses + /// ```cpp + /// #include + /// + /// int main(int argc, char* argv[]) { + /// const auto rec = rerun::RecordingStream("rerun_example_ellipses2d_batch"); + /// rec.spawn().exit_on_failure(); + /// + /// rec.log( + /// "batch", + /// rerun::Ellipses2D::from_centers_and_half_sizes( + /// {{-2.0f, 0.0f}, {0.0f, 0.0f}, {2.5f, 0.0f}}, + /// {{1.5f, 0.75f}, {0.5f, 0.5f}, {0.75f, 1.5f}} + /// ) + /// .with_line_radii({0.025f, 0.05f, 0.025f}) + /// .with_colors({ + /// rerun::Rgba32(255, 0, 0), + /// rerun::Rgba32(0, 255, 0), + /// rerun::Rgba32(0, 0, 255), + /// }) + /// .with_labels({"wide", "circle", "tall"}) + /// ); + /// } + /// ``` + struct Ellipses2D { + /// All half-extents (semi-axes) that make up the batch of ellipses. + std::optional half_sizes; + + /// Optional center positions of the ellipses. + std::optional centers; + + /// Optional colors for the ellipses. + std::optional colors; + + /// Optional radii for the lines that make up the ellipses. + std::optional line_radii; + + /// Optional text labels for the ellipses. + /// + /// If there's a single label present, it will be placed at the center of the entity. + /// Otherwise, each instance will have its own label. + std::optional labels; + + /// Whether the text labels should be shown. + /// + /// If not set, labels will automatically appear when there is exactly one label for this entity + /// or the number of instances on this entity is under a certain threshold. + std::optional show_labels; + + /// An optional floating point value that specifies the 2D drawing order. + /// + /// Objects with higher values are drawn on top of those with lower values. + /// Defaults to `10.0`. + std::optional draw_order; + + /// Optional `components::ClassId`s for the ellipses. + /// + /// The `components::ClassId` provides colors and labels if not specified explicitly. + std::optional class_ids; + + public: + /// The name of the archetype as used in `ComponentDescriptor`s. + static constexpr const char ArchetypeName[] = "rerun.archetypes.Ellipses2D"; + + /// `ComponentDescriptor` for the `half_sizes` field. + static constexpr auto Descriptor_half_sizes = ComponentDescriptor( + ArchetypeName, "Ellipses2D:half_sizes", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `centers` field. + static constexpr auto Descriptor_centers = ComponentDescriptor( + ArchetypeName, "Ellipses2D:centers", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `colors` field. + static constexpr auto Descriptor_colors = ComponentDescriptor( + ArchetypeName, "Ellipses2D:colors", Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `line_radii` field. + static constexpr auto Descriptor_line_radii = ComponentDescriptor( + ArchetypeName, "Ellipses2D:line_radii", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `labels` field. + static constexpr auto Descriptor_labels = ComponentDescriptor( + ArchetypeName, "Ellipses2D:labels", Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `show_labels` field. + static constexpr auto Descriptor_show_labels = ComponentDescriptor( + ArchetypeName, "Ellipses2D:show_labels", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `draw_order` field. + static constexpr auto Descriptor_draw_order = ComponentDescriptor( + ArchetypeName, "Ellipses2D:draw_order", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `class_ids` field. + static constexpr auto Descriptor_class_ids = ComponentDescriptor( + ArchetypeName, "Ellipses2D:class_ids", + Loggable::ComponentType + ); + + public: // START of extensions from ellipses2d_ext.cpp: + /// Creates new `Ellipses2D` with `half_sizes` centered around the local origin. + static Ellipses2D from_half_sizes(Collection half_sizes) { + return Ellipses2D().with_half_sizes(std::move(half_sizes)); + } + + /// Creates new `Ellipses2D` with `centers` and `half_sizes`. + static Ellipses2D from_centers_and_half_sizes( + Collection centers, + Collection half_sizes + ) { + return Ellipses2D() + .with_half_sizes(std::move(half_sizes)) + .with_centers(std::move(centers)); + } + + // END of extensions from ellipses2d_ext.cpp, start of generated code: + + public: + Ellipses2D() = default; + Ellipses2D(Ellipses2D&& other) = default; + Ellipses2D(const Ellipses2D& other) = default; + Ellipses2D& operator=(const Ellipses2D& other) = default; + Ellipses2D& operator=(Ellipses2D&& other) = default; + + /// Update only some specific fields of a `Ellipses2D`. + static Ellipses2D update_fields() { + return Ellipses2D(); + } + + /// Clear all the fields of a `Ellipses2D`. + static Ellipses2D clear_fields(); + + /// All half-extents (semi-axes) that make up the batch of ellipses. + Ellipses2D with_half_sizes(const Collection& _half_sizes + ) && { + half_sizes = + ComponentBatch::from_loggable(_half_sizes, Descriptor_half_sizes).value_or_throw(); + return std::move(*this); + } + + /// Optional center positions of the ellipses. + Ellipses2D with_centers(const Collection& _centers) && { + centers = ComponentBatch::from_loggable(_centers, Descriptor_centers).value_or_throw(); + return std::move(*this); + } + + /// Optional colors for the ellipses. + Ellipses2D with_colors(const Collection& _colors) && { + colors = ComponentBatch::from_loggable(_colors, Descriptor_colors).value_or_throw(); + return std::move(*this); + } + + /// Optional radii for the lines that make up the ellipses. + Ellipses2D with_line_radii(const Collection& _line_radii) && { + line_radii = + ComponentBatch::from_loggable(_line_radii, Descriptor_line_radii).value_or_throw(); + return std::move(*this); + } + + /// Optional text labels for the ellipses. + /// + /// If there's a single label present, it will be placed at the center of the entity. + /// Otherwise, each instance will have its own label. + Ellipses2D with_labels(const Collection& _labels) && { + labels = ComponentBatch::from_loggable(_labels, Descriptor_labels).value_or_throw(); + return std::move(*this); + } + + /// Whether the text labels should be shown. + /// + /// If not set, labels will automatically appear when there is exactly one label for this entity + /// or the number of instances on this entity is under a certain threshold. + Ellipses2D with_show_labels(const rerun::components::ShowLabels& _show_labels) && { + show_labels = ComponentBatch::from_loggable(_show_labels, Descriptor_show_labels) + .value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `show_labels` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_show_labels` should + /// be used when logging a single row's worth of data. + Ellipses2D with_many_show_labels( + const Collection& _show_labels + ) && { + show_labels = ComponentBatch::from_loggable(_show_labels, Descriptor_show_labels) + .value_or_throw(); + return std::move(*this); + } + + /// An optional floating point value that specifies the 2D drawing order. + /// + /// Objects with higher values are drawn on top of those with lower values. + /// Defaults to `10.0`. + Ellipses2D with_draw_order(const rerun::components::DrawOrder& _draw_order) && { + draw_order = + ComponentBatch::from_loggable(_draw_order, Descriptor_draw_order).value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `draw_order` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_draw_order` should + /// be used when logging a single row's worth of data. + Ellipses2D with_many_draw_order(const Collection& _draw_order + ) && { + draw_order = + ComponentBatch::from_loggable(_draw_order, Descriptor_draw_order).value_or_throw(); + return std::move(*this); + } + + /// Optional `components::ClassId`s for the ellipses. + /// + /// The `components::ClassId` provides colors and labels if not specified explicitly. + Ellipses2D with_class_ids(const Collection& _class_ids) && { + class_ids = + ComponentBatch::from_loggable(_class_ids, Descriptor_class_ids).value_or_throw(); + return std::move(*this); + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s + /// instead, via `ComponentBatch::partitioned`. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + Collection columns(const Collection& lengths_); + + /// Partitions the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, + /// where `n` is automatically guessed. + Collection columns(); + }; + +} // namespace rerun::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> as_batches(const archetypes::Ellipses2D& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/ellipses2d_ext.cpp b/rerun_cpp/src/rerun/archetypes/ellipses2d_ext.cpp new file mode 100644 index 000000000000..d1d9ee26e141 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/ellipses2d_ext.cpp @@ -0,0 +1,31 @@ +#include "ellipses2d.hpp" + +#include "../collection_adapter_builtins.hpp" + +// #define EDIT_EXTENSION + +namespace rerun { + namespace archetypes { + +#ifdef EDIT_EXTENSION + // + + /// Creates new `Ellipses2D` with `half_sizes` centered around the local origin. + static Ellipses2D from_half_sizes(Collection half_sizes) { + return Ellipses2D().with_half_sizes(std::move(half_sizes)); + } + + /// Creates new `Ellipses2D` with `centers` and `half_sizes`. + static Ellipses2D from_centers_and_half_sizes( + Collection centers, + Collection half_sizes + ) { + return Ellipses2D() + .with_half_sizes(std::move(half_sizes)) + .with_centers(std::move(centers)); + } + + // +#endif + } // namespace archetypes +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/ellipsoids3d.hpp b/rerun_cpp/src/rerun/archetypes/ellipsoids3d.hpp index 34d14486e8d5..8d50279bb48b 100644 --- a/rerun_cpp/src/rerun/archetypes/ellipsoids3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/ellipsoids3d.hpp @@ -65,7 +65,9 @@ namespace rerun::archetypes { /// /// rec.log( /// "points", - /// rerun::Points3D(points3d).with_radii(0.02f).with_colors(rerun::Rgba32(188, 77, 185)) + /// rerun::Points3D(points3d).with_radii(0.02f).with_colors( + /// rerun::Rgba32(188, 77, 185) + /// ) /// ); /// /// rec.log( diff --git a/rerun_cpp/src/rerun/archetypes/encoded_depth_image.hpp b/rerun_cpp/src/rerun/archetypes/encoded_depth_image.hpp index c84453dad8cc..84d1a053b60d 100644 --- a/rerun_cpp/src/rerun/archetypes/encoded_depth_image.hpp +++ b/rerun_cpp/src/rerun/archetypes/encoded_depth_image.hpp @@ -43,17 +43,20 @@ namespace rerun::archetypes { /// /// int main(int argc, char* argv[]) { /// if (argc <2) { - /// std::cerr <<"Usage: " <" <" + /// <(file), /// std::istreambuf_iterator()}; /// // Determine media type based on file extension - /// rerun::components::MediaType media_type; + /// rerun::MediaType media_type; /// if (depth_path.extension() == ".png") { - /// media_type = rerun::components::MediaType::png(); + /// media_type = rerun::MediaType::png(); /// } else { - /// media_type = rerun::components::MediaType::rvl(); + /// media_type = rerun::MediaType::rvl(); /// } /// /// rec.log( diff --git a/rerun_cpp/src/rerun/archetypes/encoded_image.hpp b/rerun_cpp/src/rerun/archetypes/encoded_image.hpp index 66e4cb2d90b2..229190bd38e6 100644 --- a/rerun_cpp/src/rerun/archetypes/encoded_image.hpp +++ b/rerun_cpp/src/rerun/archetypes/encoded_image.hpp @@ -46,7 +46,10 @@ namespace rerun::archetypes { /// /// fs::path image_filepath = fs::path(__FILE__).parent_path() / "ferris.png"; /// - /// rec.log("image", rerun::EncodedImage::from_file(image_filepath).value_or_throw()); + /// rec.log( + /// "image", + /// rerun::EncodedImage::from_file(image_filepath).value_or_throw() + /// ); /// } /// ``` struct EncodedImage { diff --git a/rerun_cpp/src/rerun/archetypes/geo_line_strings.hpp b/rerun_cpp/src/rerun/archetypes/geo_line_strings.hpp index 046e7fbf66fc..4ae91cbe1cc7 100644 --- a/rerun_cpp/src/rerun/archetypes/geo_line_strings.hpp +++ b/rerun_cpp/src/rerun/archetypes/geo_line_strings.hpp @@ -33,7 +33,7 @@ namespace rerun::archetypes { /// const auto rec = rerun::RecordingStream("rerun_example_geo_line_strings"); /// rec.spawn().exit_on_failure(); /// - /// auto line_string = rerun::components::GeoLineString::from_lat_lon( + /// auto line_string = rerun::GeoLineString::from_lat_lon( /// {{41.0000, -109.0452}, /// {41.0000, -102.0415}, /// {36.9931, -102.0415}, diff --git a/rerun_cpp/src/rerun/archetypes/graph_edges.hpp b/rerun_cpp/src/rerun/archetypes/graph_edges.hpp index cbb502d1e9fe..46782982bb56 100644 --- a/rerun_cpp/src/rerun/archetypes/graph_edges.hpp +++ b/rerun_cpp/src/rerun/archetypes/graph_edges.hpp @@ -39,7 +39,7 @@ namespace rerun::archetypes { /// .with_labels({"A", "B", "C"}), /// rerun::GraphEdges({{"a", "b"}, {"b", "c"}, {"c", "a"}}) /// // Graphs are undirected by default. - /// .with_graph_type(rerun::components::GraphType::Directed) + /// .with_graph_type(rerun::GraphType::Directed) /// ); /// } /// ``` diff --git a/rerun_cpp/src/rerun/archetypes/graph_nodes.hpp b/rerun_cpp/src/rerun/archetypes/graph_nodes.hpp index 77c27fd771cc..2b673ec3dbbf 100644 --- a/rerun_cpp/src/rerun/archetypes/graph_nodes.hpp +++ b/rerun_cpp/src/rerun/archetypes/graph_nodes.hpp @@ -41,7 +41,7 @@ namespace rerun::archetypes { /// .with_labels({"A", "B", "C"}), /// rerun::GraphEdges({{"a", "b"}, {"b", "c"}, {"c", "a"}}) /// // Graphs are undirected by default. - /// .with_graph_type(rerun::components::GraphType::Directed) + /// .with_graph_type(rerun::GraphType::Directed) /// ); /// } /// ``` diff --git a/rerun_cpp/src/rerun/archetypes/grid_map.hpp b/rerun_cpp/src/rerun/archetypes/grid_map.hpp index 888a09b78800..e5eb04ed8ec1 100644 --- a/rerun_cpp/src/rerun/archetypes/grid_map.hpp +++ b/rerun_cpp/src/rerun/archetypes/grid_map.hpp @@ -60,7 +60,7 @@ namespace rerun::archetypes { /// rec.log( /// "world/map", /// rerun::archetypes::GridMap() - /// .with_data(rerun::components::ImageBuffer(grid)) + /// .with_data(rerun::ImageBuffer(grid)) /// .with_format(rerun::components::ImageFormat( /// {width, height}, /// rerun::ColorModel::L, @@ -72,13 +72,10 @@ namespace rerun::archetypes { /// -(static_cast(height) * cell_size) / 2.0f, /// 0.0f} /// ) - /// .with_colormap(rerun::components::Colormap::RvizMap) + /// .with_colormap(rerun::Colormap::RvizMap) /// ); /// } /// ``` - /// - /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** - /// struct GridMap { /// The raw grid data. std::optional data; diff --git a/rerun_cpp/src/rerun/archetypes/image.hpp b/rerun_cpp/src/rerun/archetypes/image.hpp index c024f424429f..992befc3ae2c 100644 --- a/rerun_cpp/src/rerun/archetypes/image.hpp +++ b/rerun_cpp/src/rerun/archetypes/image.hpp @@ -91,7 +91,8 @@ namespace rerun::archetypes { /// for (size_t y = 0; y <256; ++y) { /// for (size_t x = 0; x <256; ++x) { /// image[(y * 256 + x) * 3 + 0] = static_cast(x); - /// image[(y * 256 + x) * 3 + 1] = static_cast(std::min(255, x + y)); + /// image[(y * 256 + x) * 3 + 1] = + /// static_cast(std::min(255, x + y)); /// image[(y * 256 + x) * 3 + 2] = static_cast(y); /// } /// } @@ -106,7 +107,11 @@ namespace rerun::archetypes { /// } /// rec.log( /// "image_green_only", - /// rerun::Image(rerun::borrow(green_channel), {256, 256}, rerun::ColorModel::L) + /// rerun::Image( + /// rerun::borrow(green_channel), + /// {256, 256}, + /// rerun::ColorModel::L + /// ) /// ); /// /// // BGR image @@ -118,24 +123,38 @@ namespace rerun::archetypes { /// } /// rec.log( /// "image_bgr", - /// rerun::Image(rerun::borrow(bgr_image), {256, 256}, rerun::ColorModel::BGR) + /// rerun::Image( + /// rerun::borrow(bgr_image), + /// {256, 256}, + /// rerun::ColorModel::BGR + /// ) /// ); /// /// // New image with Separate Y/U/V planes with 4:2:2 chroma downsampling /// std::vector yuv_bytes(256 * 256 + 128 * 256 * 2); - /// std::fill_n(yuv_bytes.begin(), 256 * 256, static_cast(128)); // Fixed value for Y + /// std::fill_n( + /// yuv_bytes.begin(), + /// 256 * 256, + /// static_cast(128) // Fixed value for Y + /// ); /// size_t u_plane_offset = 256 * 256; /// size_t v_plane_offset = u_plane_offset + 128 * 256; /// for (size_t y = 0; y <256; ++y) { /// for (size_t x = 0; x <128; ++x) { /// auto coord = y * 128 + x; - /// yuv_bytes[u_plane_offset + coord] = static_cast(x * 2); // Gradient for U - /// yuv_bytes[v_plane_offset + coord] = static_cast(y); // Gradient for V + /// yuv_bytes[u_plane_offset + coord] = + /// static_cast(x * 2); // Gradient for U + /// yuv_bytes[v_plane_offset + coord] = + /// static_cast(y); // Gradient for V /// } /// } /// rec.log( /// "image_yuv422", - /// rerun::Image(rerun::borrow(yuv_bytes), {256, 256}, rerun::PixelFormat::Y_U_V16_FullRange) + /// rerun::Image( + /// rerun::borrow(yuv_bytes), + /// {256, 256}, + /// rerun::PixelFormat::Y_U_V16_FullRange + /// ) /// ); /// /// return 0; diff --git a/rerun_cpp/src/rerun/archetypes/instance_poses3d.hpp b/rerun_cpp/src/rerun/archetypes/instance_poses3d.hpp index 68700ab4d38c..36bc33ed8013 100644 --- a/rerun_cpp/src/rerun/archetypes/instance_poses3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/instance_poses3d.hpp @@ -50,14 +50,17 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_instance_pose3d_combined"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_instance_pose3d_combined"); /// rec.set_time_sequence("frame", 0); /// /// // Log a box and points further down in the hierarchy. /// rec.log("world/box", rerun::Boxes3D::from_half_sizes({{1.0, 1.0, 1.0}})); /// rec.log( /// "world/box/points", - /// rerun::Points3D(rerun::demo::grid3d(-10.0f, 10.0f, 10)) + /// rerun::Points3D( + /// rerun::demo::grid3d(-10.0f, 10.0f, 10) + /// ) /// ); /// /// for (int i = 0; i <180; ++i) { @@ -75,7 +78,9 @@ namespace rerun::archetypes { /// rec.log( /// "world/box", /// rerun::InstancePoses3D().with_translations( - /// {{0.0f, 0.0f, std::abs(static_cast(i) * 0.1f - 5.0f) - 5.0f}} + /// {{0.0f, + /// 0.0f, + /// std::abs(static_cast(i) * 0.1f - 5.0f) - 5.0f}} /// ) /// ); /// } diff --git a/rerun_cpp/src/rerun/archetypes/line_strips2d.hpp b/rerun_cpp/src/rerun/archetypes/line_strips2d.hpp index d005cbe31207..14f594e839a7 100644 --- a/rerun_cpp/src/rerun/archetypes/line_strips2d.hpp +++ b/rerun_cpp/src/rerun/archetypes/line_strips2d.hpp @@ -37,9 +37,19 @@ namespace rerun::archetypes { /// const auto rec = rerun::RecordingStream("rerun_example_line_strip2d_batch"); /// rec.spawn().exit_on_failure(); /// - /// rerun::Collection strip1 = {{0.f, 0.f}, {2.f, 1.f}, {4.f, -1.f}, {6.f, 0.f}}; - /// rerun::Collection strip2 = - /// {{0.f, 3.f}, {1.f, 4.f}, {2.f, 2.f}, {3.f, 4.f}, {4.f, 2.f}, {5.f, 4.f}, {6.f, 3.f}}; + /// rerun::Collection strip1 = { + /// {0.f, 0.f}, + /// {2.f, 1.f}, + /// {4.f, -1.f}, + /// {6.f, 0.f}}; + /// rerun::Collection strip2 = { + /// {0.f, 3.f}, + /// {1.f, 4.f}, + /// {2.f, 2.f}, + /// {3.f, 4.f}, + /// {4.f, 2.f}, + /// {5.f, 4.f}, + /// {6.f, 3.f}}; /// rec.log( /// "strips", /// rerun::LineStrips2D({strip1, strip2}) @@ -57,11 +67,14 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_line_strip2d_ui_radius"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_line_strip2d_ui_radius"); /// rec.spawn().exit_on_failure(); /// /// // A blue line with a scene unit radii of 0.01. - /// rerun::LineStrip2D linestrip_blue({{0.f, 0.f}, {0.f, 1.f}, {1.f, 0.f}, {1.f, 1.f}}); + /// rerun::LineStrip2D linestrip_blue( + /// {{0.f, 0.f}, {0.f, 1.f}, {1.f, 0.f}, {1.f, 1.f}} + /// ); /// rec.log( /// "scene_unit_line", /// rerun::LineStrips2D(linestrip_blue) @@ -73,7 +86,9 @@ namespace rerun::archetypes { /// // A red line with a ui point radii of 5. /// // UI points are independent of zooming in Views, but are sensitive to the application UI scaling. /// // For 100 % ui scaling, UI points are equal to pixels. - /// rerun::LineStrip2D linestrip_red({{3.f, 0.f}, {3.f, 1.f}, {4.f, 0.f}, {4.f, 1.f}}); + /// rerun::LineStrip2D linestrip_red( + /// {{3.f, 0.f}, {3.f, 1.f}, {4.f, 0.f}, {4.f, 1.f}} + /// ); /// rec.log( /// "ui_points_line", /// rerun::LineStrips2D(linestrip_red) diff --git a/rerun_cpp/src/rerun/archetypes/line_strips3d.hpp b/rerun_cpp/src/rerun/archetypes/line_strips3d.hpp index 182e06671d17..4247f35efe8d 100644 --- a/rerun_cpp/src/rerun/archetypes/line_strips3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/line_strips3d.hpp @@ -69,7 +69,8 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_line_strip3d_ui_radius"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_line_strip3d_ui_radius"); /// rec.spawn().exit_on_failure(); /// /// // A blue line with a scene unit radii of 0.01. @@ -107,6 +108,8 @@ namespace rerun::archetypes { std::optional radii; /// Optional colors for the line strips. + /// + /// The alpha channel is ignored. std::optional colors; /// Optional text labels for the line strips. @@ -190,6 +193,8 @@ namespace rerun::archetypes { } /// Optional colors for the line strips. + /// + /// The alpha channel is ignored. LineStrips3D with_colors(const Collection& _colors) && { colors = ComponentBatch::from_loggable(_colors, Descriptor_colors).value_or_throw(); return std::move(*this); diff --git a/rerun_cpp/src/rerun/archetypes/mcap_statistics.hpp b/rerun_cpp/src/rerun/archetypes/mcap_statistics.hpp index 72df8548e19c..e74d37f4358a 100644 --- a/rerun_cpp/src/rerun/archetypes/mcap_statistics.hpp +++ b/rerun_cpp/src/rerun/archetypes/mcap_statistics.hpp @@ -17,7 +17,7 @@ #include namespace rerun::archetypes { - /// **Archetype**: Recording-level statistics about an MCAP file, logged as a part of `archetypes::RecordingInfo`. + /// **Archetype**: Recording-level statistics about an MCAP file. /// /// This archetype contains summary information about an entire MCAP recording, including /// counts of messages, schemas, channels, and other records, as well as timing information diff --git a/rerun_cpp/src/rerun/archetypes/mesh3d.hpp b/rerun_cpp/src/rerun/archetypes/mesh3d.hpp index 64c40f2c7a53..2ab547b590da 100644 --- a/rerun_cpp/src/rerun/archetypes/mesh3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/mesh3d.hpp @@ -83,11 +83,14 @@ namespace rerun::archetypes { /// rec.set_time_sequence("frame", 0); /// rec.log( /// "shape", - /// rerun::Mesh3D( - /// {{1.0f, 1.0f, 1.0f}, {-1.0f, -1.0f, 1.0f}, {-1.0f, 1.0f, -1.0f}, {1.0f, -1.0f, -1.0f}} - /// ) + /// rerun::Mesh3D({{1.0f, 1.0f, 1.0f}, + /// {-1.0f, -1.0f, 1.0f}, + /// {-1.0f, 1.0f, -1.0f}, + /// {1.0f, -1.0f, -1.0f}}) /// .with_triangle_indices({{0, 2, 1}, {0, 3, 1}, {0, 3, 2}, {1, 3, 2}}) - /// .with_vertex_colors({0xFF0000FF, 0x00FF00FF, 0x00000FFFF, 0xFFFF00FF}) + /// .with_vertex_colors( + /// {0xFF0000FF, 0x00FF00FF, 0x00000FFFF, 0xFFFF00FF} + /// ) /// ); /// // This box will not be affected by its parent's instance poses! /// rec.log("shape/box", rerun::Boxes3D::from_half_sizes({{5.0f, 5.0f, 5.0f}})); diff --git a/rerun_cpp/src/rerun/archetypes/pinhole.hpp b/rerun_cpp/src/rerun/archetypes/pinhole.hpp index 6135deb78d9a..8c8508f8d241 100644 --- a/rerun_cpp/src/rerun/archetypes/pinhole.hpp +++ b/rerun_cpp/src/rerun/archetypes/pinhole.hpp @@ -47,7 +47,10 @@ namespace rerun::archetypes { /// const auto rec = rerun::RecordingStream("rerun_example_pinhole"); /// rec.spawn().exit_on_failure(); /// - /// rec.log("world/image", rerun::Pinhole::from_focal_length_and_resolution(3.0f, {3.0f, 3.0f})); + /// rec.log( + /// "world/image", + /// rerun::Pinhole::from_focal_length_and_resolution(3.0f, {3.0f, 3.0f}) + /// ); /// /// std::vector random_data(3 * 3 * 3); /// std::generate(random_data.begin(), random_data.end(), [] { @@ -65,7 +68,8 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_pinhole_perspective"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_pinhole_perspective"); /// rec.spawn().exit_on_failure(); /// /// const float fov_y = 0.7853982f; @@ -81,7 +85,8 @@ namespace rerun::archetypes { /// /// rec.log( /// "world/points", - /// rerun::Points3D({{0.0f, 0.0f, -0.5f}, {0.1f, 0.1f, -0.5f}, {-0.1f, -0.1f, -0.5f}} + /// rerun::Points3D( + /// {{0.0f, 0.0f, -0.5f}, {0.1f, 0.1f, -0.5f}, {-0.1f, -0.1f, -0.5f}} /// ).with_radii({0.025f}) /// ); /// } diff --git a/rerun_cpp/src/rerun/archetypes/points2d.hpp b/rerun_cpp/src/rerun/archetypes/points2d.hpp index c11dbb687c6a..c1360a9ec1ca 100644 --- a/rerun_cpp/src/rerun/archetypes/points2d.hpp +++ b/rerun_cpp/src/rerun/archetypes/points2d.hpp @@ -61,7 +61,10 @@ namespace rerun::archetypes { /// std::vector radii(10); /// std::generate(radii.begin(), radii.end(), [&] { return dist_radius(gen); }); /// - /// rec.log("random", rerun::Points2D(points2d).with_colors(colors).with_radii(radii)); + /// rec.log( + /// "random", + /// rerun::Points2D(points2d).with_colors(colors).with_radii(radii) + /// ); /// /// // TODO(#5520): log VisualBounds2D /// } diff --git a/rerun_cpp/src/rerun/archetypes/points3d.cpp b/rerun_cpp/src/rerun/archetypes/points3d.cpp index b52d754d94ff..57c054a9e49c 100644 --- a/rerun_cpp/src/rerun/archetypes/points3d.cpp +++ b/rerun_cpp/src/rerun/archetypes/points3d.cpp @@ -20,6 +20,9 @@ namespace rerun::archetypes { archetype.show_labels = ComponentBatch::empty(Descriptor_show_labels) .value_or_throw(); + archetype.point_shading = + ComponentBatch::empty(Descriptor_point_shading) + .value_or_throw(); archetype.class_ids = ComponentBatch::empty(Descriptor_class_ids) .value_or_throw(); @@ -31,7 +34,7 @@ namespace rerun::archetypes { Collection Points3D::columns(const Collection& lengths_) { std::vector columns; - columns.reserve(7); + columns.reserve(8); if (positions.has_value()) { columns.push_back(positions.value().partitioned(lengths_).value_or_throw()); } @@ -47,6 +50,9 @@ namespace rerun::archetypes { if (show_labels.has_value()) { columns.push_back(show_labels.value().partitioned(lengths_).value_or_throw()); } + if (point_shading.has_value()) { + columns.push_back(point_shading.value().partitioned(lengths_).value_or_throw()); + } if (class_ids.has_value()) { columns.push_back(class_ids.value().partitioned(lengths_).value_or_throw()); } @@ -72,6 +78,9 @@ namespace rerun::archetypes { if (show_labels.has_value()) { return columns(std::vector(show_labels.value().length(), 1)); } + if (point_shading.has_value()) { + return columns(std::vector(point_shading.value().length(), 1)); + } if (class_ids.has_value()) { return columns(std::vector(class_ids.value().length(), 1)); } @@ -89,7 +98,7 @@ namespace rerun { ) { using namespace archetypes; std::vector cells; - cells.reserve(7); + cells.reserve(8); if (archetype.positions.has_value()) { cells.push_back(archetype.positions.value()); @@ -106,6 +115,9 @@ namespace rerun { if (archetype.show_labels.has_value()) { cells.push_back(archetype.show_labels.value()); } + if (archetype.point_shading.has_value()) { + cells.push_back(archetype.point_shading.value()); + } if (archetype.class_ids.has_value()) { cells.push_back(archetype.class_ids.value()); } diff --git a/rerun_cpp/src/rerun/archetypes/points3d.hpp b/rerun_cpp/src/rerun/archetypes/points3d.hpp index 57e88dcee13b..6c5b73635523 100644 --- a/rerun_cpp/src/rerun/archetypes/points3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/points3d.hpp @@ -9,6 +9,7 @@ #include "../components/class_id.hpp" #include "../components/color.hpp" #include "../components/keypoint_id.hpp" +#include "../components/point_shading.hpp" #include "../components/position3d.hpp" #include "../components/radius.hpp" #include "../components/show_labels.hpp" @@ -37,7 +38,10 @@ namespace rerun::archetypes { /// const auto rec = rerun::RecordingStream("rerun_example_points3d"); /// rec.spawn().exit_on_failure(); /// - /// rec.log("points", rerun::Points3D({{0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f}})); + /// rec.log( + /// "points", + /// rerun::Points3D({{0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f}}) + /// ); /// } /// ``` /// @@ -51,7 +55,8 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_points3d_row_updates"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_points3d_row_updates"); /// rec.spawn().exit_on_failure(); /// /// // Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. @@ -66,14 +71,17 @@ namespace rerun::archetypes { /// }; /// /// // At each timestep, all points in the cloud share the same but changing color and radius. - /// std::vector colors = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; + /// std::vector colors = + /// {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; /// std::vector radii = {0.05f, 0.01f, 0.2f, 0.1f, 0.3f}; /// /// for (size_t i = 0; i <5; i++) { /// rec.set_time_duration_secs("time", 10.0 + static_cast(i)); /// rec.log( /// "points", - /// rerun::Points3D(positions[i]).with_colors(colors[i]).with_radii(radii[i]) + /// rerun::Points3D(positions[i]) + /// .with_colors(colors[i]) + /// .with_radii(radii[i]) /// ); /// } /// } @@ -90,7 +98,8 @@ namespace rerun::archetypes { /// using namespace std::chrono_literals; /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_points3d_column_updates"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_points3d_column_updates"); /// rec.spawn().exit_on_failure(); /// /// // Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. @@ -105,16 +114,20 @@ namespace rerun::archetypes { /// }; /// /// // At each timestep, all points in the cloud share the same but changing color and radius. - /// std::vector colors = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; + /// std::vector colors = + /// {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF}; /// std::vector radii = {0.05f, 0.01f, 0.2f, 0.1f, 0.3f}; /// /// // Log at seconds 10-14 /// auto times = rerun::Collection{10s, 11s, 12s, 13s, 14s}; - /// auto time_column = rerun::TimeColumn::from_durations("time", std::move(times)); + /// auto time_column = + /// rerun::TimeColumn::from_durations("time", std::move(times)); /// /// // Partition our data as expected across the 5 timesteps. - /// auto position = rerun::Points3D().with_positions(positions).columns({2, 4, 4, 3, 4}); - /// auto color_and_radius = rerun::Points3D().with_colors(colors).with_radii(radii).columns(); + /// auto position = + /// rerun::Points3D().with_positions(positions).columns({2, 4, 4, 3, 4}); + /// auto color_and_radius = + /// rerun::Points3D().with_colors(colors).with_radii(radii).columns(); /// /// rec.send_columns("points", time_column, position, color_and_radius); /// } @@ -130,7 +143,8 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_points3d_partial_updates"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_points3d_partial_updates"); /// rec.spawn().exit_on_failure(); /// /// std::vector positions; @@ -162,7 +176,12 @@ namespace rerun::archetypes { /// /// // Update only the colors and radii, leaving everything else as-is. /// rec.set_time_sequence("frame", i); - /// rec.log("points", rerun::Points3D::update_fields().with_radii(radii).with_colors(colors)); + /// rec.log( + /// "points", + /// rerun::Points3D::update_fields().with_radii(radii).with_colors( + /// colors + /// ) + /// ); /// } /// /// std::vector radii; @@ -170,7 +189,12 @@ namespace rerun::archetypes { /// /// // Update the positions and radii, and clear everything else in the process. /// rec.set_time_sequence("frame", 20); - /// rec.log("points", rerun::Points3D::clear_fields().with_positions(positions).with_radii(radii)); + /// rec.log( + /// "points", + /// rerun::Points3D::clear_fields().with_positions(positions).with_radii( + /// radii + /// ) + /// ); /// } /// ``` struct Points3D { @@ -181,6 +205,9 @@ namespace rerun::archetypes { std::optional radii; /// Optional colors for the points. + /// + /// By default, the alpha channel affects brightness rather than transparency. + /// TODO(#1611): To use the alpha channel for transparency, enable the experimental "Transparent point clouds" feature flag. std::optional colors; /// Optional text labels for the points. @@ -195,6 +222,11 @@ namespace rerun::archetypes { /// or the number of instances on this entity is under a certain threshold. std::optional show_labels; + /// How points should be shaded. + /// + /// If not set, points are rendered with `components::PointShading::Gradient` by default. + std::optional point_shading; + /// Optional class Ids for the points. /// /// The `components::ClassId` provides colors and labels if not specified explicitly. @@ -236,6 +268,11 @@ namespace rerun::archetypes { ArchetypeName, "Points3D:show_labels", Loggable::ComponentType ); + /// `ComponentDescriptor` for the `point_shading` field. + static constexpr auto Descriptor_point_shading = ComponentDescriptor( + ArchetypeName, "Points3D:point_shading", + Loggable::ComponentType + ); /// `ComponentDescriptor` for the `class_ids` field. static constexpr auto Descriptor_class_ids = ComponentDescriptor( ArchetypeName, "Points3D:class_ids", Loggable::ComponentType @@ -279,6 +316,9 @@ namespace rerun::archetypes { } /// Optional colors for the points. + /// + /// By default, the alpha channel affects brightness rather than transparency. + /// TODO(#1611): To use the alpha channel for transparency, enable the experimental "Transparent point clouds" feature flag. Points3D with_colors(const Collection& _colors) && { colors = ComponentBatch::from_loggable(_colors, Descriptor_colors).value_or_throw(); return std::move(*this); @@ -314,6 +354,27 @@ namespace rerun::archetypes { return std::move(*this); } + /// How points should be shaded. + /// + /// If not set, points are rendered with `components::PointShading::Gradient` by default. + Points3D with_point_shading(const rerun::components::PointShading& _point_shading) && { + point_shading = ComponentBatch::from_loggable(_point_shading, Descriptor_point_shading) + .value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `point_shading` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_point_shading` should + /// be used when logging a single row's worth of data. + Points3D with_many_point_shading( + const Collection& _point_shading + ) && { + point_shading = ComponentBatch::from_loggable(_point_shading, Descriptor_point_shading) + .value_or_throw(); + return std::move(*this); + } + /// Optional class Ids for the points. /// /// The `components::ClassId` provides colors and labels if not specified explicitly. diff --git a/rerun_cpp/src/rerun/archetypes/scalars.hpp b/rerun_cpp/src/rerun/archetypes/scalars.hpp index 26514247ff34..226ab22c9b19 100644 --- a/rerun_cpp/src/rerun/archetypes/scalars.hpp +++ b/rerun_cpp/src/rerun/archetypes/scalars.hpp @@ -42,7 +42,10 @@ namespace rerun::archetypes { /// /// for (int step = 0; step <64; ++step) { /// rec.set_time_sequence("step", step); - /// rec.log("scalars", rerun::Scalars(sin(static_cast(step) / 10.0))); + /// rec.log( + /// "scalars", + /// rerun::Scalars(sin(static_cast(step) / 10.0)) + /// ); /// } /// } /// ``` @@ -58,7 +61,8 @@ namespace rerun::archetypes { /// #include /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_scalar_column_updates"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_scalar_column_updates"); /// rec.spawn().exit_on_failure(); /// /// // Native scalars & times. diff --git a/rerun_cpp/src/rerun/archetypes/segmentation_image.hpp b/rerun_cpp/src/rerun/archetypes/segmentation_image.hpp index 792f8e225253..a27f059d0b1d 100644 --- a/rerun_cpp/src/rerun/archetypes/segmentation_image.hpp +++ b/rerun_cpp/src/rerun/archetypes/segmentation_image.hpp @@ -51,11 +51,13 @@ namespace rerun::archetypes { /// const int HEIGHT = 8; /// const int WIDTH = 12; /// std::vector data(WIDTH * HEIGHT, 0); - /// for (auto y = 0; y <4; ++y) { // top half - /// std::fill_n(data.begin() + y * WIDTH, 6, static_cast(1)); // left half + /// for (auto y = 0; y <4; ++y) { // top half + /// // left half: + /// std::fill_n(data.begin() + y * WIDTH, 6, static_cast(1)); /// } - /// for (auto y = 4; y <8; ++y) { // bottom half - /// std::fill_n(data.begin() + y * WIDTH + 6, 6, static_cast(2)); // right half + /// for (auto y = 4; y <8; ++y) { // bottom half + /// // right half: + /// std::fill_n(data.begin() + y * WIDTH + 6, 6, static_cast(2)); /// } /// /// // create an annotation context to describe the classes diff --git a/rerun_cpp/src/rerun/archetypes/series_lines.hpp b/rerun_cpp/src/rerun/archetypes/series_lines.hpp index 8a13a805c3bc..642564f80fec 100644 --- a/rerun_cpp/src/rerun/archetypes/series_lines.hpp +++ b/rerun_cpp/src/rerun/archetypes/series_lines.hpp @@ -67,8 +67,14 @@ namespace rerun::archetypes { /// for (int t = 0; t (TAU * 2.0 * 100.0); ++t) { /// rec.set_time_sequence("step", t); /// - /// rec.log("trig/sin", rerun::Scalars(sin(static_cast(t) / 100.0))); - /// rec.log("trig/cos", rerun::Scalars(cos(static_cast(t) / 100.0))); + /// rec.log( + /// "trig/sin", + /// rerun::Scalars(sin(static_cast(t) / 100.0)) + /// ); + /// rec.log( + /// "trig/cos", + /// rerun::Scalars(cos(static_cast(t) / 100.0)) + /// ); /// } /// } /// ``` diff --git a/rerun_cpp/src/rerun/archetypes/series_points.hpp b/rerun_cpp/src/rerun/archetypes/series_points.hpp index 37fea6b236ba..1b52979476b0 100644 --- a/rerun_cpp/src/rerun/archetypes/series_points.hpp +++ b/rerun_cpp/src/rerun/archetypes/series_points.hpp @@ -52,7 +52,7 @@ namespace rerun::archetypes { /// rerun::SeriesPoints() /// .with_colors(rerun::Rgba32{255, 0, 0}) /// .with_names("sin(0.01t)") - /// .with_markers(rerun::components::MarkerShape::Circle) + /// .with_markers(rerun::MarkerShape::Circle) /// .with_marker_sizes(4.0f) /// ); /// rec.log_static( @@ -60,7 +60,7 @@ namespace rerun::archetypes { /// rerun::SeriesPoints() /// .with_colors(rerun::Rgba32{0, 255, 0}) /// .with_names("cos(0.01t)") - /// .with_markers(rerun::components::MarkerShape::Cross) + /// .with_markers(rerun::MarkerShape::Cross) /// .with_marker_sizes(2.0f) /// ); /// diff --git a/rerun_cpp/src/rerun/archetypes/status.cpp b/rerun_cpp/src/rerun/archetypes/state_change.cpp similarity index 53% rename from rerun_cpp/src/rerun/archetypes/status.cpp rename to rerun_cpp/src/rerun/archetypes/state_change.cpp index a3468971f792..1191d245dfd1 100644 --- a/rerun_cpp/src/rerun/archetypes/status.cpp +++ b/rerun_cpp/src/rerun/archetypes/state_change.cpp @@ -1,30 +1,30 @@ // DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs -// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/status.fbs". +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/state_change.fbs". -#include "status.hpp" +#include "state_change.hpp" #include "../collection_adapter_builtins.hpp" namespace rerun::archetypes { - Status Status::clear_fields() { - auto archetype = Status(); - archetype.status = - ComponentBatch::empty(Descriptor_status).value_or_throw(); + StateChange StateChange::clear_fields() { + auto archetype = StateChange(); + archetype.state = + ComponentBatch::empty(Descriptor_state).value_or_throw(); return archetype; } - Collection Status::columns(const Collection& lengths_) { + Collection StateChange::columns(const Collection& lengths_) { std::vector columns; columns.reserve(1); - if (status.has_value()) { - columns.push_back(status.value().partitioned(lengths_).value_or_throw()); + if (state.has_value()) { + columns.push_back(state.value().partitioned(lengths_).value_or_throw()); } return columns; } - Collection Status::columns() { - if (status.has_value()) { - return columns(std::vector(status.value().length(), 1)); + Collection StateChange::columns() { + if (state.has_value()) { + return columns(std::vector(state.value().length(), 1)); } return Collection(); } @@ -32,15 +32,15 @@ namespace rerun::archetypes { namespace rerun { - Result> AsComponents::as_batches( - const archetypes::Status& archetype + Result> AsComponents::as_batches( + const archetypes::StateChange& archetype ) { using namespace archetypes; std::vector cells; cells.reserve(1); - if (archetype.status.has_value()) { - cells.push_back(archetype.status.value()); + if (archetype.state.has_value()) { + cells.push_back(archetype.state.value()); } return rerun::take_ownership(std::move(cells)); diff --git a/rerun_cpp/src/rerun/archetypes/state_change.hpp b/rerun_cpp/src/rerun/archetypes/state_change.hpp new file mode 100644 index 000000000000..2343d5a35b4a --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/state_change.hpp @@ -0,0 +1,126 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/state_change.fbs". + +#pragma once + +#include "../collection.hpp" +#include "../component_batch.hpp" +#include "../component_column.hpp" +#include "../components/text.hpp" +#include "../result.hpp" + +#include +#include +#include +#include + +namespace rerun::archetypes { + /// **Archetype**: A state change, representing a transition of an entity into a new state. + /// + /// Useful for representing discrete state machines, mode transitions, or + /// state changes over time. Each logged `archetypes::StateChange` marks a new state + /// at the given time. A `null` state resets the state, showing a gap in the state timeline view. + /// + /// The state timeline view displays these as horizontal colored lanes over time. + /// + /// ## Example + /// + /// ### State changes over time + /// ![image](https://static.rerun.io/state_change/6654a13e984702b96547750469c368ce6e900c0f/full.png) + /// + /// ```cpp + /// #include + /// + /// int main(int argc, char* argv[]) { + /// const auto rec = rerun::RecordingStream("rerun_example_state_change"); + /// rec.spawn().exit_on_failure(); + /// + /// rec.set_time_sequence("step", 0); + /// rec.log("door", rerun::StateChange().with_state({"open"})); + /// + /// rec.set_time_sequence("step", 1); + /// rec.log("door", rerun::StateChange().with_state({"closed"})); + /// + /// rec.set_time_sequence("step", 2); + /// rec.log("door", rerun::StateChange().with_state({"open"})); + /// } + /// ``` + struct StateChange { + /// The new state values; each instance gets its own lane in the state timeline view. + /// + /// A reset ends the previous state and shows a gap in the state timeline view until the + /// next state. An empty string, a null array entry, and an empty state array (e.g. from + /// clearing the field) all act as resets. + /// + /// The length of the state array should not change over time. + std::optional state; + + public: + /// The name of the archetype as used in `ComponentDescriptor`s. + static constexpr const char ArchetypeName[] = "rerun.archetypes.StateChange"; + + /// `ComponentDescriptor` for the `state` field. + static constexpr auto Descriptor_state = ComponentDescriptor( + ArchetypeName, "StateChange:state", Loggable::ComponentType + ); + + public: + StateChange() = default; + StateChange(StateChange&& other) = default; + StateChange(const StateChange& other) = default; + StateChange& operator=(const StateChange& other) = default; + StateChange& operator=(StateChange&& other) = default; + + /// Update only some specific fields of a `StateChange`. + static StateChange update_fields() { + return StateChange(); + } + + /// Clear all the fields of a `StateChange`. + static StateChange clear_fields(); + + /// The new state values; each instance gets its own lane in the state timeline view. + /// + /// A reset ends the previous state and shows a gap in the state timeline view until the + /// next state. An empty string, a null array entry, and an empty state array (e.g. from + /// clearing the field) all act as resets. + /// + /// The length of the state array should not change over time. + StateChange with_state(const Collection& _state) && { + state = ComponentBatch::from_loggable(_state, Descriptor_state).value_or_throw(); + return std::move(*this); + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s + /// instead, via `ComponentBatch::partitioned`. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + Collection columns(const Collection& lengths_); + + /// Partitions the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, + /// where `n` is automatically guessed. + Collection columns(); + }; + +} // namespace rerun::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> as_batches( + const archetypes::StateChange& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/state_configuration.cpp b/rerun_cpp/src/rerun/archetypes/state_configuration.cpp new file mode 100644 index 000000000000..c7115aeaf538 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/state_configuration.cpp @@ -0,0 +1,81 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/state_configuration.fbs". + +#include "state_configuration.hpp" + +#include "../collection_adapter_builtins.hpp" + +namespace rerun::archetypes { + StateConfiguration StateConfiguration::clear_fields() { + auto archetype = StateConfiguration(); + archetype.values = + ComponentBatch::empty(Descriptor_values).value_or_throw(); + archetype.labels = + ComponentBatch::empty(Descriptor_labels).value_or_throw(); + archetype.colors = + ComponentBatch::empty(Descriptor_colors).value_or_throw(); + archetype.visible = + ComponentBatch::empty(Descriptor_visible).value_or_throw(); + return archetype; + } + + Collection StateConfiguration::columns(const Collection& lengths_) { + std::vector columns; + columns.reserve(4); + if (values.has_value()) { + columns.push_back(values.value().partitioned(lengths_).value_or_throw()); + } + if (labels.has_value()) { + columns.push_back(labels.value().partitioned(lengths_).value_or_throw()); + } + if (colors.has_value()) { + columns.push_back(colors.value().partitioned(lengths_).value_or_throw()); + } + if (visible.has_value()) { + columns.push_back(visible.value().partitioned(lengths_).value_or_throw()); + } + return columns; + } + + Collection StateConfiguration::columns() { + if (values.has_value()) { + return columns(std::vector(values.value().length(), 1)); + } + if (labels.has_value()) { + return columns(std::vector(labels.value().length(), 1)); + } + if (colors.has_value()) { + return columns(std::vector(colors.value().length(), 1)); + } + if (visible.has_value()) { + return columns(std::vector(visible.value().length(), 1)); + } + return Collection(); + } +} // namespace rerun::archetypes + +namespace rerun { + + Result> AsComponents::as_batches( + const archetypes::StateConfiguration& archetype + ) { + using namespace archetypes; + std::vector cells; + cells.reserve(4); + + if (archetype.values.has_value()) { + cells.push_back(archetype.values.value()); + } + if (archetype.labels.has_value()) { + cells.push_back(archetype.labels.value()); + } + if (archetype.colors.has_value()) { + cells.push_back(archetype.colors.value()); + } + if (archetype.visible.has_value()) { + cells.push_back(archetype.visible.value()); + } + + return rerun::take_ownership(std::move(cells)); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/state_configuration.hpp b/rerun_cpp/src/rerun/archetypes/state_configuration.hpp new file mode 100644 index 000000000000..cea15b313934 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/state_configuration.hpp @@ -0,0 +1,198 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/state_configuration.fbs". + +#pragma once + +#include "../collection.hpp" +#include "../component_batch.hpp" +#include "../component_column.hpp" +#include "../components/color.hpp" +#include "../components/text.hpp" +#include "../components/visible.hpp" +#include "../result.hpp" + +#include +#include +#include +#include + +namespace rerun::archetypes { + /// **Archetype**: Define the style and mapping for state values in a state timeline view. + /// + /// This archetype provides configuration for how state values are displayed. + /// It maps raw state values to display labels, colors, and visibility. + /// + /// `values`, `labels`, `colors`, and `visible` are parallel arrays: the entry + /// at index `i` of each describes the same state value, and only the + /// per-index pairing is meaningful. The four arrays should have matching + /// length; any secondary array (`labels`, `colors`, `visible`) that is shorter + /// than `values` falls back to defaults for the missing entries. + /// + /// It's generally recommended to log this type as static. + /// + /// The underlying data needs to be logged to the same entity path using `archetypes::StateChange`. + /// + /// ## Example + /// + /// ### State changes with a custom style + /// ```cpp + /// #include + /// + /// int main(int argc, char* argv[]) { + /// const auto rec = + /// rerun::RecordingStream("rerun_example_state_configuration"); + /// rec.spawn().exit_on_failure(); + /// + /// // Configure how each raw state value is displayed (label, color, visibility). + /// rec.log_static( + /// "door", + /// rerun::StateConfiguration() + /// .with_values({"open", "closed"}) + /// .with_labels({"Open", "Closed"}) + /// .with_colors({0x4CAF50FF, 0xEF5350FF}) + /// ); + /// + /// rec.set_time_sequence("step", 0); + /// rec.log("door", rerun::StateChange().with_state({"open"})); + /// + /// rec.set_time_sequence("step", 1); + /// rec.log("door", rerun::StateChange().with_state({"closed"})); + /// + /// rec.set_time_sequence("step", 2); + /// rec.log("door", rerun::StateChange().with_state({"open"})); + /// } + /// ``` + struct StateConfiguration { + /// The raw state values that this configuration applies to. + /// + /// Each entry defines a known state value. The order determines the mapping to + /// `labels`, `colors`, and `visible` (by index). + std::optional values; + + /// Display labels for each state value. + /// + /// If provided, the label at index `i` is shown instead of the raw value at index `i`. + /// If not provided or shorter than `values`, the raw value is used as the label. + std::optional labels; + + /// Colors for each state value. + /// + /// If provided, the color at index `i` is used for the state at index `i`. + /// If not provided, colors are assigned automatically from a built-in palette. + std::optional colors; + + /// Visibility for each state value. + /// + /// If provided, the visibility at index `i` controls whether the state at index `i` is shown. + /// If not provided, all state values are visible. + std::optional visible; + + public: + /// The name of the archetype as used in `ComponentDescriptor`s. + static constexpr const char ArchetypeName[] = "rerun.archetypes.StateConfiguration"; + + /// `ComponentDescriptor` for the `values` field. + static constexpr auto Descriptor_values = ComponentDescriptor( + ArchetypeName, "StateConfiguration:values", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `labels` field. + static constexpr auto Descriptor_labels = ComponentDescriptor( + ArchetypeName, "StateConfiguration:labels", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `colors` field. + static constexpr auto Descriptor_colors = ComponentDescriptor( + ArchetypeName, "StateConfiguration:colors", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `visible` field. + static constexpr auto Descriptor_visible = ComponentDescriptor( + ArchetypeName, "StateConfiguration:visible", + Loggable::ComponentType + ); + + public: + StateConfiguration() = default; + StateConfiguration(StateConfiguration&& other) = default; + StateConfiguration(const StateConfiguration& other) = default; + StateConfiguration& operator=(const StateConfiguration& other) = default; + StateConfiguration& operator=(StateConfiguration&& other) = default; + + /// Update only some specific fields of a `StateConfiguration`. + static StateConfiguration update_fields() { + return StateConfiguration(); + } + + /// Clear all the fields of a `StateConfiguration`. + static StateConfiguration clear_fields(); + + /// The raw state values that this configuration applies to. + /// + /// Each entry defines a known state value. The order determines the mapping to + /// `labels`, `colors`, and `visible` (by index). + StateConfiguration with_values(const Collection& _values) && { + values = ComponentBatch::from_loggable(_values, Descriptor_values).value_or_throw(); + return std::move(*this); + } + + /// Display labels for each state value. + /// + /// If provided, the label at index `i` is shown instead of the raw value at index `i`. + /// If not provided or shorter than `values`, the raw value is used as the label. + StateConfiguration with_labels(const Collection& _labels) && { + labels = ComponentBatch::from_loggable(_labels, Descriptor_labels).value_or_throw(); + return std::move(*this); + } + + /// Colors for each state value. + /// + /// If provided, the color at index `i` is used for the state at index `i`. + /// If not provided, colors are assigned automatically from a built-in palette. + StateConfiguration with_colors(const Collection& _colors) && { + colors = ComponentBatch::from_loggable(_colors, Descriptor_colors).value_or_throw(); + return std::move(*this); + } + + /// Visibility for each state value. + /// + /// If provided, the visibility at index `i` controls whether the state at index `i` is shown. + /// If not provided, all state values are visible. + StateConfiguration with_visible(const Collection& _visible) && { + visible = ComponentBatch::from_loggable(_visible, Descriptor_visible).value_or_throw(); + return std::move(*this); + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s + /// instead, via `ComponentBatch::partitioned`. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + Collection columns(const Collection& lengths_); + + /// Partitions the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, + /// where `n` is automatically guessed. + Collection columns(); + }; + +} // namespace rerun::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> as_batches( + const archetypes::StateConfiguration& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/status.hpp b/rerun_cpp/src/rerun/archetypes/status.hpp deleted file mode 100644 index 30fd9b88e027..000000000000 --- a/rerun_cpp/src/rerun/archetypes/status.hpp +++ /dev/null @@ -1,124 +0,0 @@ -// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs -// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/status.fbs". - -#pragma once - -#include "../collection.hpp" -#include "../component_batch.hpp" -#include "../component_column.hpp" -#include "../components/text.hpp" -#include "../result.hpp" - -#include -#include -#include -#include - -namespace rerun::archetypes { - /// **Archetype**: A status update, representing a change in the status of an entity. - /// - /// Useful for representing discrete state machines, mode transitions, or - /// status changes over time. Each logged `archetypes::Status` marks a new status - /// at the given time. A `null` status is ignored by the Status view. - /// - /// The Status view displays these as horizontal colored lanes over time. - /// - /// ## Example - /// - /// ### Status changes over time - /// ![image](https://static.rerun.io/status/8f224c6e4a9cbbb4b1e279c56a426ec4c6bfca50/full.png) - /// - /// ```cpp - /// #include - /// - /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_status"); - /// rec.spawn().exit_on_failure(); - /// - /// rec.set_time_sequence("step", 0); - /// rec.log("door", rerun::Status().with_status("open")); - /// - /// rec.set_time_sequence("step", 1); - /// rec.log("door", rerun::Status().with_status("closed")); - /// - /// rec.set_time_sequence("step", 2); - /// rec.log("door", rerun::Status().with_status("open")); - /// } - /// ``` - /// - /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** - /// - struct Status { - /// The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array. - std::optional status; - - public: - /// The name of the archetype as used in `ComponentDescriptor`s. - static constexpr const char ArchetypeName[] = "rerun.archetypes.Status"; - - /// `ComponentDescriptor` for the `status` field. - static constexpr auto Descriptor_status = ComponentDescriptor( - ArchetypeName, "Status:status", Loggable::ComponentType - ); - - public: - Status() = default; - Status(Status&& other) = default; - Status(const Status& other) = default; - Status& operator=(const Status& other) = default; - Status& operator=(Status&& other) = default; - - /// Update only some specific fields of a `Status`. - static Status update_fields() { - return Status(); - } - - /// Clear all the fields of a `Status`. - static Status clear_fields(); - - /// The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array. - Status with_status(const rerun::components::Text& _status) && { - status = ComponentBatch::from_loggable(_status, Descriptor_status).value_or_throw(); - return std::move(*this); - } - - /// This method makes it possible to pack multiple `status` in a single component batch. - /// - /// This only makes sense when used in conjunction with `columns`. `with_status` should - /// be used when logging a single row's worth of data. - Status with_many_status(const Collection& _status) && { - status = ComponentBatch::from_loggable(_status, Descriptor_status).value_or_throw(); - return std::move(*this); - } - - /// Partitions the component data into multiple sub-batches. - /// - /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s - /// instead, via `ComponentBatch::partitioned`. - /// - /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. - /// - /// The specified `lengths` must sum to the total length of the component batch. - Collection columns(const Collection& lengths_); - - /// Partitions the component data into unit-length sub-batches. - /// - /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, - /// where `n` is automatically guessed. - Collection columns(); - }; - -} // namespace rerun::archetypes - -namespace rerun { - /// \private - template - struct AsComponents; - - /// \private - template <> - struct AsComponents { - /// Serialize all set component batches. - static Result> as_batches(const archetypes::Status& archetype); - }; -} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/tensor.hpp b/rerun_cpp/src/rerun/archetypes/tensor.hpp index 4627d17026d2..31d7d61e8c76 100644 --- a/rerun_cpp/src/rerun/archetypes/tensor.hpp +++ b/rerun_cpp/src/rerun/archetypes/tensor.hpp @@ -43,11 +43,14 @@ namespace rerun::archetypes { /// std::uniform_int_distribution dist(0, 255); /// /// std::vector data(8 * 6 * 3 * 5); - /// std::generate(data.begin(), data.end(), [&] { return static_cast(dist(gen)); }); + /// std::generate(data.begin(), data.end(), [&] { + /// return static_cast(dist(gen)); + /// }); /// /// rec.log( /// "tensor", - /// rerun::Tensor({8, 6, 3, 5}, data).with_dim_names({"width", "height", "channel", "batch"}) + /// rerun::Tensor({8, 6, 3, 5}, data) + /// .with_dim_names({"width", "height", "channel", "batch"}) /// ); /// } /// ``` diff --git a/rerun_cpp/src/rerun/archetypes/text_log.hpp b/rerun_cpp/src/rerun/archetypes/text_log.hpp index 6293f20cce6d..ed97bc04f4ea 100644 --- a/rerun_cpp/src/rerun/archetypes/text_log.hpp +++ b/rerun_cpp/src/rerun/archetypes/text_log.hpp @@ -30,7 +30,8 @@ namespace rerun::archetypes { /// /// void loguru_to_rerun(void* user_data, const loguru::Message& message) { /// // NOTE: `rerun::RecordingStream` is thread-safe. - /// const rerun::RecordingStream* rec = reinterpret_cast(user_data); + /// const rerun::RecordingStream* rec = + /// reinterpret_cast(user_data); /// /// rerun::TextLogLevel level; /// if (message.verbosity == loguru::Verbosity_FATAL) { @@ -56,13 +57,15 @@ namespace rerun::archetypes { /// } /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_text_log_integration"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_text_log_integration"); /// rec.spawn().exit_on_failure(); /// /// // Log a text entry directly: /// rec.log( /// "logs", - /// rerun::TextLog("this entry has loglevel TRACE").with_level(rerun::TextLogLevel::Trace) + /// rerun::TextLog("this entry has loglevel TRACE") + /// .with_level(rerun::TextLogLevel::Trace) /// ); /// /// loguru::add_callback( @@ -72,9 +75,13 @@ namespace rerun::archetypes { /// loguru::Verbosity_INFO /// ); /// - /// LOG_F(INFO, "This INFO log got added through the standard logging interface"); + /// LOG_F( + /// INFO, + /// "This INFO log got added through the standard logging interface" + /// ); /// - /// loguru::remove_callback("rerun"); // we need to do this before `rec` goes out of scope + /// // we need to do this before `rec` goes out of scope: + /// loguru::remove_callback("rerun"); /// } /// ``` struct TextLog { diff --git a/rerun_cpp/src/rerun/archetypes/transform3d.hpp b/rerun_cpp/src/rerun/archetypes/transform3d.hpp index 50871a5a1a7e..ed66af23dc3c 100644 --- a/rerun_cpp/src/rerun/archetypes/transform3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/transform3d.hpp @@ -52,18 +52,24 @@ namespace rerun::archetypes { /// const auto rec = rerun::RecordingStream("rerun_example_transform3d"); /// rec.spawn().exit_on_failure(); /// - /// auto arrow = - /// rerun::Arrows3D::from_vectors({{0.0f, 1.0f, 0.0f}}).with_origins({{0.0f, 0.0f, 0.0f}}); + /// auto arrow = rerun::Arrows3D::from_vectors({{0.0f, 1.0f, 0.0f}} + /// ).with_origins({{0.0f, 0.0f, 0.0f}}); /// /// rec.log("base", arrow); /// - /// rec.log("base/translated", rerun::Transform3D::from_translation({1.0f, 0.0f, 0.0f})); + /// rec.log( + /// "base/translated", + /// rerun::Transform3D::from_translation({1.0f, 0.0f, 0.0f}) + /// ); /// rec.log("base/translated", arrow); /// /// rec.log( /// "base/rotated_scaled", /// rerun::Transform3D::from_rotation_scale( - /// rerun::RotationAxisAngle({0.0f, 0.0f, 1.0f}, rerun::Angle::radians(TAU / 8.0f)), + /// rerun::RotationAxisAngle( + /// {0.0f, 0.0f, 1.0f}, + /// rerun::Angle::radians(TAU / 8.0f) + /// ), /// 2.0f /// ) /// ); @@ -80,17 +86,20 @@ namespace rerun::archetypes { /// float truncated_radians(int deg) { /// auto degf = static_cast(deg); /// const auto pi = 3.14159265358979323846f; - /// return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / 1000.0f; + /// return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / + /// 1000.0f; /// } /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_transform3d_row_updates"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_transform3d_row_updates"); /// rec.spawn().exit_on_failure(); /// /// rec.set_time_sequence("tick", 0); /// rec.log( /// "box", - /// rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}}).with_fill_mode(rerun::FillMode::Solid), + /// rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}} + /// ).with_fill_mode(rerun::FillMode::Solid), /// rerun::TransformAxes3D(10.0) /// ); /// @@ -122,17 +131,20 @@ namespace rerun::archetypes { /// float truncated_radians(int deg) { /// auto degf = static_cast(deg); /// const auto pi = 3.14159265358979323846f; - /// return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / 1000.0f; + /// return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / + /// 1000.0f; /// } /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_transform3d_column_updates"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_transform3d_column_updates"); /// rec.spawn().exit_on_failure(); /// /// rec.set_time_sequence("tick", 0); /// rec.log( /// "box", - /// rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}}).with_fill_mode(rerun::FillMode::Solid), + /// rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}} + /// ).with_fill_mode(rerun::FillMode::Solid), /// rerun::TransformAxes3D(10.0) /// ); /// @@ -169,17 +181,20 @@ namespace rerun::archetypes { /// float truncated_radians(int deg) { /// auto degf = static_cast(deg); /// const auto pi = 3.14159265358979323846f; - /// return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / 1000.0f; + /// return static_cast(static_cast(degf * pi / 180.0f * 1000.0f)) / + /// 1000.0f; /// } /// /// int main(int argc, char* argv[]) { - /// const auto rec = rerun::RecordingStream("rerun_example_transform3d_partial_updates"); + /// const auto rec = + /// rerun::RecordingStream("rerun_example_transform3d_partial_updates"); /// rec.spawn().exit_on_failure(); /// /// // Set up a 3D box. /// rec.log( /// "box", - /// rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}}).with_fill_mode(rerun::FillMode::Solid) + /// rerun::Boxes3D::from_half_sizes({{4.f, 2.f, 1.0f}} + /// ).with_fill_mode(rerun::FillMode::Solid) /// ); /// /// // Update only the rotation of the box. @@ -187,9 +202,10 @@ namespace rerun::archetypes { /// auto rad = truncated_radians(deg * 4); /// rec.log( /// "box", - /// rerun::Transform3D::from_rotation( - /// rerun::RotationAxisAngle({0.0f, 1.0f, 0.0f}, rerun::Angle::radians(rad)) - /// ) + /// rerun::Transform3D::from_rotation(rerun::RotationAxisAngle( + /// {0.0f, 1.0f, 0.0f}, + /// rerun::Angle::radians(rad) + /// )) /// ); /// } /// @@ -197,7 +213,9 @@ namespace rerun::archetypes { /// for (int t = 0; t <= 50; t++) { /// rec.log( /// "box", - /// rerun::Transform3D::from_translation({0.0f, 0.0f, static_cast(t) / 10.0f}) + /// rerun::Transform3D::from_translation( + /// {0.0f, 0.0f, static_cast(t) / 10.0f} + /// ) /// ); /// } /// @@ -206,9 +224,10 @@ namespace rerun::archetypes { /// auto rad = truncated_radians((deg + 45) * 4); /// rec.log( /// "box", - /// rerun::Transform3D::from_rotation( - /// rerun::RotationAxisAngle({0.0f, 1.0f, 0.0f}, rerun::Angle::radians(rad)) - /// ) + /// rerun::Transform3D::from_rotation(rerun::RotationAxisAngle( + /// {0.0f, 1.0f, 0.0f}, + /// rerun::Angle::radians(rad) + /// )) /// ); /// } /// diff --git a/rerun_cpp/src/rerun/archetypes/transform_axes3d.hpp b/rerun_cpp/src/rerun/archetypes/transform_axes3d.hpp index f92bb30b0e1b..a7e50fb8aeb9 100644 --- a/rerun_cpp/src/rerun/archetypes/transform_axes3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/transform_axes3d.hpp @@ -39,10 +39,12 @@ namespace rerun::archetypes { /// /// rec.log( /// "base/rotated", - /// rerun::Transform3D().with_rotation_axis_angle(rerun::RotationAxisAngle( - /// {1.0f, 1.0f, 1.0f}, - /// rerun::Angle::degrees(static_cast(deg)) - /// )), + /// rerun::Transform3D().with_rotation_axis_angle( + /// rerun::RotationAxisAngle( + /// {1.0f, 1.0f, 1.0f}, + /// rerun::Angle::degrees(static_cast(deg)) + /// ) + /// ), /// rerun::TransformAxes3D(0.5) /// ); /// diff --git a/rerun_cpp/src/rerun/archetypes/video_frame_reference.hpp b/rerun_cpp/src/rerun/archetypes/video_frame_reference.hpp index 997a4dec9f2b..371b2441c9fe 100644 --- a/rerun_cpp/src/rerun/archetypes/video_frame_reference.hpp +++ b/rerun_cpp/src/rerun/archetypes/video_frame_reference.hpp @@ -42,13 +42,15 @@ namespace rerun::archetypes { /// int main(int argc, char* argv[]) { /// if (argc <2) { /// // TODO(#7354): Only mp4 is supported for now. - /// std::cerr <<"Usage: " <" <" + /// < frame_timestamps_ns = /// video_asset.read_frame_timestamps_nanos().value_or_throw(); /// // Note timeline values don't have to be the same as the video timestamps. - /// auto time_column = - /// rerun::TimeColumn::from_durations("video_time", rerun::borrow(frame_timestamps_ns)); + /// auto time_column = rerun::TimeColumn::from_durations( + /// "video_time", + /// rerun::borrow(frame_timestamps_ns) + /// ); /// - /// std::vector video_timestamps(frame_timestamps_ns.size()); + /// std::vector video_timestamps( + /// frame_timestamps_ns.size() + /// ); /// for (size_t i = 0; i " <" + /// <(Descriptor_codec).value_or_throw(); archetype.sample = ComponentBatch::empty(Descriptor_sample) .value_or_throw(); + archetype.is_keyframe = + ComponentBatch::empty(Descriptor_is_keyframe) + .value_or_throw(); archetype.opacity = ComponentBatch::empty(Descriptor_opacity).value_or_throw(); archetype.draw_order = @@ -22,13 +25,16 @@ namespace rerun::archetypes { Collection VideoStream::columns(const Collection& lengths_) { std::vector columns; - columns.reserve(4); + columns.reserve(5); if (codec.has_value()) { columns.push_back(codec.value().partitioned(lengths_).value_or_throw()); } if (sample.has_value()) { columns.push_back(sample.value().partitioned(lengths_).value_or_throw()); } + if (is_keyframe.has_value()) { + columns.push_back(is_keyframe.value().partitioned(lengths_).value_or_throw()); + } if (opacity.has_value()) { columns.push_back(opacity.value().partitioned(lengths_).value_or_throw()); } @@ -45,6 +51,9 @@ namespace rerun::archetypes { if (sample.has_value()) { return columns(std::vector(sample.value().length(), 1)); } + if (is_keyframe.has_value()) { + return columns(std::vector(is_keyframe.value().length(), 1)); + } if (opacity.has_value()) { return columns(std::vector(opacity.value().length(), 1)); } @@ -62,7 +71,7 @@ namespace rerun { ) { using namespace archetypes; std::vector cells; - cells.reserve(4); + cells.reserve(5); if (archetype.codec.has_value()) { cells.push_back(archetype.codec.value()); @@ -70,6 +79,9 @@ namespace rerun { if (archetype.sample.has_value()) { cells.push_back(archetype.sample.value()); } + if (archetype.is_keyframe.has_value()) { + cells.push_back(archetype.is_keyframe.value()); + } if (archetype.opacity.has_value()) { cells.push_back(archetype.opacity.value()); } diff --git a/rerun_cpp/src/rerun/archetypes/video_stream.hpp b/rerun_cpp/src/rerun/archetypes/video_stream.hpp index ab24dcfbe294..cd42f1ba6282 100644 --- a/rerun_cpp/src/rerun/archetypes/video_stream.hpp +++ b/rerun_cpp/src/rerun/archetypes/video_stream.hpp @@ -7,6 +7,7 @@ #include "../component_batch.hpp" #include "../component_column.hpp" #include "../components/draw_order.hpp" +#include "../components/is_keyframe.hpp" #include "../components/opacity.hpp" #include "../components/video_codec.hpp" #include "../components/video_sample.hpp" @@ -61,6 +62,17 @@ namespace rerun::archetypes { /// See `components::VideoCodec` for codec specific requirements. std::optional sample; + /// Whether the corresponding `components::VideoSample` contains a keyframe. + /// + /// A keyframe (also known as a sync sample or IDR) is a frame from which a decoder can + /// start decoding the stream with no prior decoder state. See `components::IsKeyframe` + /// and `components::VideoCodec` for the codec-specific definition. + /// + /// This field is optional. It does not change how the stream itself is decoded: it is + /// metadata that travels with the sample and can be inspected when querying the data + /// back, for example to locate sync points or build a frame index. + std::optional is_keyframe; + /// Opacity of the video stream, useful for layering several media. /// /// Defaults to 1.0 (fully opaque). @@ -86,6 +98,11 @@ namespace rerun::archetypes { ArchetypeName, "VideoStream:sample", Loggable::ComponentType ); + /// `ComponentDescriptor` for the `is_keyframe` field. + static constexpr auto Descriptor_is_keyframe = ComponentDescriptor( + ArchetypeName, "VideoStream:is_keyframe", + Loggable::ComponentType + ); /// `ComponentDescriptor` for the `opacity` field. static constexpr auto Descriptor_opacity = ComponentDescriptor( ArchetypeName, "VideoStream:opacity", @@ -170,6 +187,33 @@ namespace rerun::archetypes { return std::move(*this); } + /// Whether the corresponding `components::VideoSample` contains a keyframe. + /// + /// A keyframe (also known as a sync sample or IDR) is a frame from which a decoder can + /// start decoding the stream with no prior decoder state. See `components::IsKeyframe` + /// and `components::VideoCodec` for the codec-specific definition. + /// + /// This field is optional. It does not change how the stream itself is decoded: it is + /// metadata that travels with the sample and can be inspected when querying the data + /// back, for example to locate sync points or build a frame index. + VideoStream with_is_keyframe(const rerun::components::IsKeyframe& _is_keyframe) && { + is_keyframe = ComponentBatch::from_loggable(_is_keyframe, Descriptor_is_keyframe) + .value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `is_keyframe` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_is_keyframe` should + /// be used when logging a single row's worth of data. + VideoStream with_many_is_keyframe( + const Collection& _is_keyframe + ) && { + is_keyframe = ComponentBatch::from_loggable(_is_keyframe, Descriptor_is_keyframe) + .value_or_throw(); + return std::move(*this); + } + /// Opacity of the video stream, useful for layering several media. /// /// Defaults to 1.0 (fully opaque). diff --git a/rerun_cpp/src/rerun/archetypes/view_coordinates.hpp b/rerun_cpp/src/rerun/archetypes/view_coordinates.hpp index cd4d938005fa..94eed0d3f226 100644 --- a/rerun_cpp/src/rerun/archetypes/view_coordinates.hpp +++ b/rerun_cpp/src/rerun/archetypes/view_coordinates.hpp @@ -41,10 +41,12 @@ namespace rerun::archetypes { /// const auto rec = rerun::RecordingStream("rerun_example_view_coordinates"); /// rec.spawn().exit_on_failure(); /// - /// rec.log_static("world", rerun::ViewCoordinates::RIGHT_HAND_Z_UP); // Set an up-axis + /// // Set an up-axis: + /// rec.log_static("world", rerun::ViewCoordinates::RIGHT_HAND_Z_UP); /// rec.log( /// "world/xyz", - /// rerun::Arrows3D::from_vectors({{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}} + /// rerun::Arrows3D::from_vectors( + /// {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}} /// ).with_colors({{255, 0, 0}, {0, 255, 0}, {0, 0, 255}}) /// ); /// } diff --git a/rerun_cpp/src/rerun/archetypes/voxel_grid_map.cpp b/rerun_cpp/src/rerun/archetypes/voxel_grid_map.cpp new file mode 100644 index 000000000000..f6ca187b7d9e --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/voxel_grid_map.cpp @@ -0,0 +1,154 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/voxel_grid_map.fbs". + +#include "voxel_grid_map.hpp" + +#include "../collection_adapter_builtins.hpp" + +namespace rerun::archetypes { + VoxelGridMap VoxelGridMap::clear_fields() { + auto archetype = VoxelGridMap(); + archetype.voxel_indices = + ComponentBatch::empty(Descriptor_voxel_indices) + .value_or_throw(); + archetype.voxel_size = + ComponentBatch::empty(Descriptor_voxel_size) + .value_or_throw(); + archetype.values = ComponentBatch::empty(Descriptor_values) + .value_or_throw(); + archetype.colors = + ComponentBatch::empty(Descriptor_colors).value_or_throw(); + archetype.translation = + ComponentBatch::empty(Descriptor_translation) + .value_or_throw(); + archetype.rotation_axis_angle = ComponentBatch::empty( + Descriptor_rotation_axis_angle + ) + .value_or_throw(); + archetype.quaternion = + ComponentBatch::empty(Descriptor_quaternion) + .value_or_throw(); + archetype.opacity = + ComponentBatch::empty(Descriptor_opacity).value_or_throw(); + archetype.value_range = + ComponentBatch::empty(Descriptor_value_range) + .value_or_throw(); + archetype.colormap = ComponentBatch::empty(Descriptor_colormap) + .value_or_throw(); + return archetype; + } + + Collection VoxelGridMap::columns(const Collection& lengths_) { + std::vector columns; + columns.reserve(10); + if (voxel_indices.has_value()) { + columns.push_back(voxel_indices.value().partitioned(lengths_).value_or_throw()); + } + if (voxel_size.has_value()) { + columns.push_back(voxel_size.value().partitioned(lengths_).value_or_throw()); + } + if (values.has_value()) { + columns.push_back(values.value().partitioned(lengths_).value_or_throw()); + } + if (colors.has_value()) { + columns.push_back(colors.value().partitioned(lengths_).value_or_throw()); + } + if (translation.has_value()) { + columns.push_back(translation.value().partitioned(lengths_).value_or_throw()); + } + if (rotation_axis_angle.has_value()) { + columns.push_back(rotation_axis_angle.value().partitioned(lengths_).value_or_throw()); + } + if (quaternion.has_value()) { + columns.push_back(quaternion.value().partitioned(lengths_).value_or_throw()); + } + if (opacity.has_value()) { + columns.push_back(opacity.value().partitioned(lengths_).value_or_throw()); + } + if (value_range.has_value()) { + columns.push_back(value_range.value().partitioned(lengths_).value_or_throw()); + } + if (colormap.has_value()) { + columns.push_back(colormap.value().partitioned(lengths_).value_or_throw()); + } + return columns; + } + + Collection VoxelGridMap::columns() { + if (voxel_indices.has_value()) { + return columns(std::vector(voxel_indices.value().length(), 1)); + } + if (voxel_size.has_value()) { + return columns(std::vector(voxel_size.value().length(), 1)); + } + if (values.has_value()) { + return columns(std::vector(values.value().length(), 1)); + } + if (colors.has_value()) { + return columns(std::vector(colors.value().length(), 1)); + } + if (translation.has_value()) { + return columns(std::vector(translation.value().length(), 1)); + } + if (rotation_axis_angle.has_value()) { + return columns(std::vector(rotation_axis_angle.value().length(), 1)); + } + if (quaternion.has_value()) { + return columns(std::vector(quaternion.value().length(), 1)); + } + if (opacity.has_value()) { + return columns(std::vector(opacity.value().length(), 1)); + } + if (value_range.has_value()) { + return columns(std::vector(value_range.value().length(), 1)); + } + if (colormap.has_value()) { + return columns(std::vector(colormap.value().length(), 1)); + } + return Collection(); + } +} // namespace rerun::archetypes + +namespace rerun { + + Result> AsComponents::as_batches( + const archetypes::VoxelGridMap& archetype + ) { + using namespace archetypes; + std::vector cells; + cells.reserve(10); + + if (archetype.voxel_indices.has_value()) { + cells.push_back(archetype.voxel_indices.value()); + } + if (archetype.voxel_size.has_value()) { + cells.push_back(archetype.voxel_size.value()); + } + if (archetype.values.has_value()) { + cells.push_back(archetype.values.value()); + } + if (archetype.colors.has_value()) { + cells.push_back(archetype.colors.value()); + } + if (archetype.translation.has_value()) { + cells.push_back(archetype.translation.value()); + } + if (archetype.rotation_axis_angle.has_value()) { + cells.push_back(archetype.rotation_axis_angle.value()); + } + if (archetype.quaternion.has_value()) { + cells.push_back(archetype.quaternion.value()); + } + if (archetype.opacity.has_value()) { + cells.push_back(archetype.opacity.value()); + } + if (archetype.value_range.has_value()) { + cells.push_back(archetype.value_range.value()); + } + if (archetype.colormap.has_value()) { + cells.push_back(archetype.colormap.value()); + } + + return rerun::take_ownership(std::move(cells)); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/voxel_grid_map.hpp b/rerun_cpp/src/rerun/archetypes/voxel_grid_map.hpp new file mode 100644 index 000000000000..b5651111e11e --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/voxel_grid_map.hpp @@ -0,0 +1,438 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/voxel_grid_map.fbs". + +#pragma once + +#include "../collection.hpp" +#include "../component_batch.hpp" +#include "../component_column.hpp" +#include "../components/color.hpp" +#include "../components/colormap.hpp" +#include "../components/opacity.hpp" +#include "../components/rotation_axis_angle.hpp" +#include "../components/rotation_quat.hpp" +#include "../components/translation3d.hpp" +#include "../components/value_range.hpp" +#include "../components/voxel_index.hpp" +#include "../components/voxel_size.hpp" +#include "../components/voxel_value.hpp" +#include "../result.hpp" + +#include +#include +#include +#include + +namespace rerun::archetypes { + /// **Archetype**: A sparse 3D voxel grid map with grid indices and voxel dimensions. + /// + /// This archetype is intended for 3D occupancy maps and other volumetric data + /// represented as a sparse grid of voxels with scene-unit dimensions along the local X/Y/Z axes. + /// + /// The minimum corner of the voxel with `[0, 0, 0]` index is located at the origin of the entity's coordinate frame + /// and can have an additional offset from there through the optional translation and rotation fields. + /// + /// A voxel center is at `(index + 0.5) * voxel_size` in local grid coordinates (i.e. relative to the minimum corner). + /// + /// ## Example + /// + /// ### Simple sparse voxel grid map + /// ```cpp + /// #include + /// + /// #include + /// #include + /// + /// int main(int argc, char* argv[]) { + /// const auto rec = + /// rerun::RecordingStream("rerun_example_voxel_grid_map_simple"); + /// rec.spawn().exit_on_failure(); + /// + /// const std::vector voxel_indices = { + /// rerun::components::VoxelIndex(-1, 0, 0), + /// rerun::components::VoxelIndex(1, 0, 0), + /// rerun::components::VoxelIndex(1, 1, 0), + /// rerun::components::VoxelIndex(3, 0, 0), + /// rerun::components::VoxelIndex(3, 0, 1), + /// rerun::components::VoxelIndex(4, 0, 1), + /// }; + /// const std::vector values = { + /// 0.0f, + /// 0.2f, + /// 0.4f, + /// 0.6f, + /// 0.8f, + /// 1.0f, + /// }; + /// + /// rec.log( + /// "world/voxels", + /// rerun::archetypes::VoxelGridMap( + /// voxel_indices, + /// std::array{0.25f, 0.25f, 0.25f} + /// ) + /// .with_values(values) + /// .with_value_range( + /// rerun::components::ValueRange(std::array{0.0, 1.0}) + /// ) + /// .with_colormap(rerun::components::Colormap::Turbo) + /// .with_translation({-0.5f, -0.5f, 0.0f}) + /// ); + /// } + /// ``` + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct VoxelGridMap { + /// Indices of the voxels within the grid volume. + std::optional voxel_indices; + + /// The scene-unit dimensions of a single voxel cell. + /// + /// This defines the voxel size along the local grid X/Y/Z axes. + /// Each dimension must be finite and positive. + std::optional voxel_size; + + /// Optional scalar occupancy or value data for each voxel. + /// + /// If explicit colors are not provided, values are mapped through `colormap` and `value_range`. + std::optional values; + + /// Optional colors for each voxel. + /// + /// If set, these colors take precedence over color-mapped scalar values. + std::optional colors; + + /// Translation of the minimum corner of voxel `[0, 0, 0]`. + /// + /// Together with `components::RotationAxisAngle` or `components::RotationQuat`, this defines the pose of the + /// grid relative to the map's parent coordinate frame. + /// + /// If not set, the minimum corner is placed at the origin of the map's parent coordinate frame. + std::optional translation; + + /// Rotation of the grid via axis + angle. + /// + /// Together with `components::Translation3D`, this defines the pose of the grid relative to the + /// map's parent coordinate frame. + /// + /// Note: either this or `components::RotationQuat` can be set to specify the grid's rotation, but not both. + /// If both this and `components::RotationQuat` are set, this is ignored in favor of the quaternion. + std::optional rotation_axis_angle; + + /// Rotation of the grid via quaternion. + /// + /// Together with `components::Translation3D`, this defines the pose of the grid relative to the + /// map's parent coordinate frame. + std::optional quaternion; + + /// Opacity of the voxels after color or colormap application. + /// + /// Defaults to 1.0 (fully opaque). + std::optional opacity; + + /// Scalar value range for color-mapping. + /// + /// Defaults to `[0.0, 1.0]`. + std::optional value_range; + + /// Colormap to use when `values` are present and explicit `colors` are not provided. + /// + /// Defaults to Turbo. + std::optional colormap; + + public: + /// The name of the archetype as used in `ComponentDescriptor`s. + static constexpr const char ArchetypeName[] = "rerun.archetypes.VoxelGridMap"; + + /// `ComponentDescriptor` for the `voxel_indices` field. + static constexpr auto Descriptor_voxel_indices = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:voxel_indices", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `voxel_size` field. + static constexpr auto Descriptor_voxel_size = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:voxel_size", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `values` field. + static constexpr auto Descriptor_values = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:values", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `colors` field. + static constexpr auto Descriptor_colors = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:colors", Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `translation` field. + static constexpr auto Descriptor_translation = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:translation", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `rotation_axis_angle` field. + static constexpr auto Descriptor_rotation_axis_angle = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:rotation_axis_angle", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `quaternion` field. + static constexpr auto Descriptor_quaternion = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:quaternion", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `opacity` field. + static constexpr auto Descriptor_opacity = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:opacity", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `value_range` field. + static constexpr auto Descriptor_value_range = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:value_range", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `colormap` field. + static constexpr auto Descriptor_colormap = ComponentDescriptor( + ArchetypeName, "VoxelGridMap:colormap", + Loggable::ComponentType + ); + + public: + VoxelGridMap() = default; + VoxelGridMap(VoxelGridMap&& other) = default; + VoxelGridMap(const VoxelGridMap& other) = default; + VoxelGridMap& operator=(const VoxelGridMap& other) = default; + VoxelGridMap& operator=(VoxelGridMap&& other) = default; + + explicit VoxelGridMap( + Collection _voxel_indices, + rerun::components::VoxelSize _voxel_size + ) + : voxel_indices( + ComponentBatch::from_loggable(std::move(_voxel_indices), Descriptor_voxel_indices) + .value_or_throw() + ), + voxel_size( + ComponentBatch::from_loggable(std::move(_voxel_size), Descriptor_voxel_size) + .value_or_throw() + ) {} + + /// Update only some specific fields of a `VoxelGridMap`. + static VoxelGridMap update_fields() { + return VoxelGridMap(); + } + + /// Clear all the fields of a `VoxelGridMap`. + static VoxelGridMap clear_fields(); + + /// Indices of the voxels within the grid volume. + VoxelGridMap with_voxel_indices( + const Collection& _voxel_indices + ) && { + voxel_indices = ComponentBatch::from_loggable(_voxel_indices, Descriptor_voxel_indices) + .value_or_throw(); + return std::move(*this); + } + + /// The scene-unit dimensions of a single voxel cell. + /// + /// This defines the voxel size along the local grid X/Y/Z axes. + /// Each dimension must be finite and positive. + VoxelGridMap with_voxel_size(const rerun::components::VoxelSize& _voxel_size) && { + voxel_size = + ComponentBatch::from_loggable(_voxel_size, Descriptor_voxel_size).value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `voxel_size` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_voxel_size` should + /// be used when logging a single row's worth of data. + VoxelGridMap with_many_voxel_size( + const Collection& _voxel_size + ) && { + voxel_size = + ComponentBatch::from_loggable(_voxel_size, Descriptor_voxel_size).value_or_throw(); + return std::move(*this); + } + + /// Optional scalar occupancy or value data for each voxel. + /// + /// If explicit colors are not provided, values are mapped through `colormap` and `value_range`. + VoxelGridMap with_values(const Collection& _values) && { + values = ComponentBatch::from_loggable(_values, Descriptor_values).value_or_throw(); + return std::move(*this); + } + + /// Optional colors for each voxel. + /// + /// If set, these colors take precedence over color-mapped scalar values. + VoxelGridMap with_colors(const Collection& _colors) && { + colors = ComponentBatch::from_loggable(_colors, Descriptor_colors).value_or_throw(); + return std::move(*this); + } + + /// Translation of the minimum corner of voxel `[0, 0, 0]`. + /// + /// Together with `components::RotationAxisAngle` or `components::RotationQuat`, this defines the pose of the + /// grid relative to the map's parent coordinate frame. + /// + /// If not set, the minimum corner is placed at the origin of the map's parent coordinate frame. + VoxelGridMap with_translation(const rerun::components::Translation3D& _translation) && { + translation = ComponentBatch::from_loggable(_translation, Descriptor_translation) + .value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `translation` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_translation` should + /// be used when logging a single row's worth of data. + VoxelGridMap with_many_translation( + const Collection& _translation + ) && { + translation = ComponentBatch::from_loggable(_translation, Descriptor_translation) + .value_or_throw(); + return std::move(*this); + } + + /// Rotation of the grid via axis + angle. + /// + /// Together with `components::Translation3D`, this defines the pose of the grid relative to the + /// map's parent coordinate frame. + /// + /// Note: either this or `components::RotationQuat` can be set to specify the grid's rotation, but not both. + /// If both this and `components::RotationQuat` are set, this is ignored in favor of the quaternion. + VoxelGridMap with_rotation_axis_angle( + const rerun::components::RotationAxisAngle& _rotation_axis_angle + ) && { + rotation_axis_angle = + ComponentBatch::from_loggable(_rotation_axis_angle, Descriptor_rotation_axis_angle) + .value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `rotation_axis_angle` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_rotation_axis_angle` should + /// be used when logging a single row's worth of data. + VoxelGridMap with_many_rotation_axis_angle( + const Collection& _rotation_axis_angle + ) && { + rotation_axis_angle = + ComponentBatch::from_loggable(_rotation_axis_angle, Descriptor_rotation_axis_angle) + .value_or_throw(); + return std::move(*this); + } + + /// Rotation of the grid via quaternion. + /// + /// Together with `components::Translation3D`, this defines the pose of the grid relative to the + /// map's parent coordinate frame. + VoxelGridMap with_quaternion(const rerun::components::RotationQuat& _quaternion) && { + quaternion = + ComponentBatch::from_loggable(_quaternion, Descriptor_quaternion).value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `quaternion` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_quaternion` should + /// be used when logging a single row's worth of data. + VoxelGridMap with_many_quaternion( + const Collection& _quaternion + ) && { + quaternion = + ComponentBatch::from_loggable(_quaternion, Descriptor_quaternion).value_or_throw(); + return std::move(*this); + } + + /// Opacity of the voxels after color or colormap application. + /// + /// Defaults to 1.0 (fully opaque). + VoxelGridMap with_opacity(const rerun::components::Opacity& _opacity) && { + opacity = ComponentBatch::from_loggable(_opacity, Descriptor_opacity).value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `opacity` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_opacity` should + /// be used when logging a single row's worth of data. + VoxelGridMap with_many_opacity(const Collection& _opacity) && { + opacity = ComponentBatch::from_loggable(_opacity, Descriptor_opacity).value_or_throw(); + return std::move(*this); + } + + /// Scalar value range for color-mapping. + /// + /// Defaults to `[0.0, 1.0]`. + VoxelGridMap with_value_range(const rerun::components::ValueRange& _value_range) && { + value_range = ComponentBatch::from_loggable(_value_range, Descriptor_value_range) + .value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `value_range` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_value_range` should + /// be used when logging a single row's worth of data. + VoxelGridMap with_many_value_range( + const Collection& _value_range + ) && { + value_range = ComponentBatch::from_loggable(_value_range, Descriptor_value_range) + .value_or_throw(); + return std::move(*this); + } + + /// Colormap to use when `values` are present and explicit `colors` are not provided. + /// + /// Defaults to Turbo. + VoxelGridMap with_colormap(const rerun::components::Colormap& _colormap) && { + colormap = + ComponentBatch::from_loggable(_colormap, Descriptor_colormap).value_or_throw(); + return std::move(*this); + } + + /// This method makes it possible to pack multiple `colormap` in a single component batch. + /// + /// This only makes sense when used in conjunction with `columns`. `with_colormap` should + /// be used when logging a single row's worth of data. + VoxelGridMap with_many_colormap(const Collection& _colormap + ) && { + colormap = + ComponentBatch::from_loggable(_colormap, Descriptor_colormap).value_or_throw(); + return std::move(*this); + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s + /// instead, via `ComponentBatch::partitioned`. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + Collection columns(const Collection& lengths_); + + /// Partitions the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, + /// where `n` is automatically guessed. + Collection columns(); + }; + +} // namespace rerun::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> as_batches( + const archetypes::VoxelGridMap& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/archetypes.hpp b/rerun_cpp/src/rerun/blueprint/archetypes.hpp index d3cb29c14b38..962ad6c44aa4 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes.hpp +++ b/rerun_cpp/src/rerun/blueprint/archetypes.hpp @@ -23,9 +23,11 @@ #include "blueprint/archetypes/plot_legend.hpp" #include "blueprint/archetypes/scalar_axis.hpp" #include "blueprint/archetypes/spatial_information.hpp" +#include "blueprint/archetypes/table_blueprint.hpp" #include "blueprint/archetypes/tensor_scalar_mapping.hpp" #include "blueprint/archetypes/tensor_slice_selection.hpp" #include "blueprint/archetypes/tensor_view_fit.hpp" +#include "blueprint/archetypes/text_document_format.hpp" #include "blueprint/archetypes/text_log_columns.hpp" #include "blueprint/archetypes/text_log_format.hpp" #include "blueprint/archetypes/text_log_rows.hpp" diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes b/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes index c324eb6a8ac9..bc630054800d 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes +++ b/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes @@ -43,12 +43,16 @@ scalar_axis.cpp linguist-generated=true scalar_axis.hpp linguist-generated=true spatial_information.cpp linguist-generated=true spatial_information.hpp linguist-generated=true +table_blueprint.cpp linguist-generated=true +table_blueprint.hpp linguist-generated=true tensor_scalar_mapping.cpp linguist-generated=true tensor_scalar_mapping.hpp linguist-generated=true tensor_slice_selection.cpp linguist-generated=true tensor_slice_selection.hpp linguist-generated=true tensor_view_fit.cpp linguist-generated=true tensor_view_fit.hpp linguist-generated=true +text_document_format.cpp linguist-generated=true +text_document_format.hpp linguist-generated=true text_log_columns.cpp linguist-generated=true text_log_columns.hpp linguist-generated=true text_log_format.cpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/table_blueprint.cpp b/rerun_cpp/src/rerun/blueprint/archetypes/table_blueprint.cpp new file mode 100644 index 000000000000..45208d67a14c --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/table_blueprint.cpp @@ -0,0 +1,91 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/table_blueprint.fbs". + +#include "table_blueprint.hpp" + +#include "../../collection_adapter_builtins.hpp" + +namespace rerun::blueprint::archetypes { + TableBlueprint TableBlueprint::clear_fields() { + auto archetype = TableBlueprint(); + archetype.segment_preview_column = + ComponentBatch::empty( + Descriptor_segment_preview_column + ) + .value_or_throw(); + archetype.flag_column = + ComponentBatch::empty(Descriptor_flag_column) + .value_or_throw(); + archetype.grid_view_card_title = + ComponentBatch::empty( + Descriptor_grid_view_card_title + ) + .value_or_throw(); + archetype.url_column = + ComponentBatch::empty(Descriptor_url_column) + .value_or_throw(); + return archetype; + } + + Collection TableBlueprint::columns(const Collection& lengths_) { + std::vector columns; + columns.reserve(4); + if (segment_preview_column.has_value()) { + columns.push_back(segment_preview_column.value().partitioned(lengths_).value_or_throw() + ); + } + if (flag_column.has_value()) { + columns.push_back(flag_column.value().partitioned(lengths_).value_or_throw()); + } + if (grid_view_card_title.has_value()) { + columns.push_back(grid_view_card_title.value().partitioned(lengths_).value_or_throw()); + } + if (url_column.has_value()) { + columns.push_back(url_column.value().partitioned(lengths_).value_or_throw()); + } + return columns; + } + + Collection TableBlueprint::columns() { + if (segment_preview_column.has_value()) { + return columns(std::vector(segment_preview_column.value().length(), 1)); + } + if (flag_column.has_value()) { + return columns(std::vector(flag_column.value().length(), 1)); + } + if (grid_view_card_title.has_value()) { + return columns(std::vector(grid_view_card_title.value().length(), 1)); + } + if (url_column.has_value()) { + return columns(std::vector(url_column.value().length(), 1)); + } + return Collection(); + } +} // namespace rerun::blueprint::archetypes + +namespace rerun { + + Result> + AsComponents::as_batches( + const blueprint::archetypes::TableBlueprint& archetype + ) { + using namespace blueprint::archetypes; + std::vector cells; + cells.reserve(4); + + if (archetype.segment_preview_column.has_value()) { + cells.push_back(archetype.segment_preview_column.value()); + } + if (archetype.flag_column.has_value()) { + cells.push_back(archetype.flag_column.value()); + } + if (archetype.grid_view_card_title.has_value()) { + cells.push_back(archetype.grid_view_card_title.value()); + } + if (archetype.url_column.has_value()) { + cells.push_back(archetype.url_column.value()); + } + + return rerun::take_ownership(std::move(cells)); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/table_blueprint.hpp b/rerun_cpp/src/rerun/blueprint/archetypes/table_blueprint.hpp new file mode 100644 index 000000000000..5721dceb97dd --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/table_blueprint.hpp @@ -0,0 +1,183 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/table_blueprint.fbs". + +#pragma once + +#include "../../blueprint/components/column_name.hpp" +#include "../../collection.hpp" +#include "../../component_batch.hpp" +#include "../../component_column.hpp" +#include "../../result.hpp" + +#include +#include +#include +#include + +namespace rerun::blueprint::archetypes { + /// **Archetype**: Blueprint for configuring the styling of a table. + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct TableBlueprint { + /// The name of the column that contains recording URIs for segment previews. + /// + /// Every row can at most preview a single segment. + /// + /// For the preview, the rest of the blueprint data is read it as it would be with regular recording blueprints, + /// meaning that the regular structure of `archetypes::ViewportBlueprint`, and `archetypes::ViewBlueprint` structure applies. + /// However, this mostly ignores layout container types as well as automatic spawning. + /// + /// If unset, defaults to the first URL column in the table that points to the same Rerun server + std::optional segment_preview_column; + + /// The name of the boolean column used for flag/annotation toggles. + /// + /// Must be set for flagging to be available. The named column must exist in the + /// table and be of boolean type. + /// Additionally, the table must be remote and have another column with + /// `rerun:is_table_index` metadata since flag changes are persisted to the server + /// via upsert. + std::optional flag_column; + + /// The name of the column to use as the card title in grid view. + /// + /// If unset, the first visible string column is used as the title. + std::optional grid_view_card_title; + + /// The name of the column containing URLs to open when a card is clicked in grid view. + /// + /// If unset, defaults to the segment preview column. + std::optional url_column; + + public: + /// The name of the archetype as used in `ComponentDescriptor`s. + static constexpr const char ArchetypeName[] = "rerun.blueprint.archetypes.TableBlueprint"; + + /// `ComponentDescriptor` for the `segment_preview_column` field. + static constexpr auto Descriptor_segment_preview_column = ComponentDescriptor( + ArchetypeName, "TableBlueprint:segment_preview_column", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `flag_column` field. + static constexpr auto Descriptor_flag_column = ComponentDescriptor( + ArchetypeName, "TableBlueprint:flag_column", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `grid_view_card_title` field. + static constexpr auto Descriptor_grid_view_card_title = ComponentDescriptor( + ArchetypeName, "TableBlueprint:grid_view_card_title", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `url_column` field. + static constexpr auto Descriptor_url_column = ComponentDescriptor( + ArchetypeName, "TableBlueprint:url_column", + Loggable::ComponentType + ); + + public: + TableBlueprint() = default; + TableBlueprint(TableBlueprint&& other) = default; + TableBlueprint(const TableBlueprint& other) = default; + TableBlueprint& operator=(const TableBlueprint& other) = default; + TableBlueprint& operator=(TableBlueprint&& other) = default; + + /// Update only some specific fields of a `TableBlueprint`. + static TableBlueprint update_fields() { + return TableBlueprint(); + } + + /// Clear all the fields of a `TableBlueprint`. + static TableBlueprint clear_fields(); + + /// The name of the column that contains recording URIs for segment previews. + /// + /// Every row can at most preview a single segment. + /// + /// For the preview, the rest of the blueprint data is read it as it would be with regular recording blueprints, + /// meaning that the regular structure of `archetypes::ViewportBlueprint`, and `archetypes::ViewBlueprint` structure applies. + /// However, this mostly ignores layout container types as well as automatic spawning. + /// + /// If unset, defaults to the first URL column in the table that points to the same Rerun server + TableBlueprint with_segment_preview_column( + const rerun::blueprint::components::ColumnName& _segment_preview_column + ) && { + segment_preview_column = ComponentBatch::from_loggable( + _segment_preview_column, + Descriptor_segment_preview_column + ) + .value_or_throw(); + return std::move(*this); + } + + /// The name of the boolean column used for flag/annotation toggles. + /// + /// Must be set for flagging to be available. The named column must exist in the + /// table and be of boolean type. + /// Additionally, the table must be remote and have another column with + /// `rerun:is_table_index` metadata since flag changes are persisted to the server + /// via upsert. + TableBlueprint with_flag_column(const rerun::blueprint::components::ColumnName& _flag_column + ) && { + flag_column = ComponentBatch::from_loggable(_flag_column, Descriptor_flag_column) + .value_or_throw(); + return std::move(*this); + } + + /// The name of the column to use as the card title in grid view. + /// + /// If unset, the first visible string column is used as the title. + TableBlueprint with_grid_view_card_title( + const rerun::blueprint::components::ColumnName& _grid_view_card_title + ) && { + grid_view_card_title = ComponentBatch::from_loggable( + _grid_view_card_title, + Descriptor_grid_view_card_title + ) + .value_or_throw(); + return std::move(*this); + } + + /// The name of the column containing URLs to open when a card is clicked in grid view. + /// + /// If unset, defaults to the segment preview column. + TableBlueprint with_url_column(const rerun::blueprint::components::ColumnName& _url_column + ) && { + url_column = + ComponentBatch::from_loggable(_url_column, Descriptor_url_column).value_or_throw(); + return std::move(*this); + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s + /// instead, via `ComponentBatch::partitioned`. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + Collection columns(const Collection& lengths_); + + /// Partitions the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, + /// where `n` is automatically guessed. + Collection columns(); + }; + +} // namespace rerun::blueprint::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> as_batches( + const blueprint::archetypes::TableBlueprint& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/text_document_format.cpp b/rerun_cpp/src/rerun/blueprint/archetypes/text_document_format.cpp new file mode 100644 index 000000000000..fd047fc05765 --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/text_document_format.cpp @@ -0,0 +1,62 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/text_document_format.fbs". + +#include "text_document_format.hpp" + +#include "../../collection_adapter_builtins.hpp" + +namespace rerun::blueprint::archetypes { + TextDocumentFormat TextDocumentFormat::clear_fields() { + auto archetype = TextDocumentFormat(); + archetype.monospace = + ComponentBatch::empty(Descriptor_monospace) + .value_or_throw(); + archetype.word_wrap = + ComponentBatch::empty(Descriptor_word_wrap) + .value_or_throw(); + return archetype; + } + + Collection TextDocumentFormat::columns(const Collection& lengths_) { + std::vector columns; + columns.reserve(2); + if (monospace.has_value()) { + columns.push_back(monospace.value().partitioned(lengths_).value_or_throw()); + } + if (word_wrap.has_value()) { + columns.push_back(word_wrap.value().partitioned(lengths_).value_or_throw()); + } + return columns; + } + + Collection TextDocumentFormat::columns() { + if (monospace.has_value()) { + return columns(std::vector(monospace.value().length(), 1)); + } + if (word_wrap.has_value()) { + return columns(std::vector(word_wrap.value().length(), 1)); + } + return Collection(); + } +} // namespace rerun::blueprint::archetypes + +namespace rerun { + + Result> + AsComponents::as_batches( + const blueprint::archetypes::TextDocumentFormat& archetype + ) { + using namespace blueprint::archetypes; + std::vector cells; + cells.reserve(2); + + if (archetype.monospace.has_value()) { + cells.push_back(archetype.monospace.value()); + } + if (archetype.word_wrap.has_value()) { + cells.push_back(archetype.word_wrap.value()); + } + + return rerun::take_ownership(std::move(cells)); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/text_document_format.hpp b/rerun_cpp/src/rerun/blueprint/archetypes/text_document_format.hpp new file mode 100644 index 000000000000..0f7c0d2a442a --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/text_document_format.hpp @@ -0,0 +1,118 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/text_document_format.fbs". + +#pragma once + +#include "../../blueprint/components/enabled.hpp" +#include "../../collection.hpp" +#include "../../component_batch.hpp" +#include "../../component_column.hpp" +#include "../../result.hpp" + +#include +#include +#include +#include + +namespace rerun::blueprint::archetypes { + /// **Archetype**: Formatting options for the text document view. + /// + /// These options only apply to plain text documents and have no effect on Markdown documents. + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct TextDocumentFormat { + /// Whether to use a monospace font for the document body. + /// + /// Defaults to disabled. + std::optional monospace; + + /// Whether to wrap long lines in the document body. + /// + /// Defaults to enabled. + std::optional word_wrap; + + public: + /// The name of the archetype as used in `ComponentDescriptor`s. + static constexpr const char ArchetypeName[] = + "rerun.blueprint.archetypes.TextDocumentFormat"; + + /// `ComponentDescriptor` for the `monospace` field. + static constexpr auto Descriptor_monospace = ComponentDescriptor( + ArchetypeName, "TextDocumentFormat:monospace", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `word_wrap` field. + static constexpr auto Descriptor_word_wrap = ComponentDescriptor( + ArchetypeName, "TextDocumentFormat:word_wrap", + Loggable::ComponentType + ); + + public: + TextDocumentFormat() = default; + TextDocumentFormat(TextDocumentFormat&& other) = default; + TextDocumentFormat(const TextDocumentFormat& other) = default; + TextDocumentFormat& operator=(const TextDocumentFormat& other) = default; + TextDocumentFormat& operator=(TextDocumentFormat&& other) = default; + + /// Update only some specific fields of a `TextDocumentFormat`. + static TextDocumentFormat update_fields() { + return TextDocumentFormat(); + } + + /// Clear all the fields of a `TextDocumentFormat`. + static TextDocumentFormat clear_fields(); + + /// Whether to use a monospace font for the document body. + /// + /// Defaults to disabled. + TextDocumentFormat with_monospace(const rerun::blueprint::components::Enabled& _monospace + ) && { + monospace = + ComponentBatch::from_loggable(_monospace, Descriptor_monospace).value_or_throw(); + return std::move(*this); + } + + /// Whether to wrap long lines in the document body. + /// + /// Defaults to enabled. + TextDocumentFormat with_word_wrap(const rerun::blueprint::components::Enabled& _word_wrap + ) && { + word_wrap = + ComponentBatch::from_loggable(_word_wrap, Descriptor_word_wrap).value_or_throw(); + return std::move(*this); + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s + /// instead, via `ComponentBatch::partitioned`. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + Collection columns(const Collection& lengths_); + + /// Partitions the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, + /// where `n` is automatically guessed. + Collection columns(); + }; + +} // namespace rerun::blueprint::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> as_batches( + const blueprint::archetypes::TextDocumentFormat& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/components.hpp b/rerun_cpp/src/rerun/blueprint/components.hpp index a2c377e4c033..10540ec89977 100644 --- a/rerun_cpp/src/rerun/blueprint/components.hpp +++ b/rerun_cpp/src/rerun/blueprint/components.hpp @@ -10,6 +10,7 @@ #include "blueprint/components/auto_scroll.hpp" #include "blueprint/components/auto_views.hpp" #include "blueprint/components/background_kind.hpp" +#include "blueprint/components/column_name.hpp" #include "blueprint/components/column_order.hpp" #include "blueprint/components/column_share.hpp" #include "blueprint/components/component_column_selector.hpp" diff --git a/rerun_cpp/src/rerun/blueprint/components/.gitattributes b/rerun_cpp/src/rerun/blueprint/components/.gitattributes index d8df7ee127ad..464a167613ca 100644 --- a/rerun_cpp/src/rerun/blueprint/components/.gitattributes +++ b/rerun_cpp/src/rerun/blueprint/components/.gitattributes @@ -10,6 +10,7 @@ auto_scroll.hpp linguist-generated=true auto_views.hpp linguist-generated=true background_kind.cpp linguist-generated=true background_kind.hpp linguist-generated=true +column_name.hpp linguist-generated=true column_order.cpp linguist-generated=true column_order.hpp linguist-generated=true column_share.hpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/blueprint/components/column_name.hpp b/rerun_cpp/src/rerun/blueprint/components/column_name.hpp new file mode 100644 index 000000000000..44849669cdbc --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/components/column_name.hpp @@ -0,0 +1,75 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/column_name.fbs". + +#pragma once + +#include "../../datatypes/utf8.hpp" +#include "../../result.hpp" + +#include +#include +#include +#include + +namespace rerun::blueprint::components { + /// **Component**: The name of a column in a table. + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct ColumnName { + rerun::datatypes::Utf8 value; + + public: + ColumnName() = default; + + ColumnName(rerun::datatypes::Utf8 value_) : value(std::move(value_)) {} + + ColumnName& operator=(rerun::datatypes::Utf8 value_) { + value = std::move(value_); + return *this; + } + + ColumnName(std::string value_) : value(std::move(value_)) {} + + ColumnName& operator=(std::string value_) { + value = std::move(value_); + return *this; + } + + /// Cast to the underlying Utf8 datatype + operator rerun::datatypes::Utf8() const { + return value; + } + }; +} // namespace rerun::blueprint::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::Utf8) == sizeof(blueprint::components::ColumnName)); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.blueprint.components.ColumnName"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::blueprint:: components::ColumnName` into an arrow array. + static Result> to_arrow( + const blueprint::components::ColumnName* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow(&instances->value, num_instances); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/c/rerun.h b/rerun_cpp/src/rerun/c/rerun.h index a411ee7e9054..ff1a96ac2b3e 100644 --- a/rerun_cpp/src/rerun/c/rerun.h +++ b/rerun_cpp/src/rerun/c/rerun.h @@ -604,6 +604,22 @@ void rr_recording_stream_disable_timeline( /// No-op for destroyed/non-existing streams. extern void rr_recording_stream_reset_time(rr_recording_stream stream); +/// Enable or disable automatic injection of the `log_tick` timeline into logged data. +/// +/// `log_tick` is a per-recording counter that increments on every logging call. +/// It is disabled by default (it can also be controlled via the `RERUN_LOG_TICK` environment variable). +/// +/// No-op for destroyed/non-existing streams. +extern void rr_recording_stream_set_log_tick_enabled(rr_recording_stream stream, bool enabled); + +/// Enable or disable automatic injection of the `log_time` timeline into logged data. +/// +/// `log_time` is the wall-clock time at which data was logged. +/// It is enabled by default (it can also be controlled via the `RERUN_LOG_TIME` environment variable). +/// +/// No-op for destroyed/non-existing streams. +extern void rr_recording_stream_set_log_time_enabled(rr_recording_stream stream, bool enabled); + /// Log the given data to the given stream. /// /// If `inject_time` is set to `true`, the row's timestamp data will be @@ -623,7 +639,7 @@ extern void rr_recording_stream_log( /// This method blocks until either at least one `Importer` starts streaming data in /// or all of them fail. /// -/// See for more information. +/// See for more information. extern void rr_recording_stream_log_file_from_path( rr_recording_stream stream, rr_string path, rr_string entity_path_prefix, bool static_, rr_error* error @@ -636,7 +652,7 @@ extern void rr_recording_stream_log_file_from_path( /// This method blocks until either at least one `Importer` starts streaming data in /// or all of them fail. /// -/// See for more information. +/// See for more information. extern void rr_recording_stream_log_file_from_contents( rr_recording_stream stream, rr_string path, rr_bytes contents, rr_string entity_path_prefix, bool static_, rr_error* error diff --git a/rerun_cpp/src/rerun/c/sdk_info.h b/rerun_cpp/src/rerun/c/sdk_info.h index c61955c318e6..f57c7db80382 100644 --- a/rerun_cpp/src/rerun/c/sdk_info.h +++ b/rerun_cpp/src/rerun/c/sdk_info.h @@ -2,13 +2,13 @@ /// /// This should match the string returned by `rr_version_string` (C) or `rerun::version_string` (C++). /// If not, the SDK's binary and the C header are out of sync. -#define RERUN_SDK_HEADER_VERSION "0.32.0-alpha.1" +#define RERUN_SDK_HEADER_VERSION "0.35.0" /// Major version of the Rerun C SDK. #define RERUN_SDK_HEADER_VERSION_MAJOR 0 /// Minor version of the Rerun C SDK. -#define RERUN_SDK_HEADER_VERSION_MINOR 32 +#define RERUN_SDK_HEADER_VERSION_MINOR 35 /// Patch version of the Rerun C SDK. #define RERUN_SDK_HEADER_VERSION_PATCH 0 diff --git a/rerun_cpp/src/rerun/components.hpp b/rerun_cpp/src/rerun/components.hpp index d33b11f7fc17..97bfb8d7c062 100644 --- a/rerun_cpp/src/rerun/components.hpp +++ b/rerun_cpp/src/rerun/components.hpp @@ -32,6 +32,7 @@ #include "components/image_plane_distance.hpp" #include "components/interactive.hpp" #include "components/interpolation_mode.hpp" +#include "components/is_keyframe.hpp" #include "components/key_value_pairs.hpp" #include "components/keypoint_id.hpp" #include "components/lat_lon.hpp" @@ -48,6 +49,7 @@ #include "components/opacity.hpp" #include "components/pinhole_projection.hpp" #include "components/plane3d.hpp" +#include "components/point_shading.hpp" #include "components/position2d.hpp" #include "components/position3d.hpp" #include "components/radius.hpp" @@ -81,3 +83,6 @@ #include "components/video_timestamp.hpp" #include "components/view_coordinates.hpp" #include "components/visible.hpp" +#include "components/voxel_index.hpp" +#include "components/voxel_size.hpp" +#include "components/voxel_value.hpp" diff --git a/rerun_cpp/src/rerun/components/.gitattributes b/rerun_cpp/src/rerun/components/.gitattributes index c18781553314..822476f6ba53 100644 --- a/rerun_cpp/src/rerun/components/.gitattributes +++ b/rerun_cpp/src/rerun/components/.gitattributes @@ -39,6 +39,7 @@ image_plane_distance.hpp linguist-generated=true interactive.hpp linguist-generated=true interpolation_mode.cpp linguist-generated=true interpolation_mode.hpp linguist-generated=true +is_keyframe.hpp linguist-generated=true key_value_pairs.cpp linguist-generated=true key_value_pairs.hpp linguist-generated=true keypoint_id.hpp linguist-generated=true @@ -61,6 +62,8 @@ name.hpp linguist-generated=true opacity.hpp linguist-generated=true pinhole_projection.hpp linguist-generated=true plane3d.hpp linguist-generated=true +point_shading.cpp linguist-generated=true +point_shading.hpp linguist-generated=true position2d.hpp linguist-generated=true position3d.hpp linguist-generated=true radius.hpp linguist-generated=true @@ -96,3 +99,6 @@ video_sample.hpp linguist-generated=true video_timestamp.hpp linguist-generated=true view_coordinates.hpp linguist-generated=true visible.hpp linguist-generated=true +voxel_index.hpp linguist-generated=true +voxel_size.hpp linguist-generated=true +voxel_value.hpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/components/is_keyframe.hpp b/rerun_cpp/src/rerun/components/is_keyframe.hpp new file mode 100644 index 000000000000..04bc667a7e8b --- /dev/null +++ b/rerun_cpp/src/rerun/components/is_keyframe.hpp @@ -0,0 +1,79 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/is_keyframe.fbs". + +#pragma once + +#include "../datatypes/bool.hpp" +#include "../result.hpp" + +#include +#include + +namespace rerun::components { + /// **Component**: Whether a `components::VideoSample` contains a keyframe (also known as a sync sample or IDR). + /// + /// A keyframe in this sense must be _decoder re-entrant_: a decoder must be able to start + /// decoding the stream from this sample alone, with no prior decoder state. + /// Not every intra-coded frame qualifies. Some codecs have intra-only frames that may + /// still reference existing decoder state and are therefore not valid sync points. + /// See `components::VideoCodec` for the codec-specific definition of a keyframe. + struct IsKeyframe { + rerun::datatypes::Bool is_keyframe; + + public: + IsKeyframe() = default; + + IsKeyframe(rerun::datatypes::Bool is_keyframe_) : is_keyframe(is_keyframe_) {} + + IsKeyframe& operator=(rerun::datatypes::Bool is_keyframe_) { + is_keyframe = is_keyframe_; + return *this; + } + + IsKeyframe(bool value_) : is_keyframe(value_) {} + + IsKeyframe& operator=(bool value_) { + is_keyframe = value_; + return *this; + } + + /// Cast to the underlying Bool datatype + operator rerun::datatypes::Bool() const { + return is_keyframe; + } + }; +} // namespace rerun::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::Bool) == sizeof(components::IsKeyframe)); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.components.IsKeyframe"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::components::IsKeyframe` into an arrow array. + static Result> to_arrow( + const components::IsKeyframe* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow( + &instances->is_keyframe, + num_instances + ); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/point_shading.cpp b/rerun_cpp/src/rerun/components/point_shading.cpp new file mode 100644 index 000000000000..670ad05c22d8 --- /dev/null +++ b/rerun_cpp/src/rerun/components/point_shading.cpp @@ -0,0 +1,56 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/point_shading.fbs". + +#include "point_shading.hpp" + +#include +#include + +namespace rerun { + const std::shared_ptr& Loggable::arrow_datatype() { + static const auto datatype = arrow::uint8(); + return datatype; + } + + Result> Loggable::to_arrow( + const components::PointShading* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::UInt8Builder* builder, const components::PointShading* elements, size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + ARROW_RETURN_NOT_OK(builder->Reserve(static_cast(num_elements))); + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + const auto variant = elements[elem_idx]; + ARROW_RETURN_NOT_OK(builder->Append(static_cast(variant))); + } + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/point_shading.hpp b/rerun_cpp/src/rerun/components/point_shading.hpp new file mode 100644 index 000000000000..f34da733c773 --- /dev/null +++ b/rerun_cpp/src/rerun/components/point_shading.hpp @@ -0,0 +1,57 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/point_shading.fbs". + +#pragma once + +#include "../result.hpp" + +#include +#include + +namespace arrow { + /// \private + template + class NumericBuilder; + + class Array; + class DataType; + class UInt8Type; + using UInt8Builder = NumericBuilder; +} // namespace arrow + +namespace rerun::components { + /// **Component**: Defines how points are shaded. + enum class PointShading : uint8_t { + + /// Radial gradient for a spherical shadow effect. + Gradient = 1, + + /// Flat shading. + Flat = 2, + }; +} // namespace rerun::components + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.components.PointShading"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::components::PointShading` into an arrow array. + static Result> to_arrow( + const components::PointShading* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::UInt8Builder* builder, const components::PointShading* elements, + size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/video_codec.hpp b/rerun_cpp/src/rerun/components/video_codec.hpp index c541fb9557a4..3a62b33ed128 100644 --- a/rerun_cpp/src/rerun/components/video_codec.hpp +++ b/rerun_cpp/src/rerun/components/video_codec.hpp @@ -66,6 +66,20 @@ namespace rerun::components { /// /// Enum value is the fourcc for 'hev1' (the WebCodec string assigned to this codec) in big endian. H265 = 0x68657631, + + /// VP8 + /// + /// See + /// + /// Enum value is the fourcc for 'vp08' (the WebCodec string assigned to this codec) in big endian. + VP8 = 0x76703038, + + /// VP9 + /// + /// See + /// + /// Enum value is the fourcc for 'vp09' (the WebCodec string assigned to this codec) in big endian. + VP9 = 0x76703039, }; } // namespace rerun::components diff --git a/rerun_cpp/src/rerun/components/voxel_index.hpp b/rerun_cpp/src/rerun/components/voxel_index.hpp new file mode 100644 index 000000000000..6249b2b78adb --- /dev/null +++ b/rerun_cpp/src/rerun/components/voxel_index.hpp @@ -0,0 +1,82 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_index.fbs". + +#pragma once + +#include "../datatypes/ivec3d.hpp" +#include "../result.hpp" + +#include +#include +#include + +namespace rerun::components { + /// **Component**: Integer index of a voxel in a sparse 3D voxel grid. + /// + /// The voxel center in local grid coordinates is `(index + 0.5) * voxel_size`. + struct VoxelIndex { + rerun::datatypes::IVec3D index; + + public: // START of extensions from voxel_index_ext.cpp: + /// Construct VoxelIndex from x/y/z values. + VoxelIndex(int32_t x, int32_t y, int32_t z) : index{x, y, z} {} + + // END of extensions from voxel_index_ext.cpp, start of generated code: + + public: + VoxelIndex() = default; + + VoxelIndex(rerun::datatypes::IVec3D index_) : index(index_) {} + + VoxelIndex& operator=(rerun::datatypes::IVec3D index_) { + index = index_; + return *this; + } + + VoxelIndex(std::array xyz_) : index(xyz_) {} + + VoxelIndex& operator=(std::array xyz_) { + index = xyz_; + return *this; + } + + /// Cast to the underlying IVec3D datatype + operator rerun::datatypes::IVec3D() const { + return index; + } + }; +} // namespace rerun::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::IVec3D) == sizeof(components::VoxelIndex)); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.components.VoxelIndex"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::components::VoxelIndex` into an arrow array. + static Result> to_arrow( + const components::VoxelIndex* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow( + &instances->index, + num_instances + ); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/voxel_index_ext.cpp b/rerun_cpp/src/rerun/components/voxel_index_ext.cpp new file mode 100644 index 000000000000..6bed489ffbd7 --- /dev/null +++ b/rerun_cpp/src/rerun/components/voxel_index_ext.cpp @@ -0,0 +1,23 @@ +#include "voxel_index.hpp" + +// Uncomment for better auto-complete while editing the extension. +// #define EDIT_EXTENSION + +namespace rerun { + namespace components { + +#ifdef EDIT_EXTENSION + struct VoxelIndexExt { + int32_t index[3]; +#define VoxelIndex VoxelIndexExt + + // + + /// Construct VoxelIndex from x/y/z values. + VoxelIndex(int32_t x, int32_t y, int32_t z) : index{x, y, z} {} + + // + }; +#endif + } // namespace components +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/voxel_size.hpp b/rerun_cpp/src/rerun/components/voxel_size.hpp new file mode 100644 index 000000000000..8de1134da363 --- /dev/null +++ b/rerun_cpp/src/rerun/components/voxel_size.hpp @@ -0,0 +1,74 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_size.fbs". + +#pragma once + +#include "../datatypes/vec3d.hpp" +#include "../result.hpp" + +#include +#include +#include + +namespace rerun::components { + /// **Component**: The scene-unit dimensions of one voxel in a sparse 3D voxel grid. + /// + /// Each component is the size of a voxel along the corresponding local grid axis. + /// All components must be finite and positive. + struct VoxelSize { + rerun::datatypes::Vec3D xyz; + + public: + VoxelSize() = default; + + VoxelSize(rerun::datatypes::Vec3D xyz_) : xyz(xyz_) {} + + VoxelSize& operator=(rerun::datatypes::Vec3D xyz_) { + xyz = xyz_; + return *this; + } + + VoxelSize(std::array xyz_) : xyz(xyz_) {} + + VoxelSize& operator=(std::array xyz_) { + xyz = xyz_; + return *this; + } + + /// Cast to the underlying Vec3D datatype + operator rerun::datatypes::Vec3D() const { + return xyz; + } + }; +} // namespace rerun::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::Vec3D) == sizeof(components::VoxelSize)); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.components.VoxelSize"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::components::VoxelSize` into an arrow array. + static Result> to_arrow( + const components::VoxelSize* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow(&instances->xyz, num_instances); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/voxel_value.hpp b/rerun_cpp/src/rerun/components/voxel_value.hpp new file mode 100644 index 000000000000..e1a57efe3df1 --- /dev/null +++ b/rerun_cpp/src/rerun/components/voxel_value.hpp @@ -0,0 +1,73 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_value.fbs". + +#pragma once + +#include "../datatypes/float32.hpp" +#include "../result.hpp" + +#include +#include + +namespace rerun::components { + /// **Component**: Optional scalar occupancy or value associated with a voxel. + struct VoxelValue { + rerun::datatypes::Float32 value; + + public: + VoxelValue() = default; + + VoxelValue(rerun::datatypes::Float32 value_) : value(value_) {} + + VoxelValue& operator=(rerun::datatypes::Float32 value_) { + value = value_; + return *this; + } + + VoxelValue(float value_) : value(value_) {} + + VoxelValue& operator=(float value_) { + value = value_; + return *this; + } + + /// Cast to the underlying Float32 datatype + operator rerun::datatypes::Float32() const { + return value; + } + }; +} // namespace rerun::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::Float32) == sizeof(components::VoxelValue)); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.components.VoxelValue"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::components::VoxelValue` into an arrow array. + static Result> to_arrow( + const components::VoxelValue* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow( + &instances->value, + num_instances + ); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes.hpp b/rerun_cpp/src/rerun/datatypes.hpp index f1d18e9a3471..b0fc6aeecbae 100644 --- a/rerun_cpp/src/rerun/datatypes.hpp +++ b/rerun_cpp/src/rerun/datatypes.hpp @@ -18,6 +18,7 @@ #include "datatypes/float32.hpp" #include "datatypes/float64.hpp" #include "datatypes/image_format.hpp" +#include "datatypes/ivec3d.hpp" #include "datatypes/keypoint_id.hpp" #include "datatypes/keypoint_pair.hpp" #include "datatypes/mat3x3.hpp" diff --git a/rerun_cpp/src/rerun/datatypes/.gitattributes b/rerun_cpp/src/rerun/datatypes/.gitattributes index d92da70425ae..f3af9944c421 100644 --- a/rerun_cpp/src/rerun/datatypes/.gitattributes +++ b/rerun_cpp/src/rerun/datatypes/.gitattributes @@ -33,6 +33,8 @@ float64.cpp linguist-generated=true float64.hpp linguist-generated=true image_format.cpp linguist-generated=true image_format.hpp linguist-generated=true +ivec3d.cpp linguist-generated=true +ivec3d.hpp linguist-generated=true keypoint_id.cpp linguist-generated=true keypoint_id.hpp linguist-generated=true keypoint_pair.cpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/datatypes/ivec3d.cpp b/rerun_cpp/src/rerun/datatypes/ivec3d.cpp new file mode 100644 index 000000000000..6d4d8b87bc93 --- /dev/null +++ b/rerun_cpp/src/rerun/datatypes/ivec3d.cpp @@ -0,0 +1,63 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/datatypes/ivec3d.fbs". + +#include "ivec3d.hpp" + +#include +#include + +namespace rerun::datatypes {} + +namespace rerun { + const std::shared_ptr& Loggable::arrow_datatype() { + static const auto datatype = + arrow::fixed_size_list(arrow::field("item", arrow::int32(), false), 3); + return datatype; + } + + Result> Loggable::to_arrow( + const datatypes::IVec3D* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::FixedSizeListBuilder* builder, const datatypes::IVec3D* elements, size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + auto value_builder = static_cast(builder->value_builder()); + + ARROW_RETURN_NOT_OK(builder->AppendValues(static_cast(num_elements))); + static_assert(sizeof(elements[0].xyz) == sizeof(elements[0])); + ARROW_RETURN_NOT_OK(value_builder->AppendValues( + elements[0].xyz.data(), + static_cast(num_elements * 3), + nullptr + )); + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes/ivec3d.hpp b/rerun_cpp/src/rerun/datatypes/ivec3d.hpp new file mode 100644 index 000000000000..293a2a4a10bc --- /dev/null +++ b/rerun_cpp/src/rerun/datatypes/ivec3d.hpp @@ -0,0 +1,79 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/datatypes/ivec3d.fbs". + +#pragma once + +#include "../result.hpp" + +#include +#include +#include + +namespace arrow { + class Array; + class DataType; + class FixedSizeListBuilder; +} // namespace arrow + +namespace rerun::datatypes { + /// **Datatype**: An int32 vector in 3D space. + struct IVec3D { + std::array xyz; + + public: // START of extensions from ivec3d_ext.cpp: + /// Construct IVec3D from x/y/z values. + IVec3D(int32_t x, int32_t y, int32_t z) : xyz{x, y, z} {} + + /// Construct IVec3D from x/y/z int32_t pointer. + explicit IVec3D(const int32_t* xyz_) : xyz{xyz_[0], xyz_[1], xyz_[2]} {} + + int32_t x() const { + return xyz[0]; + } + + int32_t y() const { + return xyz[1]; + } + + int32_t z() const { + return xyz[2]; + } + + // END of extensions from ivec3d_ext.cpp, start of generated code: + + public: + IVec3D() = default; + + IVec3D(std::array xyz_) : xyz(xyz_) {} + + IVec3D& operator=(std::array xyz_) { + xyz = xyz_; + return *this; + } + }; +} // namespace rerun::datatypes + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.datatypes.IVec3D"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::datatypes::IVec3D` into an arrow array. + static Result> to_arrow( + const datatypes::IVec3D* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::FixedSizeListBuilder* builder, const datatypes::IVec3D* elements, + size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes/ivec3d_ext.cpp b/rerun_cpp/src/rerun/datatypes/ivec3d_ext.cpp new file mode 100644 index 000000000000..e0ed33e1be82 --- /dev/null +++ b/rerun_cpp/src/rerun/datatypes/ivec3d_ext.cpp @@ -0,0 +1,38 @@ +#include "ivec3d.hpp" + +// Uncomment for better auto-complete while editing the extension. +// #define EDIT_EXTENSION + +namespace rerun { + namespace datatypes { + +#ifdef EDIT_EXTENSION + struct IVec3DExt { + int32_t xyz[3]; +#define IVec3D IVec3DExt + + // + + /// Construct IVec3D from x/y/z values. + IVec3D(int32_t x, int32_t y, int32_t z) : xyz{x, y, z} {} + + /// Construct IVec3D from x/y/z int32_t pointer. + explicit IVec3D(const int32_t* xyz_) : xyz{xyz_[0], xyz_[1], xyz_[2]} {} + + int32_t x() const { + return xyz[0]; + } + + int32_t y() const { + return xyz[1]; + } + + int32_t z() const { + return xyz[2]; + } + + // + }; +#endif + } // namespace datatypes +} // namespace rerun diff --git a/rerun_cpp/src/rerun/recording_stream.cpp b/rerun_cpp/src/rerun/recording_stream.cpp index 894280c91c96..7cc79d202f0b 100644 --- a/rerun_cpp/src/rerun/recording_stream.cpp +++ b/rerun_cpp/src/rerun/recording_stream.cpp @@ -236,6 +236,14 @@ namespace rerun { rr_recording_stream_reset_time(_id); } + void RecordingStream::set_log_tick_enabled(bool enabled) const { + rr_recording_stream_set_log_tick_enabled(_id, enabled); + } + + void RecordingStream::set_log_time_enabled(bool enabled) const { + rr_recording_stream_set_log_time_enabled(_id, enabled); + } + Error RecordingStream::try_log_serialized_batches( std::string_view entity_path, bool static_, std::vector batches ) const { diff --git a/rerun_cpp/src/rerun/recording_stream.hpp b/rerun_cpp/src/rerun/recording_stream.hpp index 1d20e496af40..0ccc52eb599a 100644 --- a/rerun_cpp/src/rerun/recording_stream.hpp +++ b/rerun_cpp/src/rerun/recording_stream.hpp @@ -431,6 +431,24 @@ namespace rerun { /// @see set_time_sequence, set_time_seconds, set_time_nanos, disable_timeline void reset_time() const; + /// Enable or disable automatic injection of the `log_tick` timeline into logged data. + /// + /// `log_tick` is a per-recording counter that increments on every logging call. + /// It is **disabled** by default (it can also be controlled via the `RERUN_LOG_TICK` + /// environment variable). + /// + /// @see set_log_time_enabled + void set_log_tick_enabled(bool enabled) const; + + /// Enable or disable automatic injection of the `log_time` timeline into logged data. + /// + /// `log_time` is the wall-clock time at which data was logged. + /// It is **enabled** by default (it can also be controlled via the `RERUN_LOG_TIME` + /// environment variable). + /// + /// @see set_log_tick_enabled + void set_log_time_enabled(bool enabled) const; + /// @} // ----------------------------------------------------------------------------------------- @@ -552,7 +570,7 @@ namespace rerun { /// \param static_ If true, the logged components will be static. /// Static data has no time associated with it, exists on all timelines, and unconditionally shadows /// any temporal data of the same type. - /// Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + /// Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). /// Additional timelines set by `set_time_sequence` or `set_time` will also be included. /// \param as_components Any type for which the `AsComponents` trait is implemented. /// This is the case for any archetype as well as individual or collection of `ComponentBatch`. @@ -574,7 +592,7 @@ namespace rerun { /// \param static_ If true, the logged components will be static. /// Static data has no time associated with it, exists on all timelines, and unconditionally shadows /// any temporal data of the same type. - /// Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + /// Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). /// Additional timelines set by `set_time_sequence` or `set_time` will also be included. /// \param as_components Any type for which the `AsComponents` trait is implemented. /// This is the case for any archetype as well as individual or collection of `ComponentBatch`. @@ -631,7 +649,7 @@ namespace rerun { /// \param static_ If true, the logged components will be static. /// Static data has no time associated with it, exists on all timelines, and unconditionally shadows /// any temporal data of the same type. - /// Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + /// Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). /// Additional timelines set by `set_time_sequence` or `set_time` will also be included. /// \param batches The serialized batches to log. /// @@ -664,14 +682,14 @@ namespace rerun { /// This method blocks until either at least one `Importer` starts streaming data in /// or all of them fail. /// - /// See for more information. + /// See for more information. /// /// \param filepath Path to the file to be logged. /// \param entity_path_prefix What should the logged entity paths be prefixed with? /// \param static_ If true, the logged components will be static. /// Static data has no time associated with it, exists on all timelines, and unconditionally shadows /// any temporal data of the same type. - /// Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + /// Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). /// Additional timelines set by `set_time_sequence` or `set_time` will also be included. /// /// \see `try_log_file_from_path` @@ -689,14 +707,14 @@ namespace rerun { /// This method blocks until either at least one `Importer` starts streaming data in /// or all of them fail. /// - /// See for more information. + /// See for more information. /// /// \param filepath Path to the file to be logged. /// \param entity_path_prefix What should the logged entity paths be prefixed with? /// \param static_ If true, the logged components will be static. /// Static data has no time associated with it, exists on all timelines, and unconditionally shadows /// any temporal data of the same type. - /// Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + /// Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). /// Additional timelines set by `set_time_sequence` or `set_time` will also be included. /// /// \see `log_file_from_path` @@ -712,7 +730,7 @@ namespace rerun { /// This method blocks until either at least one `Importer` starts streaming data in /// or all of them fail. /// - /// See for more information. + /// See for more information. /// /// \param filepath Path to the file that the `contents` belong to. /// \param contents Contents to be logged. @@ -721,7 +739,7 @@ namespace rerun { /// \param static_ If true, the logged components will be static. /// Static data has no time associated with it, exists on all timelines, and unconditionally shadows /// any temporal data of the same type. - /// Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + /// Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). /// Additional timelines set by `set_time_sequence` or `set_time` will also be included. /// /// \see `try_log_file_from_contents` @@ -746,7 +764,7 @@ namespace rerun { /// This method blocks until either at least one `Importer` starts streaming data in /// or all of them fail. /// - /// See for more information. + /// See for more information. /// /// \param filepath Path to the file that the `contents` belong to. /// \param contents Contents to be logged. @@ -755,7 +773,7 @@ namespace rerun { /// \param static_ If true, the logged components will be static. /// Static data has no time associated with it, exists on all timelines, and unconditionally shadows /// any temporal data of the same type. - /// Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + /// Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). /// Additional timelines set by `set_time_sequence` or `set_time` will also be included. /// /// \see `log_file_from_contents` diff --git a/rerun_cpp/tests/generated/components.hpp b/rerun_cpp/tests/generated/components.hpp index fc15a6d72793..6780946b1617 100644 --- a/rerun_cpp/tests/generated/components.hpp +++ b/rerun_cpp/tests/generated/components.hpp @@ -25,3 +25,4 @@ #include "components/affix_fuzzer7.hpp" #include "components/affix_fuzzer8.hpp" #include "components/affix_fuzzer9.hpp" +#include "components/many_vec3.hpp" diff --git a/rerun_cpp/tests/generated/components/.gitattributes b/rerun_cpp/tests/generated/components/.gitattributes index f78f63baa59b..7836934b43a6 100644 --- a/rerun_cpp/tests/generated/components/.gitattributes +++ b/rerun_cpp/tests/generated/components/.gitattributes @@ -40,3 +40,4 @@ affix_fuzzer8.cpp linguist-generated=true affix_fuzzer8.hpp linguist-generated=true affix_fuzzer9.cpp linguist-generated=true affix_fuzzer9.hpp linguist-generated=true +many_vec3.hpp linguist-generated=true diff --git a/rerun_cpp/tests/generated/components/many_vec3.hpp b/rerun_cpp/tests/generated/components/many_vec3.hpp new file mode 100644 index 000000000000..1581d863c933 --- /dev/null +++ b/rerun_cpp/tests/generated/components/many_vec3.hpp @@ -0,0 +1,75 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/fuzzy.fbs". + +#pragma once + +#include "../datatypes/many_vec3.hpp" + +#include +#include +#include +#include + +namespace rerun::components { + struct ManyVec3 { + rerun::datatypes::ManyVec3 nested_array_of_structs; + + public: + ManyVec3() = default; + + ManyVec3(rerun::datatypes::ManyVec3 nested_array_of_structs_) + : nested_array_of_structs(nested_array_of_structs_) {} + + ManyVec3& operator=(rerun::datatypes::ManyVec3 nested_array_of_structs_) { + nested_array_of_structs = nested_array_of_structs_; + return *this; + } + + ManyVec3(std::array, 2> triples_) + : nested_array_of_structs(triples_) {} + + ManyVec3& operator=(std::array, 2> triples_) { + nested_array_of_structs = triples_; + return *this; + } + + /// Cast to the underlying ManyVec3 datatype + operator rerun::datatypes::ManyVec3() const { + return nested_array_of_structs; + } + }; +} // namespace rerun::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::ManyVec3) == sizeof(components::ManyVec3)); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.testing.components.ManyVec3"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::components::ManyVec3` into an arrow array. + static Result> to_arrow( + const components::ManyVec3* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow( + &instances->nested_array_of_structs, + num_instances + ); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/tests/generated/datatypes.hpp b/rerun_cpp/tests/generated/datatypes.hpp index fa422b24ee1b..3bbe09f98c57 100644 --- a/rerun_cpp/tests/generated/datatypes.hpp +++ b/rerun_cpp/tests/generated/datatypes.hpp @@ -11,8 +11,12 @@ #include "datatypes/affix_fuzzer4.hpp" #include "datatypes/affix_fuzzer5.hpp" #include "datatypes/enum_test.hpp" +#include "datatypes/fixed_size_enum_array.hpp" +#include "datatypes/fixed_size_wide_enum_array.hpp" #include "datatypes/flattened_scalar.hpp" +#include "datatypes/many_vec3.hpp" #include "datatypes/multi_enum.hpp" #include "datatypes/primitive_component.hpp" #include "datatypes/string_component.hpp" #include "datatypes/valued_enum.hpp" +#include "datatypes/wide_enum.hpp" diff --git a/rerun_cpp/tests/generated/datatypes/.gitattributes b/rerun_cpp/tests/generated/datatypes/.gitattributes index a7a68d41cc19..1195c00c71e9 100644 --- a/rerun_cpp/tests/generated/datatypes/.gitattributes +++ b/rerun_cpp/tests/generated/datatypes/.gitattributes @@ -19,8 +19,14 @@ affix_fuzzer5.cpp linguist-generated=true affix_fuzzer5.hpp linguist-generated=true enum_test.cpp linguist-generated=true enum_test.hpp linguist-generated=true +fixed_size_enum_array.cpp linguist-generated=true +fixed_size_enum_array.hpp linguist-generated=true +fixed_size_wide_enum_array.cpp linguist-generated=true +fixed_size_wide_enum_array.hpp linguist-generated=true flattened_scalar.cpp linguist-generated=true flattened_scalar.hpp linguist-generated=true +many_vec3.cpp linguist-generated=true +many_vec3.hpp linguist-generated=true multi_enum.cpp linguist-generated=true multi_enum.hpp linguist-generated=true primitive_component.cpp linguist-generated=true @@ -29,3 +35,5 @@ string_component.cpp linguist-generated=true string_component.hpp linguist-generated=true valued_enum.cpp linguist-generated=true valued_enum.hpp linguist-generated=true +wide_enum.cpp linguist-generated=true +wide_enum.hpp linguist-generated=true diff --git a/rerun_cpp/tests/generated/datatypes/fixed_size_enum_array.cpp b/rerun_cpp/tests/generated/datatypes/fixed_size_enum_array.cpp new file mode 100644 index 000000000000..1d7726d0cf6f --- /dev/null +++ b/rerun_cpp/tests/generated/datatypes/fixed_size_enum_array.cpp @@ -0,0 +1,75 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#include "fixed_size_enum_array.hpp" + +#include "enum_test.hpp" + +#include +#include + +namespace rerun::datatypes {} + +namespace rerun { + const std::shared_ptr& Loggable::arrow_datatype( + ) { + static const auto datatype = arrow::fixed_size_list( + arrow::field("item", Loggable::arrow_datatype(), false), + 3 + ); + return datatype; + } + + Result> Loggable::to_arrow( + const datatypes::FixedSizeEnumArray* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::FixedSizeListBuilder* builder, const datatypes::FixedSizeEnumArray* elements, + size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + auto value_builder = static_cast(builder->value_builder()); + ARROW_RETURN_NOT_OK(builder->Reserve(static_cast(num_elements))); + ARROW_RETURN_NOT_OK(value_builder->Reserve(static_cast(num_elements * 3))); + + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + const auto& element = elements[elem_idx]; + ARROW_RETURN_NOT_OK(builder->Append()); + if (element.values.data()) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + value_builder, + element.values.data(), + 3 + )); + } + } + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/tests/generated/datatypes/fixed_size_enum_array.hpp b/rerun_cpp/tests/generated/datatypes/fixed_size_enum_array.hpp new file mode 100644 index 000000000000..e766d306ae98 --- /dev/null +++ b/rerun_cpp/tests/generated/datatypes/fixed_size_enum_array.hpp @@ -0,0 +1,61 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#pragma once + +#include "enum_test.hpp" + +#include +#include +#include +#include + +namespace arrow { + class Array; + class DataType; + class FixedSizeListBuilder; +} // namespace arrow + +namespace rerun::datatypes { + /// **Datatype**: Test datatype for fixed-size enum arrays. + struct FixedSizeEnumArray { + /// Fixed-size enum array. + std::array values; + + public: + FixedSizeEnumArray() = default; + + FixedSizeEnumArray(std::array values_) : values(values_) {} + + FixedSizeEnumArray& operator=(std::array values_) { + values = values_; + return *this; + } + }; +} // namespace rerun::datatypes + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = + "rerun.testing.datatypes.FixedSizeEnumArray"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::datatypes::FixedSizeEnumArray` into an arrow array. + static Result> to_arrow( + const datatypes::FixedSizeEnumArray* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::FixedSizeListBuilder* builder, const datatypes::FixedSizeEnumArray* elements, + size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_cpp/tests/generated/datatypes/fixed_size_wide_enum_array.cpp b/rerun_cpp/tests/generated/datatypes/fixed_size_wide_enum_array.cpp new file mode 100644 index 000000000000..37a27fb76a63 --- /dev/null +++ b/rerun_cpp/tests/generated/datatypes/fixed_size_wide_enum_array.cpp @@ -0,0 +1,75 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#include "fixed_size_wide_enum_array.hpp" + +#include "wide_enum.hpp" + +#include +#include + +namespace rerun::datatypes {} + +namespace rerun { + const std::shared_ptr& + Loggable::arrow_datatype() { + static const auto datatype = arrow::fixed_size_list( + arrow::field("item", Loggable::arrow_datatype(), false), + 2 + ); + return datatype; + } + + Result> Loggable::to_arrow( + const datatypes::FixedSizeWideEnumArray* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::FixedSizeListBuilder* builder, const datatypes::FixedSizeWideEnumArray* elements, + size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + auto value_builder = static_cast(builder->value_builder()); + ARROW_RETURN_NOT_OK(builder->Reserve(static_cast(num_elements))); + ARROW_RETURN_NOT_OK(value_builder->Reserve(static_cast(num_elements * 2))); + + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + const auto& element = elements[elem_idx]; + ARROW_RETURN_NOT_OK(builder->Append()); + if (element.values.data()) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + value_builder, + element.values.data(), + 2 + )); + } + } + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/tests/generated/datatypes/fixed_size_wide_enum_array.hpp b/rerun_cpp/tests/generated/datatypes/fixed_size_wide_enum_array.hpp new file mode 100644 index 000000000000..9780129278fd --- /dev/null +++ b/rerun_cpp/tests/generated/datatypes/fixed_size_wide_enum_array.hpp @@ -0,0 +1,62 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#pragma once + +#include "wide_enum.hpp" + +#include +#include +#include +#include + +namespace arrow { + class Array; + class DataType; + class FixedSizeListBuilder; +} // namespace arrow + +namespace rerun::datatypes { + /// **Datatype**: Test datatype for fixed-size arrays of wide enums. + struct FixedSizeWideEnumArray { + /// Fixed-size wide enum array. + std::array values; + + public: + FixedSizeWideEnumArray() = default; + + FixedSizeWideEnumArray(std::array values_) + : values(values_) {} + + FixedSizeWideEnumArray& operator=(std::array values_) { + values = values_; + return *this; + } + }; +} // namespace rerun::datatypes + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = + "rerun.testing.datatypes.FixedSizeWideEnumArray"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::datatypes::FixedSizeWideEnumArray` into an arrow array. + static Result> to_arrow( + const datatypes::FixedSizeWideEnumArray* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::FixedSizeListBuilder* builder, const datatypes::FixedSizeWideEnumArray* elements, + size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_cpp/tests/generated/datatypes/many_vec3.cpp b/rerun_cpp/tests/generated/datatypes/many_vec3.cpp new file mode 100644 index 000000000000..08c44e5d7edc --- /dev/null +++ b/rerun_cpp/tests/generated/datatypes/many_vec3.cpp @@ -0,0 +1,73 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/datatypes/fuzzy.fbs". + +#include "many_vec3.hpp" + +#include +#include + +namespace rerun::datatypes {} + +namespace rerun { + const std::shared_ptr& Loggable::arrow_datatype() { + static const auto datatype = arrow::fixed_size_list( + arrow::field( + "item", + arrow::fixed_size_list(arrow::field("item", arrow::float32(), false), 3), + false + ), + 2 + ); + return datatype; + } + + Result> Loggable::to_arrow( + const datatypes::ManyVec3* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::FixedSizeListBuilder* builder, const datatypes::ManyVec3* elements, + size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + auto value_builder = static_cast(builder->value_builder()); + + ARROW_RETURN_NOT_OK(builder->AppendValues(static_cast(num_elements))); + static_assert(sizeof(elements[0].triples) == sizeof(elements[0])); + ARROW_RETURN_NOT_OK(value_builder->AppendValues(static_cast(num_elements * 2))); + auto value_builder_inner1 = + static_cast(value_builder->value_builder()); + ARROW_RETURN_NOT_OK(value_builder_inner1->AppendValues( + reinterpret_cast(elements[0].triples.data()), + static_cast(num_elements * 2 * 3), + nullptr + )); + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/tests/generated/datatypes/many_vec3.hpp b/rerun_cpp/tests/generated/datatypes/many_vec3.hpp new file mode 100644 index 000000000000..90bc6db0eb56 --- /dev/null +++ b/rerun_cpp/tests/generated/datatypes/many_vec3.hpp @@ -0,0 +1,57 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/datatypes/fuzzy.fbs". + +#pragma once + +#include +#include +#include +#include + +namespace arrow { + class Array; + class DataType; + class FixedSizeListBuilder; +} // namespace arrow + +namespace rerun::datatypes { + /// **Datatype**: A fixed-size array of arrays — exercises nested fixed-size lists in Arrow. + struct ManyVec3 { + std::array, 2> triples; + + public: + ManyVec3() = default; + + ManyVec3(std::array, 2> triples_) : triples(triples_) {} + + ManyVec3& operator=(std::array, 2> triples_) { + triples = triples_; + return *this; + } + }; +} // namespace rerun::datatypes + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.testing.datatypes.ManyVec3"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::datatypes::ManyVec3` into an arrow array. + static Result> to_arrow( + const datatypes::ManyVec3* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::FixedSizeListBuilder* builder, const datatypes::ManyVec3* elements, + size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_cpp/tests/generated/datatypes/wide_enum.cpp b/rerun_cpp/tests/generated/datatypes/wide_enum.cpp new file mode 100644 index 000000000000..ff70e1b7a7ad --- /dev/null +++ b/rerun_cpp/tests/generated/datatypes/wide_enum.cpp @@ -0,0 +1,56 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#include "wide_enum.hpp" + +#include +#include + +namespace rerun { + const std::shared_ptr& Loggable::arrow_datatype() { + static const auto datatype = arrow::uint32(); + return datatype; + } + + Result> Loggable::to_arrow( + const datatypes::WideEnum* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::UInt32Builder* builder, const datatypes::WideEnum* elements, size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + ARROW_RETURN_NOT_OK(builder->Reserve(static_cast(num_elements))); + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + const auto variant = elements[elem_idx]; + ARROW_RETURN_NOT_OK(builder->Append(static_cast(variant))); + } + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/tests/generated/datatypes/wide_enum.hpp b/rerun_cpp/tests/generated/datatypes/wide_enum.hpp new file mode 100644 index 000000000000..20aa87897afe --- /dev/null +++ b/rerun_cpp/tests/generated/datatypes/wide_enum.hpp @@ -0,0 +1,55 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +#pragma once + +#include +#include +#include + +namespace arrow { + /// \private + template + class NumericBuilder; + + class Array; + class DataType; + class UInt32Type; + using UInt32Builder = NumericBuilder; +} // namespace arrow + +namespace rerun::datatypes { + /// **Datatype**: A test enum with values that require more than one byte. + enum class WideEnum : uint32_t { + + /// Low value. + Low = 0x1, + + /// High value. + High = 0x10000, + }; +} // namespace rerun::datatypes + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.testing.datatypes.WideEnum"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::datatypes::WideEnum` into an arrow array. + static Result> to_arrow( + const datatypes::WideEnum* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::UInt32Builder* builder, const datatypes::WideEnum* elements, size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_js/scripts/docs.mjs b/rerun_js/scripts/docs.mjs index 949432d87dba..4234b87e5055 100644 --- a/rerun_js/scripts/docs.mjs +++ b/rerun_js/scripts/docs.mjs @@ -28,7 +28,7 @@ const index_html = ` - Redirecting to
${main_package}/... + Redirecting to ${main_package}/… `; diff --git a/rerun_js/scripts/publish.mjs b/rerun_js/scripts/publish.mjs index 132f0189580b..ba66b44a1601 100755 --- a/rerun_js/scripts/publish.mjs +++ b/rerun_js/scripts/publish.mjs @@ -13,12 +13,6 @@ import { const root_dir = path.resolve(script_dir, ".."); -if (!process.env.NODE_AUTH_TOKEN) { - fail( - `"NODE_AUTH_TOKEN" env is not set. https://docs.npmjs.com/creating-and-viewing-access-tokens`, - ); -} - /** @type {{ workspaces: string[] }} */ const root_package_json = JSON.parse( fs.readFileSync(path.join(root_dir, "package.json"), "utf-8"), diff --git a/rerun_js/web-viewer-react/README.md b/rerun_js/web-viewer-react/README.md index 8a99619890ed..9bafa7e63af2 100644 --- a/rerun_js/web-viewer-react/README.md +++ b/rerun_js/web-viewer-react/README.md @@ -35,7 +35,7 @@ export default function App() { ``` The `rrd` in the snippet above should be a URL pointing to either: -- A hosted `.rrd` file, such as +- A hosted `.rrd` file, such as - A gRPC connection to the SDK opened via the [`serve`](https://www.rerun.io/docs/reference/sdk/operating-modes#serve) API If `rrd` is not set, the Viewer will display the same welcome screen as . diff --git a/rerun_js/web-viewer-react/index.js b/rerun_js/web-viewer-react/index.js index b7d9de8e8b74..2c276bc72595 100644 --- a/rerun_js/web-viewer-react/index.js +++ b/rerun_js/web-viewer-react/index.js @@ -9,8 +9,6 @@ import * as rerun from "@rerun-io/web-viewer"; * @property {string | string[]} rrd URL(s) of the `.rrd` file(s) to load. * Changing this prop will open any new unique URLs as recordings, * and close any URLs which are not present. - * @property {boolean} [follow_if_http] Whether to open HTTP `.rrd` sources in following mode. - * Defaults to `false`. Ignored for non-HTTP sources. * @property {string} [width] CSS width of the viewer's parent div * @property {string} [height] CSS height of the viewer's parent div * @@ -81,8 +79,6 @@ export default class WebViewer extends React.Component { this.#handle, toArray(prevProps.rrd), toArray(this.props.rrd), - prevProps.follow_if_http, - this.props.follow_if_http, ); } } @@ -129,39 +125,25 @@ function pascalToSnake(str) { function startViewer(handle, parent, getProps) { const props = getProps(); const initial = toArray(props.rrd); - const initialFollowIfHttp = props.follow_if_http; handle - .start( - initial, - parent, - { - manifest_url: props.manifest_url, - render_backend: props.render_backend, - hide_welcome_screen: props.hide_welcome_screen, - theme: props.theme, - - // NOTE: `width`, `height` intentionally ignored, they will - // instead be used on the parent `div` element - width: "100%", - height: "100%", - }, - { - follow_if_http: initialFollowIfHttp, - }, - ) + .start(initial, parent, { + manifest_url: props.manifest_url, + render_backend: props.render_backend, + hide_welcome_screen: props.hide_welcome_screen, + theme: props.theme, + + // NOTE: `width`, `height` intentionally ignored, they will + // instead be used on the parent `div` element + width: "100%", + height: "100%", + }) .then(() => { if (!handle.ready) { return; } - const { rrd, follow_if_http } = getProps(); - syncRecordings( - handle, - initial, - toArray(rrd), - initialFollowIfHttp, - follow_if_http, - ); + const { rrd } = getProps(); + syncRecordings(handle, initial, toArray(rrd)); }) .catch(() => {}); @@ -198,50 +180,15 @@ function diff(prev, current) { * @param {rerun.WebViewer} handle * @param {string[]} prev * @param {string[]} current - * @param {boolean | undefined} prevFollowIfHttp - * @param {boolean | undefined} followIfHttp */ -function syncRecordings(handle, prev, current, prevFollowIfHttp, followIfHttp) { +function syncRecordings(handle, prev, current) { const { added, removed } = diff(prev, current); - const reopened = - prevFollowIfHttp !== followIfHttp - ? intersection(prev, current).filter(isHttpSource) - : []; - if (removed.length > 0 || reopened.length > 0) { - handle.close([...removed, ...reopened]); + if (removed.length > 0) { + handle.close(removed); } if (added.length > 0) { - handle.open(added, { follow_if_http: followIfHttp }); - } - if (reopened.length > 0) { - handle.open(reopened, { follow_if_http: followIfHttp }); - } -} - -/** - * Return the values present in both arrays. - * - * @param {string[]} prev - * @param {string[]} current - * @returns {string[]} - */ -function intersection(prev, current) { - const prevSet = new Set(prev); - return current.filter((v) => prevSet.has(v)); -} - -/** - * Returns `true` if the recording source is affected by `follow_if_http`. - * - * @param {string} url - */ -function isHttpSource(url) { - try { - const protocol = new URL(url, document.baseURI).protocol; - return protocol === "http:" || protocol === "https:"; - } catch { - return false; + handle.open(added); } } diff --git a/rerun_js/web-viewer-react/package.json b/rerun_js/web-viewer-react/package.json index 3fbb251cd70c..31307973386b 100644 --- a/rerun_js/web-viewer-react/package.json +++ b/rerun_js/web-viewer-react/package.json @@ -1,6 +1,6 @@ { "name": "@rerun-io/web-viewer-react", - "version": "0.32.0-alpha.1", + "version": "0.35.0", "description": "Embed the Rerun web viewer in your React app", "licenses": [ { @@ -43,7 +43,7 @@ "tsconfig.json" ], "dependencies": { - "@rerun-io/web-viewer": "0.32.0-alpha.1" + "@rerun-io/web-viewer": "0.35.0" }, "peerDependencies": { "@types/react": "^18.2.33 || ^19.0.0", diff --git a/rerun_js/web-viewer/README.md b/rerun_js/web-viewer/README.md index 3e1a5fdf4e40..bc5b333c943a 100644 --- a/rerun_js/web-viewer/README.md +++ b/rerun_js/web-viewer/README.md @@ -28,7 +28,7 @@ This means that: ## Usage -The entrypoint for this packages is the [`WebViewer`](https://ref.rerun.io/docs/js/0.32.0-alpha.1/web-viewer/classes/WebViewer.html) class. +The entrypoint for this packages is the [`WebViewer`](https://ref.rerun.io/docs/js/0.35.0/web-viewer/classes/WebViewer.html) class. The web viewer is an object which manages a canvas element: ```js @@ -44,7 +44,7 @@ viewer.stop(); ``` The `rrd` in the snippet above should be a URL pointing to either: -- A hosted `.rrd` file, such as +- A hosted `.rrd` file, such as - A gRPC connection to the SDK opened via the [`serve`](https://www.rerun.io/docs/reference/sdk/operating-modes#serve) API If `rrd` is not set, the Viewer will display the same welcome screen as . diff --git a/rerun_js/web-viewer/index.ts b/rerun_js/web-viewer/index.ts index 2c814811ccf1..fd9b81cffc9e 100644 --- a/rerun_js/web-viewer/index.ts +++ b/rerun_js/web-viewer/index.ts @@ -4,6 +4,33 @@ import type { WebHandle, wasm_bindgen } from "./re_viewer"; let get_wasm_bindgen: (() => typeof wasm_bindgen) | null = null; let _wasm_module: WebAssembly.Module | null = null; +/** + * Feature-detect WebAssembly SIMD (`simd128`). + * + * The viewer .wasm is compiled with `-Ctarget-feature=+simd128`, so a browser + * without SIMD support will fail to instantiate the module with a cryptic + * `CompileError`. We probe up-front and surface a clear error instead. + * + * The probe is a minimal module that uses the `v128.any_true` instruction. + * Supported in: Chrome 91+, Firefox 89+, Safari 16.4+. + */ +function has_wasm_simd(): boolean { + try { + return WebAssembly.validate(new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, + 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11, + ])); + } catch { + return false; + } +} + +const UNSUPPORTED_BROWSER_MESSAGE = + "Your browser is too old to run the Rerun Viewer. " + + "The Viewer requires WebAssembly SIMD support, available in " + + "Chrome 91+, Firefox 89+, Safari 16.4+, or any modern Chromium-based browser. " + + "Please update your browser and try again."; + async function fetch_viewer_js(base_url?: string): Promise<(() => typeof wasm_bindgen)> { // @ts-ignore return (await import("./re_viewer")).default; @@ -20,7 +47,7 @@ async function fetch_viewer_wasm( const response = await fetch(url); if (!response.ok) { throw new Error( - `Failed to fetch viewer WASM: ${response.status} ${response.statusText}`, + `Failed to fetch viewer Wasm: ${response.status} ${response.statusText}`, ); } return wrap_fetch_with_progress(response, on_progress); @@ -95,6 +122,10 @@ async function load( base_url?: string, on_progress?: (received: number, total: number | null) => void, ): Promise { + if (!has_wasm_simd()) { + throw new Error(UNSUPPORTED_BROWSER_MESSAGE); + } + // instantiate wbg globals+module for every invocation of `load`, // but don't load the JS/Wasm source every time if (!get_wasm_bindgen || !_wasm_module) { @@ -210,15 +241,6 @@ export interface WebViewerOptions { login?: LoginOptions; } -export interface WebViewerOpenOptions { - /** - * Whether Rerun should open an HTTP resource in "Following" mode when streaming. - * - * Defaults to `false`. Ignored for non-HTTP URLs. - */ - follow_if_http?: boolean; -} - // `AppOptions` and `WebViewerOptions` must be compatible // otherwise we need to restructure how we pass options to the viewer @@ -430,7 +452,7 @@ function resolveAbsoluteUrl(url: string): string { * ``` * * Data may be provided to the Viewer as: - * - An HTTP file URL, e.g. `viewer.start("https://app.rerun.io/version/0.32.0-alpha.1/examples/dna.rrd")` + * - An HTTP file URL, e.g. `viewer.start("https://app.rerun.io/version/0.35.0/examples/dna.rrd")` * - A Rerun gRPC URL, e.g. `viewer.start("rerun+http://127.0.0.1:9876/proxy")` * - A stream of log messages, via {@link WebViewer.open_channel}. * @@ -464,13 +486,11 @@ export class WebViewer { * @param rrd URLs to `.rrd` files or gRPC connections to our SDK. * @param parent The element to attach the canvas onto. * @param options Web Viewer configuration. - * @param open_options Open options forwarded to the initial {@link WebViewer.open} call. */ async start( rrd: string | string[] | null, parent: HTMLElement | null, options: WebViewerOptions | null, - open_options: WebViewerOpenOptions | null = null, ): Promise { parent ??= document.body; options ??= {}; @@ -592,7 +612,7 @@ export class WebViewer { this.#dispatch_event("ready"); if (rrd) { - this.open(rrd, open_options ?? undefined); + this.open(rrd); } let self = this; @@ -745,7 +765,7 @@ export class WebViewer { * * @param rrd URLs to `.rrd` files or gRPC connections to our SDK. */ - open(rrd: string | string[], options: WebViewerOpenOptions = {}) { + open(rrd: string | string[]) { if (!this.#handle) { throw new Error(`attempted to open \`${rrd}\` in a stopped viewer`); } @@ -753,7 +773,7 @@ export class WebViewer { const urls = Array.isArray(rrd) ? rrd : [rrd]; for (const url of urls) { try { - this.#handle.add_receiver(url, options.follow_if_http); + this.#handle.add_receiver(url); } catch (e) { this.#fail("Failed to open recording", String(e)); throw e; diff --git a/rerun_js/web-viewer/package.json b/rerun_js/web-viewer/package.json index d92a68398458..64f937f77c20 100644 --- a/rerun_js/web-viewer/package.json +++ b/rerun_js/web-viewer/package.json @@ -1,6 +1,6 @@ { "name": "@rerun-io/web-viewer", - "version": "0.32.0-alpha.1", + "version": "0.35.0", "description": "Embed the Rerun web viewer in your app", "licenses": [ { diff --git a/rerun_notebook/package-lock.json b/rerun_notebook/package-lock.json index 2d08e057c493..bf08c8accf29 100644 --- a/rerun_notebook/package-lock.json +++ b/rerun_notebook/package-lock.json @@ -15,7 +15,7 @@ }, "../rerun_js/web-viewer": { "name": "@rerun-io/web-viewer", - "version": "0.32.0-alpha.1+dev", + "version": "0.34.0-alpha.1+dev", "license": "MIT", "devDependencies": { "dts-buddy": "^0.3.0", diff --git a/rerun_notebook/pyproject.toml b/rerun_notebook/pyproject.toml index 9daa5bdee94e..4e1f4ef21adc 100644 --- a/rerun_notebook/pyproject.toml +++ b/rerun_notebook/pyproject.toml @@ -5,13 +5,12 @@ build-backend = "hatchling.build" [project] name = "rerun-notebook" description = "Implementation helper for running rerun-sdk in notebooks" -version = "0.32.0-alpha.1" +version = "0.35.0" dependencies = [ "anywidget", "jupyter-ui-poll", - # ipykernel 7.0.0 has a bug that prevents it from sending display data from a different thread. - # TODO(ipython/ipykernel/#1450) - "ipykernel<7.0.0", + # ipykernel 7.0.x has a bug that prevents it from sending display data from a different thread. + "ipykernel != 7.0.*", ] readme = "README.md" keywords = ["rerun", "notebook"] diff --git a/rerun_py/.gitignore b/rerun_py/.gitignore index 2cbf5071cf0e..2c4bebb096c1 100644 --- a/rerun_py/.gitignore +++ b/rerun_py/.gitignore @@ -3,6 +3,7 @@ pyo3-build.cfg rerun_sdk/rerun_cli/rerun rerun_sdk/rerun_cli/rerun.exe +rerun_sdk/rerun_cli/Rerun.app/ site/ venv/ wheels/ diff --git a/rerun_py/.non_sdk_mypy.ini b/rerun_py/.non_sdk_mypy.ini index b979437fd9d3..49a9209087a4 100644 --- a/rerun_py/.non_sdk_mypy.ini +++ b/rerun_py/.non_sdk_mypy.ini @@ -5,7 +5,6 @@ exclude = (?x)( ^examples/python/objectron/.* | ^examples/python/ros_node/.* | ^examples/python/dataloader/.* - | ^examples/python/rerun_export/.* | ^docs/snippets/all/howto/lerobot_export\.py | docs/snippets/all/concepts/how_helix_was_logged.py | docs/snippets/all/concepts/static/log_static.py @@ -30,7 +29,7 @@ no_implicit_reexport = false disallow_untyped_calls = false # Cloud and data processing libraries -[mypy-google.cloud.*,lancedb.*,pyarrow.*,geopandas.*,pyproj.*,shapely.*] +[mypy-google.cloud.*,lancedb.*,qdrant_client.*,pyarrow.*,geopandas.*,pyproj.*,shapely.*] ignore_missing_imports = true # Development and build tools diff --git a/rerun_py/Cargo.toml b/rerun_py/Cargo.toml index 82d0b40f35f9..54e58af7cdee 100644 --- a/rerun_py/Cargo.toml +++ b/rerun_py/Cargo.toml @@ -41,7 +41,7 @@ nasm = ["re_video/nasm"] perf_telemetry = [ "dep:re_perf_telemetry", "re_redap_client/perf_telemetry", - "re_perf_telemetry/pyo3", + "re_perf_telemetry/session_id_reader", ] ## Add support for the in-memory OSS server. @@ -58,26 +58,30 @@ web_viewer = ["re_sdk/web_viewer", "dep:re_web_viewer_server", "dep:re_grpc_serv re_arrow_util.workspace = true re_auth.workspace = true re_build_info.workspace = true -re_byte_size.workspace = true re_chunk.workspace = true re_lenses_core.workspace = true +re_lenses.workspace = true re_chunk_store.workspace = true +re_format.workspace = true re_importer.workspace = true re_datafusion.workspace = true re_error.workspace = true -re_format.workspace = true re_redap_client.workspace = true re_grpc_client.workspace = true re_grpc_server = { workspace = true, optional = true } +re_hdf5.workspace = true re_log = { workspace = true, features = ["setup"] } re_log_encoding.workspace = true re_log_types.workspace = true re_mcap.workspace = true +re_mp4_reader.workspace = true re_parquet.workspace = true re_memory.workspace = true re_quota_channel.workspace = true re_sdk = { workspace = true, features = ["importers"] } +re_sdk_types = { workspace = true, features = ["video"] } re_sorbet.workspace = true +re_tracing = { workspace = true, features = ["server"] } re_tuid.workspace = true re_types_core.workspace = true re_uri.workspace = true @@ -87,7 +91,6 @@ re_server = { workspace = true, features = ["lance"] } anyhow.workspace = true arrow = { workspace = true, features = ["pyarrow"] } -bytes.workspace = true chrono.workspace = true #TODO(#9317): migrate to jiff comfy-table.workspace = true crossbeam.workspace = true @@ -123,7 +126,11 @@ thiserror.workspace = true # Native dependencies: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -re_perf_telemetry = { workspace = true, features = ["pyo3"], optional = true } +re_perf_telemetry = { workspace = true, features = ["session_id_reader"], optional = true } + + +[dev-dependencies] +tempfile.workspace = true [build-dependencies] diff --git a/rerun_py/README.md b/rerun_py/README.md index 0dae773b8063..3afd49fd6adc 100644 --- a/rerun_py/README.md +++ b/rerun_py/README.md @@ -99,3 +99,13 @@ If you run into a problem, run `rm -rf .pixi .venv` and try again. ```sh pixi run py-build && pixi run uvpy -m pytest rerun_py/tests/unit/test_tensor.py ``` + +# Profiling the Python SDK + +Set `RERUN_PUFFIN=1` to spawn a [`puffin_viewer`](https://github.com/EmbarkStudios/puffin) attached to the SDK on startup. The Rust side of the SDK then streams scopes (anything wrapped in `re_tracing::profile_function!` / `profile_scope!`) to the viewer for the lifetime of the process. + +```sh +RERUN_PUFFIN=1 pixi run uvpy your_script.py +``` + +Save a recording from the viewer for offline analysis (use the `investigate-puffin` skill in `.claude/skills/`). diff --git a/rerun_py/build.rs b/rerun_py/build.rs index d8d57890a7bf..207ca24de3e1 100644 --- a/rerun_py/build.rs +++ b/rerun_py/build.rs @@ -43,7 +43,18 @@ fn main() { .unwrap() .join("rerun_sdk/rerun_cli/rerun.exe"); - #[cfg(not(target_os = "windows"))] + #[cfg(target_os = "macos")] + let rerun_bin = { + let base = std::env::current_dir().expect("std::env::current_dir() failed"); + let bundled = base.join("rerun_sdk/rerun_cli/Rerun.app/Contents/MacOS/Rerun"); + if bundled.exists() { + bundled + } else { + base.join("rerun_sdk/rerun_cli/rerun") + } + }; + + #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))] let rerun_bin = std::env::current_dir() .expect("std::env::current_dir() failed") .join("rerun_sdk/rerun_cli/rerun"); diff --git a/rerun_py/docs/SUMMARY.txt b/rerun_py/docs/SUMMARY.txt deleted file mode 100644 index 2691a5c5b641..000000000000 --- a/rerun_py/docs/SUMMARY.txt +++ /dev/null @@ -1,2 +0,0 @@ -* [Common APIs](common/) -* [Writing Docs](writing_docs.md) diff --git a/rerun_py/docs/gen_common_index.py b/rerun_py/docs/gen_common_index.py index 09dd0a9400dd..a28344f94c69 100755 --- a/rerun_py/docs/gen_common_index.py +++ b/rerun_py/docs/gen_common_index.py @@ -1,36 +1,32 @@ #!/usr/bin/env python3 """ -Generate an index table and rendered pages for the common APIs. - -NOTE: When changing anything in this file, also consider how it affects `crates/build/re_dev_tools/src/build_search_index/ingest/python.rs`. - -The top-level index file should look like -``` -## Initialization -Function | Description --------- | ----------- -[rerun.init()](initialization/#rerun.init) | Initialize the Rerun SDK … -[rerun.connect_grpc()](initialization/#rerun.connect_grpc) | Connect to a remote Rerun Viewer on the … -[rerun.spawn()](initialization/#rerun.spawn) | Spawn a Rerun Viewer … -… - -The Summary should look like: -``` -* [index](index.md) -* [Initialization](initialization.md) -* [Logging Primitives](primitives.md) -* [Logging Images](images.md) -* [Annotations](annotation.md) -* [Extension Components](extension_components.md) -* [Plotting](plotting.md) -* [Transforms](transforms.md) -* [Helpers](helpers.md) -``` +Generate API reference pages and a landing index for the rerun Python SDK. + +The script emits two kinds of output (at the docs root): + +1. **Track A — auto-generated per-package pages** (`.md`). + Each entry in `DOCUMENTED_PACKAGES` gets one page that renders every + public symbol of that package. Public symbols are determined by `griffe` + with the `griffe-public-redundant-aliases` extension installed (see + `mkdocs.yml`), which honors three signals: `__all__`, `from x import Foo + as Foo` redundant aliases, and in-file non-underscore definitions. + +2. **Track B — curated overlay** (tables on `index.md`). + `CURATED_GROUPS` defines themed tables on the landing page only — they + never gate coverage. Missing curation only affects the landing page. + +A pre-emission validator fails the build if any new subpackage/module +appears under `rerun_sdk/rerun/` without being either documented or +explicitly excluded, if a documented or excluded path no longer exists +on disk, if a documented package's public surface is empty or fully +excluded, or if a curated table references an unknown symbol. + +NOTE: When changing anything in this file, also consider how it affects +`crates/build/re_dev_tools/src/build_search_index/ingest/python.rs`. """ from __future__ import annotations -import re import sys from dataclasses import dataclass from pathlib import Path @@ -39,112 +35,100 @@ import griffe import mkdocs_gen_files -# Modules we want public but get captured in other doc sections -EXCLUDE_SUBMODULE_CHECK = ["recording_stream", "sinks", "time", "web"] - - -def all_archetypes() -> list[str]: - file_path = Path(__file__).parent.parent.parent.joinpath("rerun_py/rerun_sdk/rerun/archetypes/__init__.py") +# Packages that get an auto-generated `.md` page at the docs root. +# Maps each dotted package path to its nav title path: a 1-tuple for a +# top-level nav entry, or a 2-tuple `(parent, child)` for a nested entry +# (used by the Blueprint sub-packages and `experimental.dataloader`). +# To document a brand-new subpackage, add a row here. Iteration order +# determines nav order in the rendered sidebar. +DOCUMENTED_PACKAGES: Final[dict[str, tuple[str, ...]]] = { + "rerun": ("Core",), + "rerun.archetypes": ("Archetypes",), + "rerun.components": ("Components",), + "rerun.datatypes": ("Datatypes",), + "rerun.blueprint": ("Blueprint", "APIs"), + "rerun.blueprint.archetypes": ("Blueprint", "Archetypes"), + "rerun.blueprint.components": ("Blueprint", "Components"), + "rerun.blueprint.datatypes": ("Blueprint", "Datatypes"), + "rerun.blueprint.views": ("Blueprint", "Views"), + "rerun.catalog": ("Catalog",), + "rerun.experimental": ("Experimental",), + "rerun.experimental.dataloader": ("Experimental", "Dataloader"), + "rerun.server": ("Server",), + "rerun.urdf": ("URDF Support",), + "rerun.notebook": ("Notebook",), + "rerun.auth": ("Authentication",), + "rerun.utilities": ("Utilities",), +} + +# Subpackages/modules under `rerun.` that deliberately do NOT get a Track A +# page. Their public symbols surface elsewhere (typically re-exported flat +# into top-level `rerun`). The freshness check (bottom of file) requires +# every non-underscore subpackage/module under `rerun_sdk/rerun/` to appear +# either here or in `DOCUMENTED_PACKAGES`, which makes it impossible to add +# a new submodule and silently miss it. +EXCLUDED_FROM_TRACK_A: Final[set[str]] = { + # Single-file modules whose public symbols are re-exported flat into + # `rerun` and surface on the `rerun` page. Listing them as their own + # Track A page would just duplicate already-documented content. + "rerun.any_batch_value", + "rerun.any_value", + "rerun.dynamic_archetype", + "rerun.error_utils", + "rerun.recording_stream", + "rerun.sinks", + "rerun.time", + "rerun.web", + # Internal organization for blueprint code; only exposes + # `Visualizer`/`VisualizableArchetype` which are implementation contracts, + # not user-facing API. + "rerun.blueprint.visualizers", + # Namespace-only packages with empty `__init__.py`; users import + # deeper symbols (e.g. `from rerun.utilities.datafusion.collect import ...`). + # No aggregated surface to document at the namespace level. + "rerun.utilities.datafusion", + "rerun.utilities.datafusion.functions", +} + +# Per-package, per-symbol allow-list of public symbols that should NOT be +# documented. Each entry must carry a comment explaining why. +EXPLICIT_DOC_EXCLUDES: Final[dict[str, set[str]]] = { + "rerun": { + # Internal arrow-IPC constants used by send_dataframe; not user-facing. + "RECORDING_PROPERTIES_PATH", + "RERUN_KIND", + "RERUN_KIND_CONTROL", + "RERUN_KIND_INDEX", + "SORBET_ARCHETYPE_NAME", + "SORBET_COMPONENT", + "SORBET_COMPONENT_TYPE", + "SORBET_ENTITY_PATH", + "SORBET_INDEX_NAME", + "SORBET_IS_TABLE_INDEX", + # Per-developer profiling; opt-in via env var. + "tracing_session", + # Internal numpy compat shim re-exported for use within rerun_py. + "asarray", + }, +} - # Initialize an empty list to store the quoted strings - quoted_strings = [] - - # Regular expression pattern to match quoted strings - pattern = r'"([^"]*)"' - - # Open the file for reading - with open(file_path, encoding="utf8") as file: - # Read the file line by line - for line in file: - # Use re.findall to find all quoted strings in the line - matches = re.findall(pattern, line) - - # Append the matched strings to the list - quoted_strings.extend(matches) - - assert len(quoted_strings) > 0, f"Found no archetypes in {file_path}" - return quoted_strings - - -def all_submodules(max_depth: int | None = None, ignore_hidden: bool = True) -> list[str]: - """ - Walk the rerun package structure to find all submodules. - Args: - max_depth: Maximum depth to traverse. If None, traverse all levels. - Depth 1 would return 'blueprint' but not 'blueprint.archetypes'. - ignore_hidden: If True, skip modules starting with underscores (e.g., _baseclasses, __main__). - - """ - - rerun_package_path = Path(__file__).parent.parent.parent.joinpath("rerun_py/rerun_sdk/rerun") - - # Walk the filesystem directly instead of importing packages - submodules = [] - - # We do this because we build and test our docs without rerun_bindings built. - def _walk_package_dir(directory: Path, base_path: Path, current_relative_path: str = "") -> None: - """Recursively walk a Python package directory to find submodules.""" - if not directory.is_dir(): - return - - # Check if this directory is a Python package (has __init__.py) - init_file = directory / "__init__.py" - if not init_file.exists(): - return - - # If we're not at the root, add this as a submodule - if current_relative_path: - # Skip hidden modules if requested - if ignore_hidden: - parts = current_relative_path.split(".") - if any(part.startswith("_") for part in parts): - return - - # Check depth if max_depth is specified - if max_depth is not None: - depth = current_relative_path.count(".") + 1 - if depth > max_depth: - return - - submodules.append(current_relative_path) - - # Recursively walk subdirectories - for item in directory.iterdir(): - if item.is_dir() and not item.name.startswith("."): - new_relative_path = current_relative_path + "." + item.name if current_relative_path else item.name - _walk_package_dir(item, base_path, new_relative_path) - - _walk_package_dir(rerun_package_path, rerun_package_path) +@dataclass +class Group: + """A curated themed table rendered on the landing page only.""" - assert len(submodules) > 0, f"Found no submodules in {rerun_package_path}" - return sorted(submodules) + title: str + items: list[str] -@dataclass -class Section: - title: str - sub_title: str | None = None - func_list: list[str] | None = None - class_list: list[str] | None = None - gen_page: bool = True - mod_path: list[str] | None = None - show_tables: bool = True - default_filters: bool = True - show_submodules: bool = False - - def __post_init__(self) -> None: - if self.mod_path is None: - self.mod_path = ["rerun"] - - -# This is the list of sections and functions that will be included in the index -# for each of them. -SECTION_TABLE: Final[list[Section]] = [ - ################################################################################ - Section( +# Curated overlay: themed tables shown on the landing `index.md`. These never +# gate coverage — the auto-generated per-package pages are the source of +# truth. Items are dotted relative paths into the `rerun` package +# (e.g., `init`, `archetypes.Points3D`, `experimental.send_chunk`). +CURATED_GROUPS: Final[list[Group]] = [ + Group( title="Initialization functions", - func_list=[ + items=[ "init", "set_sinks", "connect_grpc", @@ -157,124 +141,74 @@ def __post_init__(self) -> None: "memory_recording", "notebook_show", "legacy_notebook_show", + "ChunkBatcherConfig", + "DescribedComponentBatch", + "RecordingStream", + "TimeColumnLike", ], - class_list=["ChunkBatcherConfig", "DescribedComponentBatch", "RecordingStream", "TimeColumnLike"], ), - Section( + Group( title="Logging functions", - func_list=[ - "log", - "log_file_from_path", - "log_file_from_contents", - ], + items=["log", "log_file_from_path", "log_file_from_contents"], ), - Section( + Group( title="Property functions", - func_list=[ - "send_property", - "send_recording_name", - "send_recording_start_time_nanos", - ], + items=["send_property", "send_recording_name", "send_recording_start_time_nanos"], ), - Section( + Group( title="Timeline functions", - func_list=[ - "set_time", - "disable_timeline", - "reset_time", - ], + items=["set_time", "disable_timeline", "reset_time", "set_log_tick_enabled", "set_log_time_enabled"], ), - Section( + Group( title="Columnar API", - func_list=[ - "send_columns", - "send_record_batch", - "send_dataframe", - ], - class_list=[ - "TimeColumn", - ], + items=["send_columns", "send_record_batch", "send_dataframe", "TimeColumn"], ), - ################################################################################ - # These sections don't have tables, but generate pages containing all the archetypes, components, datatypes - Section( - title="Archetypes", - mod_path=["rerun.archetypes"], - show_tables=False, - ), - Section( - title="Components", - mod_path=["rerun.components"], - show_tables=False, - ), - Section( - title="Datatypes", - mod_path=["rerun.datatypes"], - show_tables=False, - ), - Section( - title="Custom Data", - mod_path=["rerun.any_value", "rerun.any_batch_value", "rerun.dynamic_archetype"], - ), - ################################################################################ - # These are tables but don't need their own pages since they refer to types that - # were added in the pages up above - Section( + Group( title="General", - class_list=[ + items=[ "archetypes.Clear", "blueprint.archetypes.EntityBehavior", "archetypes.RecordingInfo", ], - gen_page=False, ), - Section( + Group( title="Annotations", - class_list=[ + items=[ "archetypes.AnnotationContext", "datatypes.AnnotationInfo", "datatypes.ClassDescription", ], - gen_page=False, - ), - Section( - title="ErrorUtils", - mod_path=["rerun.error_utils"], - show_tables=False, ), - Section( + Group( title="Images", - class_list=[ + items=[ "archetypes.DepthImage", "archetypes.Image", "archetypes.EncodedImage", "archetypes.EncodedDepthImage", "archetypes.SegmentationImage", ], - gen_page=False, ), - Section( + Group( title="Video", - class_list=[ + items=[ "archetypes.VideoStream", "archetypes.AssetVideo", "archetypes.VideoFrameReference", ], - gen_page=False, ), - Section( + Group( title="Plotting", - class_list=[ + items=[ "archetypes.BarChart", "archetypes.Scalars", "archetypes.SeriesLines", "archetypes.SeriesPoints", ], - gen_page=False, ), - Section( + Group( title="Spatial Archetypes", - class_list=[ + items=[ "archetypes.Arrows3D", "archetypes.Arrows2D", "archetypes.Asset3D", @@ -282,6 +216,7 @@ def __post_init__(self) -> None: "archetypes.Boxes3D", "archetypes.Capsules3D", "archetypes.Cylinders3D", + "archetypes.Ellipses2D", "archetypes.Ellipsoids3D", "archetypes.GridMap", "archetypes.LineStrips2D", @@ -291,42 +226,30 @@ def __post_init__(self) -> None: "archetypes.Points3D", "archetypes.TransformAxes3D", ], - gen_page=False, ), - Section( + Group( title="Geospatial Archetypes", - class_list=[ - "archetypes.GeoLineStrings", - "archetypes.GeoPoints", - ], - gen_page=False, + items=["archetypes.GeoLineStrings", "archetypes.GeoPoints"], ), - Section( + Group( title="Graphs", - class_list=[ - "archetypes.GraphNodes", - "archetypes.GraphEdges", - ], - gen_page=False, + items=["archetypes.GraphNodes", "archetypes.GraphEdges"], ), - Section( + Group( title="Tensors", - class_list=["archetypes.Tensor"], - gen_page=False, + items=["archetypes.Tensor"], ), - Section( + Group( title="Text", - class_list=["LoggingHandler", "archetypes.TextDocument", "archetypes.TextLog"], - gen_page=False, + items=["LoggingHandler", "archetypes.TextDocument", "archetypes.TextLog"], ), - Section( - title="Status", - class_list=["archetypes.Status"], - gen_page=False, + Group( + title="State timeline", + items=["archetypes.StateChange", "archetypes.StateConfiguration"], ), - Section( + Group( title="Transforms and Coordinate Systems", - class_list=[ + items=[ "archetypes.Pinhole", "archetypes.Transform3D", "archetypes.InstancePoses3D", @@ -336,227 +259,37 @@ def __post_init__(self) -> None: "datatypes.RotationAxisAngle", "archetypes.CoordinateFrame", ], - gen_page=False, ), - Section( + Group( title="MCAP", - class_list=[ + items=[ "archetypes.McapChannel", "archetypes.McapMessage", "archetypes.McapSchema", "archetypes.McapStatistics", ], - gen_page=False, - ), - # Section( - # title="Deprecated", - # class_list=[], - # gen_page=False, - # ), - ################################################################################ - # Other referenced things - Section( - title="Enums", - mod_path=["rerun"], - class_list=[ - "Box2DFormat", - "ImageFormat", - "MeshFormat", - ], - show_tables=False, ), - Section( + Group( title="Interfaces", - mod_path=["rerun"], - class_list=[ + items=[ "ComponentMixin", "ComponentBatchLike", "AsComponents", - "ComponentBatchLike", "ComponentColumn", ], - default_filters=False, - show_tables=True, - ), - ################################################################################ - # Blueprint APIs - Section( - title="Blueprint", - sub_title="APIs", - mod_path=["rerun.blueprint"], - class_list=[ - "Blueprint", - "BlueprintLike", - "BlueprintPart", - "Container", - "ContainerLike", - "Horizontal", - "Vertical", - "Grid", - "Tabs", - "View", - "BarChartView", - "Spatial2DView", - "Spatial3DView", - "TensorView", - "TextDocumentView", - "TextLogView", - "TimeSeriesView", - "BlueprintPanel", - "SelectionPanel", - "TimePanel", - ], - ), - Section( - title="Blueprint", - sub_title="Archetypes", - mod_path=["rerun.blueprint.archetypes"], - show_tables=False, - ), - Section( - title="Blueprint", - sub_title="Components", - mod_path=["rerun.blueprint.components"], - show_tables=False, - ), - Section( - title="Blueprint", - sub_title="Datatypes", - mod_path=["rerun.blueprint.datatypes"], - show_tables=False, - ), - Section( - title="Blueprint", - sub_title="Views", - mod_path=["rerun.blueprint.views"], - show_tables=False, - ), - ################################################################################ - # Remaining sections - Section( - title="Catalog", - show_tables=True, - mod_path=["rerun.catalog"], - show_submodules=True, - class_list=[ - "Schema", - "ComponentColumnDescriptor", - "ComponentColumnSelector", - "IndexColumnDescriptor", - "IndexColumnSelector", - "AlreadyExistsError", - "CatalogClient", - "DatasetEntry", - "DatasetView", - "Entry", - "EntryId", - "EntryKind", - "IndexValuesLike", - "NotFoundError", - "RegistrationHandle", - "RegistrationResult", - "SegmentRegistrationResult", - "TableEntry", - "VectorDistanceMetric", - "VectorDistanceMetricLike", - ], - ), - Section( - title="Server", - show_tables=True, - mod_path=["rerun.server"], - show_submodules=True, ), - Section( - title="Authentication", - show_tables=True, - mod_path=["rerun.auth"], - func_list=["login", "logout", "get_credentials"], - class_list=["Credentials"], - show_submodules=True, - ), - Section( - title="Recording", - mod_path=["rerun.recording"], - func_list=[ - "load_archive", - "load_recording", - ], - class_list=[ - "Recording", - "RRDArchive", - ], - show_tables=True, - ), - Section( - title="URDF Support", - show_tables=True, - mod_path=["rerun.urdf"], - show_submodules=True, - ), - Section( - title="Utilities", - show_tables=False, - mod_path=["rerun.utilities"], - show_submodules=True, - ), - Section( - title="Experimental", - show_tables=True, - mod_path=["rerun.experimental"], - show_submodules=True, - func_list=[ - "send_chunk", - ], - class_list=[ - "Chunk", - "Lens", - "LensOutput", - "Selector", - "ViewerClient", - ], - ), - Section( - title="Notebook", - show_tables=True, - mod_path=["rerun.notebook"], - show_submodules=True, - func_list=[ - "set_default_size", - ], - class_list=[ - "Viewer", - "ViewerEvent", - "PlayEvent", - "PauseEvent", - "TimeUpdateEvent", - "TimelineChangeEvent", - "SelectionChangeEvent", - "RecordingOpenEvent", - "SelectionItem", - "EntitySelectionItem", - "ViewSelectionItem", - "ContainerSelectionItem", - ], - ), - Section( + Group( title="Script Helpers", - func_list=[ - "script_add_args", - "script_setup", - "script_teardown", - ], + items=["script_add_args", "script_setup", "script_teardown"], ), - Section( + Group( title="Other classes and functions", - show_tables=False, - func_list=[ + items=[ "get_data_recording", "get_global_data_recording", "get_recording_id", "get_thread_local_data_recording", "is_enabled", - "new_recording", "set_global_data_recording", "set_thread_local_data_recording", "start_web_viewer_server", @@ -564,72 +297,227 @@ def __post_init__(self) -> None: "new_entity_path", "thread_local_stream", "recording_stream_generator_ctx", + "MemoryRecording", + "BinaryStream", + "GrpcSink", + "FileSink", ], - class_list=["LoggingHandler", "MemoryRecording", "BinaryStream", "GrpcSink", "FileSink"], ), ] -def is_archetype_mentioned(thing: str) -> bool: - for section in SECTION_TABLE: - if section.class_list is not None: - if f"archetypes.{thing}" in section.class_list: - return True - return False +def public_surface(pkg: griffe.Module) -> set[str]: + """ + Return the set of names that `griffe.is_public` considers public. + Relies on the `griffe-public-redundant-aliases` extension to honor the + `from x import Foo as Foo` convention; combined with griffe's built-in + `__all__` handling and underscore-name filtering, this matches the + rerun codebase's public-API conventions. + """ + return {name for name, member in pkg.members.items() if member.is_public and not name.startswith("_")} -def is_submodule_mentioned(thing: str) -> bool: - if thing in EXCLUDE_SUBMODULE_CHECK: - return True - for section in SECTION_TABLE: - if section.mod_path is not None: - for mod_path in section.mod_path: - if thing == mod_path[len("rerun.") :]: - return True - return False +# --------------------------------------------------------------------------- +# Setup griffe loader and resolve documented packages. -# Virtual folder where we will generate the md files rerun_py_root = Path(__file__).parent.parent.resolve() sdk_root = Path(__file__).parent.parent.joinpath("rerun_sdk").resolve() -common_dir = Path("common") - -# Make sure all archetypes are included in the index: -for submodule in all_submodules(1, True): - assert is_submodule_mentioned(submodule), ( - f"Submodule '{submodule}' is not mentioned in the index of {__file__};" - " please add it to SECTION_TABLE for documentation, or prefix with underscore to hide it." - ) -for archetype in all_archetypes(): - assert is_archetype_mentioned(archetype), f"Archetype '{archetype}' is not mentioned in the index of {__file__}" +out_dir = Path() # generated pages live at the docs root -# We use griffe to access docstrings -# Lots of other potentially interesting stuff we could pull out in the future -# This is what mkdocstrings uses under the hood -search_paths = [path for path in sys.path if path] # eliminate empty path - -# This is where maturin puts rerun_bindings +search_paths = [path for path in sys.path if path] search_paths.insert(0, rerun_py_root.as_posix()) -# This is where the rerun package is search_paths.insert(0, sdk_root.as_posix()) -loader = griffe.GriffeLoader(search_paths=search_paths) - +# Load the same extension that mkdocs.yml configures for mkdocstrings, so this +# script and the rendered docs agree on what counts as a public symbol. +extensions = griffe.load_extensions("griffe_public_redundant_aliases") +loader = griffe.GriffeLoader(search_paths=search_paths, extensions=extensions) bindings_pkg = loader.load("rerun_bindings", find_stubs_package=True) rerun_pkg = loader.load("rerun") -# Create the nav for this section + +def griffe_module_for(pkg: str) -> griffe.Module: + """Return the griffe Module for a `DOCUMENTED_PACKAGES` entry.""" + if pkg == "rerun": + return rerun_pkg + assert pkg.startswith("rerun.") + return rerun_pkg[pkg[len("rerun.") :]] + + +def discover_subpackages_and_modules() -> set[str]: + """ + Return the dotted paths of every public subpackage/top-level module in `rerun_sdk/rerun/`. + + Includes every non-underscore subpackage at any depth (a directory with + `__init__.py`), and every non-underscore single-file module at the top + level only (e.g., `rerun.notebook`, `rerun.server`). + + Single-file `.py` modules nested *inside* subpackages are treated as + implementation detail and skipped — these are typically codegen output + (e.g., `rerun.archetypes.points3d` backing `rerun.archetypes.Points3D`) + that users are not expected to import directly. + """ + base = sdk_root.joinpath("rerun") + found = {"rerun"} + + for entry in base.iterdir(): + if entry.name.startswith("_") or entry.name.startswith("."): + continue + if entry.is_dir() and (entry / "__init__.py").exists(): + found.add(f"rerun.{entry.name}") + _walk_nested_subpackages(entry, f"rerun.{entry.name}", found) + elif entry.is_file() and entry.suffix == ".py" and entry.stem != "__init__": + found.add(f"rerun.{entry.stem}") + + return found + + +def _walk_nested_subpackages(pkg_dir: Path, dotted: str, found: set[str]) -> None: + """Recurse into `pkg_dir`, collecting nested subpackages (dirs with `__init__.py`).""" + for entry in pkg_dir.iterdir(): + if entry.name.startswith("_") or entry.name.startswith("."): + continue + if entry.is_dir() and (entry / "__init__.py").exists(): + child = f"{dotted}.{entry.name}" + found.add(child) + _walk_nested_subpackages(entry, child, found) + + +# --------------------------------------------------------------------------- +# Pre-emission validator: fail loud on stale config or new modules before +# any output is written, with friendlier messages than a raw KeyError mid-render. + + +def validate_config() -> None: + """ + Fail the build if any docs config has gone stale. + + Together these checks make it impossible to add (or rename, or remove) a + submodule without docs noticing. + """ + discovered = discover_subpackages_and_modules() + documented = set(DOCUMENTED_PACKAGES) + + stale = documented - discovered + if stale: + raise SystemExit( + f"DOCUMENTED_PACKAGES references modules that no longer exist on disk: " + f"{sorted(stale)}. Remove them from DOCUMENTED_PACKAGES.", + ) + + stale = EXCLUDED_FROM_TRACK_A - discovered + if stale: + raise SystemExit( + f"EXCLUDED_FROM_TRACK_A references modules that no longer exist on disk: " + f"{sorted(stale)}. Remove them from EXCLUDED_FROM_TRACK_A.", + ) + + unaccounted = discovered - documented - EXCLUDED_FROM_TRACK_A - {"rerun"} + if unaccounted: + raise SystemExit( + f"New subpackages/modules under `rerun.` are neither documented nor " + f"excluded: {sorted(unaccounted)}.\n" + f" - Add a row to DOCUMENTED_PACKAGES to give each its own Track A page, OR\n" + f" - Add to EXCLUDED_FROM_TRACK_A with an inline comment if its public\n" + f" symbols are re-exported elsewhere (typically flat into `rerun`).", + ) + + for pkg in DOCUMENTED_PACKAGES: + expected = public_surface(griffe_module_for(pkg)) + excludes = EXPLICIT_DOC_EXCLUDES.get(pkg, set()) + if not expected: + raise SystemExit( + f"`{pkg}` is in DOCUMENTED_PACKAGES but griffe sees no public symbols. " + f"Either add `__all__`, add public re-exports, or remove `{pkg}` from " + f"DOCUMENTED_PACKAGES.", + ) + if not (expected - excludes): + raise SystemExit( + f"All public symbols of `{pkg}` are in EXPLICIT_DOC_EXCLUDES; " + f"remove `{pkg}` from DOCUMENTED_PACKAGES or trim the excludes.", + ) + + for group in CURATED_GROUPS: + for item in group.items: + try: + _ = rerun_pkg[item] + except KeyError: + raise SystemExit( + f"Curated table '{group.title}' references unknown symbol '{item}'.", + ) from None + + +validate_config() + + +# --------------------------------------------------------------------------- +# Track A: emit per-package pages. + nav = mkdocs_gen_files.Nav() -nav["index"] = "index.md" +nav[("Overview",)] = "index.md" + -# This is the top-level index which will include a table-view of each sub-section -index_path = common_dir.joinpath("index.md") +def slug_for(pkg: str) -> str: + # The codegen in `re_types_builder` writes Python doc URLs as + # `ref.rerun.io/docs/python/stable/` (e.g. `/archetypes`, + # `/blueprint_views`) — i.e. without a leading `rerun_`. Match that here + # so the autogenerated links in `docs/content/reference/types/**` resolve. + if pkg == "rerun": + return "rerun.md" + return pkg.removeprefix("rerun.").replace(".", "_") + ".md" -def make_slug(s: str) -> str: - s = s.lower().strip() - s = re.sub(r"[\s]+", "_", s) - return s +for pkg, nav_path in DOCUMENTED_PACKAGES.items(): + excludes = EXPLICIT_DOC_EXCLUDES.get(pkg, set()) + members = sorted(public_surface(griffe_module_for(pkg)) - excludes) + + md_file = slug_for(pkg) + nav[nav_path] = md_file + + write_path = out_dir.joinpath(md_file) + with mkdocs_gen_files.open(write_path, "w") as fd: + fd.write(f"::: {pkg}\n") + fd.write(" options:\n") + fd.write(" show_root_heading: True\n") + fd.write(" heading_level: 3\n") + fd.write(" members_order: alphabetical\n") + fd.write(" members:\n") + for name in members: + fd.write(f" - {name}\n") + + +# --------------------------------------------------------------------------- +# Track B: emit landing page with static prefix, curated tables, static suffix. + +index_path = out_dir.joinpath("index.md") + + +def docstring_first_line(item: str) -> str: + """Return the first line of `rerun.`'s docstring, with bindings fallback.""" + obj = rerun_pkg[item] + if "rerun_bindings" in obj.canonical_path: + # The class is defined in the maturin extension; griffe sees the stub. + # Get the docstring from the bindings package instead. + obj = bindings_pkg[obj.canonical_path[len("rerun_bindings.") :]] + if obj.docstring is None: + raise SystemExit(f"No docstring for `rerun.{item}` (referenced from a curated table).") + return obj.docstring.lines[0] + + +def display_name(item: str) -> str: + """ + Compute the rendered name for a curated-table entry. + + Strip `archetypes.` / `components.` / `datatypes.` prefixes when the + symbol is also flat-re-exported into top-level `rerun`, so the table + shows `rerun.Points3D` rather than `rerun.archetypes.Points3D`. + """ + for prefix in ("archetypes.", "components.", "datatypes."): + stripped = item.removeprefix(prefix) + if stripped != item and stripped in rerun_pkg.members: + return f"rerun.{stripped}" + return f"rerun.{item}" with mkdocs_gen_files.open(index_path, "w") as index_file: @@ -655,12 +543,19 @@ def make_slug(s: str) -> str: | **Rerun Version** | **Release Date** | **Supported Python Version** | |-------------------|------------------|------------------------------| +| 0.34 | Jul. 6, 2026 | 3.10+ | +| 0.33 | May. 29, 2026 | 3.10+ | +| 0.32 | May. 13, 2026 | 3.10+ | +| 0.31 | Mar. 31, 2026 | 3.10+ | +| 0.30 | Feb. 25, 2026 | 3.10+ | +| 0.29 | Jan. 30, 2026 | 3.10+ | +| 0.28 | Dec. 18, 2025 | 3.10+ | | 0.27 | Nov. 10, 2025 | 3.10+ | | 0.26 | Oct. 13, 2025 | 3.9+ | | 0.25 | Sep. 16, 2025 | 3.9+ | | 0.24 | Jul. 17, 2025 | 3.9+ | | 0.23 | Apr. 24, 2025 | 3.9+ | -| 0.22 | Feb. 6, 2025 | 3.9+ | +| 0.22 | Feb. 6, 2025 | 3.9+ | | 0.21 | Dec. 18. 2024 | 3.9+ | | 0.20 | Nov. 14, 2024 | 3.9+ | | 0.19 | Oct. 17, 2024 | 3.8+ | @@ -670,92 +565,31 @@ def make_slug(s: str) -> str: """, ) - for section in SECTION_TABLE: - if section.gen_page: - # Turn the heading into a slug and add it to the nav - if section.sub_title: - md_name = make_slug("_".join([section.title, section.sub_title])) - md_file = md_name + ".md" - nav[(section.title, section.sub_title)] = md_file - else: - md_name = make_slug(section.title) - md_file = md_name + ".md" - nav[section.title] = md_file - - # Write out the contents of this section - write_path = common_dir.joinpath(md_file) - with mkdocs_gen_files.open(write_path, "w") as fd: - for mod_path in section.mod_path: - fd.write(f"::: {mod_path}\n") - fd.write(" options:\n") - fd.write(" show_root_heading: True\n") - fd.write(" heading_level: 3\n") - fd.write(" members_order: alphabetical\n") - # fd.write(" show_object_full_path: True\n") - if section.func_list or section.class_list: - fd.write(" members:\n") - for func_name in section.func_list or []: - fd.write(f" - {func_name}\n") - for class_name in section.class_list or []: - fd.write(f" - {class_name}\n") - if not section.default_filters: - fd.write(" filters: []\n") - if section.show_submodules: - fd.write(" show_submodules: True\n") - # Helpful for debugging - if 0: - with mkdocs_gen_files.open(write_path, "r") as fd: - print("FOR SECTION", section.title) - print(fd.read()) - print() - - # Write out a table for the section in the index_file - if section.show_tables: - index_file.write(f"### {section.title}\n") - if section.func_list: - index_file.write("Function | Description\n") - index_file.write("-------- | -----------\n") - for func_name in section.func_list: - # Check if any mod_path is not "rerun" to determine formatting - non_rerun_paths = [path for path in section.mod_path if path != "rerun"] - if non_rerun_paths: - # Use the first non-rerun path for formatting - mod_tail = non_rerun_paths[0].split(".")[1:] - func_name = ".".join([*mod_tail, func_name]) - func = rerun_pkg[func_name] - index_file.write(f"[`rerun.{func_name}()`][rerun.{func_name}] | {func.docstring.lines[0]}\n") - if section.class_list: - index_file.write("\n") - index_file.write("Class | Description\n") - index_file.write("-------- | -----------\n") - for class_name in section.class_list: - # Check if any mod_path is not "rerun" to determine formatting - non_rerun_paths = [path for path in section.mod_path if path != "rerun"] - if non_rerun_paths: - # Use the first non-rerun path for formatting - mod_tail = non_rerun_paths[0].split(".")[1:] - class_name = ".".join([*mod_tail, class_name]) - cls = rerun_pkg[class_name] - bindings_class = False - if "rerun_bindings" in cls.canonical_path: - bindings_class = True - # Get the docstring from the bindings package, but keep the rerun display path - cls = bindings_pkg[cls.canonical_path[len("rerun_bindings.") :]] - # Don't overwrite class_name - keep the rerun module path for display - show_class = class_name - for maybe_strip in ["archetypes.", "components.", "datatypes."]: - if class_name.startswith(maybe_strip): - stripped = class_name.replace(maybe_strip, "") - if stripped in rerun_pkg.classes: - show_class = stripped - # Always show as rerun.* in documentation, even for bindings classes - show_class = "rerun." + show_class - class_name = "rerun." + class_name - if cls.docstring is None: - raise ValueError(f"No docstring for class {class_name}") - index_file.write(f"[`{show_class}`][{class_name}] | {cls.docstring.lines[0]}\n") - - index_file.write("\n") + for group in CURATED_GROUPS: + index_file.write(f"### {group.title}\n") + + # `is_function` follows alias chains, so this works for redundant + # aliases as well as in-file definitions. + funcs = [item for item in group.items if rerun_pkg[item].is_function] + classes = [item for item in group.items if not rerun_pkg[item].is_function] + + if funcs: + index_file.write("Function | Description\n") + index_file.write("-------- | -----------\n") + for item in funcs: + index_file.write( + f"[`{display_name(item)}()`][rerun.{item}] | {docstring_first_line(item)}\n", + ) + index_file.write("\n") + + if classes: + index_file.write("Class | Description\n") + index_file.write("-------- | -----------\n") + for item in classes: + index_file.write( + f"[`{display_name(item)}`][rerun.{item}] | {docstring_first_line(item)}\n", + ) + index_file.write("\n") index_file.write( """ @@ -768,6 +602,7 @@ def make_slug(s: str) -> str: """, ) + # Generate the SUMMARY.txt file -with mkdocs_gen_files.open(common_dir.joinpath("SUMMARY.txt"), "w") as nav_file: +with mkdocs_gen_files.open(out_dir.joinpath("SUMMARY.txt"), "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/rerun_py/docs/templates/python/material/class.html b/rerun_py/docs/templates/python/material/class.html.jinja similarity index 100% rename from rerun_py/docs/templates/python/material/class.html rename to rerun_py/docs/templates/python/material/class.html.jinja diff --git a/rerun_py/docs/templates/python/material/function.html b/rerun_py/docs/templates/python/material/function.html deleted file mode 100644 index dabfa64bfafb..000000000000 --- a/rerun_py/docs/templates/python/material/function.html +++ /dev/null @@ -1,84 +0,0 @@ -{# Very minimally patched from: -https://github.com/mkdocstrings/python/blob/b0123719ae90cb2d47e9b923166ac69fdce86632/src/mkdocstrings_handlers/python/templates/material/_base/function.html - -See: CHANGE comments below -#} -{{ log.debug("Rendering " + function.path) }} - -
-{% with html_id = function.path %} - - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} - {% else %} - {% set show_full_path = config.show_object_full_path %} - {% endif %} - - {% if not root or config.show_root_heading %} - - {% filter heading(heading_level, - role="function", - id=html_id, - class="doc doc-heading", - toc_label=function.name ~ "()") %} - - {% if config.separate_signature %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% else %} - {% filter highlight(language="python", inline=True) %} - {# CHANGE: Insert def before function for better highlighting #} - def {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} - {% endif %} - - {% with labels = function.labels %} - {% include "labels.html" with context %} - {% endwith %} - - {% endfilter %} - - {% if config.separate_signature %} - {% filter highlight(language="python", inline=False) %} - {# CHANGE: Insert def before function for better highlighting #} - def {% filter format_signature(config.line_length) %} - {% if show_full_path %}{{ function.path }}{% else %}{{ function.name }}{% endif %} - {% include "signature.html" with context %} - {% endfilter %} - {% endfilter %} - {% endif %} - - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="function", - id=html_id, - toc_label=function.path if config.show_root_full_path else function.name, - hidden=True) %} - {% endfilter %} - {% endif %} - {% set heading_level = heading_level - 1 %} - {% endif %} - - {# CHANGE Relative to Upstream: don't apply the 'first' class since it causes worse CSS formatting for our #} - {# generated common API index since we inline bare functions that become root-level rather than modules. #} - {#
#} -
- {% with docstring_sections = function.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} - - {% if config.show_source and function.source %} -
- Source code in {{ function.relative_filepath }} - {{ function.source|highlight(language="python", linestart=function.lineno, linenums=True) }} -
- {% endif %} -
- -{% endwith %} -
diff --git a/rerun_py/docs/templates/python/material/function.html.jinja b/rerun_py/docs/templates/python/material/function.html.jinja new file mode 100644 index 000000000000..0aeb0daabbde --- /dev/null +++ b/rerun_py/docs/templates/python/material/function.html.jinja @@ -0,0 +1,142 @@ +{#- Minimally patched from upstream mkdocstrings-python: +material/_base/function.html.jinja + +Our deviations from upstream are marked with `CHANGE relative to upstream` +comments below: + - prefix each signature (and overload) with a `def ` keyword + - drop the `first` CSS class (better formatting for the common API index) +Re-derive from the installed upstream template when bumping mkdocstrings, +re-applying those changes. +-#} + +{% block logs scoped %} + {{ log.debug("Rendering " + function.path) }} +{% endblock logs %} + +{% import "language"|get_template as lang with context %} + +
+ {% with obj = function, html_id = function.path %} + + {% if root %} + {% set show_full_path = config.show_root_full_path %} + {% set root_members = True %} + {% elif root_members %} + {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} + {% set root_members = False %} + {% else %} + {% set show_full_path = config.show_object_full_path %} + {% endif %} + + {% set function_name = function.path if show_full_path else function.name %} + {% set symbol_type = "method" if function.parent.is_class else "function" %} + + {% if not root or config.show_root_heading %} + {% filter heading( + heading_level, + role="function", + id=html_id, + class="doc doc-heading", + toc_label=((' ')|safe if config.show_symbol_type_toc else '') + (config.toc_label if config.toc_label and root else function.name), + ) %} + + {% block heading scoped %} + {% if config.show_symbol_type_heading %}{% endif %} + {% if config.heading and root %} + {{ config.heading }} + {% elif config.separate_signature %} + {{ function_name }} + {% else %} + {%+ filter highlight(language="python", inline=True) -%} + {{ function_name }}{% include "signature"|get_template with context %} + {%- endfilter %} + {% endif %} + {% endblock heading %} + + {% block labels scoped %} + {% with labels = function.labels %} + {% include "labels"|get_template with context %} + {% endwith %} + {% endblock labels %} + + {% endfilter %} + + {% block signature scoped %} + {% if function.overloads and config.show_overloads %} + {# CHANGE relative to upstream: prefix each signature with a `def ` keyword. #} + {# `format_signature` deliberately omits it, but we prefer the fuller form. #} + {# It always emits the function name as the first `nf` span, so inject before that. #} +
+ {% for overload in function.overloads %} + {% set rendered_overload %} + {% filter format_signature(overload, config.line_length, annotations=True, crossrefs=config.signature_crossrefs) %} + {{ overload.name }} + {% endfilter %} + {% endset %} + {% autoescape false %} + {{ rendered_overload | replace('', 'def ', 1) }} + {% endautoescape %} + {% endfor %} +
+ {% endif %} + {% if config.separate_signature %} + {% set rendered_signature %} + {% filter format_signature(function, config.line_length, crossrefs=config.signature_crossrefs) %} + {{ function_name }} + {% endfilter %} + {% endset %} + {% autoescape false %} + {{ rendered_signature | replace('', 'def ', 1) }} + {% endautoescape %} + {% endif %} + {% endblock signature %} + + {% else %} + + {% if config.show_root_toc_entry %} + {% filter heading( + heading_level, + role="function", + id=html_id, + toc_label=((' ')|safe if config.show_symbol_type_toc else '') + (config.toc_label if config.toc_label and root else function.name), + hidden=True, + ) %} + {% endfilter %} + {% endif %} + {% set heading_level = heading_level - 1 %} + {% endif %} + + {# CHANGE relative to upstream: don't apply the 'first' class. It causes worse CSS #} + {# formatting for our generated common API index, since we inline bare functions #} + {# that become root-level rather than modules. #} +
+ {% block contents scoped %} + {% block docstring scoped %} + {% with docstring_sections = function.docstring.parsed %} + {% include "docstring"|get_template with context %} + {% endwith %} + {% endblock docstring %} + + {% if config.backlinks %} + + {% endif %} + + {% block source scoped %} + {% if config.show_source and function.source %} +
+ {{ lang.t("Source code in") }} + {%- if function.relative_filepath.is_absolute() -%} + {{ function.relative_package_filepath }} + {%- else -%} + {{ function.relative_filepath }} + {%- endif -%} + + {{ function.source|highlight(language="python", linestart=function.lineno or 0, linenums=True) }} +
+ {% endif %} + {% endblock source %} + {% endblock contents %} +
+ + {% endwith %} +
diff --git a/rerun_py/docs/writing_docs.md b/rerun_py/docs/writing_docs.md index 6739956a18d0..0837ad599100 100644 --- a/rerun_py/docs/writing_docs.md +++ b/rerun_py/docs/writing_docs.md @@ -2,6 +2,75 @@ A high-level overview of writing and previewing the Rerun Python documentation. +## How docs coverage works + +The set of symbols documented at `ref.rerun.io/docs/python/` is derived +automatically from the public-API conventions in the SDK source. There is no +hand-curated index to maintain in lockstep — adding a new public symbol via +the conventions below is sufficient to surface it in the docs. + +A symbol is considered public (and gets documented) when any of the following +applies in its module: + +- It is listed in the module's `__all__`. +- It is re-exported via `from ._x import Foo as Foo` (PEP 484 redundant alias). +- It is defined in-file with a non-underscore name and the module does not + define `__all__`. + +The conventions are detected by [`griffe`](https://mkdocstrings.github.io/griffe/) +plus the [`griffe-public-redundant-aliases`](https://mkdocstrings.github.io/griffe/extensions/official/public-redundant-aliases/) +extension (configured in `mkdocs.yml`). + +### Adding a new public symbol + +- **Re-export from a subpackage:** add `from ._impl import Foo as Foo` to the + relevant `__init__.py`. The redundant `as Foo` form matters — it is also + required by pyright's strict-mode `reportPrivateUsage` rule. +- **Define in-file in a single-file module:** include `"Foo"` in the module's + `__all__` (e.g., see `rerun_sdk/rerun/urdf.py`, `rerun_sdk/rerun/server.py`). +- **Stand up a Track A page for a brand-new subpackage:** add a row to + `DOCUMENTED_PACKAGES` in `docs/gen_common_index.py` mapping the dotted path + to its nav title (e.g., `"rerun.foo": ("Foo",)` for a top-level entry, or + `"rerun.bar.baz": ("Bar", "Baz")` to nest it under "Bar"). The first build + will tell you about every public symbol so you can decide what (if anything) + belongs in `EXPLICIT_DOC_EXCLUDES`. +- **Group symbols on the landing page:** add to `CURATED_GROUPS` in + `docs/gen_common_index.py`. Curated groups are tables only — they do not + gate coverage and may safely duplicate symbols already listed by Track A. + +### Hiding a public symbol from docs + +Add it (per package) to `EXPLICIT_DOC_EXCLUDES` in `docs/gen_common_index.py` +with an inline comment explaining why. Each entry is a deliberate decision; +unexplained entries get rejected in code review. + +### What the build validates + +`pixi run py-docs-build` fails (and CI fails) on any of: + +- A new top-level subpackage or module under `rerun_sdk/rerun/` — or a new + nested subpackage (a directory with `__init__.py`) under any documented + package — that is neither in `DOCUMENTED_PACKAGES` nor in + `EXCLUDED_FROM_TRACK_A`. This is the freshness check that prevents new + modules from going silently undocumented. +- A `DOCUMENTED_PACKAGES` or `EXCLUDED_FROM_TRACK_A` entry that no longer + exists on disk (catches renames and removals). +- A `DOCUMENTED_PACKAGES` entry whose module has no public surface + (no `__all__`, no redundant aliases, no in-file definitions). +- A `DOCUMENTED_PACKAGES` entry whose entire public surface is in + `EXPLICIT_DOC_EXCLUDES` (the page would emit `members: []`, which is + undefined in mkdocstrings). +- A `CURATED_GROUPS` entry that references a symbol that doesn't exist — + catches stale entries when symbols are renamed or removed. + +### Known limitations + +- **PEP 562 `__getattr__` aliases** (used for deprecated re-exports) are + invisible to static analysis. To document such a name, expose it via a + real redundant-alias re-export and accept the validator's prompt. +- **Dynamic `__all__` constructions** (e.g., `__all__ = list(SOMETHING)`) + are not supported; keep `__all__` a static list/tuple of string constants. + ## Getting started with docs ### Serving the docs locally diff --git a/rerun_py/mkdocs.yml b/rerun_py/mkdocs.yml index 5ddae4cf6a23..bd84974ade79 100644 --- a/rerun_py/mkdocs.yml +++ b/rerun_py/mkdocs.yml @@ -5,6 +5,15 @@ site_name: Rerun Python APIs site_url: https://ref.rerun.io/docs/python/ repo_url: https://github.com/rerun-io/rerun/ +# Files in `docs/` that are contributor-facing source materials and should +# not ship as part of the published site (mkdocs copies non-`.md` files +# verbatim by default). +exclude_docs: | + writing_docs.md + gen_common_index.py + SUMMARY.txt + templates/ + # Use the material theme # Override some options for nav: https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/ theme: @@ -12,8 +21,6 @@ theme: features: - navigation.indexes - navigation.instant - - navigation.tabs - - navigation.tabs.sticky - navigation.tracking - search.share @@ -30,7 +37,7 @@ plugins: handlers: python: paths: ["rerun_sdk", "."] # Lookup python modules relative to this path - import: # Cross-references for python and numpy + inventories: # Cross-references for python and numpy - https://arrow.apache.org/docs/objects.inv - https://docs.python.org/3.10/objects.inv - https://numpy.org/doc/stable/objects.inv @@ -52,9 +59,12 @@ plugins: inherited_members: true members_order: source # The order of class members merge_init_into_class: false # Not compatible with `inherited_members` + separate_signature: true # Render signature via griffe (needed for signature_crossrefs) + show_signature_annotations: true # Show type annotations (incl. return types) in the signature show_if_no_docstring: false # We intentionally hide archetype fields show_source: no - load_external_modules: true + extra: + load_external_modules: true preload_modules: - rerun_bindings annotations_path: brief @@ -62,15 +72,42 @@ plugins: find_stubs_package: true extensions: - griffe_warnings_deprecated + - griffe_public_redundant_aliases - gen-files: # https://oprypin.github.io/mkdocs-gen-files scripts: - docs/gen_common_index.py - literate-nav: # https://oprypin.github.io/mkdocs-literate-nav nav_file: SUMMARY.txt + # Keep legacy `common/` URLs working. We dropped the `common/` URL + # prefix and collapsed the old function-category pages + # (initialization_functions, logging_functions, etc.) into the flat + # `rerun` page. These stubs preserve the old URLs for inbound links + # (docs, examples, blog posts, external sites). - redirects: # https://github.com/mkdocs/mkdocs-redirects redirect_maps: - "index.md": "common/index.md" + common/index.md: index.md + common/archetypes.md: archetypes.md + common/components.md: components.md + common/datatypes.md: datatypes.md + common/catalog.md: catalog.md + common/utilities.md: utilities.md + common/server.md: server.md + common/notebook.md: notebook.md + common/authentication.md: auth.md + common/urdf_support.md: urdf.md + common/blueprint_apis.md: blueprint.md + common/blueprint_archetypes.md: blueprint_archetypes.md + common/blueprint_components.md: blueprint_components.md + common/blueprint_datatypes.md: blueprint_datatypes.md + common/blueprint_views.md: blueprint_views.md + common/initialization_functions.md: rerun.md + common/logging_functions.md: rerun.md + common/property_functions.md: rerun.md + common/columnar_api.md: rerun.md + common/script_helpers.md: rerun.md + common/other_classes_and_functions.md: rerun.md + common/custom_data.md: rerun.md # https://www.mkdocs.org/user-guide/configuration/#markdown_extensions # https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/ diff --git a/rerun_py/pyproject.toml b/rerun_py/pyproject.toml index 7119f38fba40..027e45de8edd 100644 --- a/rerun_py/pyproject.toml +++ b/rerun_py/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "attrs>=23.1.0", "numpy>=2", "pillow>=8.0.0", # Used for JPEG encoding. 8.0.0 added the `format` arguments to `Image.open` + "psutil>=7.0", # Used by `tracing_session()` to capture per-session CPU and network metrics. "pyarrow>=18.0.0", "typing_extensions>=4.5", ] @@ -42,33 +43,38 @@ tests = [ "opencv-python>4.6", "pandas>=2", "polars==1.36.1", - "pytest==8.4.2", + "pytest==9.0.3", "semver>=3.0,<3.1", "syrupy==5.0.0", "tomli==2.0.1", "torch>=2.5", # Needs numpy 2 support "torchvision", # Imported at top of `rerun.experimental.dataloader._decoders`, so needed to import the dataloader module at all - "datafusion==52.3.0", + "datafusion==53.0.0", ] -notebook = ["rerun-notebook==0.32.0-alpha.1"] +notebook = ["rerun-notebook==0.35.0"] # TODO(RR-3786): when pyarrow is fixed, we should remove the pandas dependency -datafusion = ["datafusion==52.3.0", "pandas>=2"] # deprecated, replaced by `dataplatform` (NOLINT) -dataplatform = [ # NOLINT - "datafusion==52.3.0", +datafusion = ["datafusion==53.0.0", "pandas>=2"] # deprecated, replaced by `catalog` +dataplatform = [ # deprecated, replaced by `catalog` (NOLINT) + "datafusion==53.0.0", + "pandas>=2", +] +catalog = [ + "datafusion==53.0.0", "pandas>=2", ] # pandas is needed so pyarrow properly converts nanoseconds (RR-3532) -# PyTorch `Dataset` for the Rerun Data Platform. +# PyTorch `Dataset` for a catalog server. dataloader = ["torch>=2.5", "torchvision", "av", "pillow>=8.0.0"] -# Client-side OpenTelemetry export, enabled by `TELEMETRY_ENABLED=true OTEL_SDK_ENABLED=true`. +# Client-side OpenTelemetry export, enabled by setting `TELEMETRY_ENABLED=true` +# along with an OTLP endpoint env var (e.g. `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=...`). # Only needed for developers profiling the SDK — see `rerun._tracing`. tracing = ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-grpc"] # Note: We avoid self-referential extras like "rerun-sdk[notebook]" because they cause # uv to fetch rerun-sdk from PyPI when the local package isn't built yet. # TODO(RR-3786): when pyarrow is fixed, we should remove the pandas dependency -all = ["datafusion==52.3.0", "pandas>=2", "rerun-notebook==0.32.0-alpha.1"] +all = ["datafusion==53.0.0", "pandas>=2", "rerun-notebook==0.35.0"] [project.urls] documentation = "https://www.rerun.io/docs" @@ -247,10 +253,15 @@ known-first-party = ["rerun_bindings"] # with the other `rerun` pypi package. The rerun_sdk.pth adds this to the pythonpath # which then allows `import rerun` to work as expected. # See https://github.com/rerun-io/rerun/pull/1085 for more details -# Even though both `rerun` and `rerun.exe` are here, only one will be included since -# they both should not be fetched in CI when running the build. +# Each platform fetches only one of these — `rerun` (Linux), `rerun.exe` (Windows), +# or the `Rerun.app` bundle (macOS — see scripts/ci/bundle_macos_app.py). # Files missing from this list is not a packaging failure. -include = ["rerun_sdk.pth", "rerun_sdk/rerun_cli/rerun", "rerun_sdk/rerun_cli/rerun.exe"] +include = [ + "rerun_sdk.pth", + "rerun_sdk/rerun_cli/rerun", + "rerun_sdk/rerun_cli/rerun.exe", + "rerun_sdk/rerun_cli/Rerun.app/**/*", +] locked = true name = "rerun_bindings" python-packages = ["rerun_sdk/rerun", "rerun_sdk/rerun_cli"] diff --git a/rerun_py/rerun_bindings/__init__.py b/rerun_py/rerun_bindings/__init__.py index f8704436b8d5..312676eaf101 100644 --- a/rerun_py/rerun_bindings/__init__.py +++ b/rerun_py/rerun_bindings/__init__.py @@ -4,8 +4,16 @@ # Private classes don't automatically get re-exported from .rerun_bindings import ( + _dec_active_tracing_sessions as _dec_active_tracing_sessions, _get_trace_context_var as _get_trace_context_var, + _get_tracing_session_var as _get_tracing_session_var, + _inc_active_tracing_sessions as _inc_active_tracing_sessions, _IndexValuesLikeInternal as _IndexValuesLikeInternal, + _is_telemetry_active as _is_telemetry_active, + _log_tracing_session_finished as _log_tracing_session_finished, + _log_tracing_session_started as _log_tracing_session_started, + _new_metrics_collector as _new_metrics_collector, + _optimization_profile_values as _optimization_profile_values, _ServerInternal as _ServerInternal, _UrdfJointInternal as _UrdfJointInternal, _UrdfLinkInternal as _UrdfLinkInternal, diff --git a/rerun_py/rerun_bindings/rerun_bindings.pyi b/rerun_py/rerun_bindings/rerun_bindings.pyi index 88e149e4e29b..4e46f98c50c8 100644 --- a/rerun_py/rerun_bindings/rerun_bindings.pyi +++ b/rerun_py/rerun_bindings/rerun_bindings.pyi @@ -1,26 +1,25 @@ from __future__ import annotations import os -from collections.abc import Callable, Iterator, Mapping +from collections.abc import Callable, Iterable, Iterator from datetime import datetime, timedelta -from enum import Enum from pathlib import Path -from typing import Any +from typing import Any, Literal import datafusion as dfn import numpy as np import numpy.typing as npt import pyarrow as pa -from typing_extensions import deprecated from .types import ( IndexValuesLike as IndexValuesLike, - VectorDistanceMetricLike as VectorDistanceMetricLike, ) # NOTE # # The pure Python wrapper/internal pyo3 object is documented in `rerun_py/ARCHITECTURE.md`. +# +# Refrain from adding doc strings for APIs that is wrapped on the Python side as they are unchecked and just add duplication. class IndexColumnDescriptor: """ @@ -172,14 +171,6 @@ class ComponentColumnSelector: This property is read-only. """ -class VectorDistanceMetric(Enum): # type: ignore[misc] - """Which distance metric for use for vector index.""" - - L2: VectorDistanceMetric - COSINE: VectorDistanceMetric - DOT: VectorDistanceMetric - HAMMING: VectorDistanceMetric - class SchemaInternal: def index_columns(self) -> list[IndexColumnDescriptor]: ... def component_columns(self) -> list[ComponentColumnDescriptor]: ... @@ -189,17 +180,6 @@ class SchemaInternal: ) -> ComponentColumnDescriptor: ... def __arrow_c_schema__(self) -> Any: ... -class RecordingInternal: - def schema(self) -> SchemaInternal: ... - def recording_id(self) -> str: ... - def application_id(self) -> str: ... - def chunks(self) -> ChunkIterator: ... - def save(self, path: str) -> None: ... - -class RRDArchiveInternal: - def num_recordings(self) -> int: ... - def all_recordings(self) -> list[RecordingInternal]: ... - class ChunkInternal: @property def id(self) -> str: ... @@ -216,41 +196,37 @@ class ChunkInternal: @property def timeline_names(self) -> list[str]: ... def to_record_batch(self) -> pa.RecordBatch: ... + def with_entity_path(self, entity_path: str) -> ChunkInternal: ... @staticmethod - def from_record_batch(record_batch: pa.RecordBatch) -> ChunkInternal: ... + def from_record_batch( + record_batch: pa.RecordBatch, + index_mode: str, + index_columns: list[str], + entity_path: str | None, + ) -> list[ChunkInternal]: ... @staticmethod def from_columns( entity_path: str, timelines: dict[str, Any], components: dict[ComponentDescriptor, Any], ) -> ChunkInternal: ... - def format(self, *, width: int = 240, redact: bool = False) -> str: ... + def format(self, *, width: int, redact: bool, trim_metadata_keys: bool) -> str: ... def apply_lenses(self, lenses: list[LensInternal]) -> list[ChunkInternal]: ... def apply_selector(self, source: str, selector: SelectorInternal) -> ChunkInternal: ... def __repr__(self) -> str: ... def __len__(self) -> int: ... -class ChunkIterator: - """An iterator over chunks in a recording.""" - - def __iter__(self) -> ChunkIterator: - """Implement iter(self).""" - - def __next__(self) -> ChunkInternal: - """Implement next(self).""" - -def recording_from_chunks( - chunks: Any, - application_id: str, - recording_id: str, -) -> RecordingInternal: - """Create a new recording from an iterable of chunks.""" +def _optimization_profile_values(name: str) -> dict[str, object]: + """ + Test-only: return a dict of the Rust `OptimizationProfile::` field values. -def load_recording(path_to_rrd: str | os.PathLike[str]) -> RecordingInternal: - """Load a single recording from an RRD file.""" + Used by the Python parity test to confirm that + `OptimizationProfile.{LIVE,OBJECT_STORE}` on the Python side stays in sync + with the Rust constants this module forwards into `ChunkStoreConfig` / + `CompactionOptions` above. -def load_archive(path_to_rrd: str | os.PathLike[str]) -> RRDArchiveInternal: - """Load a rerun archive from an RRD file.""" + Names: `"LIVE"`, `"OBJECT_STORE"`. + """ # AI generated stubs for `PyRecordingStream` related class and functions # TODO(#9187): this will be entirely replaced when `RecordingStream` is itself written in Rust @@ -373,8 +349,18 @@ class ChunkBatcherConfig: """Low-latency configuration, preferred when streaming directly to a viewer.""" @staticmethod - def ALWAYS() -> ChunkBatcherConfig: - """Always flushes ASAP.""" + def ALWAYS_TEST_ONLY() -> ChunkBatcherConfig: + """ + Always flushes ASAP. + + !!! warning + Test-only configuration. Produces an unrealistically large number of chunks and is + not suitable for production workloads. With a file sink in particular, per-chunk + metadata is accumulated in memory until the SDK process ends and the file footer + can be written, which can drive memory usage through the roof. Use + [`LOW_LATENCY`][rerun_bindings.ChunkBatcherConfig.LOW_LATENCY] instead for fast + flushing in real applications. + """ @staticmethod def NEVER() -> ChunkBatcherConfig: @@ -480,7 +466,8 @@ def spawn( executable_path: str | None = None, extra_args: list[str] = ..., extra_env: list[tuple[str, str]] = ..., -) -> None: + headless: bool = False, +) -> int | None: """Spawn a new viewer.""" # @@ -650,7 +637,7 @@ class FileSink: Save the recording stream to a file. """ - def __init__(self, path: str | os.PathLike[str]) -> None: + def __init__(self, path: str | os.PathLike[str], *, write_footer: bool = True) -> None: """ Initialize a file sink. @@ -658,6 +645,17 @@ class FileSink: ---------- path: Path to write to. The file will be overwritten. + write_footer: + Whether to emit a complete RRD footer (including a manifest of every chunk) at the + end of the stream. Defaults to `True`. + + Producing a footer keeps per-chunk metadata in memory for the lifetime of the sink, + which grows linearly with the number of chunks logged. Pass `write_footer=False` for + long-running streaming sessions; the resulting file is still a valid RRD and a + footer can be added after the fact via `rerun rrd optimize`. + + *Warning*: lack of footer will significantly hurt random-access performance and some + tools (e.g. LazyStore) may not work properly. """ @@ -687,6 +685,8 @@ def save( path: str, default_blueprint: PyMemorySinkStorage | None = None, recording: PyRecordingStream | None = None, + *, + write_footer: bool = True, ) -> None: """Save the recording stream to a file.""" @@ -696,6 +696,8 @@ def save_blueprint(path: str, blueprint_stream: PyRecordingStream) -> None: def stdout( default_blueprint: PyMemorySinkStorage | None = None, recording: PyRecordingStream | None = None, + *, + write_footer: bool = True, ) -> None: """Save to stdout.""" @@ -755,6 +757,18 @@ def disconnect(recording: PyRecordingStream | None = None) -> None: Subsequent log messages will be buffered and either sent on the next call to `connect_grpc` or `spawn`. """ +def finalize_deferred_sinks(recording: PyRecordingStream | None = None) -> None: + """ + Finalize any deferred-finalization sinks (i.e. file-like sinks that write a footer at the end). + + For a bare `FileSink` this is equivalent to `disconnect()`. For a `MultiSink` containing both + streaming and file-like children, only the file-like children are dropped — the streaming + children stay live. For all other sinks this is a no-op. + + Used by `RecordingStream.__exit__` so that file-backed recordings are consumable as soon as + the `with`-block exits, without waiting for `__del__` / GC. + """ + def flush(*, timeout_sec: float = 1e38, recording: PyRecordingStream | None = None) -> None: """Block until outstanding data has been flushed to the sink.""" @@ -804,6 +818,12 @@ def disable_timeline( def reset_time(recording: PyRecordingStream | None = None) -> None: """Clear all timeline information on this thread.""" +def set_log_tick_enabled(enabled: bool, recording: PyRecordingStream | None = None) -> None: + """Enable or disable automatic injection of the `log_tick` timeline (disabled by default).""" + +def set_log_time_enabled(enabled: bool, recording: PyRecordingStream | None = None) -> None: + """Enable or disable automatic injection of the `log_time` timeline (enabled by default).""" + # # log any # @@ -835,11 +855,16 @@ def send_arrow_chunk( A dictionary mapping component types to their values. """ -def send_chunk( - chunk: ChunkInternal, +def send_chunks( + chunks: ChunkInternal | Iterable[ChunkInternal], recording: PyRecordingStream | None = None, ) -> None: - """Send a pre-built chunk to the recording stream.""" + """ + Send chunks to the recording stream. + + Accepts a single chunk or any iterable of chunks. Blocks until every chunk + has been pushed to the recording's batcher. + """ def log_file_from_path( file_path: str | os.PathLike[str], @@ -866,14 +891,6 @@ def send_blueprint( ) -> None: """Send a blueprint to the given recording stream.""" -def send_recording(rrd: RecordingInternal, recording: PyRecordingStream | None = None) -> None: - """ - Send all chunks from a [`PyRecording`] to the given recording stream. - - !!! Warning - ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ - """ - # # misc # @@ -914,6 +931,17 @@ def asset_video_read_frame_timestamps_nanos(video_bytes_arrow_array: Any, media_ So instead, we pass the arrow array directly. """ +def video_detect_gop_start(sample: bytes, codec_fourcc: int) -> bool: + """ + Detect whether a video sample starts a group of pictures, i.e. is a keyframe. + + H.264/H.265 samples must be in Annex B format. + `codec_fourcc` is a `rerun.components.VideoCodec` enum value. + """ + +def video_length_prefixed_to_annex_b(sample: bytes, length_prefix_size: int = 4) -> bytes: + """Convert a length-prefixed (AVCC-style) NAL unit sample to Annex B (start-code-prefixed).""" + ##################################################################################################################### ## CATALOG ## ##################################################################################################################### @@ -938,6 +966,7 @@ class EntryKind: TABLE: EntryKind TABLE_VIEW: EntryKind BLUEPRINT_DATASET: EntryKind + ASSET_DATASET: EntryKind def __str__(self, /) -> str: """Return str(self).""" @@ -973,8 +1002,12 @@ class DatasetEntryInternal: # --- def blueprint_dataset(self) -> DatasetEntryInternal | None: ... + def asset_dataset(self) -> DatasetEntryInternal | None: ... + def _ensure_asset_dataset(self) -> None: ... def default_blueprint_segment_id(self) -> str | None: ... def set_default_blueprint_segment_id(self, segment_id: str | None) -> None: ... + def default_segment_table_blueprint_segment_id(self) -> str | None: ... + def set_default_segment_table_blueprint_segment_id(self, segment_id: str | None) -> None: ... # --- @@ -985,8 +1018,8 @@ class DatasetEntryInternal: self, segment_id: str, timeline: str | None = None, - start: datetime | int | None = None, - end: datetime | int | None = None, + start: datetime | timedelta | int | None = None, + end: datetime | timedelta | int | None = None, ) -> str: ... # --- @@ -1008,59 +1041,11 @@ class DatasetEntryInternal: segments_to_drop: list[str], layers_to_drop: list[str], force: bool = False, - ) -> None: ... + ) -> UnregistrationHandleInternal: ... # --- - def download_segment(self, segment_id: str) -> RecordingInternal: ... - - # --- - - @deprecated( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def create_fts_search_index( - self, - *, - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - time_index: IndexColumnSelector, - store_position: bool = False, - base_tokenizer: str = "simple", - ) -> None: ... - @deprecated( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def create_vector_search_index( - self, - *, - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - time_index: IndexColumnSelector, - target_partition_num_rows: int | None = None, - num_sub_vectors: int = 16, - distance_metric: VectorDistanceMetric | str = ..., - ) -> IndexingResult: ... - def list_search_indexes(self) -> list[IndexingResult]: ... - def delete_search_indexes( - self, - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - ) -> list[IndexConfig]: ... - @deprecated( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def search_fts( - self, - query: str, - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - ) -> dfn.DataFrame: ... - @deprecated( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def search_vector( - self, - query: Any, # VectorLike - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - top_k: int, - ) -> dfn.DataFrame: ... + def segment_store(self, segment_id: str) -> LazyStoreInternal: ... # --- @@ -1114,6 +1099,12 @@ class TableEntryInternal: # --- + def blueprint_dataset(self) -> DatasetEntryInternal: ... + def default_blueprint_segment_id(self) -> str | None: ... + def set_default_blueprint_segment_id(self, segment_id: str | None) -> None: ... + + # --- + def __datafusion_table_provider__(self, session: Any) -> Any: ... def reader(self) -> dfn.DataFrame: ... def to_arrow_reader(self) -> pa.RecordBatchReader: ... @@ -1159,6 +1150,13 @@ class _UrdfTreeInternal: def get_visual_geometry_paths(self, link: str | _UrdfLinkInternal) -> list[str]: ... def log(self, recording: PyRecordingStream | None = None) -> None: ... def stream(self, *, include_joint_transforms: bool = True) -> LazyChunkStreamInternal: ... + def compute_joint_transform_batches( + self, + names: pa.Array, + values: pa.Array, + *, + clamp: bool = False, + ) -> pa.Array: ... class _UrdfJointInternal: """Internal Rust representation of a URDF joint.""" @@ -1239,52 +1237,10 @@ class _IndexValuesLikeInternal: def to_index_values(self) -> npt.NDArray[np.int64]: ... def len(self) -> int: ... -class IndexProperties: - """The properties and configuration of a user-defined index.""" - -class IndexConfig: - """The complete description of a user-defined index.""" - - @property - def time_column(self) -> IndexColumnSelector: - """Returns the time column that this index applies to.""" - - @property - def component_column(self) -> ComponentColumnSelector: - """Returns the component column that this index applies to.""" - - @property - def properties(self) -> IndexProperties: - """Returns the properties/configuration of the index.""" - -class IndexingResult: - """Indexing operation status result.""" +class TableProviderAdapterInternal: + """Internal opaque adapter exposing a Rust DataFusion `TableProvider` to Python via the FFI capsule protocol.""" - @property - def properties(self) -> IndexConfig: - """Returns configuration information and properties about the newly created index.""" - - @property - def column(self) -> ComponentColumnSelector: - """Returns the component column that this index was created on.""" - - @property - def statistics(self) -> str: - """Returns best-effort backend-specific statistics about the newly created index.""" - - def debug_info(self) -> dict[str, Any] | None: - """ - Get debug information about the indexing operation. - - The exact contents of debug information may vary depending on the indexing operation performed - and the server implementation. - - Returns - ------- - Optional[dict] - A dictionary containing debug information, or `None` if no debug information is available - - """ + def __datafusion_table_provider__(self, session: Any) -> Any: ... class CatalogClientInternal: def __init__(self, url: str, token: str | None = None) -> None: ... @@ -1302,6 +1258,8 @@ class CatalogClientInternal: # --- def version_info(self) -> tuple[str, str | None, str | None]: ... + def rtt_seconds(self, num_pings: int) -> float: ... + def bandwidth_bytes_per_sec(self, num_bytes: int, rtt_seconds: float) -> float | None: ... def datasets(self, include_hidden: bool) -> list[DatasetEntryInternal]: ... def tables(self, include_hidden: bool) -> list[TableEntryInternal]: ... @@ -1330,6 +1288,10 @@ class RegistrationHandleInternal: def wait(self, timeout_secs: int | None = None) -> list[str]: ... def cancel(self) -> None: ... +class UnregistrationHandleInternal: + def wait(self, timeout_secs: int | None = None) -> None: ... + def cancel(self) -> None: ... + ##################################################################################################################### ## VIEWER_CLIENT ## ##################################################################################################################### @@ -1352,23 +1314,37 @@ class SelectorInternal: def execute(self, source: pa.Array) -> pa.Array | None: ... def execute_per_row(self, source: pa.Array) -> pa.Array | None: ... def pipe(self, func: Any) -> SelectorInternal: ... + def try_to_string(self) -> str | None: ... def __repr__(self) -> str: ... def __str__(self) -> str: ... -class LensOutputInternal: - def __init__(self) -> None: ... - def to_component(self, component: ComponentDescriptor, selector: SelectorInternal) -> LensOutputInternal: ... - def to_timeline(self, timeline_name: str, timeline_type: str, selector: SelectorInternal) -> LensOutputInternal: ... +class DeriveLensInternal: + def __init__( + self, + input_component: str, + *, + output_entity: str | None = None, + scatter: bool = False, + ) -> None: ... + def to_component( + self, + component: ComponentDescriptor, + selector: SelectorInternal, + cast_to: pa.DataType | Literal["auto"] | None = None, + ) -> DeriveLensInternal: ... + def to_timeline(self, timeline_name: str, timeline_type: str, selector: SelectorInternal) -> DeriveLensInternal: ... -class LensInternal: +class MutateLensInternal: def __init__( self, input_component: str, - output: LensOutputInternal | None = None, + selector: SelectorInternal, *, - to_entity: Mapping[str, LensOutputInternal] | None = None, + keep_row_ids: bool = False, ) -> None: ... +LensInternal = DeriveLensInternal | MutateLensInternal + class _ServerInternal: def __init__( self, @@ -1480,6 +1456,50 @@ def _get_trace_context_var() -> Any: Returns `None` when `perf_telemetry` is disabled. """ +def _get_tracing_session_var() -> Any: + """ + Return the `ContextVar` carrying the active rerun session id. + + Set by the `tracing_session()` context manager and read on every outbound + gRPC call to merge `rerun_session_id=` into the W3C `tracestate` header. + + Returns `None` when `perf_telemetry` is disabled. + """ + +def _is_telemetry_active() -> bool: + """ + Return `True` if the rerun telemetry stack initialized successfully. + + `tracing_session()` requires this to be true; otherwise the W3C propagator + is not registered and the session id has no transport. + """ + +def _inc_active_tracing_sessions() -> None: + """Increment the process-wide active-tracing-session gate. Called by `tracing_session().__enter__`.""" + +def _dec_active_tracing_sessions() -> None: + """Decrement the process-wide active-tracing-session gate. Called by `tracing_session().__exit__`.""" + +def _log_tracing_session_started(rerun_session_id: str) -> None: + """Emit `rerun tracing session started: ` through the Rust `tracing` stack at INFO level.""" + +def _log_tracing_session_finished( + rerun_session_id: str, + elapsed_s: float, + cpu_user_s: float | None, + cpu_system_s: float | None, + cpu_iowait_s: float | None, + net_rx_mb: float | None, +) -> None: + """ + Emit a single structured INFO event summarizing the tracing session at scope exit. + + `Option` fields are `None` when the host platform or runtime can't supply + the metric (psutil missing, or `iowait` unavailable on macOS/Windows). Routed + through the Rust `tracing` stack so it follows `RUST_LOG` and the fmt-layer + pipeline like `_log_tracing_session_started`. + """ + ##################################################################################################################### ## PIPELINE APIS ## ##################################################################################################################### @@ -1493,17 +1513,44 @@ class ChunkStoreInternal: def num_chunks(self) -> int: ... def summary(self) -> str: ... def stream(self) -> LazyChunkStreamInternal: ... + def reader( + self, + *, + index: str | None, + contents: list[str] | None, + include_semantically_empty_columns: bool, + include_tombstone_columns: bool, + fill_latest_at: bool, + using_index_values: IndexValuesLike | None, + ) -> TableProviderAdapterInternal: ... + +class LazyStoreInternal: + """Internal implementation. Use LazyStore from rerun.experimental instead.""" + + def schema(self) -> SchemaInternal: ... + def num_chunks(self) -> int: ... + def summary(self) -> str: ... + def stream(self) -> LazyChunkStreamInternal: ... + @property + def _chunks_loaded(self) -> int: ... + +class StoreEntryInternal: + """Internal implementation. Use StoreEntry from rerun.experimental instead.""" + + @property + def kind(self) -> Literal["recording", "blueprint"]: ... + @property + def application_id(self) -> str: ... + @property + def recording_id(self) -> str: ... class RrdReaderInternal: """Internal implementation. Use RrdReader from rerun.experimental instead.""" def __init__(self, path: str) -> None: ... - def stream(self) -> LazyChunkStreamInternal: ... - def store(self) -> ChunkStoreInternal: ... - @property - def application_id(self) -> str | None: ... - @property - def recording_id(self) -> str | None: ... + def store_entries(self) -> list[StoreEntryInternal]: ... + def stream(self, store: StoreEntryInternal | None = None) -> LazyChunkStreamInternal: ... + def store(self, store: StoreEntryInternal | None = None) -> LazyStoreInternal: ... @property def path(self) -> Path: ... @@ -1518,13 +1565,69 @@ class McapReaderInternal: decoders: list[str] | None, include_topic_regex: list[str] | None, exclude_topic_regex: list[str] | None, + start_time_ns: int | None, + end_time_ns: int | None, + recover: bool, ) -> None: ... - def stream(self) -> LazyChunkStreamInternal: ... + def stream( + self, + *, + start_time_ns: int | None = None, + end_time_ns: int | None = None, + ) -> LazyChunkStreamInternal: ... + def time_bounds(self) -> tuple[int, int]: ... @property def path(self) -> Path: ... @staticmethod def available_decoders() -> list[str]: ... +class Mp4TranscodeOptionsInternal: + """Internal implementation. Use Mp4TranscodeOptions from rerun.experimental instead.""" + + def __init__( + self, + gop_size: int | None = None, + output_codec: int | None = None, + try_gpu: bool = False, + ffmpeg_override: Path | None = None, + ) -> None: ... + +class Mp4ReaderInternal: + """Internal implementation. Use Mp4Reader from rerun.experimental instead.""" + + def __init__( + self, + path: Path, + mode: Literal["asset", "stream"] = "stream", + chunk_by_gop: bool = True, + timeline_name: str = "video", + timeline_type: Literal["duration", "timestamp"] = "duration", + transcode: Mp4TranscodeOptionsInternal | None = None, + entity_path: str | None = None, + ) -> None: ... + def stream(self) -> LazyChunkStreamInternal: ... + @property + def path(self) -> Path: ... + @property + def entity_path(self) -> str: ... + +class Hdf5ReaderInternal: + """Internal implementation. Use Hdf5Reader from rerun.experimental instead.""" + + def __init__(self, path: str) -> None: ... + def stream( + self, + entity_path_prefix: str | None = None, + index_column: tuple[str, str, str | None] | None = None, + ignore_datasets: list[str] | None = None, + use_structs: bool = True, + ) -> LazyChunkStreamInternal: ... + def groups(self, path: str = "/") -> list[str]: ... + def datasets(self, path: str = "/") -> list[tuple[str, list[int], str]]: ... + def attributes(self, path: str = "/") -> dict[str, int | float | str | bytes | list[int | float | str]]: ... + @property + def path(self) -> Path: ... + class ParquetReaderInternal: """Internal implementation. Use ParquetReader from rerun.experimental instead.""" @@ -1538,7 +1641,6 @@ class ParquetReaderInternal: use_structs: bool = True, static_columns: list[str] | None = None, index_columns: list[tuple[str, str, str | None]] | None = None, - column_rules: list[Any] | None = None, ) -> None: ... def stream(self) -> LazyChunkStreamInternal: ... @property @@ -1591,12 +1693,29 @@ class LazyChunkStreamInternal: extra_passes: int = 0, gop_batching: bool = False, split_size_ratio: float | None = None, + fix_keyframe: bool = False, ) -> ChunkStoreInternal: - """Consume the stream and materialize all chunks into a ChunkStore.""" + """ + Run the pipeline and materialize all chunks into a ChunkStore. + + The defaults (`extra_passes=0`, `gop_batching=False`) produce a store that + has only received the single-pass compaction that happens naturally during + chunk insertion. The Python wrapper `LazyChunkStream.collect(optimize=...)` + is the intended entry point. + """ + def to_chunks(self) -> list[ChunkInternal]: ... def __iter__(self) -> LazyChunkStreamIterator: ... @staticmethod def from_iter(iterable: Any) -> LazyChunkStreamInternal: ... + def send_to_recording(self, recording: PyRecordingStream | None = None) -> None: + """ + Drain this stream into a recording stream. + + If `recording` is `None`, the active recording is used. Blocks until every + chunk has been pushed to the recording's batcher. A silent no-op when + there is no active recording. + """ class LazyChunkStreamIterator: """Iterator over chunks from a compiled stream.""" @@ -1606,3 +1725,163 @@ class LazyChunkStreamIterator: def __next__(self) -> ChunkInternal: """Implement next(self).""" + +##################################################################################################################### +## METRICS APIS ## +##################################################################################################################### + +class _QueryMetrics: + """Frozen mirror of `re_datafusion::QuerySnapshot`. One per query.""" + + # Plan-time + dataset_id: str + """The dataset being queried.""" + + query_chunks: int + """Number of unique chunks returned by `query_dataset` (subset of the dataset).""" + + query_segments: int + """Number of distinct segments involved in the query.""" + + query_layers: int + """Number of distinct layers touched by the query.""" + + query_columns: int + """Number of columns in the query output schema.""" + + query_entities: int + """Number of entity paths in the query request.""" + + query_bytes: int + """Total size of all queried chunks in bytes (from chunk metadata).""" + + query_chunks_per_segment_min: int + """Min number of chunks touched within any single segment in this query.""" + + query_chunks_per_segment_max: int + """Max number of chunks touched within any single segment in this query.""" + + query_chunks_per_segment_mean: float + """Mean number of chunks touched per segment in this query.""" + + query_type: str + """Query shape: one of `"static"`, `"latest_at"`, `"range"`, `"dataframe"`, or `"full_scan"`.""" + + primary_index_name: str | None + """Name of the sort/filter index (timeline) for this query, if any.""" + + time_to_first_chunk_info: timedelta | None + """Time from sending `query_dataset` until the first response message arrives (the chunk metadata, not actual chunk data).""" + + filters_pushed_down: int + """Number of filter expressions the table provider was able to push down to the server (`Exact` or `Inexact` from `supports_filters_pushdown`).""" + + filters_applied_client_side: int + """Number of filter expressions that could not be pushed down — applied client-side by DataFusion via a downstream `FilterExec`.""" + + entity_path_narrowing_applied: bool + """True when projection-based entity-path narrowing actually trimmed the set of entity paths sent to `query_dataset`.""" + + # Execution-time + total_duration: timedelta + """Wall-clock time from the start of `scan()` until the query finished (cleanly or via error). Always populated.""" + + time_to_first_chunk: timedelta | None + """Time from scan start until the first chunk reached the consumer. `None` when no chunk was ever delivered (e.g. early error, empty result).""" + + error_kind: str | None + """`None` on success. On failure, one of the stable string labels `"grpc_fetch"`, `"direct_fetch"`, `"decode"`, or `"other"`.""" + + direct_terminal_reason: str | None + """Reason a direct (HTTP Range) fetch hit a terminal failure — i.e. a non-retryable error or retries exhausted. `None` when no direct fetch terminally failed (can be `None` even when `error_kind` is set, if the failure was on the gRPC or decode path).""" + + # Fetch counters + fetch_grpc_requests: int + """Number of gRPC fetch calls the scanner issued.""" + + fetch_grpc_bytes: int + """Sum of `chunk_byte_length` (catalog metadata, compressed on-disk size) over chunks fetched via gRPC. Excludes framing overhead and bytes consumed by failed retries — a lower bound on wire traffic.""" + + fetch_direct_requests: int + """Number of direct (HTTP Range) fetches the scanner issued. Counts each merged request once, regardless of byte ranges or retry attempts.""" + + fetch_direct_bytes: int + """Sum of `chunk_byte_length` (catalog metadata, compressed on-disk size) over chunks fetched via direct HTTP. Does **not** count filler bytes that range-merging pulls between adjacent chunks, so actual wire traffic can exceed this value. Includes successful merged-range fetches even when a sibling range makes the overall batch fail.""" + + fetch_direct_retries: int + """Total number of direct-fetch retry *attempts* across all requests. A request retried 3 times contributes 3 here.""" + + fetch_direct_requests_retried: int + """Number of distinct direct-fetch requests that needed at least one retry. Always `≤ fetch_direct_retries`; the ratio between them is the average retries per retried request.""" + + fetch_direct_retry_sleep: timedelta + """Total backoff time slept across all direct-fetch retries.""" + + fetch_direct_max_attempt: int + """True maximum attempt number across all partitions.""" + + fetch_direct_original_ranges: int + """Number of byte ranges the planner *wanted* to fetch directly, before adjacent ranges were coalesced. With `fetch_direct_merged_ranges`, gives the range-merging ratio.""" + + fetch_direct_merged_ranges: int + """Number of combined HTTP Range requests produced by merging adjacent byte ranges. Normally equals `fetch_direct_requests` after a completed scan, but can differ when cancellation stops only part of the planned work from being issued.""" + + planned_fetch_batches: int + """Transport batches planned before splitting direct and gRPC work.""" + + planned_segment_waves: int + """Segment waves produced by the current admission scheduler.""" + + segment_admission_limit: int + """Maximum concurrently admitted segments configured for this query.""" + + max_segments_per_fetch_batch: int + """Largest distinct-segment count in a planned transport batch.""" + + max_segments_per_wave: int + """Largest distinct-segment count in a planned admission wave.""" + + peak_active_segments: int + """Highest observed number of active admitted segments. May exceed `segment_admission_limit` when the stall breaker admits bypass segments.""" + + pipeline_budget_bytes: int + """Total decoded-byte capacity shared across all query partitions.""" + + pipeline_peak_decoded_bytes: int + """Highest observed number of decoded bytes charged to the pipeline budget.""" + + pipeline_byte_waits: int + """Reservations that first parked because decoded-byte capacity was full.""" + + segment_admission_waits: int + """Reservations that first parked because segment admission was full.""" + + pipeline_stall_breaker_activations: int + """Number of saturated-pipeline stall-breaker activations.""" + +class _MetricsCollectorHandle: + """Opaque handle held by the `query_metrics()` context manager.""" + + def snapshot(self) -> list[_QueryMetrics]: + """ + Non-destructive copy of all snapshots received so far. + + Suitable for use mid-scope (`collector.queries` in the Python wrapper). + """ + + def drain(self) -> list[_QueryMetrics]: + """ + Take and clear all snapshots. + + Used by the context manager on `__exit__` to drain any remaining + snapshots into the user-visible Python `MetricsCollector` wrapper. + """ + +def _new_metrics_collector() -> _MetricsCollectorHandle: + """ + Allocate a fresh [`MetricsCollector`] and wrap it in a Python handle. + + The Python `query_metrics()` context manager pushes the returned handle + onto the `_active_collectors` `ContextVar` for the duration of the + `with` block; nothing is registered globally. + """ diff --git a/rerun_py/rerun_bindings/types.py b/rerun_py/rerun_bindings/types.py index 39c5f71ed339..dd9062107c35 100644 --- a/rerun_py/rerun_bindings/types.py +++ b/rerun_py/rerun_bindings/types.py @@ -1,15 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, TypeAlias +from typing import TYPE_CHECKING, TypeAlias import numpy as np import numpy.typing as npt import pyarrow as pa -from .rerun_bindings import ( - VectorDistanceMetric as VectorDistanceMetric, -) - if TYPE_CHECKING: from .rerun_bindings import ( ComponentColumnDescriptor as ComponentColumnDescriptor, @@ -17,7 +13,6 @@ ComponentDescriptor as ComponentDescriptor, IndexColumnDescriptor as IndexColumnDescriptor, IndexColumnSelector as IndexColumnSelector, - IndexingResult as IndexingResult, ) IndexValuesLike: TypeAlias = npt.NDArray[np.int_] | npt.NDArray[np.datetime64] | pa.Int64Array @@ -31,13 +26,3 @@ """ A type alias for TableLike pyarrow objects. """ - -VectorDistanceMetricLike: TypeAlias = VectorDistanceMetric | Literal["L2", "Cosine", "Dot", "Hamming"] -""" -A type alias for vector distance metrics. -""" - -VectorLike = npt.NDArray[np.float64] | list[float] -""" -A type alias for vector-like objects. -""" diff --git a/rerun_py/rerun_dev_fixup/rerun_dev_fixup/__init__.py b/rerun_py/rerun_dev_fixup/rerun_dev_fixup/__init__.py index 98589dce4cf5..f6776f87e75b 100644 --- a/rerun_py/rerun_dev_fixup/rerun_dev_fixup/__init__.py +++ b/rerun_py/rerun_dev_fixup/rerun_dev_fixup/__init__.py @@ -14,11 +14,13 @@ def _find_repo_root() -> Path | None: if pixi_root: return Path(pixi_root) - # Try to find repo root from the venv location using sys.prefix - # sys.prefix points to the venv root (e.g., /path/to/repo/.venv) - venv_path = Path(sys.prefix) - if venv_path.name == ".venv": - return venv_path.parent + # Otherwise walk up from the venv (sys.prefix) to find the repo root. For the + # workspace .venv this is the immediate parent; for an isolated example's .venv + # (examples/python//.venv) it is several levels up. The repo root is the + # first ancestor that holds both `pixi.toml` and the `rerun_py` source tree. + for parent in Path(sys.prefix).parents: + if (parent / "pixi.toml").is_file() and (parent / "rerun_py").is_dir(): + return parent return None diff --git a/rerun_py/rerun_sdk/rerun/__init__.py b/rerun_py/rerun_sdk/rerun/__init__.py index 6b2e5bebe778..192d145348cb 100644 --- a/rerun_py/rerun_sdk/rerun/__init__.py +++ b/rerun_py/rerun_sdk/rerun/__init__.py @@ -9,8 +9,8 @@ import numpy as np -__version__ = "0.32.0-alpha.1" -__version_info__ = (0, 32, 0, "alpha.1") +__version__ = "0.35.0" +__version_info__ = (0, 35, 0, None) if sys.version_info < (3, 10): # noqa: UP036 raise RuntimeError("Rerun SDK requires Python 3.10 or later.") @@ -27,7 +27,6 @@ blueprint as blueprint, catalog as catalog, experimental as experimental, - recording as recording, server as server, urdf as urdf, ) @@ -75,6 +74,8 @@ send_columns as send_columns, ) from ._send_dataframe import ( + AUTO_INDEX as AUTO_INDEX, + RECORDING_PROPERTIES_PATH as RECORDING_PROPERTIES_PATH, RERUN_KIND as RERUN_KIND, RERUN_KIND_CONTROL as RERUN_KIND_CONTROL, RERUN_KIND_INDEX as RERUN_KIND_INDEX, @@ -87,6 +88,9 @@ send_dataframe as send_dataframe, send_record_batch as send_record_batch, ) +from ._tracing_session import ( + tracing_session as tracing_session, +) from .any_batch_value import ( AnyBatchValue as AnyBatchValue, ComponentValueLike as ComponentValueLike, @@ -108,6 +112,7 @@ CoordinateFrame as CoordinateFrame, Cylinders3D as Cylinders3D, DepthImage as DepthImage, + Ellipses2D as Ellipses2D, Ellipsoids3D as Ellipsoids3D, EncodedDepthImage as EncodedDepthImage, EncodedImage as EncodedImage, @@ -132,7 +137,8 @@ SegmentationImage as SegmentationImage, SeriesLines as SeriesLines, SeriesPoints as SeriesPoints, - Status as Status, + StateChange as StateChange, + StateConfiguration as StateConfiguration, Tensor as Tensor, TextDocument as TextDocument, TextLog as TextLog, @@ -141,6 +147,7 @@ VideoFrameReference as VideoFrameReference, VideoStream as VideoStream, ViewCoordinates as ViewCoordinates, + VoxelGridMap as VoxelGridMap, ) from .archetypes.boxes2d_ext import ( Box2DFormat as Box2DFormat, @@ -207,7 +214,6 @@ disconnect as disconnect, save as save, send_blueprint as send_blueprint, - send_recording as send_recording, serve_grpc as serve_grpc, set_sinks as set_sinks, spawn as spawn, @@ -216,6 +222,8 @@ from .time import ( disable_timeline as disable_timeline, reset_time as reset_time, + set_log_tick_enabled as set_log_tick_enabled, + set_log_time_enabled as set_log_time_enabled, set_time as set_time, ) from .web import serve_web_viewer as serve_web_viewer diff --git a/rerun_py/rerun_sdk/rerun/_baseclasses.py b/rerun_py/rerun_sdk/rerun/_baseclasses.py index e700ecaab0ef..a67a454f4ee1 100644 --- a/rerun_py/rerun_sdk/rerun/_baseclasses.py +++ b/rerun_py/rerun_sdk/rerun/_baseclasses.py @@ -409,7 +409,7 @@ def partition(self, lengths: npt.ArrayLike) -> ComponentColumn: class ComponentColumnList(Iterable[ComponentColumn]): """ - A collection of [ComponentColumn][]s. + A collection of [`ComponentColumn`][rerun.ComponentColumn]s. Useful to partition and log multiple columns at once. """ diff --git a/rerun_py/rerun_sdk/rerun/_log.py b/rerun_py/rerun_sdk/rerun/_log.py index c5cbdcbd269e..683daaf8ceb3 100644 --- a/rerun_py/rerun_sdk/rerun/_log.py +++ b/rerun_py/rerun_sdk/rerun/_log.py @@ -85,7 +85,7 @@ def log( Static data has no time associated with it, exists on all timelines, and unconditionally shadows any temporal data of the same type. - Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). Additional timelines set by [`rerun.set_time`][] will also be included. recording: @@ -170,7 +170,7 @@ def _log_components( Static data has no time associated with it, exists on all timelines, and unconditionally shadows any temporal data of the same type. - Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). Additional timelines set by [`rerun.set_time`][] will also be included. recording: @@ -250,7 +250,7 @@ def log_file_from_path( Static data has no time associated with it, exists on all timelines, and unconditionally shadows any temporal data of the same type. - Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). Additional timelines set by [`rerun.set_time`][] will also be included. recording: @@ -304,7 +304,7 @@ def log_file_from_contents( Static data has no time associated with it, exists on all timelines, and unconditionally shadows any temporal data of the same type. - Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). Additional timelines set by [`rerun.set_time`][] will also be included. recording: diff --git a/rerun_py/rerun_sdk/rerun/_send_dataframe.py b/rerun_py/rerun_sdk/rerun/_send_dataframe.py index a14f272f5801..9cb2301909a6 100644 --- a/rerun_py/rerun_sdk/rerun/_send_dataframe.py +++ b/rerun_py/rerun_sdk/rerun/_send_dataframe.py @@ -1,16 +1,23 @@ from __future__ import annotations -from collections import defaultdict -from typing import TYPE_CHECKING, Any - -import pyarrow as pa - -from ._baseclasses import ComponentColumn, ComponentDescriptor -from ._send_columns import TimeColumnLike, send_columns +from typing import TYPE_CHECKING if TYPE_CHECKING: + import pyarrow as pa + + from .experimental._chunk import DataframeLike from .recording_stream import RecordingStream + +class _AutoIndex: + """Sentinel for the `index=…` argument: derive index columns from metadata.""" + + +AUTO_INDEX = _AutoIndex() +"""Sentinel for the `index=…` argument: derive index columns from metadata.""" + +# The following constants mirror the Rerun Arrow metadata keys (see `re_sorbet::metadata`). They are +# kept here for backwards compatibility; the dataframe → chunk interpretation now lives in Rust. SORBET_INDEX_NAME = b"rerun:index_name" SORBET_ENTITY_PATH = b"rerun:entity_path" SORBET_ARCHETYPE_NAME = b"rerun:archetype" @@ -21,83 +28,83 @@ RERUN_KIND_CONTROL = b"control" RERUN_KIND_INDEX = b"index" - -class _RawIndexColumn(TimeColumnLike): - def __init__(self, metadata: dict[bytes, bytes], col: pa.Array) -> None: - self.metadata = metadata - self.col = col - - def timeline_name(self) -> str: - name = self.metadata.get(SORBET_INDEX_NAME, "unknown") - if isinstance(name, bytes): - name = name.decode("utf-8") - return name - - def as_arrow_array(self) -> pa.Array: - return self.col - - -class _RawComponentBatchLike(ComponentColumn): - def __init__(self, metadata: dict[bytes, bytes], col: pa.Array) -> None: - self.metadata = metadata - self.col = col - - def component_descriptor(self) -> ComponentDescriptor: - kwargs = {} - if SORBET_ARCHETYPE_NAME in self.metadata: - kwargs["archetype"] = self.metadata[SORBET_ARCHETYPE_NAME].decode("utf-8") - if SORBET_COMPONENT_TYPE in self.metadata: - kwargs["component_type"] = self.metadata[SORBET_COMPONENT_TYPE].decode("utf-8") - if SORBET_COMPONENT in self.metadata: - kwargs["component"] = self.metadata[SORBET_COMPONENT].decode("utf-8") - - if "component_type" not in kwargs: - kwargs["component_type"] = "Unknown" - - return ComponentDescriptor(**kwargs) - - def as_arrow_array(self) -> pa.Array: - return self.col - - -def send_record_batch(batch: pa.RecordBatch, recording: RecordingStream | None = None) -> None: - """Coerce a single pyarrow `RecordBatch` to Rerun structure.""" - - indexes = [] - data: defaultdict[str, list[Any]] = defaultdict(list) - archetypes: defaultdict[str, set[Any]] = defaultdict(set) - for col in batch.schema: - metadata = col.metadata or {} - if metadata.get(RERUN_KIND) == RERUN_KIND_CONTROL: - continue - if SORBET_INDEX_NAME in metadata or metadata.get(RERUN_KIND) == RERUN_KIND_INDEX: - if SORBET_INDEX_NAME not in metadata: - metadata[SORBET_INDEX_NAME] = col.name - indexes.append(_RawIndexColumn(metadata, batch.column(col.name))) - else: - entity_path = metadata.get(SORBET_ENTITY_PATH, col.name.split(":")[0]) - if isinstance(entity_path, bytes): - entity_path = entity_path.decode("utf-8") - data[entity_path].append(_RawComponentBatchLike(metadata, batch.column(col.name))) - if SORBET_ARCHETYPE_NAME in metadata: - archetypes[entity_path].add(metadata[SORBET_ARCHETYPE_NAME].decode("utf-8")) - - for entity_path, columns in data.items(): - send_columns( - entity_path, - indexes, - columns, - # This is fine, send_columns will handle the conversion - recording=recording, # NOLINT - ) - - -# TODO(RR-3198): this should accept a `datafusion.DataFrame` as a soft dependency -def send_dataframe(df: pa.RecordBatchReader | pa.Table, recording: RecordingStream | None = None) -> None: - """Coerce a pyarrow `RecordBatchReader` or `Table` to Rerun structure.""" - - if isinstance(df, pa.Table): - df = df.to_reader() - - for batch in df: - send_record_batch(batch, recording) +# Root entity path used for recording-scope properties (e.g. `start_time`). +# Mirrors `re_log_types::EntityPath::properties()` on the Rust side. +RECORDING_PROPERTIES_PATH = "/__properties" + + +def send_record_batch( + batch: pa.RecordBatch, + recording: RecordingStream | None = None, + *, + index: str | list[str] | None | _AutoIndex = AUTO_INDEX, + entity_path: str | None = None, +) -> None: + """ + Coerce a single pyarrow `RecordBatch` to Rerun structure and log it. + + A thin wrapper over [`Chunk.from_record_batch`][rerun.experimental.Chunk.from_record_batch] + followed by [`send_chunks`][rerun.experimental.send_chunks]. See `Chunk.from_record_batch` for + the full column-classification semantics and the conditions under which a `ValueError` is raised. + + Parameters + ---------- + batch: + The Arrow record batch to interpret. + recording: + Specifies the [`rerun.RecordingStream`][] to use. + If left unspecified, defaults to the current active data recording, if there is one. + See also: [`rerun.init`][], [`rerun.set_global_data_recording`][]. + index: + Determines which columns are index (timeline) columns. See + [`Chunk.from_record_batch`][rerun.experimental.Chunk.from_record_batch] for the full + semantics. Defaults to deriving the index from the batch's Rerun metadata. + entity_path: + Default entity path for component columns that do not otherwise specify one. + + """ + from .experimental._chunk import Chunk + from .experimental._send_chunks import send_chunks + + chunks = Chunk.from_record_batch(batch, index=index, entity_path=entity_path) + send_chunks(chunks, recording=recording) # NOLINT: send_chunks casts the RecordingStream itself + + +def send_dataframe( + df: DataframeLike, + recording: RecordingStream | None = None, + *, + index: str | list[str] | None | _AutoIndex = AUTO_INDEX, + entity_path: str | None = None, +) -> None: + """ + Coerce a pyarrow `Table` / `RecordBatch` / `RecordBatchReader`, or a datafusion `DataFrame`, to Rerun structure and log it. + + A thin wrapper over [`Chunk.from_dataframe`][rerun.experimental.Chunk.from_dataframe] followed by + [`send_chunks`][rerun.experimental.send_chunks]. See `Chunk.from_dataframe` for the accepted input + types, and `Chunk.from_record_batch` for the full column-classification semantics and the + conditions under which a `ValueError` is raised. + + Parameters + ---------- + df: + The dataframe to interpret. Must be a pyarrow `Table`, pyarrow `RecordBatch`, pyarrow + `RecordBatchReader`, or datafusion `DataFrame` (an optional dependency) — each has a single + fixed schema. + recording: + Specifies the [`rerun.RecordingStream`][] to use. + If left unspecified, defaults to the current active data recording, if there is one. + See also: [`rerun.init`][], [`rerun.set_global_data_recording`][]. + index: + Determines which columns are index (timeline) columns. See + [`Chunk.from_record_batch`][rerun.experimental.Chunk.from_record_batch] for the full + semantics. Defaults to deriving the index from the dataframe's Rerun metadata. + entity_path: + Default entity path for component columns that do not otherwise specify one. + + """ + from .experimental._chunk import Chunk + from .experimental._send_chunks import send_chunks + + chunks = Chunk.from_dataframe(df, index=index, entity_path=entity_path) + send_chunks(chunks, recording=recording) # NOLINT: send_chunks casts the RecordingStream itself diff --git a/rerun_py/rerun_sdk/rerun/_spawn.py b/rerun_py/rerun_sdk/rerun/_spawn.py index 3fbb375ea462..f9ca8738d636 100644 --- a/rerun_py/rerun_sdk/rerun/_spawn.py +++ b/rerun_py/rerun_sdk/rerun/_spawn.py @@ -12,13 +12,16 @@ def _spawn_viewer( detach_process: bool = True, executable_name: str = "rerun", executable_path: str | None = None, -) -> None: + headless: bool = False, +) -> int | None: """ Internal helper to spawn a Rerun Viewer, listening on the given port. - Blocks until the viewer is ready to accept connections. + Blocks until the viewer is ready to accept connections. Returns the spawned + viewer's pid, or `None` if spawning was skipped (e.g. when + `_RERUN_TEST_FORCE_SAVE` is set). - Used by [rerun.spawn][] + Used by [rerun.spawn][] and [rerun.experimental.ViewerClient][]. Parameters ---------- @@ -49,20 +52,21 @@ def _spawn_viewer( through PATH for `executable_name`. Unspecified by default. + headless: + Run the spawned viewer in headless mode (no OS window). + The viewer still listens for gRPC connections, so the SDK can keep + logging data and request screenshots via + [`rerun.experimental.ViewerClient.save_screenshot`][]. """ import rerun_bindings - # Let the spawned rerun process know it's just an app - new_env = os.environ.copy() # NOTE: If `_RERUN_TEST_FORCE_SAVE` is set, all recording streams will write to disk no matter # what, thus spawning a viewer is pointless (and probably not intended). if os.environ.get("_RERUN_TEST_FORCE_SAVE") is not None: - return - new_env["RERUN_APP_ONLY"] = "true" - - rerun_bindings.spawn( + return None + return rerun_bindings.spawn( port=port, memory_limit=memory_limit, server_memory_limit=server_memory_limit, @@ -70,4 +74,7 @@ def _spawn_viewer( detach_process=detach_process, executable_name=executable_name, executable_path=executable_path, + # Let the spawned rerun process know it's just an app (skips analytics opt-in etc.). + extra_env=[("RERUN_APP_ONLY", "true")], + headless=headless, ) diff --git a/rerun_py/rerun_sdk/rerun/_tracing.py b/rerun_py/rerun_sdk/rerun/_tracing.py index e3949d1a2bc3..1c7a1d3ee4da 100644 --- a/rerun_py/rerun_sdk/rerun/_tracing.py +++ b/rerun_py/rerun_sdk/rerun/_tracing.py @@ -7,8 +7,9 @@ Rust SDK so `#[instrument]` spans on the Rust side become children of the Python span. -Active only when `TELEMETRY_ENABLED=true` and `OTEL_SDK_ENABLED=true` are set in -the environment. Otherwise both helpers are pass-throughs. +Active only when `TELEMETRY_ENABLED=true` is set in the environment AND an OTLP +endpoint is configured via `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` (or the umbrella +`OTEL_EXPORTER_OTLP_ENDPOINT`). Otherwise both helpers are pass-throughs. This module is private — external callers should re-export these helpers from the consumer package rather than importing `rerun._tracing` directly. @@ -45,7 +46,7 @@ def _init_once() -> None: return _initialized = True - if not _env_bool("TELEMETRY_ENABLED") or not _env_bool("OTEL_SDK_ENABLED"): + if not _env_bool("TELEMETRY_ENABLED"): return try: @@ -58,8 +59,14 @@ def _init_once() -> None: logger.warning("`with_tracing` is a no-op: install OpenTelemetry via `pip install rerun-sdk[tracing]`") return + endpoint = os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") + if not endpoint: + logger.info( + "`with_tracing` is a no-op: set OTEL_EXPORTER_OTLP_TRACES_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT to enable", + ) + return + service_name = os.environ.get("OTEL_SERVICE_NAME") or "rerun-py" - endpoint = os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or "http://localhost:4317" provider = TracerProvider(resource=Resource.create({"service.name": service_name})) provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint))) @@ -150,7 +157,8 @@ def tracing_scope(name: str) -> Iterator[None]: function. Any Rust-side `#[instrument]` spans triggered from within will be parented under this span in Jaeger. - No-op unless `TELEMETRY_ENABLED=true` and `OTEL_SDK_ENABLED=true`. + No-op unless `TELEMETRY_ENABLED=true` and an OTLP endpoint is configured + (`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` or `OTEL_EXPORTER_OTLP_ENDPOINT`). Examples -------- @@ -188,7 +196,8 @@ def with_tracing(name: str) -> Callable[[F], F]: For ad-hoc blocks that don't belong in a dedicated function, use [`tracing_scope`][rerun._tracing.tracing_scope] instead. - No-op unless `TELEMETRY_ENABLED=true` and `OTEL_SDK_ENABLED=true`. + No-op unless `TELEMETRY_ENABLED=true` and an OTLP endpoint is configured + (`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` or `OTEL_EXPORTER_OTLP_ENDPOINT`). """ def decorator(func: F) -> F: diff --git a/rerun_py/rerun_sdk/rerun/_tracing_session.py b/rerun_py/rerun_sdk/rerun/_tracing_session.py new file mode 100644 index 000000000000..85b3b1cc91f1 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/_tracing_session.py @@ -0,0 +1,209 @@ +""" +Opt-in correlation handle that tags every outbound gRPC request with a session id. + +Use [`tracing_session`][rerun._tracing_session.tracing_session] when you want to +attach a single, copy-pasteable identifier to a block of catalog calls so support +can look them up in distributed tracing. + +```python +from rerun import tracing_session + +with tracing_session(): + dataset.scan(...).read_all() +# → INFO message printed at scope entry: "rerun tracing session started: rs_8f3a91e2" +``` + +The implementation is a thin Python wrapper around a Rust-side `ContextVar`. +The Rust gRPC client reads the variable on every outbound request and merges +`rerun_session_id=` into the W3C `tracestate` header. The catalog server +records it as a span attribute, queryable in Tempo as +`{ .rerun_session_id = "…" }`. + +This module is private — public re-export lives in `rerun.__init__`. +""" + +from __future__ import annotations + +import contextlib +import logging +import secrets +import time +from typing import TYPE_CHECKING + +import psutil + +if TYPE_CHECKING: + from collections.abc import Iterator + +logger = logging.getLogger("rerun") + +_SESSION_ID_PREFIX = "rs_" +_SESSION_ID_HEX_LEN = 8 + + +def _generate_session_id() -> str: + """Return a fresh session id of the form `rs_<8 lowercase hex digits>`.""" + return f"{_SESSION_ID_PREFIX}{secrets.token_hex(_SESSION_ID_HEX_LEN // 2)}" + + +def _is_valid_session_id(sid: str) -> bool: + """Mirror of `re_perf_telemetry::is_valid_rerun_session_id`.""" + if not sid.startswith(_SESSION_ID_PREFIX): + return False + rest = sid[len(_SESSION_ID_PREFIX) :] + return len(rest) == _SESSION_ID_HEX_LEN and all(c in "0123456789abcdef" for c in rest) + + +@contextlib.contextmanager +def tracing_session() -> Iterator[str]: + """ + Tag every gRPC request inside the `with` block with a fresh session id. + + The id is logged to the `rerun` logger at INFO level the moment the scope is + entered, so it stays visible even if the workflow crashes or hangs before + completing. Send the logged id to support and they can query + `{ .rerun_session_id = "" }` in Tempo to surface every related request. + + The id is also yielded as the `as` target for programmatic access — handy + for tests or integration code, but not the main customer-facing way to + retrieve it. + + The session id is propagated through the W3C `tracestate` header. When you + later opt into exporting client-side traces (by setting an OTLP endpoint) + those exported spans automatically carry the same id, so the client→server + trace tree stays correlated end-to-end. + + When the rerun telemetry stack is not active (typically because + `TELEMETRY_ENABLED=true` was not set before importing rerun), the context + manager logs a warning, yields the empty string, and proceeds as a no-op. + Catalog calls inside the block are not tagged in this case. + + Examples + -------- + ```python + import rerun as rr + from rerun import tracing_session + + client = rr.catalog.CatalogClient("rerun://…") + with tracing_session(): + ds = client.get_dataset(name="…") + _ = ds.scan().read_all() + # The session id appears in the logs at scope entry: + # INFO rerun: rerun tracing session started: rs_8f3a91e2 + ``` + + """ + from rerun_bindings import ( + _dec_active_tracing_sessions, + _get_tracing_session_var, + _inc_active_tracing_sessions, + _is_telemetry_active, + _log_tracing_session_finished, + _log_tracing_session_started, + ) + + var = _get_tracing_session_var() if _is_telemetry_active() else None + + if var is None: + logger.warning( + "tracing_session() is a no-op: the rerun telemetry stack is not active. " + "Set the environment variable TELEMETRY_ENABLED=true before importing " + "rerun to enable session correlation.", + ) + # Yield an obviously-invalid id so callers that bind via `as sid` still + # work, but the value is clearly not a real session id. + yield "" + return + + sid = _generate_session_id() + assert _is_valid_session_id(sid), f"generated invalid session id: {sid!r}" + + # The atomic counter lets the Rust enricher skip GIL acquisitions when no + # `tracing_session()` scope is active anywhere in the process. Increment + # first so any RPC fired between `var.set` and the yield still sees a + # non-zero gate, and pair it with an outer `try` so the counter is always + # decremented even if `var.set` itself raises. + _inc_active_tracing_sessions() + try: + token = var.set(sid) + try: + # Surface the id immediately so the customer can grab it even if + # their workflow crashes or hangs before exiting the `with` block. + # Routed through the Rust `tracing` stack so it follows the same + # `RUST_LOG` and fmt-layer pipeline as every other rerun log, + # rather than the Python `logging` pipeline (which has no default + # handler attached to the `rerun` logger and would silently drop + # INFO records in environments like ipython). + _log_tracing_session_started(sid) + + # Snapshot before yielding. Metrics collection is best-effort: any + # psutil failure (AccessDenied, NoSuchProcess, OSError, ...) must + # never propagate out of `__enter__` or `__exit__`, since that + # would either block the user's code from running or mask its + # successful completion. Each psutil source is wrapped + # individually so a failure in one still allows the other to be + # reported. + # + # If the `with` block raises, we skip the finished-log entirely; + # the started-log already gave the customer the session id. + t0 = time.perf_counter() + proc: psutil.Process | None + cpu0 = None + try: + proc = psutil.Process() + cpu0 = proc.cpu_times() + except Exception: + proc = None + cpu0 = None + try: + net0 = psutil.net_io_counters() + except Exception: + net0 = None + + yield sid + + elapsed_s = time.perf_counter() - t0 + cpu_user_s: float | None = None + cpu_system_s: float | None = None + cpu_iowait_s: float | None = None + net_rx_mb: float | None = None + if cpu0 is not None and proc is not None: + try: + cpu1 = proc.cpu_times() + cpu_user_s = cpu1.user - cpu0.user + cpu_system_s = cpu1.system - cpu0.system + # `iowait` is Linux-only on psutil's Process.cpu_times(). + iowait1 = getattr(cpu1, "iowait", None) + iowait0 = getattr(cpu0, "iowait", None) + if iowait1 is not None and iowait0 is not None: + cpu_iowait_s = iowait1 - iowait0 + except Exception: + cpu_user_s = cpu_system_s = cpu_iowait_s = None + if net0 is not None: + # Host-wide counter, not per-process. Captures every byte the + # machine received during the scope, including unrelated + # traffic. Good enough for support correlation, not a precise + # per-rerun metric. + try: + net1 = psutil.net_io_counters() + net_rx_mb = (net1.bytes_recv - net0.bytes_recv) / (1024 * 1024) + except Exception: + net_rx_mb = None + + try: + _log_tracing_session_finished( + sid, + elapsed_s, + cpu_user_s, + cpu_system_s, + cpu_iowait_s, + net_rx_mb, + ) + except Exception: + # The finished-log is purely informational; never let a + # logging failure mask the user's successful work. + pass + finally: + var.reset(token) + finally: + _dec_active_tracing_sessions() diff --git a/rerun_py/rerun_sdk/rerun/_validators.py b/rerun_py/rerun_sdk/rerun/_validators.py index 32cca0d5fcb2..181d896fec16 100644 --- a/rerun_py/rerun_sdk/rerun/_validators.py +++ b/rerun_py/rerun_sdk/rerun/_validators.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any -from ._converters import to_np_float32, to_np_float64, to_np_uint32, to_np_uint64 +from ._converters import to_np_float32, to_np_float64, to_np_int32, to_np_uint32, to_np_uint64 if TYPE_CHECKING: import numpy as np @@ -61,6 +61,13 @@ def flat_np_float64_array_from_array_like(data: Any, dimension: int) -> npt.NDAr return flat_np_array_from_array_like(array, dimension) +def flat_np_int32_array_from_array_like(data: Any, dimension: int) -> npt.NDArray[np.int32]: + """Converts to a flat int numpy array from an arbitrary vector, validating for an expected dimensionality.""" + + array = to_np_int32(data) + return flat_np_array_from_array_like(array, dimension) + + def flat_np_uint32_array_from_array_like(data: Any, dimension: int) -> npt.NDArray[np.uint32]: """Converts to a flat uint numpy array from an arbitrary vector, validating for an expected dimensionality.""" diff --git a/rerun_py/rerun_sdk/rerun/archetypes/.gitattributes b/rerun_py/rerun_sdk/rerun/archetypes/.gitattributes index 74f560255166..6a7d65413b9c 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/archetypes/.gitattributes @@ -15,6 +15,7 @@ clear.py linguist-generated=true coordinate_frame.py linguist-generated=true cylinders3d.py linguist-generated=true depth_image.py linguist-generated=true +ellipses2d.py linguist-generated=true ellipsoids3d.py linguist-generated=true encoded_depth_image.py linguist-generated=true encoded_image.py linguist-generated=true @@ -40,7 +41,8 @@ scalars.py linguist-generated=true segmentation_image.py linguist-generated=true series_lines.py linguist-generated=true series_points.py linguist-generated=true -status.py linguist-generated=true +state_change.py linguist-generated=true +state_configuration.py linguist-generated=true tensor.py linguist-generated=true text_document.py linguist-generated=true text_log.py linguist-generated=true @@ -49,3 +51,4 @@ transform_axes3d.py linguist-generated=true video_frame_reference.py linguist-generated=true video_stream.py linguist-generated=true view_coordinates.py linguist-generated=true +voxel_grid_map.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/archetypes/__init__.py b/rerun_py/rerun_sdk/rerun/archetypes/__init__.py index 8ff4a61799d7..a16a7033f565 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/__init__.py @@ -15,6 +15,7 @@ from .coordinate_frame import CoordinateFrame from .cylinders3d import Cylinders3D from .depth_image import DepthImage +from .ellipses2d import Ellipses2D from .ellipsoids3d import Ellipsoids3D from .encoded_depth_image import EncodedDepthImage from .encoded_image import EncodedImage @@ -40,7 +41,8 @@ from .segmentation_image import SegmentationImage from .series_lines import SeriesLines from .series_points import SeriesPoints -from .status import Status +from .state_change import StateChange +from .state_configuration import StateConfiguration from .tensor import Tensor from .text_document import TextDocument from .text_log import TextLog @@ -49,6 +51,7 @@ from .video_frame_reference import VideoFrameReference from .video_stream import VideoStream from .view_coordinates import ViewCoordinates +from .voxel_grid_map import VoxelGridMap __all__ = [ "AnnotationContext", @@ -64,6 +67,7 @@ "CoordinateFrame", "Cylinders3D", "DepthImage", + "Ellipses2D", "Ellipsoids3D", "EncodedDepthImage", "EncodedImage", @@ -89,7 +93,8 @@ "SegmentationImage", "SeriesLines", "SeriesPoints", - "Status", + "StateChange", + "StateConfiguration", "Tensor", "TextDocument", "TextLog", @@ -98,4 +103,5 @@ "VideoFrameReference", "VideoStream", "ViewCoordinates", + "VoxelGridMap", ] diff --git a/rerun_py/rerun_sdk/rerun/archetypes/annotation_context.py b/rerun_py/rerun_sdk/rerun/archetypes/annotation_context.py index 08cb34d4b818..6917886d2c43 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/annotation_context.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/annotation_context.py @@ -53,7 +53,11 @@ class AnnotationContext(Archetype): image[100:180, 130:280] = 2 # Log an annotation context to assign a label and color to each class - rr.log("segmentation", rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), static=True) + rr.log( + "segmentation", + rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), + static=True, + ) rr.log("segmentation/image", rr.SegmentationImage(image)) ``` diff --git a/rerun_py/rerun_sdk/rerun/archetypes/arrows3d.py b/rerun_py/rerun_sdk/rerun/archetypes/arrows3d.py index 8657ab1fdc14..c665c8e61f0e 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/arrows3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/arrows3d.py @@ -47,7 +47,11 @@ class Arrows3D(Arrows3DExt, Archetype, VisualizableArchetype): lengths = np.log2(np.arange(0, 100) + 1) angles = np.arange(start=0, stop=tau, step=tau * 0.01) origins = np.zeros((100, 3)) - vectors = np.column_stack([np.sin(angles) * lengths, np.zeros(100), np.cos(angles) * lengths]) + vectors = np.column_stack([ + np.sin(angles) * lengths, + np.zeros(100), + np.cos(angles) * lengths, + ]) colors = [[1.0 - c, c, 0.5, 0.5] for c in angles / tau] rr.log("arrows", rr.Arrows3D(origins=origins, vectors=vectors, colors=colors)) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/asset3d.py b/rerun_py/rerun_sdk/rerun/archetypes/asset3d.py index 52d23f1a827c..abc3168bd95d 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/asset3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/asset3d.py @@ -51,7 +51,9 @@ class Asset3D(Asset3DExt, Archetype, VisualizableArchetype): rr.init("rerun_example_asset3d", spawn=True) - rr.log("world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True) # Set an up-axis + rr.log( + "world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True + ) # Set an up-axis rr.log("world/asset", rr.Asset3D(path=sys.argv[1])) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/asset_video.py b/rerun_py/rerun_sdk/rerun/archetypes/asset_video.py index 2345d1b59429..00497c829647 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/asset_video.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/asset_video.py @@ -100,7 +100,12 @@ class AssetVideo(AssetVideoExt, Archetype): ) # Send blueprint that shows two 2D views next to each other. - rr.send_blueprint(rrb.Horizontal(rrb.Spatial2DView(origin="frame_1s"), rrb.Spatial2DView(origin="frame_2s"))) + rr.send_blueprint( + rrb.Horizontal( + rrb.Spatial2DView(origin="frame_1s"), + rrb.Spatial2DView(origin="frame_2s"), + ) + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/bar_chart.py b/rerun_py/rerun_sdk/rerun/archetypes/bar_chart.py index ba0196cab438..51f35af5583f 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/bar_chart.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/bar_chart.py @@ -42,10 +42,17 @@ class BarChart(BarChartExt, Archetype, VisualizableArchetype): rr.init("rerun_example_bar_chart", spawn=True) rr.log("bar_chart", rr.BarChart([8, 4, 0, 9, 1, 4, 1, 6, 9, 0])) - rr.log("bar_chart_custom_abscissa", rr.BarChart([8, 4, 0, 9, 1, 4], abscissa=[0, 1, 3, 4, 7, 11])) + rr.log( + "bar_chart_custom_abscissa", + rr.BarChart([8, 4, 0, 9, 1, 4], abscissa=[0, 1, 3, 4, 7, 11]), + ) rr.log( "bar_chart_custom_abscissa_and_widths", - rr.BarChart([8, 4, 0, 9, 1, 4], abscissa=[0, 1, 3, 4, 7, 11], widths=[1, 2, 1, 3, 4, 1]), + rr.BarChart( + [8, 4, 0, 9, 1, 4], + abscissa=[0, 1, 3, 4, 7, 11], + widths=[1, 2, 1, 3, 4, 1], + ), ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/bar_chart_ext.py b/rerun_py/rerun_sdk/rerun/archetypes/bar_chart_ext.py index f066f3098749..490efd889072 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/bar_chart_ext.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/bar_chart_ext.py @@ -23,7 +23,9 @@ def values__field_converter_override(data: TensorDataArrayLike) -> TensorDataBat # once we coerce to a canonical non-arrow type. shape_dims = tensor_data.as_arrow_array()[0][0].values.to_numpy() - if len([d for d in shape_dims if d != 1]) != 1: + # Ignore singleton dimensions so (1, N) and single-element vectors shaped (1,) remain valid. + num_non_singleton_dims = sum(d != 1 for d in shape_dims) + if len(shape_dims) == 0 or num_non_singleton_dims > 1: _send_warning_or_raise( f"Bar chart data should only be 1D. Got values with shape: {shape_dims}", 2, diff --git a/rerun_py/rerun_sdk/rerun/archetypes/boxes3d.py b/rerun_py/rerun_sdk/rerun/archetypes/boxes3d.py index 85a834e239ea..ee4d29963c41 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/boxes3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/boxes3d.py @@ -50,7 +50,9 @@ class Boxes3D(Boxes3DExt, Archetype, VisualizableArchetype): half_sizes=[[2.0, 2.0, 1.0], [1.0, 1.0, 0.5], [2.0, 0.5, 1.0]], quaternions=[ rr.Quaternion.identity(), - rr.Quaternion(xyzw=[0.0, 0.0, 0.382683, 0.923880]), # 45 degrees around Z + rr.Quaternion( + xyzw=[0.0, 0.0, 0.382683, 0.923880] + ), # 45 degrees around Z ], radii=0.025, colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], diff --git a/rerun_py/rerun_sdk/rerun/archetypes/clear.py b/rerun_py/rerun_sdk/rerun/archetypes/clear.py index b72c5fd337dc..1d178fe756ac 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/clear.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/clear.py @@ -47,12 +47,21 @@ class Clear(ClearExt, Archetype): rr.init("rerun_example_clear", spawn=True) vectors = [(1.0, 0.0, 0.0), (0.0, -1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] - origins = [(-0.5, 0.5, 0.0), (0.5, 0.5, 0.0), (0.5, -0.5, 0.0), (-0.5, -0.5, 0.0)] + origins = [ + (-0.5, 0.5, 0.0), + (0.5, 0.5, 0.0), + (0.5, -0.5, 0.0), + (-0.5, -0.5, 0.0), + ] colors = [(200, 0, 0), (0, 200, 0), (0, 0, 200), (200, 0, 200)] # Log a handful of arrows. - for i, (vector, origin, color) in enumerate(zip(vectors, origins, colors, strict=False)): - rr.log(f"arrows/{i}", rr.Arrows3D(vectors=vector, origins=origin, colors=color)) + for i, (vector, origin, color) in enumerate( + zip(vectors, origins, colors, strict=False) + ): + rr.log( + f"arrows/{i}", rr.Arrows3D(vectors=vector, origins=origin, colors=color) + ) # Now clear them, one by one on each tick. for i in range(len(vectors)): diff --git a/rerun_py/rerun_sdk/rerun/archetypes/coordinate_frame.py b/rerun_py/rerun_sdk/rerun/archetypes/coordinate_frame.py index ca30e4b80586..d34509066b7c 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/coordinate_frame.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/coordinate_frame.py @@ -44,13 +44,15 @@ class CoordinateFrame(Archetype): rr.log( "red_box", rr.Boxes3D(half_sizes=[0.5, 0.5, 0.5], colors=[255, 0, 0]), - # Use Transform3D to place the box, so we actually change the underlying coordinate frame and not just the box's pose. + # Use Transform3D to place the box, so we actually change the underlying + # coordinate frame and not just the box's pose. rr.Transform3D(translation=[2.0, 0.0, 0.0]), ) rr.log( "blue_box", rr.Boxes3D(half_sizes=[0.5, 0.5, 0.5], colors=[0, 0, 255]), - # Use Transform3D to place the box, so we actually change the underlying coordinate frame and not just the box's pose. + # Use Transform3D to place the box, so we actually change the underlying + # coordinate frame and not just the box's pose. rr.Transform3D(translation=[-2.0, 0.0, 0.0]), ) rr.log("point", rr.Points3D([0.0, 0.0, 0.0], radii=0.5)) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/depth_image.py b/rerun_py/rerun_sdk/rerun/archetypes/depth_image.py index 483ae67e5e60..f04d0d3bab64 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/depth_image.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/depth_image.py @@ -48,7 +48,8 @@ class DepthImage(DepthImageExt, Archetype, VisualizableArchetype): rr.init("rerun_example_depth_image_3d", spawn=True) - # If we log a pinhole camera model, the depth gets automatically back-projected to 3D + # If we log a pinhole camera model, the depth gets automatically + # back-projected to 3D rr.log( "world/camera", rr.Pinhole( @@ -59,7 +60,10 @@ class DepthImage(DepthImageExt, Archetype, VisualizableArchetype): ) # Log the tensor. - rr.log("world/camera/depth", rr.DepthImage(depth_image, meter=10_000.0, colormap="viridis")) + rr.log( + "world/camera/depth", + rr.DepthImage(depth_image, meter=10_000.0, colormap="viridis"), + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/ellipses2d.py b/rerun_py/rerun_sdk/rerun/archetypes/ellipses2d.py new file mode 100644 index 000000000000..c3f75dbc4338 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/archetypes/ellipses2d.py @@ -0,0 +1,447 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/ellipses2d.fbs". + +# You can extend this class by creating a "Ellipses2DExt" class in "ellipses2d_ext.py". + +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar + +import numpy as np +import pyarrow as pa +from attrs import define, field + +from .. import components, datatypes +from .._baseclasses import ( + Archetype, + ComponentColumnList, + ComponentDescriptor, +) +from ..blueprint import VisualizableArchetype, Visualizer +from ..error_utils import catch_and_log_exceptions +from .ellipses2d_ext import Ellipses2DExt + +if TYPE_CHECKING: + from ..blueprint.datatypes import VisualizerComponentMappingLike + +__all__ = ["Ellipses2D"] + + +@define(str=False, repr=False, init=False) +class Ellipses2D(Ellipses2DExt, Archetype, VisualizableArchetype): + """ + **Archetype**: 2D ellipses with half-extents (semi-axes) and optional center, colors etc. + + The half-sizes specify the lengths of the ellipse's two axes along the local x and y directions. + If both half-sizes are equal, the ellipse is a circle. + + Examples + -------- + ### Simple 2D ellipses: + ```python + import rerun as rr + + rr.init("rerun_example_ellipses2d", spawn=True) + + rr.log("simple", rr.Ellipses2D(half_sizes=[(2.0, 1.0)], centers=[(0.0, 0.0)])) + ``` + + ### Batch of 2D ellipses: + ```python + import rerun as rr + + rr.init("rerun_example_ellipses2d_batch", spawn=True) + + rr.log( + "batch", + rr.Ellipses2D( + centers=[(-2.0, 0.0), (0.0, 0.0), (2.5, 0.0)], + half_sizes=[(1.5, 0.75), (0.5, 0.5), (0.75, 1.5)], + line_radii=[0.025, 0.05, 0.025], + colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)], + labels=["wide", "circle", "tall"], + ), + ) + ``` + + """ + + NAME: ClassVar[str] = "rerun.archetypes.Ellipses2D" + + # __init__ can be found in ellipses2d_ext.py + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + half_sizes=None, + centers=None, + colors=None, + line_radii=None, + labels=None, + show_labels=None, + draw_order=None, + class_ids=None, + ) + + @classmethod + def _clear(cls) -> Ellipses2D: + """Produce an empty Ellipses2D, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + @classmethod + def from_fields( + cls, + *, + clear_unset: bool = False, + half_sizes: datatypes.Vec2DArrayLike | None = None, + centers: datatypes.Vec2DArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + line_radii: datatypes.Float32ArrayLike | None = None, + labels: datatypes.Utf8ArrayLike | None = None, + show_labels: datatypes.BoolLike | None = None, + draw_order: datatypes.Float32Like | None = None, + class_ids: datatypes.ClassIdArrayLike | None = None, + ) -> Ellipses2D: + """ + Update only some specific fields of a `Ellipses2D`. + + Parameters + ---------- + clear_unset: + If true, all unspecified fields will be explicitly cleared. + half_sizes: + All half-extents (semi-axes) that make up the batch of ellipses. + centers: + Optional center positions of the ellipses. + colors: + Optional colors for the ellipses. + line_radii: + Optional radii for the lines that make up the ellipses. + labels: + Optional text labels for the ellipses. + + If there's a single label present, it will be placed at the center of the entity. + Otherwise, each instance will have its own label. + show_labels: + Whether the text labels should be shown. + + If not set, labels will automatically appear when there is exactly one label for this entity + or the number of instances on this entity is under a certain threshold. + draw_order: + An optional floating point value that specifies the 2D drawing order. + + Objects with higher values are drawn on top of those with lower values. + Defaults to `10.0`. + class_ids: + Optional [`components.ClassId`][rerun.components.ClassId]s for the ellipses. + + The [`components.ClassId`][rerun.components.ClassId] provides colors and labels if not specified explicitly. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + kwargs = { + "half_sizes": half_sizes, + "centers": centers, + "colors": colors, + "line_radii": line_radii, + "labels": labels, + "show_labels": show_labels, + "draw_order": draw_order, + "class_ids": class_ids, + } + + if clear_unset: + kwargs = {k: v if v is not None else [] for k, v in kwargs.items()} # type: ignore[misc] + + inst.__attrs_init__(**kwargs) + return inst + + inst.__attrs_clear__() + return inst + + @classmethod + def cleared(cls) -> Ellipses2D: + """Clear all the fields of a `Ellipses2D`.""" + return cls.from_fields(clear_unset=True) + + @staticmethod + def descriptor_half_sizes() -> ComponentDescriptor: + return ComponentDescriptor( + "Ellipses2D:half_sizes", + archetype=Ellipses2D.NAME, + component_type=components.HalfSize2DBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_centers() -> ComponentDescriptor: + return ComponentDescriptor( + "Ellipses2D:centers", + archetype=Ellipses2D.NAME, + component_type=components.Position2DBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_colors() -> ComponentDescriptor: + return ComponentDescriptor( + "Ellipses2D:colors", + archetype=Ellipses2D.NAME, + component_type=components.ColorBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_line_radii() -> ComponentDescriptor: + return ComponentDescriptor( + "Ellipses2D:line_radii", + archetype=Ellipses2D.NAME, + component_type=components.RadiusBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_labels() -> ComponentDescriptor: + return ComponentDescriptor( + "Ellipses2D:labels", + archetype=Ellipses2D.NAME, + component_type=components.TextBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_show_labels() -> ComponentDescriptor: + return ComponentDescriptor( + "Ellipses2D:show_labels", + archetype=Ellipses2D.NAME, + component_type=components.ShowLabelsBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_draw_order() -> ComponentDescriptor: + return ComponentDescriptor( + "Ellipses2D:draw_order", + archetype=Ellipses2D.NAME, + component_type=components.DrawOrderBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_class_ids() -> ComponentDescriptor: + return ComponentDescriptor( + "Ellipses2D:class_ids", + archetype=Ellipses2D.NAME, + component_type=components.ClassIdBatch._COMPONENT_TYPE, + ) + + @classmethod + def columns( + cls, + *, + half_sizes: datatypes.Vec2DArrayLike | None = None, + centers: datatypes.Vec2DArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + line_radii: datatypes.Float32ArrayLike | None = None, + labels: datatypes.Utf8ArrayLike | None = None, + show_labels: datatypes.BoolArrayLike | None = None, + draw_order: datatypes.Float32ArrayLike | None = None, + class_ids: datatypes.ClassIdArrayLike | None = None, + ) -> ComponentColumnList: + """ + Construct a new column-oriented component bundle. + + This makes it possible to use `rr.send_columns` to send columnar data directly into Rerun. + + The returned columns will be partitioned into unit-length sub-batches by default. + Use `ComponentColumnList.partition` to repartition the data as needed. + + Parameters + ---------- + half_sizes: + All half-extents (semi-axes) that make up the batch of ellipses. + centers: + Optional center positions of the ellipses. + colors: + Optional colors for the ellipses. + line_radii: + Optional radii for the lines that make up the ellipses. + labels: + Optional text labels for the ellipses. + + If there's a single label present, it will be placed at the center of the entity. + Otherwise, each instance will have its own label. + show_labels: + Whether the text labels should be shown. + + If not set, labels will automatically appear when there is exactly one label for this entity + or the number of instances on this entity is under a certain threshold. + draw_order: + An optional floating point value that specifies the 2D drawing order. + + Objects with higher values are drawn on top of those with lower values. + Defaults to `10.0`. + class_ids: + Optional [`components.ClassId`][rerun.components.ClassId]s for the ellipses. + + The [`components.ClassId`][rerun.components.ClassId] provides colors and labels if not specified explicitly. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + inst.__attrs_init__( + half_sizes=half_sizes, + centers=centers, + colors=colors, + line_radii=line_radii, + labels=labels, + show_labels=show_labels, + draw_order=draw_order, + class_ids=class_ids, + ) + + batches = inst.as_component_batches() + if len(batches) == 0: + return ComponentColumnList([]) + + kwargs = { + "Ellipses2D:half_sizes": half_sizes, + "Ellipses2D:centers": centers, + "Ellipses2D:colors": colors, + "Ellipses2D:line_radii": line_radii, + "Ellipses2D:labels": labels, + "Ellipses2D:show_labels": show_labels, + "Ellipses2D:draw_order": draw_order, + "Ellipses2D:class_ids": class_ids, + } + columns = [] + + for batch in batches: + arrow_array = batch.as_arrow_array() + + # For primitive arrays and fixed size list arrays, we infer partition size from the input shape. + if pa.types.is_primitive(arrow_array.type) or pa.types.is_fixed_size_list(arrow_array.type): + param = kwargs[batch.component_descriptor().component] # type: ignore[index] + shape = np.shape(param) # type: ignore[arg-type] + num_rows = shape[0] if len(shape) >= 1 else 1 # type: ignore[redundant-expr,misc] + + if pa.types.is_fixed_size_list(arrow_array.type): + elem_flat_len = int(np.prod(shape[1:])) if len(shape) > 1 else 1 # type: ignore[redundant-expr,misc] + if arrow_array.type.list_size == elem_flat_len: + # The product of the last dimensions of the shape are equal to the size of the fixed size list array, + # so we have `num_rows` single element batches (each element is a fixed sized list). + batch_length = 1 + else: + batch_length = shape[1] if len(shape) > 1 else 1 # type: ignore[redundant-expr,misc] + else: + # For primitive types, derive batch_length from the actual arrow array length + # since the input shape can be misleading (e.g. colors [R,G,B] -> single uint32). + batch_length = len(arrow_array) // num_rows if num_rows > 0 else 1 + + sizes = batch_length * np.ones(num_rows) + else: + # For non-primitive types, default to partitioning each element separately. + sizes = np.ones(len(arrow_array)) + + columns.append(batch.partition(sizes)) + + return ComponentColumnList(columns) + + half_sizes: components.HalfSize2DBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.HalfSize2DBatch._converter, # type: ignore[misc] + ) + # All half-extents (semi-axes) that make up the batch of ellipses. + # + # (Docstring intentionally commented out to hide this field from the docs) + + centers: components.Position2DBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.Position2DBatch._converter, # type: ignore[misc] + ) + # Optional center positions of the ellipses. + # + # (Docstring intentionally commented out to hide this field from the docs) + + colors: components.ColorBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.ColorBatch._converter, # type: ignore[misc] + ) + # Optional colors for the ellipses. + # + # (Docstring intentionally commented out to hide this field from the docs) + + line_radii: components.RadiusBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.RadiusBatch._converter, # type: ignore[misc] + ) + # Optional radii for the lines that make up the ellipses. + # + # (Docstring intentionally commented out to hide this field from the docs) + + labels: components.TextBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.TextBatch._converter, # type: ignore[misc] + ) + # Optional text labels for the ellipses. + # + # If there's a single label present, it will be placed at the center of the entity. + # Otherwise, each instance will have its own label. + # + # (Docstring intentionally commented out to hide this field from the docs) + + show_labels: components.ShowLabelsBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.ShowLabelsBatch._converter, # type: ignore[misc] + ) + # Whether the text labels should be shown. + # + # If not set, labels will automatically appear when there is exactly one label for this entity + # or the number of instances on this entity is under a certain threshold. + # + # (Docstring intentionally commented out to hide this field from the docs) + + draw_order: components.DrawOrderBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.DrawOrderBatch._converter, # type: ignore[misc] + ) + # An optional floating point value that specifies the 2D drawing order. + # + # Objects with higher values are drawn on top of those with lower values. + # Defaults to `10.0`. + # + # (Docstring intentionally commented out to hide this field from the docs) + + class_ids: components.ClassIdBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.ClassIdBatch._converter, # type: ignore[misc] + ) + # Optional [`components.ClassId`][rerun.components.ClassId]s for the ellipses. + # + # The [`components.ClassId`][rerun.components.ClassId] provides colors and labels if not specified explicitly. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] + + def visualizer(self, *, mappings: list[VisualizerComponentMappingLike] | None = None) -> Visualizer: + """ + Creates a visualizer for this archetype, using all currently set values as overrides. + + Parameters + ---------- + mappings: + Optional component mappings to control how the visualizer sources its data. + + ⚠️ **Experimental**: Component mappings are an experimental feature and may change. + See https://github.com/rerun-io/rerun/issues/10631 for more information. + + """ + return Visualizer("Ellipses2D", overrides=self.as_component_batches(), mappings=mappings) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/ellipses2d_ext.py b/rerun_py/rerun_sdk/rerun/archetypes/ellipses2d_ext.py new file mode 100644 index 000000000000..45ef96a948b8 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/archetypes/ellipses2d_ext.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from ..error_utils import catch_and_log_exceptions + +if TYPE_CHECKING: + from .. import datatypes + + +class Ellipses2DExt: + """Extension for [Ellipses2D][rerun.archetypes.Ellipses2D].""" + + def __init__( + self: Any, + *, + half_sizes: datatypes.Vec2DArrayLike | None = None, + centers: datatypes.Vec2DArrayLike | None = None, + line_radii: datatypes.Float32ArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + labels: datatypes.Utf8ArrayLike | None = None, + show_labels: datatypes.BoolLike | None = None, + draw_order: datatypes.Float32ArrayLike | None = None, + class_ids: datatypes.ClassIdArrayLike | None = None, + ) -> None: + """ + Create a new instance of the Ellipses2D archetype. + + Parameters + ---------- + half_sizes: + All half-extents (semi-axes) that make up the batch of ellipses. + centers: + Optional center positions of the ellipses. + colors: + Optional colors for the ellipses. + line_radii: + Optional radii for the lines that make up the ellipses. + labels: + Optional text labels for the ellipses. + show_labels: + Optional choice of whether the text labels should be shown by default. + draw_order: + An optional floating point value that specifies the 2D drawing order. + Objects with higher values are drawn on top of those with lower values. + + The default for 2D ellipses is 10.0. + class_ids: + Optional `ClassId`s for the ellipses. + + The class ID provides colors and labels if not specified explicitly. + + """ + + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__( + half_sizes=half_sizes, + centers=centers, + line_radii=line_radii, + colors=colors, + labels=labels, + show_labels=show_labels, + draw_order=draw_order, + class_ids=class_ids, + ) + return + + self.__attrs_clear__() diff --git a/rerun_py/rerun_sdk/rerun/archetypes/encoded_depth_image.py b/rerun_py/rerun_sdk/rerun/archetypes/encoded_depth_image.py index d8ad544a85c5..6edd058998d8 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/encoded_depth_image.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/encoded_depth_image.py @@ -45,7 +45,9 @@ class EncodedDepthImage(Archetype, VisualizableArchetype): import rerun as rr if len(sys.argv) < 2: - print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + print( + f"Usage: {sys.argv[0]} ", file=sys.stderr + ) sys.exit(1) depth_path = Path(sys.argv[1]) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/graph_edges.py b/rerun_py/rerun_sdk/rerun/archetypes/graph_edges.py index 695695b7b48b..72a7c0581864 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/graph_edges.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/graph_edges.py @@ -48,7 +48,9 @@ class GraphEdges(Archetype, VisualizableArchetype): positions=[(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)], labels=["A", "B", "C"], ), - rr.GraphEdges(edges=[("a", "b"), ("b", "c"), ("c", "a")], graph_type="directed"), + rr.GraphEdges( + edges=[("a", "b"), ("b", "c"), ("c", "a")], graph_type="directed" + ), ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/graph_nodes.py b/rerun_py/rerun_sdk/rerun/archetypes/graph_nodes.py index ba9f1b26c822..03798e988226 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/graph_nodes.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/graph_nodes.py @@ -46,7 +46,9 @@ class GraphNodes(Archetype, VisualizableArchetype): positions=[(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)], labels=["A", "B", "C"], ), - rr.GraphEdges(edges=[("a", "b"), ("b", "c"), ("c", "a")], graph_type="directed"), + rr.GraphEdges( + edges=[("a", "b"), ("b", "c"), ("c", "a")], graph_type="directed" + ), ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/grid_map.py b/rerun_py/rerun_sdk/rerun/archetypes/grid_map.py index b1f040d5195d..e72174a01eee 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/grid_map.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/grid_map.py @@ -29,10 +29,8 @@ class GridMap(Archetype): This archetype is intended for robotics applications like occupancy maps or navigation costmaps. - ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** - - Example - ------- + Examples + -------- ### Simple occupancy grid map: ```python import numpy as np @@ -42,8 +40,8 @@ class GridMap(Archetype): width, height = 64, 64 cell_size = 0.1 - # Create a synthetic image with ROS `nav_msgs/OccupancyGrid` cell value conventions: - # -1 (255) unknown, 0 free, 100 occupied. + # Create a synthetic image with ROS `nav_msgs/OccupancyGrid` cell value + # conventions: -1 (255) unknown, 0 free, 100 occupied. grid = np.full((height, width), -1, dtype=np.int8) grid[8:56, 8:56] = 0 grid[20:44, 20:44] = 100 @@ -61,12 +59,90 @@ class GridMap(Archetype): channel_datatype="U8", ), cell_size=cell_size, - translation=[-(width * cell_size) / 2.0, -(height * cell_size) / 2.0, 0.0], + translation=[ + -(width * cell_size) / 2.0, + -(height * cell_size) / 2.0, + 0.0, + ], colormap=rr.components.Colormap.RvizMap, ), ) ``` + ### Log a grid map at a specific pose: + ```python + import math + from pathlib import Path + + from PIL import Image as PILImage + + import rerun as rr + import rerun.blueprint as rrb + + rr.init("rerun_example_grid_map_pose", spawn=True) + + # Log the transform for the map origin. + # Here we use ROS TF-style parent & child frame names. + rr.log( + "/tf", + rr.Transform3D( + translation=[1.0, 2.0, 0.0], + rotation_axis_angle=rr.components.RotationAxisAngle( + [0, 0, 1], -math.pi / 3 + ), + parent_frame="world", + child_frame="map", + ), + static=True, + ) + + # We use a dummy image for the map in this example. + image = PILImage.open(Path(__file__).parent / "ferris.png").convert("RGBA") + + # Log the grid map at the map origin. + rr.log( + "demo_map", + rr.CoordinateFrame("map"), + rr.GridMap( + data=image.tobytes(), + format=rr.components.ImageFormat( + width=image.size[0], + height=image.size[1], + color_model="RGBA", + channel_datatype="U8", + ), + opacity=0.5, + # The size of a pixel in scene units. + cell_size=0.01, + # Specify the pose of the lower-left image corner relative to the + # map frame, in scene units. + translation=[1.1, -1.6, 0.0], + rotation_axis_angle=rr.components.RotationAxisAngle( + [0, 0, 1], math.pi / 4.0 + ), + ), + ) + + # Show transform axes with frame names. + rr.send_blueprint( + rrb.Spatial3DView( + origin="/", + overrides={ + "/tf": [rr.TransformAxes3D(axis_length=0.5, show_frame=True)], + }, + ) + ) + ``` +
+ + + + + + + +
+ """ NAME: ClassVar[str] = "rerun.archetypes.GridMap" diff --git a/rerun_py/rerun_sdk/rerun/archetypes/image.py b/rerun_py/rerun_sdk/rerun/archetypes/image.py index 0ca549bbb8de..d4c0cf9193a8 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/image.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/image.py @@ -81,16 +81,31 @@ class Image(ImageExt, Archetype, VisualizableArchetype): rr.init("rerun_example_image_formats", spawn=True) # Simple gradient image, logged in different formats. - image = np.array([[[x, min(255, x + y), y] for x in range(256)] for y in range(256)], dtype=np.uint8) + image = np.array( + [[[x, min(255, x + y), y] for x in range(256)] for y in range(256)], + dtype=np.uint8, + ) rr.log("image_rgb", rr.Image(image)) - rr.log("image_green_only", rr.Image(image[:, :, 1], color_model="l")) # Luminance only + rr.log( + "image_green_only", rr.Image(image[:, :, 1], color_model="l") + ) # Luminance only rr.log("image_bgr", rr.Image(image[:, :, ::-1], color_model="bgr")) # BGR # New image with Separate Y/U/V planes with 4:2:2 chroma downsampling y = bytes([128 for y in range(256) for x in range(256)]) - u = bytes([x * 2 for y in range(256) for x in range(128)]) # Half horizontal resolution for chroma. + u = bytes([ + x * 2 for y in range(256) for x in range(128) + ]) # Half horizontal resolution for chroma. v = bytes([y for y in range(256) for x in range(128)]) - rr.log("image_yuv422", rr.Image(bytes=y + u + v, width=256, height=256, pixel_format=rr.PixelFormat.Y_U_V16_FullRange)) + rr.log( + "image_yuv422", + rr.Image( + bytes=y + u + v, + width=256, + height=256, + pixel_format=rr.PixelFormat.Y_U_V16_FullRange, + ), + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/instance_poses3d.py b/rerun_py/rerun_sdk/rerun/archetypes/instance_poses3d.py index bb90f3f0c363..3ec000cc1e0f 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/instance_poses3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/instance_poses3d.py @@ -69,10 +69,20 @@ class InstancePoses3D(Archetype): rr.set_time("frame", sequence=i) # Log a regular transform which affects both the box and the points. - rr.log("world/box", rr.Transform3D(rotation_axis_angle=rr.RotationAxisAngle([0, 0, 1], angle=rr.Angle(deg=i * 2)))) + rr.log( + "world/box", + rr.Transform3D( + rotation_axis_angle=rr.RotationAxisAngle( + [0, 0, 1], angle=rr.Angle(deg=i * 2) + ) + ), + ) # Log an instance pose which affects only the box. - rr.log("world/box", rr.InstancePoses3D(translations=[0, 0, abs(i * 0.1 - 5.0) - 5.0])) + rr.log( + "world/box", + rr.InstancePoses3D(translations=[0, 0, abs(i * 0.1 - 5.0) - 5.0]), + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/line_strips2d.py b/rerun_py/rerun_sdk/rerun/archetypes/line_strips2d.py index a53aa7638fae..5d3c0fad517c 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/line_strips2d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/line_strips2d.py @@ -54,7 +54,11 @@ class LineStrips2D(Archetype, VisualizableArchetype): ) # Set view bounds: - rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 7], y_range=[-3, 6]))) + rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 7], y_range=[-3, 6]) + ) + ) ```
@@ -86,21 +90,27 @@ class LineStrips2D(Archetype, VisualizableArchetype): ) # A red line with a ui point radii of 5. - # UI points are independent of zooming in Views, but are sensitive to the application UI scaling. + # UI points are independent of zooming in Views, but are sensitive to the + # application UI scaling. # For 100% ui scaling, UI points are equal to pixels. points = [[3, 0], [3, 1], [4, 0], [4, 1]] rr.log( "ui_points_line", rr.LineStrips2D( [points], - # rr.Radius.ui_points produces radii that the viewer interprets as given in ui points. + # rr.Radius.ui_points produces radii that the viewer interprets + # as given in ui points. radii=rr.Radius.ui_points(5.0), colors=[255, 0, 0], ), ) # Set view bounds: - rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 5], y_range=[-1, 2]))) + rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 5], y_range=[-1, 2]) + ) + ) ``` """ diff --git a/rerun_py/rerun_sdk/rerun/archetypes/line_strips3d.py b/rerun_py/rerun_sdk/rerun/archetypes/line_strips3d.py index e632f2953b17..b7efbea7a679 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/line_strips3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/line_strips3d.py @@ -95,14 +95,16 @@ class LineStrips3D(Archetype, VisualizableArchetype): ) # A red line with a ui point radii of 5. - # UI points are independent of zooming in Views, but are sensitive to the application UI scaling. + # UI points are independent of zooming in Views, but are sensitive to the + # application UI scaling. # For 100% ui scaling, UI points are equal to pixels. points = [[3, 0, 0], [3, 0, 1], [4, 0, 0], [4, 0, 1]] rr.log( "ui_points_line", rr.LineStrips3D( [points], - # rr.Radius.ui_points produces radii that the viewer interprets as given in ui points. + # rr.Radius.ui_points produces radii that the viewer interprets + # as given in ui points. radii=rr.Radius.ui_points(5.0), colors=[255, 0, 0], ), @@ -118,6 +120,63 @@ class LineStrips3D(Archetype, VisualizableArchetype):
+ ### Time-windowed trails (e.g. Trajectories): + ```python + import math + + import rerun as rr + import rerun.blueprint as rrb + + + def point(t: float, phase: float) -> list[float]: + # Sample a point on a helix. + angle = 0.5 * t + phase + return [math.cos(angle), math.sin(angle), 0.1 * t] + + + rr.init("rerun_example_line_strips3d_time_window", spawn=True) + + # Configure the visible time range in the blueprint. + # You can also override this per entity. + rr.send_blueprint( + rrb.Spatial3DView( + origin="/", + time_ranges=rrb.VisibleTimeRange( + "time", + start=rrb.TimeRangeBoundary.cursor_relative(seconds=-5.0), + end=rrb.TimeRangeBoundary.cursor_relative(), + ), + ) + ) + + # Log the line strip increments with timestamps. + for i in range(600): + t0 = i / 30.0 + t1 = (i + 1) / 30.0 + + rr.set_time("time", duration=t1) + rr.log( + "trails", + rr.LineStrips3D( + [ + [point(t0, 0.0), point(t1, 0.0)], + [point(t0, math.pi), point(t1, math.pi)], + ], + colors=[[255, 120, 0], [0, 180, 255]], + radii=0.02, + ), + ) + ``` +
+ + + + + + + +
+ """ NAME: ClassVar[str] = "rerun.archetypes.LineStrips3D" @@ -143,6 +202,8 @@ def __init__( Optional radii for the line strips. colors: Optional colors for the line strips. + + The alpha channel is ignored. labels: Optional text labels for the line strips. @@ -211,6 +272,8 @@ def from_fields( Optional radii for the line strips. colors: Optional colors for the line strips. + + The alpha channel is ignored. labels: Optional text labels for the line strips. @@ -328,6 +391,8 @@ def columns( Optional radii for the line strips. colors: Optional colors for the line strips. + + The alpha channel is ignored. labels: Optional text labels for the line strips. @@ -426,6 +491,8 @@ def columns( ) # Optional colors for the line strips. # + # The alpha channel is ignored. + # # (Docstring intentionally commented out to hide this field from the docs) labels: components.TextBatch | None = field( diff --git a/rerun_py/rerun_sdk/rerun/archetypes/mcap_statistics.py b/rerun_py/rerun_sdk/rerun/archetypes/mcap_statistics.py index e0cbbde1268a..5107f5761626 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/mcap_statistics.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/mcap_statistics.py @@ -25,7 +25,7 @@ @define(str=False, repr=False, init=False) class McapStatistics(Archetype): """ - **Archetype**: Recording-level statistics about an MCAP file, logged as a part of [`archetypes.RecordingInfo`][rerun.archetypes.RecordingInfo]. + **Archetype**: Recording-level statistics about an MCAP file. This archetype contains summary information about an entire MCAP recording, including counts of messages, schemas, channels, and other records, as well as timing information diff --git a/rerun_py/rerun_sdk/rerun/archetypes/mesh3d.py b/rerun_py/rerun_sdk/rerun/archetypes/mesh3d.py index ca16f540f592..52acc32e84c9 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/mesh3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/mesh3d.py @@ -96,7 +96,9 @@ class Mesh3D(Mesh3DExt, Archetype, VisualizableArchetype): "shape", rr.InstancePoses3D( translations=[[2, 0, 0], [0, 2, 0], [0, -2, 0], [-2, 0, 0]], - rotation_axis_angles=rr.RotationAxisAngle([0, 0, 1], rr.Angle(deg=i * 2)), + rotation_axis_angles=rr.RotationAxisAngle( + [0, 0, 1], rr.Angle(deg=i * 2) + ), ), ) ``` diff --git a/rerun_py/rerun_sdk/rerun/archetypes/pinhole.py b/rerun_py/rerun_sdk/rerun/archetypes/pinhole.py index 9c5b2393725e..c1ebf50478f5 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/pinhole.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/pinhole.py @@ -81,7 +81,12 @@ class Pinhole(PinholeExt, Archetype, VisualizableArchetype): ), ) - rr.log("world/points", rr.Points3D([(0.0, 0.0, -0.5), (0.1, 0.1, -0.5), (-0.1, -0.1, -0.5)], radii=0.025)) + rr.log( + "world/points", + rr.Points3D( + [(0.0, 0.0, -0.5), (0.1, 0.1, -0.5), (-0.1, -0.1, -0.5)], radii=0.025 + ), + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/points2d.py b/rerun_py/rerun_sdk/rerun/archetypes/points2d.py index 1f701b44f8d2..28b31392be7b 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/points2d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/points2d.py @@ -51,7 +51,11 @@ class Points2D(Points2DExt, Archetype, VisualizableArchetype): rr.log("random", rr.Points2D(positions, colors=colors, radii=radii)) # Set view bounds: - rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-4, 4], y_range=[-4, 4]))) + rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-4, 4], y_range=[-4, 4]) + ) + ) ```
@@ -82,20 +86,26 @@ class Points2D(Points2DExt, Archetype, VisualizableArchetype): ) # Two red points with ui point radii of 40 and 60. - # UI points are independent of zooming in Views, but are sensitive to the application UI scaling. + # UI points are independent of zooming in Views, but are sensitive to the + # application UI scaling. # For 100% ui scaling, UI points are equal to pixels. rr.log( "ui_points", rr.Points2D( [[1, 0], [1, 1]], - # rr.Radius.ui_points produces radii that the viewer interprets as given in ui points. + # rr.Radius.ui_points produces radii that the viewer interprets + # as given in ui points. radii=rr.Radius.ui_points([40.0, 60.0]), colors=[255, 0, 0], ), ) # Set view bounds: - rr.send_blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]))) + rr.send_blueprint( + rrb.Spatial2DView( + visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]) + ) + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/points3d.py b/rerun_py/rerun_sdk/rerun/archetypes/points3d.py index 17f3921b6415..40f28650110e 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/points3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/points3d.py @@ -62,7 +62,8 @@ class Points3D(Points3DExt, Archetype, VisualizableArchetype): rr.init("rerun_example_points3d_row_updates", spawn=True) - # Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. + # Prepare a point cloud that evolves over 5 timesteps, changing the + # number of points in the process. times = np.arange(10, 15, 1.0) # fmt: off positions = [ @@ -74,13 +75,16 @@ class Points3D(Points3DExt, Archetype, VisualizableArchetype): ] # fmt: on - # At each timestep, all points in the cloud share the same but changing color and radius. + # At each timestep, all points in the cloud share the same but changing + # color and radius. colors = [0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF] radii = [0.05, 0.01, 0.2, 0.1, 0.3] for i in range(5): rr.set_time("time", duration=10 + i) - rr.log("points", rr.Points3D(positions[i], colors=colors[i], radii=radii[i])) + rr.log( + "points", rr.Points3D(positions[i], colors=colors[i], radii=radii[i]) + ) ```
@@ -102,7 +106,8 @@ class Points3D(Points3DExt, Archetype, VisualizableArchetype): rr.init("rerun_example_points3d_column_updates", spawn=True) - # Prepare a point cloud that evolves over 5 timesteps, changing the number of points in the process. + # Prepare a point cloud that evolves over 5 timesteps, changing the + # number of points in the process. times = np.arange(10, 15, 1.0) # fmt: off positions = [ @@ -114,7 +119,8 @@ class Points3D(Points3DExt, Archetype, VisualizableArchetype): ] # fmt: on - # At each timestep, all points in the cloud share the same but changing color and radius. + # At each timestep, all points in the cloud share the same but changing + # color and radius. colors = [0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0x00FFFFFF] radii = [0.05, 0.01, 0.2, 0.1, 0.3] @@ -122,7 +128,9 @@ class Points3D(Points3DExt, Archetype, VisualizableArchetype): "points", indexes=[rr.TimeColumn("time", duration=times)], columns=[ - *rr.Points3D.columns(positions=positions).partition(lengths=[2, 4, 4, 3, 4]), + *rr.Points3D.columns(positions=positions).partition( + lengths=[2, 4, 4, 3, 4] + ), *rr.Points3D.columns(colors=colors, radii=radii), ], ) @@ -158,7 +166,10 @@ class Points3D(Points3DExt, Archetype, VisualizableArchetype): # Update the positions and radii, and clear everything else in the process. rr.set_time("frame", sequence=20) - rr.log("points", rr.Points3D.from_fields(clear_unset=True, positions=positions, radii=0.3)) + rr.log( + "points", + rr.Points3D.from_fields(clear_unset=True, positions=positions, radii=0.3), + ) ```
@@ -184,6 +195,7 @@ def __attrs_clear__(self) -> None: colors=None, labels=None, show_labels=None, + point_shading=None, class_ids=None, keypoint_ids=None, ) @@ -205,6 +217,7 @@ def from_fields( colors: datatypes.Rgba32ArrayLike | None = None, labels: datatypes.Utf8ArrayLike | None = None, show_labels: datatypes.BoolLike | None = None, + point_shading: components.PointShadingLike | None = None, class_ids: datatypes.ClassIdArrayLike | None = None, keypoint_ids: datatypes.KeypointIdArrayLike | None = None, ) -> Points3D: @@ -224,6 +237,9 @@ def from_fields( The colors are interpreted as RGB or RGBA in sRGB gamma-space, As either 0-1 floats or 0-255 integers, with separate alpha. + + By default, the alpha channel affects brightness rather than transparency. + TODO(#1611): To use the alpha channel for transparency, enable the experimental "Transparent point clouds" feature flag. labels: Optional text labels for the points. @@ -234,6 +250,10 @@ def from_fields( If not set, labels will automatically appear when there is exactly one label for this entity or the number of instances on this entity is under a certain threshold. + point_shading: + How points should be shaded. + + If not set, points are rendered with [`components.PointShading.Gradient`][rerun.components.PointShading.Gradient] by default. class_ids: Optional class Ids for the points. @@ -258,6 +278,7 @@ def from_fields( "colors": colors, "labels": labels, "show_labels": show_labels, + "point_shading": point_shading, "class_ids": class_ids, "keypoint_ids": keypoint_ids, } @@ -316,6 +337,14 @@ def descriptor_show_labels() -> ComponentDescriptor: component_type=components.ShowLabelsBatch._COMPONENT_TYPE, ) + @staticmethod + def descriptor_point_shading() -> ComponentDescriptor: + return ComponentDescriptor( + "Points3D:point_shading", + archetype=Points3D.NAME, + component_type=components.PointShadingBatch._COMPONENT_TYPE, + ) + @staticmethod def descriptor_class_ids() -> ComponentDescriptor: return ComponentDescriptor( @@ -341,6 +370,7 @@ def columns( colors: datatypes.Rgba32ArrayLike | None = None, labels: datatypes.Utf8ArrayLike | None = None, show_labels: datatypes.BoolArrayLike | None = None, + point_shading: components.PointShadingArrayLike | None = None, class_ids: datatypes.ClassIdArrayLike | None = None, keypoint_ids: datatypes.KeypointIdArrayLike | None = None, ) -> ComponentColumnList: @@ -363,6 +393,9 @@ def columns( The colors are interpreted as RGB or RGBA in sRGB gamma-space, As either 0-1 floats or 0-255 integers, with separate alpha. + + By default, the alpha channel affects brightness rather than transparency. + TODO(#1611): To use the alpha channel for transparency, enable the experimental "Transparent point clouds" feature flag. labels: Optional text labels for the points. @@ -373,6 +406,10 @@ def columns( If not set, labels will automatically appear when there is exactly one label for this entity or the number of instances on this entity is under a certain threshold. + point_shading: + How points should be shaded. + + If not set, points are rendered with [`components.PointShading.Gradient`][rerun.components.PointShading.Gradient] by default. class_ids: Optional class Ids for the points. @@ -397,6 +434,7 @@ def columns( colors=colors, labels=labels, show_labels=show_labels, + point_shading=point_shading, class_ids=class_ids, keypoint_ids=keypoint_ids, ) @@ -411,6 +449,7 @@ def columns( "Points3D:colors": colors, "Points3D:labels": labels, "Points3D:show_labels": show_labels, + "Points3D:point_shading": point_shading, "Points3D:class_ids": class_ids, "Points3D:keypoint_ids": keypoint_ids, } @@ -475,6 +514,9 @@ def columns( # The colors are interpreted as RGB or RGBA in sRGB gamma-space, # As either 0-1 floats or 0-255 integers, with separate alpha. # + # By default, the alpha channel affects brightness rather than transparency. + # TODO(#1611): To use the alpha channel for transparency, enable the experimental "Transparent point clouds" feature flag. + # # (Docstring intentionally commented out to hide this field from the docs) labels: components.TextBatch | None = field( @@ -501,6 +543,17 @@ def columns( # # (Docstring intentionally commented out to hide this field from the docs) + point_shading: components.PointShadingBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.PointShadingBatch._converter, # type: ignore[misc] + ) + # How points should be shaded. + # + # If not set, points are rendered with [`components.PointShading.Gradient`][rerun.components.PointShading.Gradient] by default. + # + # (Docstring intentionally commented out to hide this field from the docs) + class_ids: components.ClassIdBatch | None = field( metadata={"component": True}, default=None, diff --git a/rerun_py/rerun_sdk/rerun/archetypes/points3d_ext.py b/rerun_py/rerun_sdk/rerun/archetypes/points3d_ext.py index 12ae4c8037a8..af7c87ffdafa 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/points3d_ext.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/points3d_ext.py @@ -5,7 +5,7 @@ from ..error_utils import catch_and_log_exceptions if TYPE_CHECKING: - from .. import datatypes + from .. import components, datatypes class Points3DExt: @@ -19,6 +19,7 @@ def __init__( colors: datatypes.Rgba32ArrayLike | None = None, labels: datatypes.Utf8ArrayLike | None = None, show_labels: datatypes.BoolLike | None = None, + point_shading: components.PointShadingLike | None = None, class_ids: datatypes.ClassIdArrayLike | None = None, keypoint_ids: datatypes.KeypointIdArrayLike | None = None, ) -> None: @@ -40,6 +41,8 @@ def __init__( Optional text labels for the points. show_labels: Optional choice of whether the text labels should be shown by default. + point_shading: + Optional choice of whether points should be shaded like spheres. class_ids: Optional class Ids for the points. @@ -66,6 +69,7 @@ def __init__( colors=colors, labels=labels, show_labels=show_labels, + point_shading=point_shading, class_ids=class_ids, keypoint_ids=keypoint_ids, ) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/segmentation_image.py b/rerun_py/rerun_sdk/rerun/archetypes/segmentation_image.py index 21d14f927f99..b315a1f0c618 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/segmentation_image.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/segmentation_image.py @@ -55,7 +55,11 @@ class SegmentationImage(SegmentationImageExt, Archetype, VisualizableArchetype): rr.init("rerun_example_segmentation_image", spawn=True) # Assign a label and color to each class - rr.log("/", rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), static=True) + rr.log( + "/", + rr.AnnotationContext([(1, "red", (255, 0, 0)), (2, "green", (0, 255, 0))]), + static=True, + ) rr.log("image", rr.SegmentationImage(image)) ``` diff --git a/rerun_py/rerun_sdk/rerun/archetypes/series_lines.py b/rerun_py/rerun_sdk/rerun/archetypes/series_lines.py index 0aa50cc4e007..13d17c8ce10d 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/series_lines.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/series_lines.py @@ -49,10 +49,20 @@ class SeriesLines(Archetype, VisualizableArchetype): rr.init("rerun_example_series_line_style", spawn=True) # Set up plot styling: - # They are logged as static as they don't change over time and apply to all timelines. - # Log two lines series under a shared root so that they show in the same plot by default. - rr.log("trig/sin", rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)", widths=2), static=True) - rr.log("trig/cos", rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)", widths=4), static=True) + # They are logged as static as they don't change over time and apply to + # all timelines. + # Log two lines series under a shared root so that they show in the same + # plot by default. + rr.log( + "trig/sin", + rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)", widths=2), + static=True, + ) + rr.log( + "trig/cos", + rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)", widths=4), + static=True, + ) # Log the data on a timeline called "step". for t in range(int(tau * 2 * 100.0)): diff --git a/rerun_py/rerun_sdk/rerun/archetypes/series_points.py b/rerun_py/rerun_sdk/rerun/archetypes/series_points.py index c4f0b181a9bc..3346e344fdc4 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/series_points.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/series_points.py @@ -50,8 +50,9 @@ class SeriesPoints(SeriesPointsExt, Archetype, VisualizableArchetype): rr.init("rerun_example_series_point_style", spawn=True) # Set up plot styling: - # They are logged as static as they don't change over time and apply to all timelines. - # Log two point series under a shared root so that they show in the same plot by default. + # They are logged as static as they don't change over time and apply to all + # timelines. Log two point series under a shared root so that they show in the + # same plot by default. rr.log( "trig/sin", rr.SeriesPoints( diff --git a/rerun_py/rerun_sdk/rerun/archetypes/status.py b/rerun_py/rerun_sdk/rerun/archetypes/state_change.py similarity index 60% rename from rerun_py/rerun_sdk/rerun/archetypes/status.py rename to rerun_py/rerun_sdk/rerun/archetypes/state_change.py index e09cd454ac3e..1bb55e0c684c 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/status.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/state_change.py @@ -1,7 +1,7 @@ # DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs -# Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/status.fbs". +# Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/state_change.fbs". -# You can extend this class by creating a "StatusExt" class in "status_ext.py". +# You can extend this class by creating a "StateChangeExt" class in "state_change_ext.py". from __future__ import annotations @@ -23,81 +23,85 @@ if TYPE_CHECKING: from ..blueprint.datatypes import VisualizerComponentMappingLike -__all__ = ["Status"] +__all__ = ["StateChange"] @define(str=False, repr=False, init=False) -class Status(Archetype, VisualizableArchetype): +class StateChange(Archetype, VisualizableArchetype): """ - **Archetype**: A status update, representing a change in the status of an entity. + **Archetype**: A state change, representing a transition of an entity into a new state. Useful for representing discrete state machines, mode transitions, or - status changes over time. Each logged [`archetypes.Status`][rerun.archetypes.Status] marks a new status - at the given time. A `null` status is ignored by the Status view. + state changes over time. Each logged [`archetypes.StateChange`][rerun.archetypes.StateChange] marks a new state + at the given time. A `null` state resets the state, showing a gap in the state timeline view. - The Status view displays these as horizontal colored lanes over time. - - ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + The state timeline view displays these as horizontal colored lanes over time. Example ------- - ### Status changes over time: + ### State changes over time: ```python - # Log a `Status`. + # Log a `StateChange`. import rerun as rr - rr.init("rerun_example_status", spawn=True) + rr.init("rerun_example_state_change", spawn=True) rr.set_time("step", sequence=0) - rr.log("door", rr.Status(status="open")) + rr.log("door", rr.StateChange(state="open")) rr.set_time("step", sequence=1) - rr.log("door", rr.Status(status="closed")) + rr.log("door", rr.StateChange(state="closed")) rr.set_time("step", sequence=2) - rr.log("door", rr.Status(status="open")) + rr.log("door", rr.StateChange(state="open")) ```
- - - - - + + + + +
""" - NAME: ClassVar[str] = "rerun.archetypes.Status" + NAME: ClassVar[str] = "rerun.archetypes.StateChange" - def __init__(self: Any, *, status: datatypes.Utf8Like | None = None) -> None: + def __init__(self: Any, *, state: datatypes.Utf8ArrayLike | None = None) -> None: """ - Create a new instance of the Status archetype. + Create a new instance of the StateChange archetype. Parameters ---------- - status: - The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array. + state: + The new state values; each instance gets its own lane in the state timeline view. + + A reset ends the previous state and shows a gap in the state timeline view until the + next state. An empty string, a null array entry, and an empty state array (e.g. from + clearing the field) all act as resets. + + The length of the state array should not change over time. """ - # You can define your own __init__ function as a member of StatusExt in status_ext.py + # You can define your own __init__ function as a member of StateChangeExt in state_change_ext.py with catch_and_log_exceptions(context=self.__class__.__name__): - self.__attrs_init__(status=status) + self.__attrs_init__(state=state) return self.__attrs_clear__() def __attrs_clear__(self) -> None: """Convenience method for calling `__attrs_init__` with all `None`s.""" self.__attrs_init__( - status=None, + state=None, ) @classmethod - def _clear(cls) -> Status: - """Produce an empty Status, bypassing `__init__`.""" + def _clear(cls) -> StateChange: + """Produce an empty StateChange, bypassing `__init__`.""" inst = cls.__new__(cls) inst.__attrs_clear__() return inst @@ -107,24 +111,30 @@ def from_fields( cls, *, clear_unset: bool = False, - status: datatypes.Utf8Like | None = None, - ) -> Status: + state: datatypes.Utf8ArrayLike | None = None, + ) -> StateChange: """ - Update only some specific fields of a `Status`. + Update only some specific fields of a `StateChange`. Parameters ---------- clear_unset: If true, all unspecified fields will be explicitly cleared. - status: - The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array. + state: + The new state values; each instance gets its own lane in the state timeline view. + + A reset ends the previous state and shows a gap in the state timeline view until the + next state. An empty string, a null array entry, and an empty state array (e.g. from + clearing the field) all act as resets. + + The length of the state array should not change over time. """ inst = cls.__new__(cls) with catch_and_log_exceptions(context=cls.__name__): kwargs = { - "status": status, + "state": state, } if clear_unset: @@ -137,15 +147,15 @@ def from_fields( return inst @classmethod - def cleared(cls) -> Status: - """Clear all the fields of a `Status`.""" + def cleared(cls) -> StateChange: + """Clear all the fields of a `StateChange`.""" return cls.from_fields(clear_unset=True) @staticmethod - def descriptor_status() -> ComponentDescriptor: + def descriptor_state() -> ComponentDescriptor: return ComponentDescriptor( - "Status:status", - archetype=Status.NAME, + "StateChange:state", + archetype=StateChange.NAME, component_type=components.TextBatch._COMPONENT_TYPE, ) @@ -153,7 +163,7 @@ def descriptor_status() -> ComponentDescriptor: def columns( cls, *, - status: datatypes.Utf8ArrayLike | None = None, + state: datatypes.Utf8ArrayLike | None = None, ) -> ComponentColumnList: """ Construct a new column-oriented component bundle. @@ -165,22 +175,28 @@ def columns( Parameters ---------- - status: - The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array. + state: + The new state values; each instance gets its own lane in the state timeline view. + + A reset ends the previous state and shows a gap in the state timeline view until the + next state. An empty string, a null array entry, and an empty state array (e.g. from + clearing the field) all act as resets. + + The length of the state array should not change over time. """ inst = cls.__new__(cls) with catch_and_log_exceptions(context=cls.__name__): inst.__attrs_init__( - status=status, + state=state, ) batches = inst.as_component_batches() if len(batches) == 0: return ComponentColumnList([]) - kwargs = {"Status:status": status} + kwargs = {"StateChange:state": state} columns = [] for batch in batches: @@ -214,12 +230,18 @@ def columns( return ComponentColumnList(columns) - status: components.TextBatch | None = field( + state: components.TextBatch | None = field( metadata={"component": True}, default=None, converter=components.TextBatch._converter, # type: ignore[misc] ) - # The new status value. A `null` status is ignored, it can be used to partially update a multi-instance status array. + # The new state values; each instance gets its own lane in the state timeline view. + # + # A reset ends the previous state and shows a gap in the state timeline view until the + # next state. An empty string, a null array entry, and an empty state array (e.g. from + # clearing the field) all act as resets. + # + # The length of the state array should not change over time. # # (Docstring intentionally commented out to hide this field from the docs) @@ -239,4 +261,4 @@ def visualizer(self, *, mappings: list[VisualizerComponentMappingLike] | None = See https://github.com/rerun-io/rerun/issues/10631 for more information. """ - return Visualizer("StatusVisualizer", overrides=self.as_component_batches(), mappings=mappings) + return Visualizer("StateVisualizer", overrides=self.as_component_batches(), mappings=mappings) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/state_configuration.py b/rerun_py/rerun_sdk/rerun/archetypes/state_configuration.py new file mode 100644 index 000000000000..cc3935d5088e --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/archetypes/state_configuration.py @@ -0,0 +1,394 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/state_configuration.fbs". + +# You can extend this class by creating a "StateConfigurationExt" class in "state_configuration_ext.py". + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar + +import numpy as np +import pyarrow as pa +from attrs import define, field + +from .. import components, datatypes +from .._baseclasses import ( + Archetype, + ComponentColumnList, + ComponentDescriptor, +) +from ..blueprint import VisualizableArchetype, Visualizer +from ..error_utils import catch_and_log_exceptions + +if TYPE_CHECKING: + from ..blueprint.datatypes import VisualizerComponentMappingLike + +__all__ = ["StateConfiguration"] + + +@define(str=False, repr=False, init=False) +class StateConfiguration(Archetype, VisualizableArchetype): + """ + **Archetype**: Define the style and mapping for state values in a state timeline view. + + This archetype provides configuration for how state values are displayed. + It maps raw state values to display labels, colors, and visibility. + + `values`, `labels`, `colors`, and `visible` are parallel arrays: the entry + at index `i` of each describes the same state value, and only the + per-index pairing is meaningful. The four arrays should have matching + length; any secondary array (`labels`, `colors`, `visible`) that is shorter + than `values` falls back to defaults for the missing entries. + + It's generally recommended to log this type as static. + + The underlying data needs to be logged to the same entity path using [`archetypes.StateChange`][rerun.archetypes.StateChange]. + + Example + ------- + ### State changes with a custom style: + ```python + # Log a `StateChange` together with a `StateConfiguration` that customizes + # its display. + + import rerun as rr + + rr.init("rerun_example_state_configuration", spawn=True) + + # Configure how each raw state value is displayed (label, color, visibility). + rr.log( + "door", + rr.StateConfiguration( + values=["open", "closed"], + labels=["Open", "Closed"], + colors=[0x4CAF50FF, 0xEF5350FF], + ), + static=True, + ) + + rr.set_time("step", sequence=0) + rr.log("door", rr.StateChange(state="open")) + + rr.set_time("step", sequence=1) + rr.log("door", rr.StateChange(state="closed")) + + rr.set_time("step", sequence=2) + rr.log("door", rr.StateChange(state="open")) + ``` + + """ + + NAME: ClassVar[str] = "rerun.archetypes.StateConfiguration" + + def __init__( + self: Any, + *, + values: datatypes.Utf8ArrayLike | None = None, + labels: datatypes.Utf8ArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + visible: datatypes.BoolArrayLike | None = None, + ) -> None: + """ + Create a new instance of the StateConfiguration archetype. + + Parameters + ---------- + values: + The raw state values that this configuration applies to. + + Each entry defines a known state value. The order determines the mapping to + `labels`, `colors`, and `visible` (by index). + labels: + Display labels for each state value. + + If provided, the label at index `i` is shown instead of the raw value at index `i`. + If not provided or shorter than `values`, the raw value is used as the label. + colors: + Colors for each state value. + + If provided, the color at index `i` is used for the state at index `i`. + If not provided, colors are assigned automatically from a built-in palette. + visible: + Visibility for each state value. + + If provided, the visibility at index `i` controls whether the state at index `i` is shown. + If not provided, all state values are visible. + + """ + + # You can define your own __init__ function as a member of StateConfigurationExt in state_configuration_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__(values=values, labels=labels, colors=colors, visible=visible) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + values=None, + labels=None, + colors=None, + visible=None, + ) + + @classmethod + def _clear(cls) -> StateConfiguration: + """Produce an empty StateConfiguration, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + @classmethod + def from_fields( + cls, + *, + clear_unset: bool = False, + values: datatypes.Utf8ArrayLike | None = None, + labels: datatypes.Utf8ArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + visible: datatypes.BoolArrayLike | None = None, + ) -> StateConfiguration: + """ + Update only some specific fields of a `StateConfiguration`. + + Parameters + ---------- + clear_unset: + If true, all unspecified fields will be explicitly cleared. + values: + The raw state values that this configuration applies to. + + Each entry defines a known state value. The order determines the mapping to + `labels`, `colors`, and `visible` (by index). + labels: + Display labels for each state value. + + If provided, the label at index `i` is shown instead of the raw value at index `i`. + If not provided or shorter than `values`, the raw value is used as the label. + colors: + Colors for each state value. + + If provided, the color at index `i` is used for the state at index `i`. + If not provided, colors are assigned automatically from a built-in palette. + visible: + Visibility for each state value. + + If provided, the visibility at index `i` controls whether the state at index `i` is shown. + If not provided, all state values are visible. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + kwargs = { + "values": values, + "labels": labels, + "colors": colors, + "visible": visible, + } + + if clear_unset: + kwargs = {k: v if v is not None else [] for k, v in kwargs.items()} # type: ignore[misc] + + inst.__attrs_init__(**kwargs) + return inst + + inst.__attrs_clear__() + return inst + + @classmethod + def cleared(cls) -> StateConfiguration: + """Clear all the fields of a `StateConfiguration`.""" + return cls.from_fields(clear_unset=True) + + @staticmethod + def descriptor_values() -> ComponentDescriptor: + return ComponentDescriptor( + "StateConfiguration:values", + archetype=StateConfiguration.NAME, + component_type=components.TextBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_labels() -> ComponentDescriptor: + return ComponentDescriptor( + "StateConfiguration:labels", + archetype=StateConfiguration.NAME, + component_type=components.TextBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_colors() -> ComponentDescriptor: + return ComponentDescriptor( + "StateConfiguration:colors", + archetype=StateConfiguration.NAME, + component_type=components.ColorBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_visible() -> ComponentDescriptor: + return ComponentDescriptor( + "StateConfiguration:visible", + archetype=StateConfiguration.NAME, + component_type=components.VisibleBatch._COMPONENT_TYPE, + ) + + @classmethod + def columns( + cls, + *, + values: datatypes.Utf8ArrayLike | None = None, + labels: datatypes.Utf8ArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + visible: datatypes.BoolArrayLike | None = None, + ) -> ComponentColumnList: + """ + Construct a new column-oriented component bundle. + + This makes it possible to use `rr.send_columns` to send columnar data directly into Rerun. + + The returned columns will be partitioned into unit-length sub-batches by default. + Use `ComponentColumnList.partition` to repartition the data as needed. + + Parameters + ---------- + values: + The raw state values that this configuration applies to. + + Each entry defines a known state value. The order determines the mapping to + `labels`, `colors`, and `visible` (by index). + labels: + Display labels for each state value. + + If provided, the label at index `i` is shown instead of the raw value at index `i`. + If not provided or shorter than `values`, the raw value is used as the label. + colors: + Colors for each state value. + + If provided, the color at index `i` is used for the state at index `i`. + If not provided, colors are assigned automatically from a built-in palette. + visible: + Visibility for each state value. + + If provided, the visibility at index `i` controls whether the state at index `i` is shown. + If not provided, all state values are visible. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + inst.__attrs_init__( + values=values, + labels=labels, + colors=colors, + visible=visible, + ) + + batches = inst.as_component_batches() + if len(batches) == 0: + return ComponentColumnList([]) + + kwargs = { + "StateConfiguration:values": values, + "StateConfiguration:labels": labels, + "StateConfiguration:colors": colors, + "StateConfiguration:visible": visible, + } + columns = [] + + for batch in batches: + arrow_array = batch.as_arrow_array() + + # For primitive arrays and fixed size list arrays, we infer partition size from the input shape. + if pa.types.is_primitive(arrow_array.type) or pa.types.is_fixed_size_list(arrow_array.type): + param = kwargs[batch.component_descriptor().component] # type: ignore[index] + shape = np.shape(param) # type: ignore[arg-type] + num_rows = shape[0] if len(shape) >= 1 else 1 # type: ignore[redundant-expr,misc] + + if pa.types.is_fixed_size_list(arrow_array.type): + elem_flat_len = int(np.prod(shape[1:])) if len(shape) > 1 else 1 # type: ignore[redundant-expr,misc] + if arrow_array.type.list_size == elem_flat_len: + # The product of the last dimensions of the shape are equal to the size of the fixed size list array, + # so we have `num_rows` single element batches (each element is a fixed sized list). + batch_length = 1 + else: + batch_length = shape[1] if len(shape) > 1 else 1 # type: ignore[redundant-expr,misc] + else: + # For primitive types, derive batch_length from the actual arrow array length + # since the input shape can be misleading (e.g. colors [R,G,B] -> single uint32). + batch_length = len(arrow_array) // num_rows if num_rows > 0 else 1 + + sizes = batch_length * np.ones(num_rows) + else: + # For non-primitive types, default to partitioning each element separately. + sizes = np.ones(len(arrow_array)) + + columns.append(batch.partition(sizes)) + + return ComponentColumnList(columns) + + values: components.TextBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.TextBatch._converter, # type: ignore[misc] + ) + # The raw state values that this configuration applies to. + # + # Each entry defines a known state value. The order determines the mapping to + # `labels`, `colors`, and `visible` (by index). + # + # (Docstring intentionally commented out to hide this field from the docs) + + labels: components.TextBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.TextBatch._converter, # type: ignore[misc] + ) + # Display labels for each state value. + # + # If provided, the label at index `i` is shown instead of the raw value at index `i`. + # If not provided or shorter than `values`, the raw value is used as the label. + # + # (Docstring intentionally commented out to hide this field from the docs) + + colors: components.ColorBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.ColorBatch._converter, # type: ignore[misc] + ) + # Colors for each state value. + # + # If provided, the color at index `i` is used for the state at index `i`. + # If not provided, colors are assigned automatically from a built-in palette. + # + # (Docstring intentionally commented out to hide this field from the docs) + + visible: components.VisibleBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.VisibleBatch._converter, # type: ignore[misc] + ) + # Visibility for each state value. + # + # If provided, the visibility at index `i` controls whether the state at index `i` is shown. + # If not provided, all state values are visible. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] + + def visualizer(self, *, mappings: list[VisualizerComponentMappingLike] | None = None) -> Visualizer: + """ + Creates a visualizer for this archetype, using all currently set values as overrides. + + Parameters + ---------- + mappings: + Optional component mappings to control how the visualizer sources its data. + + ⚠️ **Experimental**: Component mappings are an experimental feature and may change. + See https://github.com/rerun-io/rerun/issues/10631 for more information. + + """ + return Visualizer("StateVisualizer", overrides=self.as_component_batches(), mappings=mappings) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/tensor.py b/rerun_py/rerun_sdk/rerun/archetypes/tensor.py index 5f48c74eb9d4..d1dda9358d60 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/tensor.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/tensor.py @@ -44,12 +44,17 @@ class Tensor(TensorExt, Archetype, VisualizableArchetype): import rerun as rr - tensor = np.random.randint(0, 256, (8, 6, 3, 5), dtype=np.uint8) # 4-dimensional tensor + tensor = np.random.randint( + 0, 256, (8, 6, 3, 5), dtype=np.uint8 + ) # 4-dimensional tensor rr.init("rerun_example_tensor", spawn=True) # Log the tensor, assigning names to each dimension - rr.log("tensor", rr.Tensor(tensor, dim_names=("width", "height", "channel", "batch"))) + rr.log( + "tensor", + rr.Tensor(tensor, dim_names=("width", "height", "channel", "batch")), + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/text_log.py b/rerun_py/rerun_sdk/rerun/archetypes/text_log.py index 773c6f945ee3..d8518162dc22 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/text_log.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/text_log.py @@ -42,7 +42,10 @@ class TextLog(Archetype, VisualizableArchetype): rr.init("rerun_example_text_log_integration", spawn=True) # Log a text entry directly - rr.log("logs", rr.TextLog("this entry has loglevel TRACE", level=rr.TextLogLevel.TRACE)) + rr.log( + "logs", + rr.TextLog("this entry has loglevel TRACE", level=rr.TextLogLevel.TRACE), + ) # Or log via a logging handler logging.getLogger().addHandler(rr.LoggingHandler("logs/handler")) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/transform3d.py b/rerun_py/rerun_sdk/rerun/archetypes/transform3d.py index 05caae2ab002..cdb3086d30d2 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/transform3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/transform3d.py @@ -93,7 +93,9 @@ def truncated_radians(deg: float) -> float: rr.set_time("tick", sequence=0) rr.log( "box", - rr.Boxes3D(half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid), + rr.Boxes3D( + half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid + ), rr.TransformAxes3D(10.0), ) @@ -103,7 +105,9 @@ def truncated_radians(deg: float) -> float: "box", rr.Transform3D( translation=[0, 0, t / 10.0], - rotation_axis_angle=rr.RotationAxisAngle(axis=[0.0, 1.0, 0.0], radians=truncated_radians(t * 4)), + rotation_axis_angle=rr.RotationAxisAngle( + axis=[0.0, 1.0, 0.0], radians=truncated_radians(t * 4) + ), ), ) ``` @@ -133,7 +137,9 @@ def truncated_radians(deg: float) -> float: rr.set_time("tick", sequence=0) rr.log( "box", - rr.Boxes3D(half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid), + rr.Boxes3D( + half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid + ), rr.TransformAxes3D(10.0), ) @@ -143,7 +149,10 @@ def truncated_radians(deg: float) -> float: columns=rr.Transform3D.columns( translation=[[0, 0, t / 10.0] for t in range(100)], rotation_axis_angle=[ - rr.RotationAxisAngle(axis=[0.0, 1.0, 0.0], radians=truncated_radians(t * 4)) for t in range(100) + rr.RotationAxisAngle( + axis=[0.0, 1.0, 0.0], radians=truncated_radians(t * 4) + ) + for t in range(100) ], ), ) @@ -174,7 +183,9 @@ def truncated_radians(deg: float) -> float: # Set up a 3D box. rr.log( "box", - rr.Boxes3D(half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid), + rr.Boxes3D( + half_sizes=[4.0, 2.0, 1.0], fill_mode=rr.components.FillMode.Solid + ), ) # Update only the rotation of the box. @@ -183,7 +194,9 @@ def truncated_radians(deg: float) -> float: rr.log( "box", rr.Transform3D.from_fields( - rotation_axis_angle=rr.RotationAxisAngle(axis=[0.0, 1.0, 0.0], radians=rad), + rotation_axis_angle=rr.RotationAxisAngle( + axis=[0.0, 1.0, 0.0], radians=rad + ), ), ) @@ -200,7 +213,9 @@ def truncated_radians(deg: float) -> float: rr.log( "box", rr.Transform3D.from_fields( - rotation_axis_angle=rr.RotationAxisAngle(axis=[0.0, 1.0, 0.0], radians=rad), + rotation_axis_angle=rr.RotationAxisAngle( + axis=[0.0, 1.0, 0.0], radians=rad + ), ), ) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/video_frame_reference.py b/rerun_py/rerun_sdk/rerun/archetypes/video_frame_reference.py index b03cabef237d..7f1ba187ebb6 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/video_frame_reference.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/video_frame_reference.py @@ -105,7 +105,12 @@ class VideoFrameReference(VideoFrameReferenceExt, Archetype, VisualizableArchety ) # Send blueprint that shows two 2D views next to each other. - rr.send_blueprint(rrb.Horizontal(rrb.Spatial2DView(origin="frame_1s"), rrb.Spatial2DView(origin="frame_2s"))) + rr.send_blueprint( + rrb.Horizontal( + rrb.Spatial2DView(origin="frame_1s"), + rrb.Spatial2DView(origin="frame_2s"), + ) + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/archetypes/video_stream.py b/rerun_py/rerun_sdk/rerun/archetypes/video_stream.py index 8cf7ce4cf174..d2c5e25f0aa6 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/video_stream.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/video_stream.py @@ -65,7 +65,11 @@ class VideoStream(Archetype, VisualizableArchetype): def create_example_video_frame(frame_i: int) -> npt.NDArray[np.uint8]: img = np.zeros((height, width, 3), dtype=np.uint8) for h in range(height): - img[h, :] = [0, int(100 * h / height), int(200 * h / height)] # Blue to purple gradient. + img[h, :] = [ + 0, + int(100 * h / height), + int(200 * h / height), + ] # Blue to purple gradient. x_pos = width // 2 # Center horizontally. y_pos = height // 2 + 80 * np.sin(2 * np.pi * frame_i / fps) @@ -80,17 +84,21 @@ def create_example_video_frame(frame_i: int) -> npt.NDArray[np.uint8]: # Setup encoding pipeline. av.logging.set_level(av.logging.VERBOSE) - container = av.open("/dev/null", "w", format=formats[codec]) # Use AnnexB H.265 stream. + container = av.open( + "/dev/null", "w", format=formats[codec] + ) # Use AnnexB H.265 stream. stream = container.add_stream(encoders[codec], rate=fps) # Type narrowing assert isinstance(stream, av.video.stream.VideoStream) stream.width = width stream.height = height # TODO(#10090): Rerun Video Streams don't support b-frames yet. - # Note that b-frames are generally not recommended for low-latency streaming and may make logging more complex. + # Note that b-frames are generally not recommended for low-latency streaming + # and may make logging more complex. stream.max_b_frames = 0 - # Log codec only once as static data (it naturally never changes). This isn't strictly necessary, but good practice. + # Log codec only once as static data (it naturally never changes). + # This isn't strictly necessary, but good practice. rr.log("video_stream", rr.VideoStream(codec=codec), static=True) # Generate frames and stream them directly to Rerun. @@ -129,6 +137,7 @@ def __init__( codec: components.VideoCodecLike, *, sample: datatypes.BlobLike | None = None, + is_keyframe: datatypes.BoolLike | None = None, opacity: datatypes.Float32Like | None = None, draw_order: datatypes.Float32Like | None = None, ) -> None: @@ -165,6 +174,16 @@ def __init__( previous samples which may be required to decode an image. See [`components.VideoCodec`][rerun.components.VideoCodec] for codec specific requirements. + is_keyframe: + Whether the corresponding [`components.VideoSample`][rerun.components.VideoSample] contains a keyframe. + + A keyframe (also known as a sync sample or IDR) is a frame from which a decoder can + start decoding the stream with no prior decoder state. See [`components.IsKeyframe`][rerun.components.IsKeyframe] + and [`components.VideoCodec`][rerun.components.VideoCodec] for the codec-specific definition. + + This field is optional. It does not change how the stream itself is decoded: it is + metadata that travels with the sample and can be inspected when querying the data + back, for example to locate sync points or build a frame index. opacity: Opacity of the video stream, useful for layering several media. @@ -179,7 +198,9 @@ def __init__( # You can define your own __init__ function as a member of VideoStreamExt in video_stream_ext.py with catch_and_log_exceptions(context=self.__class__.__name__): - self.__attrs_init__(codec=codec, sample=sample, opacity=opacity, draw_order=draw_order) + self.__attrs_init__( + codec=codec, sample=sample, is_keyframe=is_keyframe, opacity=opacity, draw_order=draw_order + ) return self.__attrs_clear__() @@ -188,6 +209,7 @@ def __attrs_clear__(self) -> None: self.__attrs_init__( codec=None, sample=None, + is_keyframe=None, opacity=None, draw_order=None, ) @@ -206,6 +228,7 @@ def from_fields( clear_unset: bool = False, codec: components.VideoCodecLike | None = None, sample: datatypes.BlobLike | None = None, + is_keyframe: datatypes.BoolLike | None = None, opacity: datatypes.Float32Like | None = None, draw_order: datatypes.Float32Like | None = None, ) -> VideoStream: @@ -244,6 +267,16 @@ def from_fields( previous samples which may be required to decode an image. See [`components.VideoCodec`][rerun.components.VideoCodec] for codec specific requirements. + is_keyframe: + Whether the corresponding [`components.VideoSample`][rerun.components.VideoSample] contains a keyframe. + + A keyframe (also known as a sync sample or IDR) is a frame from which a decoder can + start decoding the stream with no prior decoder state. See [`components.IsKeyframe`][rerun.components.IsKeyframe] + and [`components.VideoCodec`][rerun.components.VideoCodec] for the codec-specific definition. + + This field is optional. It does not change how the stream itself is decoded: it is + metadata that travels with the sample and can be inspected when querying the data + back, for example to locate sync points or build a frame index. opacity: Opacity of the video stream, useful for layering several media. @@ -261,6 +294,7 @@ def from_fields( kwargs = { "codec": codec, "sample": sample, + "is_keyframe": is_keyframe, "opacity": opacity, "draw_order": draw_order, } @@ -295,6 +329,14 @@ def descriptor_sample() -> ComponentDescriptor: component_type=components.VideoSampleBatch._COMPONENT_TYPE, ) + @staticmethod + def descriptor_is_keyframe() -> ComponentDescriptor: + return ComponentDescriptor( + "VideoStream:is_keyframe", + archetype=VideoStream.NAME, + component_type=components.IsKeyframeBatch._COMPONENT_TYPE, + ) + @staticmethod def descriptor_opacity() -> ComponentDescriptor: return ComponentDescriptor( @@ -317,6 +359,7 @@ def columns( *, codec: components.VideoCodecArrayLike | None = None, sample: datatypes.BlobArrayLike | None = None, + is_keyframe: datatypes.BoolArrayLike | None = None, opacity: datatypes.Float32ArrayLike | None = None, draw_order: datatypes.Float32ArrayLike | None = None, ) -> ComponentColumnList: @@ -358,6 +401,16 @@ def columns( previous samples which may be required to decode an image. See [`components.VideoCodec`][rerun.components.VideoCodec] for codec specific requirements. + is_keyframe: + Whether the corresponding [`components.VideoSample`][rerun.components.VideoSample] contains a keyframe. + + A keyframe (also known as a sync sample or IDR) is a frame from which a decoder can + start decoding the stream with no prior decoder state. See [`components.IsKeyframe`][rerun.components.IsKeyframe] + and [`components.VideoCodec`][rerun.components.VideoCodec] for the codec-specific definition. + + This field is optional. It does not change how the stream itself is decoded: it is + metadata that travels with the sample and can be inspected when querying the data + back, for example to locate sync points or build a frame index. opacity: Opacity of the video stream, useful for layering several media. @@ -375,6 +428,7 @@ def columns( inst.__attrs_init__( codec=codec, sample=sample, + is_keyframe=is_keyframe, opacity=opacity, draw_order=draw_order, ) @@ -386,6 +440,7 @@ def columns( kwargs = { "VideoStream:codec": codec, "VideoStream:sample": sample, + "VideoStream:is_keyframe": is_keyframe, "VideoStream:opacity": opacity, "VideoStream:draw_order": draw_order, } @@ -464,6 +519,23 @@ def columns( # # (Docstring intentionally commented out to hide this field from the docs) + is_keyframe: components.IsKeyframeBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.IsKeyframeBatch._converter, # type: ignore[misc] + ) + # Whether the corresponding [`components.VideoSample`][rerun.components.VideoSample] contains a keyframe. + # + # A keyframe (also known as a sync sample or IDR) is a frame from which a decoder can + # start decoding the stream with no prior decoder state. See [`components.IsKeyframe`][rerun.components.IsKeyframe] + # and [`components.VideoCodec`][rerun.components.VideoCodec] for the codec-specific definition. + # + # This field is optional. It does not change how the stream itself is decoded: it is + # metadata that travels with the sample and can be inspected when querying the data + # back, for example to locate sync points or build a frame index. + # + # (Docstring intentionally commented out to hide this field from the docs) + opacity: components.OpacityBatch | None = field( metadata={"component": True}, default=None, diff --git a/rerun_py/rerun_sdk/rerun/archetypes/view_coordinates.py b/rerun_py/rerun_sdk/rerun/archetypes/view_coordinates.py index fdf7322f2812..de5832f19544 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/view_coordinates.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/view_coordinates.py @@ -49,7 +49,9 @@ class ViewCoordinates(ViewCoordinatesExt, Archetype): rr.init("rerun_example_view_coordinates", spawn=True) - rr.log("world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True) # Set an up-axis + rr.log( + "world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True + ) # Set an up-axis rr.log( "world/xyz", rr.Arrows3D( diff --git a/rerun_py/rerun_sdk/rerun/archetypes/voxel_grid_map.py b/rerun_py/rerun_sdk/rerun/archetypes/voxel_grid_map.py new file mode 100644 index 000000000000..26c10f8c6f8a --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/archetypes/voxel_grid_map.py @@ -0,0 +1,646 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/archetypes/voxel_grid_map.fbs". + +# You can extend this class by creating a "VoxelGridMapExt" class in "voxel_grid_map_ext.py". + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar + +import numpy as np +import pyarrow as pa +from attrs import define, field + +from .. import components, datatypes +from .._baseclasses import ( + Archetype, + ComponentColumnList, + ComponentDescriptor, +) +from ..blueprint import VisualizableArchetype, Visualizer +from ..error_utils import catch_and_log_exceptions + +if TYPE_CHECKING: + from ..blueprint.datatypes import VisualizerComponentMappingLike + +__all__ = ["VoxelGridMap"] + + +@define(str=False, repr=False, init=False) +class VoxelGridMap(Archetype, VisualizableArchetype): + """ + **Archetype**: A sparse 3D voxel grid map with grid indices and voxel dimensions. + + This archetype is intended for 3D occupancy maps and other volumetric data + represented as a sparse grid of voxels with scene-unit dimensions along the local X/Y/Z axes. + + The minimum corner of the voxel with `[0, 0, 0]` index is located at the origin of the entity's coordinate frame + and can have an additional offset from there through the optional translation and rotation fields. + + A voxel center is at `(index + 0.5) * voxel_size` in local grid coordinates (i.e. relative to the minimum corner). + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + + Example + ------- + ### Simple sparse voxel grid map: + ```python + import numpy as np + + import rerun as rr + + voxel_indices = np.array( + [ + [-1, 0, 0], + [1, 0, 0], + [1, 1, 0], + [3, 0, 0], + [3, 0, 1], + [4, 0, 1], + ], + dtype=np.int32, + ) + values = np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0], dtype=np.float32) + + rr.init("rerun_example_voxel_grid_map_simple", spawn=True) + + rr.log( + "world/voxels", + rr.VoxelGridMap( + voxel_indices, + voxel_size=[0.25, 0.25, 0.25], + values=values, + value_range=[0.0, 1.0], + colormap=rr.components.Colormap.Turbo, + translation=[-0.5, -0.5, 0.0], + ), + ) + ``` + + """ + + NAME: ClassVar[str] = "rerun.archetypes.VoxelGridMap" + + def __init__( + self: Any, + voxel_indices: datatypes.IVec3DArrayLike, + voxel_size: datatypes.Vec3DLike, + *, + values: datatypes.Float32ArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + translation: datatypes.Vec3DLike | None = None, + rotation_axis_angle: datatypes.RotationAxisAngleLike | None = None, + quaternion: datatypes.QuaternionLike | None = None, + opacity: datatypes.Float32Like | None = None, + value_range: datatypes.Range1DLike | None = None, + colormap: components.ColormapLike | None = None, + ) -> None: + """ + Create a new instance of the VoxelGridMap archetype. + + Parameters + ---------- + voxel_indices: + Indices of the voxels within the grid volume. + voxel_size: + The scene-unit dimensions of a single voxel cell. + + This defines the voxel size along the local grid X/Y/Z axes. + Each dimension must be finite and positive. + values: + Optional scalar occupancy or value data for each voxel. + + If explicit colors are not provided, values are mapped through `colormap` and `value_range`. + colors: + Optional colors for each voxel. + + If set, these colors take precedence over color-mapped scalar values. + translation: + Translation of the minimum corner of voxel `[0, 0, 0]`. + + Together with [`components.RotationAxisAngle`][rerun.components.RotationAxisAngle] or [`components.RotationQuat`][rerun.components.RotationQuat], this defines the pose of the + grid relative to the map's parent coordinate frame. + + If not set, the minimum corner is placed at the origin of the map's parent coordinate frame. + rotation_axis_angle: + Rotation of the grid via axis + angle. + + Together with [`components.Translation3D`][rerun.components.Translation3D], this defines the pose of the grid relative to the + map's parent coordinate frame. + + Note: either this or [`components.RotationQuat`][rerun.components.RotationQuat] can be set to specify the grid's rotation, but not both. + If both this and [`components.RotationQuat`][rerun.components.RotationQuat] are set, this is ignored in favor of the quaternion. + quaternion: + Rotation of the grid via quaternion. + + Together with [`components.Translation3D`][rerun.components.Translation3D], this defines the pose of the grid relative to the + map's parent coordinate frame. + opacity: + Opacity of the voxels after color or colormap application. + + Defaults to 1.0 (fully opaque). + value_range: + Scalar value range for color-mapping. + + Defaults to `[0.0, 1.0]`. + colormap: + Colormap to use when `values` are present and explicit `colors` are not provided. + + Defaults to Turbo. + + """ + + # You can define your own __init__ function as a member of VoxelGridMapExt in voxel_grid_map_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__( + voxel_indices=voxel_indices, + voxel_size=voxel_size, + values=values, + colors=colors, + translation=translation, + rotation_axis_angle=rotation_axis_angle, + quaternion=quaternion, + opacity=opacity, + value_range=value_range, + colormap=colormap, + ) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + voxel_indices=None, + voxel_size=None, + values=None, + colors=None, + translation=None, + rotation_axis_angle=None, + quaternion=None, + opacity=None, + value_range=None, + colormap=None, + ) + + @classmethod + def _clear(cls) -> VoxelGridMap: + """Produce an empty VoxelGridMap, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + @classmethod + def from_fields( + cls, + *, + clear_unset: bool = False, + voxel_indices: datatypes.IVec3DArrayLike | None = None, + voxel_size: datatypes.Vec3DLike | None = None, + values: datatypes.Float32ArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + translation: datatypes.Vec3DLike | None = None, + rotation_axis_angle: datatypes.RotationAxisAngleLike | None = None, + quaternion: datatypes.QuaternionLike | None = None, + opacity: datatypes.Float32Like | None = None, + value_range: datatypes.Range1DLike | None = None, + colormap: components.ColormapLike | None = None, + ) -> VoxelGridMap: + """ + Update only some specific fields of a `VoxelGridMap`. + + Parameters + ---------- + clear_unset: + If true, all unspecified fields will be explicitly cleared. + voxel_indices: + Indices of the voxels within the grid volume. + voxel_size: + The scene-unit dimensions of a single voxel cell. + + This defines the voxel size along the local grid X/Y/Z axes. + Each dimension must be finite and positive. + values: + Optional scalar occupancy or value data for each voxel. + + If explicit colors are not provided, values are mapped through `colormap` and `value_range`. + colors: + Optional colors for each voxel. + + If set, these colors take precedence over color-mapped scalar values. + translation: + Translation of the minimum corner of voxel `[0, 0, 0]`. + + Together with [`components.RotationAxisAngle`][rerun.components.RotationAxisAngle] or [`components.RotationQuat`][rerun.components.RotationQuat], this defines the pose of the + grid relative to the map's parent coordinate frame. + + If not set, the minimum corner is placed at the origin of the map's parent coordinate frame. + rotation_axis_angle: + Rotation of the grid via axis + angle. + + Together with [`components.Translation3D`][rerun.components.Translation3D], this defines the pose of the grid relative to the + map's parent coordinate frame. + + Note: either this or [`components.RotationQuat`][rerun.components.RotationQuat] can be set to specify the grid's rotation, but not both. + If both this and [`components.RotationQuat`][rerun.components.RotationQuat] are set, this is ignored in favor of the quaternion. + quaternion: + Rotation of the grid via quaternion. + + Together with [`components.Translation3D`][rerun.components.Translation3D], this defines the pose of the grid relative to the + map's parent coordinate frame. + opacity: + Opacity of the voxels after color or colormap application. + + Defaults to 1.0 (fully opaque). + value_range: + Scalar value range for color-mapping. + + Defaults to `[0.0, 1.0]`. + colormap: + Colormap to use when `values` are present and explicit `colors` are not provided. + + Defaults to Turbo. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + kwargs = { + "voxel_indices": voxel_indices, + "voxel_size": voxel_size, + "values": values, + "colors": colors, + "translation": translation, + "rotation_axis_angle": rotation_axis_angle, + "quaternion": quaternion, + "opacity": opacity, + "value_range": value_range, + "colormap": colormap, + } + + if clear_unset: + kwargs = {k: v if v is not None else [] for k, v in kwargs.items()} # type: ignore[misc] + + inst.__attrs_init__(**kwargs) + return inst + + inst.__attrs_clear__() + return inst + + @classmethod + def cleared(cls) -> VoxelGridMap: + """Clear all the fields of a `VoxelGridMap`.""" + return cls.from_fields(clear_unset=True) + + @staticmethod + def descriptor_voxel_indices() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:voxel_indices", + archetype=VoxelGridMap.NAME, + component_type=components.VoxelIndexBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_voxel_size() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:voxel_size", + archetype=VoxelGridMap.NAME, + component_type=components.VoxelSizeBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_values() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:values", + archetype=VoxelGridMap.NAME, + component_type=components.VoxelValueBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_colors() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:colors", + archetype=VoxelGridMap.NAME, + component_type=components.ColorBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_translation() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:translation", + archetype=VoxelGridMap.NAME, + component_type=components.Translation3DBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_rotation_axis_angle() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:rotation_axis_angle", + archetype=VoxelGridMap.NAME, + component_type=components.RotationAxisAngleBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_quaternion() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:quaternion", + archetype=VoxelGridMap.NAME, + component_type=components.RotationQuatBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_opacity() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:opacity", + archetype=VoxelGridMap.NAME, + component_type=components.OpacityBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_value_range() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:value_range", + archetype=VoxelGridMap.NAME, + component_type=components.ValueRangeBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_colormap() -> ComponentDescriptor: + return ComponentDescriptor( + "VoxelGridMap:colormap", + archetype=VoxelGridMap.NAME, + component_type=components.ColormapBatch._COMPONENT_TYPE, + ) + + @classmethod + def columns( + cls, + *, + voxel_indices: datatypes.IVec3DArrayLike | None = None, + voxel_size: datatypes.Vec3DArrayLike | None = None, + values: datatypes.Float32ArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + translation: datatypes.Vec3DArrayLike | None = None, + rotation_axis_angle: datatypes.RotationAxisAngleArrayLike | None = None, + quaternion: datatypes.QuaternionArrayLike | None = None, + opacity: datatypes.Float32ArrayLike | None = None, + value_range: datatypes.Range1DArrayLike | None = None, + colormap: components.ColormapArrayLike | None = None, + ) -> ComponentColumnList: + """ + Construct a new column-oriented component bundle. + + This makes it possible to use `rr.send_columns` to send columnar data directly into Rerun. + + The returned columns will be partitioned into unit-length sub-batches by default. + Use `ComponentColumnList.partition` to repartition the data as needed. + + Parameters + ---------- + voxel_indices: + Indices of the voxels within the grid volume. + voxel_size: + The scene-unit dimensions of a single voxel cell. + + This defines the voxel size along the local grid X/Y/Z axes. + Each dimension must be finite and positive. + values: + Optional scalar occupancy or value data for each voxel. + + If explicit colors are not provided, values are mapped through `colormap` and `value_range`. + colors: + Optional colors for each voxel. + + If set, these colors take precedence over color-mapped scalar values. + translation: + Translation of the minimum corner of voxel `[0, 0, 0]`. + + Together with [`components.RotationAxisAngle`][rerun.components.RotationAxisAngle] or [`components.RotationQuat`][rerun.components.RotationQuat], this defines the pose of the + grid relative to the map's parent coordinate frame. + + If not set, the minimum corner is placed at the origin of the map's parent coordinate frame. + rotation_axis_angle: + Rotation of the grid via axis + angle. + + Together with [`components.Translation3D`][rerun.components.Translation3D], this defines the pose of the grid relative to the + map's parent coordinate frame. + + Note: either this or [`components.RotationQuat`][rerun.components.RotationQuat] can be set to specify the grid's rotation, but not both. + If both this and [`components.RotationQuat`][rerun.components.RotationQuat] are set, this is ignored in favor of the quaternion. + quaternion: + Rotation of the grid via quaternion. + + Together with [`components.Translation3D`][rerun.components.Translation3D], this defines the pose of the grid relative to the + map's parent coordinate frame. + opacity: + Opacity of the voxels after color or colormap application. + + Defaults to 1.0 (fully opaque). + value_range: + Scalar value range for color-mapping. + + Defaults to `[0.0, 1.0]`. + colormap: + Colormap to use when `values` are present and explicit `colors` are not provided. + + Defaults to Turbo. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + inst.__attrs_init__( + voxel_indices=voxel_indices, + voxel_size=voxel_size, + values=values, + colors=colors, + translation=translation, + rotation_axis_angle=rotation_axis_angle, + quaternion=quaternion, + opacity=opacity, + value_range=value_range, + colormap=colormap, + ) + + batches = inst.as_component_batches() + if len(batches) == 0: + return ComponentColumnList([]) + + kwargs = { + "VoxelGridMap:voxel_indices": voxel_indices, + "VoxelGridMap:voxel_size": voxel_size, + "VoxelGridMap:values": values, + "VoxelGridMap:colors": colors, + "VoxelGridMap:translation": translation, + "VoxelGridMap:rotation_axis_angle": rotation_axis_angle, + "VoxelGridMap:quaternion": quaternion, + "VoxelGridMap:opacity": opacity, + "VoxelGridMap:value_range": value_range, + "VoxelGridMap:colormap": colormap, + } + columns = [] + + for batch in batches: + arrow_array = batch.as_arrow_array() + + # For primitive arrays and fixed size list arrays, we infer partition size from the input shape. + if pa.types.is_primitive(arrow_array.type) or pa.types.is_fixed_size_list(arrow_array.type): + param = kwargs[batch.component_descriptor().component] # type: ignore[index] + shape = np.shape(param) # type: ignore[arg-type] + num_rows = shape[0] if len(shape) >= 1 else 1 # type: ignore[redundant-expr,misc] + + if pa.types.is_fixed_size_list(arrow_array.type): + elem_flat_len = int(np.prod(shape[1:])) if len(shape) > 1 else 1 # type: ignore[redundant-expr,misc] + if arrow_array.type.list_size == elem_flat_len: + # The product of the last dimensions of the shape are equal to the size of the fixed size list array, + # so we have `num_rows` single element batches (each element is a fixed sized list). + batch_length = 1 + else: + batch_length = shape[1] if len(shape) > 1 else 1 # type: ignore[redundant-expr,misc] + else: + # For primitive types, derive batch_length from the actual arrow array length + # since the input shape can be misleading (e.g. colors [R,G,B] -> single uint32). + batch_length = len(arrow_array) // num_rows if num_rows > 0 else 1 + + sizes = batch_length * np.ones(num_rows) + else: + # For non-primitive types, default to partitioning each element separately. + sizes = np.ones(len(arrow_array)) + + columns.append(batch.partition(sizes)) + + return ComponentColumnList(columns) + + voxel_indices: components.VoxelIndexBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.VoxelIndexBatch._converter, # type: ignore[misc] + ) + # Indices of the voxels within the grid volume. + # + # (Docstring intentionally commented out to hide this field from the docs) + + voxel_size: components.VoxelSizeBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.VoxelSizeBatch._converter, # type: ignore[misc] + ) + # The scene-unit dimensions of a single voxel cell. + # + # This defines the voxel size along the local grid X/Y/Z axes. + # Each dimension must be finite and positive. + # + # (Docstring intentionally commented out to hide this field from the docs) + + values: components.VoxelValueBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.VoxelValueBatch._converter, # type: ignore[misc] + ) + # Optional scalar occupancy or value data for each voxel. + # + # If explicit colors are not provided, values are mapped through `colormap` and `value_range`. + # + # (Docstring intentionally commented out to hide this field from the docs) + + colors: components.ColorBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.ColorBatch._converter, # type: ignore[misc] + ) + # Optional colors for each voxel. + # + # If set, these colors take precedence over color-mapped scalar values. + # + # (Docstring intentionally commented out to hide this field from the docs) + + translation: components.Translation3DBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.Translation3DBatch._converter, # type: ignore[misc] + ) + # Translation of the minimum corner of voxel `[0, 0, 0]`. + # + # Together with [`components.RotationAxisAngle`][rerun.components.RotationAxisAngle] or [`components.RotationQuat`][rerun.components.RotationQuat], this defines the pose of the + # grid relative to the map's parent coordinate frame. + # + # If not set, the minimum corner is placed at the origin of the map's parent coordinate frame. + # + # (Docstring intentionally commented out to hide this field from the docs) + + rotation_axis_angle: components.RotationAxisAngleBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.RotationAxisAngleBatch._converter, # type: ignore[misc] + ) + # Rotation of the grid via axis + angle. + # + # Together with [`components.Translation3D`][rerun.components.Translation3D], this defines the pose of the grid relative to the + # map's parent coordinate frame. + # + # Note: either this or [`components.RotationQuat`][rerun.components.RotationQuat] can be set to specify the grid's rotation, but not both. + # If both this and [`components.RotationQuat`][rerun.components.RotationQuat] are set, this is ignored in favor of the quaternion. + # + # (Docstring intentionally commented out to hide this field from the docs) + + quaternion: components.RotationQuatBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.RotationQuatBatch._converter, # type: ignore[misc] + ) + # Rotation of the grid via quaternion. + # + # Together with [`components.Translation3D`][rerun.components.Translation3D], this defines the pose of the grid relative to the + # map's parent coordinate frame. + # + # (Docstring intentionally commented out to hide this field from the docs) + + opacity: components.OpacityBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.OpacityBatch._converter, # type: ignore[misc] + ) + # Opacity of the voxels after color or colormap application. + # + # Defaults to 1.0 (fully opaque). + # + # (Docstring intentionally commented out to hide this field from the docs) + + value_range: components.ValueRangeBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.ValueRangeBatch._converter, # type: ignore[misc] + ) + # Scalar value range for color-mapping. + # + # Defaults to `[0.0, 1.0]`. + # + # (Docstring intentionally commented out to hide this field from the docs) + + colormap: components.ColormapBatch | None = field( + metadata={"component": True}, + default=None, + converter=components.ColormapBatch._converter, # type: ignore[misc] + ) + # Colormap to use when `values` are present and explicit `colors` are not provided. + # + # Defaults to Turbo. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] + + def visualizer(self, *, mappings: list[VisualizerComponentMappingLike] | None = None) -> Visualizer: + """ + Creates a visualizer for this archetype, using all currently set values as overrides. + + Parameters + ---------- + mappings: + Optional component mappings to control how the visualizer sources its data. + + ⚠️ **Experimental**: Component mappings are an experimental feature and may change. + See https://github.com/rerun-io/rerun/issues/10631 for more information. + + """ + return Visualizer("VoxelGridMap", overrides=self.as_component_batches(), mappings=mappings) diff --git a/rerun_py/rerun_sdk/rerun/blueprint/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/__init__.py index 71ea8123c1aa..991dd59dcc0b 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/__init__.py @@ -12,6 +12,7 @@ from . import ( archetypes as archetypes, components as components, + experimental as experimental, ) from .api import ( Blueprint as Blueprint, @@ -65,7 +66,7 @@ MapView as MapView, Spatial2DView as Spatial2DView, Spatial3DView as Spatial3DView, - StatusView as StatusView, + StateTimelineView as StateTimelineView, TensorView as TensorView, TextDocumentView as TextDocumentView, TextLogView as TextLogView, diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes index 73782fdeccc1..c8b97a376569 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes @@ -23,9 +23,11 @@ plot_background.py linguist-generated=true plot_legend.py linguist-generated=true scalar_axis.py linguist-generated=true spatial_information.py linguist-generated=true +table_blueprint.py linguist-generated=true tensor_scalar_mapping.py linguist-generated=true tensor_slice_selection.py linguist-generated=true tensor_view_fit.py linguist-generated=true +text_document_format.py linguist-generated=true text_log_columns.py linguist-generated=true text_log_format.py linguist-generated=true text_log_rows.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py index 7e961d129f7f..c2eaeb31ee5c 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py @@ -23,9 +23,11 @@ from .plot_legend import PlotLegend from .scalar_axis import ScalarAxis from .spatial_information import SpatialInformation +from .table_blueprint import TableBlueprint from .tensor_scalar_mapping import TensorScalarMapping from .tensor_slice_selection import TensorSliceSelection from .tensor_view_fit import TensorViewFit +from .text_document_format import TextDocumentFormat from .text_log_columns import TextLogColumns from .text_log_format import TextLogFormat from .text_log_rows import TextLogRows @@ -60,9 +62,11 @@ "PlotLegend", "ScalarAxis", "SpatialInformation", + "TableBlueprint", "TensorScalarMapping", "TensorSliceSelection", "TensorViewFit", + "TextDocumentFormat", "TextLogColumns", "TextLogFormat", "TextLogRows", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/entity_behavior.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/entity_behavior.py index 16b75651a9dd..7b6149365848 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/entity_behavior.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/entity_behavior.py @@ -35,7 +35,8 @@ class EntityBehavior(Archetype): rr.init("rerun_example_entity_behavior", spawn=True) - # Use `EntityBehavior` to override visibility & interactivity of entities in the blueprint. + # Use `EntityBehavior` to override visibility & interactivity of entities + # in the blueprint. rr.send_blueprint( rrb.Spatial2DView( overrides={ @@ -50,7 +51,10 @@ class EntityBehavior(Archetype): rr.log("hidden_subtree/also_hidden", rr.LineStrips2D(strips=[(-1, 1), (1, -1)])) rr.log("hidden_subtree/not_hidden", rr.LineStrips2D(strips=[(1, 1), (-1, -1)])) rr.log("non_interactive_subtree", rr.Boxes2D(centers=(0, 0), half_sizes=(1, 1))) - rr.log("non_interactive_subtree/also_non_interactive", rr.Boxes2D(centers=(0, 0), half_sizes=(0.5, 0.5))) + rr.log( + "non_interactive_subtree/also_non_interactive", + rr.Boxes2D(centers=(0, 0), half_sizes=(0.5, 0.5)), + ) ```
diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/table_blueprint.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/table_blueprint.py new file mode 100644 index 000000000000..6bc706c8e322 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/table_blueprint.py @@ -0,0 +1,260 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/table_blueprint.fbs". + +# You can extend this class by creating a "TableBlueprintExt" class in "table_blueprint_ext.py". + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar + +from attrs import define, field + +from ..._baseclasses import ( + Archetype, + ComponentDescriptor, +) +from ...blueprint import components as blueprint_components +from ...error_utils import catch_and_log_exceptions + +if TYPE_CHECKING: + from ... import datatypes + +__all__ = ["TableBlueprint"] + + +@define(str=False, repr=False, init=False) +class TableBlueprint(Archetype): + """ + **Archetype**: Blueprint for configuring the styling of a table. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + NAME: ClassVar[str] = "rerun.blueprint.archetypes.TableBlueprint" + + def __init__( + self: Any, + *, + segment_preview_column: datatypes.Utf8Like | None = None, + flag_column: datatypes.Utf8Like | None = None, + grid_view_card_title: datatypes.Utf8Like | None = None, + url_column: datatypes.Utf8Like | None = None, + ) -> None: + """ + Create a new instance of the TableBlueprint archetype. + + Parameters + ---------- + segment_preview_column: + The name of the column that contains recording URIs for segment previews. + + Every row can at most preview a single segment. + + For the preview, the rest of the blueprint data is read it as it would be with regular recording blueprints, + meaning that the regular structure of [`archetypes.ViewportBlueprint`][rerun.blueprint.archetypes.ViewportBlueprint], and [`archetypes.ViewBlueprint`][rerun.blueprint.archetypes.ViewBlueprint] structure applies. + However, this mostly ignores layout container types as well as automatic spawning. + + If unset, defaults to the first URL column in the table that points to the same Rerun server + flag_column: + The name of the boolean column used for flag/annotation toggles. + + Must be set for flagging to be available. The named column must exist in the + table and be of boolean type. + Additionally, the table must be remote and have another column with + `rerun:is_table_index` metadata since flag changes are persisted to the server + via upsert. + grid_view_card_title: + The name of the column to use as the card title in grid view. + + If unset, the first visible string column is used as the title. + url_column: + The name of the column containing URLs to open when a card is clicked in grid view. + + If unset, defaults to the segment preview column. + + """ + + # You can define your own __init__ function as a member of TableBlueprintExt in table_blueprint_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__( + segment_preview_column=segment_preview_column, + flag_column=flag_column, + grid_view_card_title=grid_view_card_title, + url_column=url_column, + ) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + segment_preview_column=None, + flag_column=None, + grid_view_card_title=None, + url_column=None, + ) + + @classmethod + def _clear(cls) -> TableBlueprint: + """Produce an empty TableBlueprint, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + @classmethod + def from_fields( + cls, + *, + clear_unset: bool = False, + segment_preview_column: datatypes.Utf8Like | None = None, + flag_column: datatypes.Utf8Like | None = None, + grid_view_card_title: datatypes.Utf8Like | None = None, + url_column: datatypes.Utf8Like | None = None, + ) -> TableBlueprint: + """ + Update only some specific fields of a `TableBlueprint`. + + Parameters + ---------- + clear_unset: + If true, all unspecified fields will be explicitly cleared. + segment_preview_column: + The name of the column that contains recording URIs for segment previews. + + Every row can at most preview a single segment. + + For the preview, the rest of the blueprint data is read it as it would be with regular recording blueprints, + meaning that the regular structure of [`archetypes.ViewportBlueprint`][rerun.blueprint.archetypes.ViewportBlueprint], and [`archetypes.ViewBlueprint`][rerun.blueprint.archetypes.ViewBlueprint] structure applies. + However, this mostly ignores layout container types as well as automatic spawning. + + If unset, defaults to the first URL column in the table that points to the same Rerun server + flag_column: + The name of the boolean column used for flag/annotation toggles. + + Must be set for flagging to be available. The named column must exist in the + table and be of boolean type. + Additionally, the table must be remote and have another column with + `rerun:is_table_index` metadata since flag changes are persisted to the server + via upsert. + grid_view_card_title: + The name of the column to use as the card title in grid view. + + If unset, the first visible string column is used as the title. + url_column: + The name of the column containing URLs to open when a card is clicked in grid view. + + If unset, defaults to the segment preview column. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + kwargs = { + "segment_preview_column": segment_preview_column, + "flag_column": flag_column, + "grid_view_card_title": grid_view_card_title, + "url_column": url_column, + } + + if clear_unset: + kwargs = {k: v if v is not None else [] for k, v in kwargs.items()} # type: ignore[misc] + + inst.__attrs_init__(**kwargs) + return inst + + inst.__attrs_clear__() + return inst + + @classmethod + def cleared(cls) -> TableBlueprint: + """Clear all the fields of a `TableBlueprint`.""" + return cls.from_fields(clear_unset=True) + + @staticmethod + def descriptor_segment_preview_column() -> ComponentDescriptor: + return ComponentDescriptor( + "TableBlueprint:segment_preview_column", + archetype=TableBlueprint.NAME, + component_type=blueprint_components.ColumnNameBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_flag_column() -> ComponentDescriptor: + return ComponentDescriptor( + "TableBlueprint:flag_column", + archetype=TableBlueprint.NAME, + component_type=blueprint_components.ColumnNameBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_grid_view_card_title() -> ComponentDescriptor: + return ComponentDescriptor( + "TableBlueprint:grid_view_card_title", + archetype=TableBlueprint.NAME, + component_type=blueprint_components.ColumnNameBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_url_column() -> ComponentDescriptor: + return ComponentDescriptor( + "TableBlueprint:url_column", + archetype=TableBlueprint.NAME, + component_type=blueprint_components.ColumnNameBatch._COMPONENT_TYPE, + ) + + segment_preview_column: blueprint_components.ColumnNameBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.ColumnNameBatch._converter, # type: ignore[misc] + ) + # The name of the column that contains recording URIs for segment previews. + # + # Every row can at most preview a single segment. + # + # For the preview, the rest of the blueprint data is read it as it would be with regular recording blueprints, + # meaning that the regular structure of [`archetypes.ViewportBlueprint`][rerun.blueprint.archetypes.ViewportBlueprint], and [`archetypes.ViewBlueprint`][rerun.blueprint.archetypes.ViewBlueprint] structure applies. + # However, this mostly ignores layout container types as well as automatic spawning. + # + # If unset, defaults to the first URL column in the table that points to the same Rerun server + # + # (Docstring intentionally commented out to hide this field from the docs) + + flag_column: blueprint_components.ColumnNameBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.ColumnNameBatch._converter, # type: ignore[misc] + ) + # The name of the boolean column used for flag/annotation toggles. + # + # Must be set for flagging to be available. The named column must exist in the + # table and be of boolean type. + # Additionally, the table must be remote and have another column with + # `rerun:is_table_index` metadata since flag changes are persisted to the server + # via upsert. + # + # (Docstring intentionally commented out to hide this field from the docs) + + grid_view_card_title: blueprint_components.ColumnNameBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.ColumnNameBatch._converter, # type: ignore[misc] + ) + # The name of the column to use as the card title in grid view. + # + # If unset, the first visible string column is used as the title. + # + # (Docstring intentionally commented out to hide this field from the docs) + + url_column: blueprint_components.ColumnNameBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.ColumnNameBatch._converter, # type: ignore[misc] + ) + # The name of the column containing URLs to open when a card is clicked in grid view. + # + # If unset, defaults to the segment preview column. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/text_document_format.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/text_document_format.py new file mode 100644 index 000000000000..2e3a4b7425ff --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/text_document_format.py @@ -0,0 +1,162 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/archetypes/text_document_format.fbs". + +# You can extend this class by creating a "TextDocumentFormatExt" class in "text_document_format_ext.py". + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar + +from attrs import define, field + +from ..._baseclasses import ( + Archetype, + ComponentDescriptor, +) +from ...blueprint import components as blueprint_components +from ...error_utils import catch_and_log_exceptions + +if TYPE_CHECKING: + from ... import datatypes + +__all__ = ["TextDocumentFormat"] + + +@define(str=False, repr=False, init=False) +class TextDocumentFormat(Archetype): + """ + **Archetype**: Formatting options for the text document view. + + These options only apply to plain text documents and have no effect on Markdown documents. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + NAME: ClassVar[str] = "rerun.blueprint.archetypes.TextDocumentFormat" + + def __init__( + self: Any, *, monospace: datatypes.BoolLike | None = None, word_wrap: datatypes.BoolLike | None = None + ) -> None: + """ + Create a new instance of the TextDocumentFormat archetype. + + Parameters + ---------- + monospace: + Whether to use a monospace font for the document body. + + Defaults to disabled. + word_wrap: + Whether to wrap long lines in the document body. + + Defaults to enabled. + + """ + + # You can define your own __init__ function as a member of TextDocumentFormatExt in text_document_format_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__(monospace=monospace, word_wrap=word_wrap) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + monospace=None, + word_wrap=None, + ) + + @classmethod + def _clear(cls) -> TextDocumentFormat: + """Produce an empty TextDocumentFormat, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + @classmethod + def from_fields( + cls, + *, + clear_unset: bool = False, + monospace: datatypes.BoolLike | None = None, + word_wrap: datatypes.BoolLike | None = None, + ) -> TextDocumentFormat: + """ + Update only some specific fields of a `TextDocumentFormat`. + + Parameters + ---------- + clear_unset: + If true, all unspecified fields will be explicitly cleared. + monospace: + Whether to use a monospace font for the document body. + + Defaults to disabled. + word_wrap: + Whether to wrap long lines in the document body. + + Defaults to enabled. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + kwargs = { + "monospace": monospace, + "word_wrap": word_wrap, + } + + if clear_unset: + kwargs = {k: v if v is not None else [] for k, v in kwargs.items()} # type: ignore[misc] + + inst.__attrs_init__(**kwargs) + return inst + + inst.__attrs_clear__() + return inst + + @classmethod + def cleared(cls) -> TextDocumentFormat: + """Clear all the fields of a `TextDocumentFormat`.""" + return cls.from_fields(clear_unset=True) + + @staticmethod + def descriptor_monospace() -> ComponentDescriptor: + return ComponentDescriptor( + "TextDocumentFormat:monospace", + archetype=TextDocumentFormat.NAME, + component_type=blueprint_components.EnabledBatch._COMPONENT_TYPE, + ) + + @staticmethod + def descriptor_word_wrap() -> ComponentDescriptor: + return ComponentDescriptor( + "TextDocumentFormat:word_wrap", + archetype=TextDocumentFormat.NAME, + component_type=blueprint_components.EnabledBatch._COMPONENT_TYPE, + ) + + monospace: blueprint_components.EnabledBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.EnabledBatch._converter, # type: ignore[misc] + ) + # Whether to use a monospace font for the document body. + # + # Defaults to disabled. + # + # (Docstring intentionally commented out to hide this field from the docs) + + word_wrap: blueprint_components.EnabledBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.EnabledBatch._converter, # type: ignore[misc] + ) + # Whether to wrap long lines in the document body. + # + # Defaults to enabled. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes index 80919b9b1ae0..e14b0c5aa9f5 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes @@ -10,6 +10,7 @@ auto_layout.py linguist-generated=true auto_scroll.py linguist-generated=true auto_views.py linguist-generated=true background_kind.py linguist-generated=true +column_name.py linguist-generated=true column_order.py linguist-generated=true column_share.py linguist-generated=true component_column_selector.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py index 4a999fcefc59..fc6f32a0aabd 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py @@ -10,6 +10,7 @@ from .auto_scroll import AutoScroll, AutoScrollBatch from .auto_views import AutoViews, AutoViewsBatch from .background_kind import BackgroundKind, BackgroundKindArrayLike, BackgroundKindBatch, BackgroundKindLike +from .column_name import ColumnName, ColumnNameBatch from .column_order import ColumnOrder, ColumnOrderArrayLike, ColumnOrderBatch, ColumnOrderLike from .column_share import ColumnShare, ColumnShareBatch from .component_column_selector import ComponentColumnSelector, ComponentColumnSelectorBatch @@ -75,6 +76,8 @@ "BackgroundKindArrayLike", "BackgroundKindBatch", "BackgroundKindLike", + "ColumnName", + "ColumnNameBatch", "ColumnOrder", "ColumnOrderArrayLike", "ColumnOrderBatch", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/column_name.py b/rerun_py/rerun_sdk/rerun/blueprint/components/column_name.py new file mode 100644 index 000000000000..66f75b76421f --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/column_name.py @@ -0,0 +1,35 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/components/column_name.fbs". + +# You can extend this class by creating a "ColumnNameExt" class in "column_name_ext.py". + +from __future__ import annotations + +from ... import datatypes +from ..._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["ColumnName", "ColumnNameBatch"] + + +class ColumnName(datatypes.Utf8, ComponentMixin): + """ + **Component**: The name of a column in a table. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of ColumnNameExt in column_name_ext.py + + # Note: there are no fields here because ColumnName delegates to datatypes.Utf8 + + +class ColumnNameBatch(datatypes.Utf8Batch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.blueprint.components.ColumnName" + + +# This is patched in late to avoid circular dependencies. +ColumnName._BATCH_TYPE = ColumnNameBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/experimental.py b/rerun_py/rerun_sdk/rerun/blueprint/experimental.py new file mode 100644 index 000000000000..7a9ce1db9cfd --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/experimental.py @@ -0,0 +1,7 @@ +"""Experimental blueprint types — API may change without notice.""" + +from __future__ import annotations + +from .archetypes import ( + TableBlueprint as TableBlueprint, +) diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes index 1cf2a57418bd..d0e847026e7c 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes @@ -8,7 +8,7 @@ graph_view.py linguist-generated=true map_view.py linguist-generated=true spatial2d_view.py linguist-generated=true spatial3d_view.py linguist-generated=true -status_view.py linguist-generated=true +state_timeline_view.py linguist-generated=true tensor_view.py linguist-generated=true text_document_view.py linguist-generated=true text_log_view.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py index d800db82767f..08c7ca6e72c1 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py @@ -8,7 +8,7 @@ from .map_view import MapView from .spatial2d_view import Spatial2DView from .spatial3d_view import Spatial3DView -from .status_view import StatusView +from .state_timeline_view import StateTimelineView from .tensor_view import TensorView from .text_document_view import TextDocumentView from .text_log_view import TextLogView @@ -21,7 +21,7 @@ "MapView", "Spatial2DView", "Spatial3DView", - "StatusView", + "StateTimelineView", "TensorView", "TextDocumentView", "TextLogView", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/bar_chart_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/bar_chart_view.py index 587b4b5364c7..b265d5e4b7ee 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/bar_chart_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/bar_chart_view.py @@ -43,7 +43,9 @@ class BarChartView(View): rrb.BarChartView( origin="bar_chart", name="Bar Chart", - background=rrb.archetypes.PlotBackground(color=[50, 0, 50, 255], show_grid=False), + background=rrb.archetypes.PlotBackground( + color=[50, 0, 50, 255], show_grid=False + ), ), collapse_panels=True, ) diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/dataframe_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/dataframe_view.py index be4ed3840253..94f755bed6ec 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/dataframe_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/dataframe_view.py @@ -61,7 +61,13 @@ class DataframeView(View): timeline="t", filter_by_range=(rr.TimeInt(seconds=0), rr.TimeInt(seconds=20)), filter_is_not_null="/trig/tan_sparse:Scalar", - select=["t", "log_tick", "/trig/sin:Scalar", "/trig/cos:Scalar", "/trig/tan_sparse:Scalar"], + select=[ + "t", + "log_tick", + "/trig/sin:Scalar", + "/trig/cos:Scalar", + "/trig/tan_sparse:Scalar", + ], entity_order=["/trig/cos", "/trig/sin", "/trig/tan_sparse"], auto_scroll=True, ), diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/graph_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/graph_view.py index 133d3ffced4a..397b017ea109 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/graph_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/graph_view.py @@ -52,7 +52,9 @@ class GraphView(View): origin="/", name="Graph", # Note that this translates the viewbox. - visual_bounds=rrb.VisualBounds2D(x_range=[-150, 150], y_range=[-50, 150]), + visual_bounds=rrb.VisualBounds2D( + x_range=[-150, 150], y_range=[-50, 150] + ), background=rrb.archetypes.GraphBackground(color=[30, 10, 10]), ), collapse_panels=True, @@ -131,7 +133,7 @@ def __init__( visual_bounds: Everything within these bounds is guaranteed to be visible. - Somethings outside of these bounds may also be visible due to letterboxing. + Some things outside of these bounds may also be visible due to letterboxing. force_link: Allows to control the interaction between two nodes connected by an edge. force_many_body: diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/map_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/map_view.py index 3fe560f069c2..c602c4869b8b 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/map_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/map_view.py @@ -37,7 +37,13 @@ class MapView(View): rr.init("rerun_example_map_view", spawn=True) - rr.log("points", rr.GeoPoints(lat_lon=[[47.6344, 19.1397], [47.6334, 19.1399]], radii=rr.Radius.ui_points(20.0))) + rr.log( + "points", + rr.GeoPoints( + lat_lon=[[47.6344, 19.1397], [47.6334, 19.1399]], + radii=rr.Radius.ui_points(20.0), + ), + ) # Create a map view to display the chart. blueprint = rrb.Blueprint( diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/spatial2d_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/spatial2d_view.py index 8c91e80dba5e..e8505c602b66 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/spatial2d_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/spatial2d_view.py @@ -43,8 +43,15 @@ class Spatial2DView(View): n = 150 angle = np.linspace(0, 10 * np.pi, n) spiral_radius = np.linspace(0.0, 3.0, n) ** 2 - positions = np.column_stack((np.cos(angle) * spiral_radius, np.sin(angle) * spiral_radius)) - colors = np.dstack((np.linspace(255, 255, n), np.linspace(255, 0, n), np.linspace(0, 255, n)))[0].astype(int) + positions = np.column_stack(( + np.cos(angle) * spiral_radius, + np.sin(angle) * spiral_radius, + )) + colors = np.dstack(( + np.linspace(255, 255, n), + np.linspace(255, 0, n), + np.linspace(0, 255, n), + ))[0].astype(int) radii = np.linspace(0.01, 0.7, n) rr.log("points", rr.Points2D(positions, colors=colors, radii=radii)) @@ -91,6 +98,7 @@ def __init__( | blueprint_components.BackgroundKindLike | None = None, visual_bounds: blueprint_archetypes.VisualBounds2D | None = None, + spatial_information: blueprint_archetypes.SpatialInformation | None = None, time_ranges: blueprint_archetypes.VisibleTimeRanges | datatypes.VisibleTimeRangeLike | Sequence[datatypes.VisibleTimeRangeLike] @@ -140,6 +148,8 @@ def __init__( Everything within these bounds are guaranteed to be visible. Somethings outside of these bounds may also be visible due to letterboxing. + spatial_information: + Configuration of spatial information shown in the view. time_ranges: Configures which range on each timeline is shown by this view (unless specified differently per entity). @@ -159,6 +169,11 @@ def __init__( visual_bounds = blueprint_archetypes.VisualBounds2D(visual_bounds) properties["VisualBounds2D"] = visual_bounds + if spatial_information is not None: + if not isinstance(spatial_information, blueprint_archetypes.SpatialInformation): + spatial_information = blueprint_archetypes.SpatialInformation(spatial_information) + properties["SpatialInformation"] = spatial_information + if time_ranges is not None: if not isinstance(time_ranges, blueprint_archetypes.VisibleTimeRanges): time_ranges = blueprint_archetypes.VisibleTimeRanges(time_ranges) diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/spatial3d_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/spatial3d_view.py index c3761d3268ed..891fa9ebb86e 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/spatial3d_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/spatial3d_view.py @@ -66,12 +66,19 @@ class Spatial3DView(View): ), # Configure the line grid. line_grid=rrb.LineGrid3D( - visible=True, # The grid is enabled by default, but you can hide it with this property. + # The grid is enabled by default, but you can hide it. + visible=True, spacing=0.1, # Makes the grid more fine-grained. - # By default, the plane is inferred from view coordinates setup, but you can set arbitrary planes. + # By default, the plane is inferred from view coordinates setup, + # but you can set arbitrary planes. plane=rr.components.Plane3D.XY.with_distance(-5.0), stroke_width=2.0, # Makes the grid lines twice as thick as usual. - color=[255, 255, 255, 128], # Colors the grid a half-transparent white. + color=[ + 255, + 255, + 255, + 128, + ], # Colors the grid a half-transparent white. ), spatial_information=rrb.SpatialInformation( target_frame="tf#/", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/status_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/state_timeline_view.py similarity index 84% rename from rerun_py/rerun_sdk/rerun/blueprint/views/status_view.py rename to rerun_py/rerun_sdk/rerun/blueprint/views/state_timeline_view.py index 3f278b2c7210..c977c8e26651 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/status_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/state_timeline_view.py @@ -1,11 +1,11 @@ # DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs -# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/views/status.fbs". +# Based on "crates/store/re_sdk_types/definitions/rerun/blueprint/views/state_timeline.fbs". from __future__ import annotations from typing import TYPE_CHECKING -__all__ = ["StatusView"] +__all__ = ["StateTimelineView"] from ..api import View, ViewContentsLike, VisualizerLike @@ -21,37 +21,37 @@ from ...datatypes import EntityPathLike, Utf8Like -class StatusView(View): +class StateTimelineView(View): """ - **View**: A view for displaying status transitions over time, for use with [`archetypes.Status`][rerun.archetypes.Status]. + **View**: A view for displaying state transitions over time, for use with [`archetypes.StateChange`][rerun.archetypes.StateChange]. ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** Example ------- - ### Use a blueprint to show a StatusView.: + ### Use a blueprint to show a StateTimelineView.: ```python - # Use a blueprint to show a StatusView. + # Use a blueprint to show a StateTimelineView. import rerun as rr import rerun.blueprint as rrb - rr.init("rerun_example_status", spawn=True) + rr.init("rerun_example_state_timeline", spawn=True) rr.set_time("step", sequence=0) - rr.log("door", rr.Status(status="open")) + rr.log("door", rr.StateChange(state="open")) rr.set_time("step", sequence=1) - rr.log("door", rr.Status(status="closed")) + rr.log("door", rr.StateChange(state="closed")) rr.set_time("step", sequence=2) - rr.log("door", rr.Status(status="open")) + rr.log("door", rr.StateChange(state="open")) - # Create a status view to display the status transitions. + # Create a state timeline view to display the state transitions. blueprint = rrb.Blueprint( - rrb.StatusView( + rrb.StateTimelineView( origin="/", - name="Status Transitions", + name="State Transitions", ), collapse_panels=True, ) @@ -81,7 +81,7 @@ def __init__( overrides: Mapping[EntityPathLike, VisualizerLike | Iterable[VisualizerLike]] | None = None, ) -> None: """ - Construct a blueprint for a new StatusView view. + Construct a blueprint for a new StateTimelineView view. Parameters ---------- @@ -121,7 +121,7 @@ def __init__( properties: dict[str, AsComponents] = {} super().__init__( - class_identifier="Status", + class_identifier="StateTimeline", origin=origin, contents=contents, name=name, diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/tensor_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/tensor_view.py index 189aebad26c3..689fdd43bc12 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/tensor_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/tensor_view.py @@ -57,11 +57,15 @@ class TensorView(View): rr.TensorDimensionIndexSelection(dimension=2, index=4), rr.TensorDimensionIndexSelection(dimension=3, index=5), ], - # Show a slider for dimension 2 only. If not specified, all dimensions in `indices` will have sliders. + # Show a slider for dimension 2 only. If not specified, all + # dimensions in `indices` will have sliders. slider=[2], ), - # Set a scalar mapping with a custom colormap, gamma and magnification filter. - scalar_mapping=rrb.TensorScalarMapping(colormap="turbo", gamma=1.5, mag_filter="linear"), + # Set a scalar mapping with a custom colormap, gamma and + # magnification filter. + scalar_mapping=rrb.TensorScalarMapping( + colormap="turbo", gamma=1.5, mag_filter="linear" + ), # Fill the view, ignoring aspect ratio. view_fit="fill", ), diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/text_document_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/text_document_view.py index dab5ed832451..7dcdbf3ad144 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/text_document_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/text_document_view.py @@ -8,6 +8,7 @@ __all__ = ["TextDocumentView"] +from .. import archetypes as blueprint_archetypes from ..api import View, ViewContentsLike, VisualizerLike if TYPE_CHECKING: @@ -104,6 +105,7 @@ def __init__( visible: datatypes.BoolLike | None = None, defaults: Iterable[AsComponents | Iterable[DescribedComponentBatch]] | None = None, overrides: Mapping[EntityPathLike, VisualizerLike | Iterable[VisualizerLike]] | None = None, + format_options: blueprint_archetypes.TextDocumentFormat | None = None, ) -> None: """ Construct a blueprint for a new TextDocumentView view. @@ -142,9 +144,17 @@ def __init__( do not yet support `$origin` relative paths or glob expressions. This will be addressed in . + format_options: + Formatting options for the text document view. + """ properties: dict[str, AsComponents] = {} + if format_options is not None: + if not isinstance(format_options, blueprint_archetypes.TextDocumentFormat): + format_options = blueprint_archetypes.TextDocumentFormat(format_options) + properties["TextDocumentFormat"] = format_options + super().__init__( class_identifier="TextDocument", origin=origin, diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/text_log_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/text_log_view.py index 4b78834cbba1..534013ad1626 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/text_log_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/text_log_view.py @@ -38,12 +38,17 @@ class TextLogView(View): rr.init("rerun_example_text_log", spawn=True) rr.set_time("time", sequence=0) - rr.log("log/status", rr.TextLog("Application started.", level=rr.TextLogLevel.INFO)) + rr.log( + "log/status", rr.TextLog("Application started.", level=rr.TextLogLevel.INFO) + ) rr.set_time("time", sequence=5) rr.log("log/other", rr.TextLog("A warning.", level=rr.TextLogLevel.WARN)) for i in range(10): rr.set_time("time", sequence=i) - rr.log("log/status", rr.TextLog(f"Processing item {i}.", level=rr.TextLogLevel.INFO)) + rr.log( + "log/status", + rr.TextLog(f"Processing item {i}.", level=rr.TextLogLevel.INFO), + ) # Create a text view that displays all logs. blueprint = rrb.Blueprint( diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/time_series_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/time_series_view.py index fb4b15dc2cc4..03ba0aacf947 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/time_series_view.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/time_series_view.py @@ -40,9 +40,21 @@ class TimeSeriesView(View): rr.init("rerun_example_timeseries", spawn=True) # Log some trigonometric functions - rr.log("trig/sin", rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)"), static=True) - rr.log("trig/cos", rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)"), static=True) - rr.log("trig/cos_scaled", rr.SeriesLines(colors=[0, 0, 255], names="cos(0.01t) scaled"), static=True) + rr.log( + "trig/sin", + rr.SeriesLines(colors=[255, 0, 0], names="sin(0.01t)"), + static=True, + ) + rr.log( + "trig/cos", + rr.SeriesLines(colors=[0, 255, 0], names="cos(0.01t)"), + static=True, + ) + rr.log( + "trig/cos_scaled", + rr.SeriesLines(colors=[0, 0, 255], names="cos(0.01t) scaled"), + static=True, + ) for t in range(int(math.pi * 4 * 100.0)): rr.set_time("timeline0", sequence=t) rr.set_time("timeline1", duration=t) @@ -62,13 +74,15 @@ class TimeSeriesView(View): plot_legend=rrb.PlotLegend(visible=False), # Set time different time ranges for different timelines. time_ranges=[ - # Sliding window depending on the time cursor for the first timeline. + # Sliding window depending on the time cursor for the + # first timeline. rrb.VisibleTimeRange( "timeline0", start=rrb.TimeRangeBoundary.cursor_relative(seq=-100), end=rrb.TimeRangeBoundary.cursor_relative(), ), - # Time range from some point to the end of the timeline for the second timeline. + # Time range from some point to the end of the timeline + # for the second timeline. rrb.VisibleTimeRange( "timeline1", start=rrb.TimeRangeBoundary.absolute(seconds=300.0), @@ -80,14 +94,18 @@ class TimeSeriesView(View): origin="/trig", axis_x=rrb.TimeAxis( view_range=rr.TimeRange( - start=rrb.TimeRangeBoundary.cursor_relative(seconds=-100), + start=rrb.TimeRangeBoundary.cursor_relative( + seconds=-100 + ), end=rrb.TimeRangeBoundary.cursor_relative(seconds=100), ), zoom_lock=True, ), # Configure the legend. plot_legend=rrb.PlotLegend(visible=True), - background=rrb.archetypes.PlotBackground(color=[128, 128, 128], show_grid=False), + background=rrb.archetypes.PlotBackground( + color=[128, 128, 128], show_grid=False + ), ), ] ), diff --git a/rerun_py/rerun_sdk/rerun/catalog/__init__.py b/rerun_py/rerun_sdk/rerun/catalog/__init__.py index 566023c65506..202930c2ceb1 100644 --- a/rerun_py/rerun_sdk/rerun/catalog/__init__.py +++ b/rerun_py/rerun_sdk/rerun/catalog/__init__.py @@ -8,14 +8,10 @@ EntryKind as EntryKind, IndexColumnDescriptor as IndexColumnDescriptor, IndexColumnSelector as IndexColumnSelector, - IndexConfig as IndexConfig, - IndexingResult as IndexingResult, NotFoundError as NotFoundError, - VectorDistanceMetric as VectorDistanceMetric, ) from rerun_bindings.types import ( IndexValuesLike as IndexValuesLike, - VectorDistanceMetricLike as VectorDistanceMetricLike, ) from ._catalog_client import CatalogClient as CatalogClient, VersionInfo as VersionInfo @@ -33,3 +29,6 @@ SegmentRegistrationResult as SegmentRegistrationResult, ) from ._schema import Schema as Schema +from ._unregistration_handle import ( + UnregistrationHandle as UnregistrationHandle, +) diff --git a/rerun_py/rerun_sdk/rerun/catalog/_catalog_client.py b/rerun_py/rerun_sdk/rerun/catalog/_catalog_client.py index 41ee5eeda40b..3646c31085b7 100644 --- a/rerun_py/rerun_sdk/rerun/catalog/_catalog_client.py +++ b/rerun_py/rerun_sdk/rerun/catalog/_catalog_client.py @@ -1,8 +1,10 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import timedelta from typing import TYPE_CHECKING, overload +from rerun._tracing import with_tracing from rerun.error_utils import _send_warning_or_raise from rerun_bindings import ( CatalogClientInternal, @@ -27,7 +29,7 @@ def _are_datafusion_versions_compatible(v1: int, v2: int) -> bool: """ Determine compatibility between two DataFusion versions. - In some rare cases, we may need to have a mismatch, e.g. in some deployed Rerun Cloud docker images. So we have a + In some rare cases, we may need to have a mismatch, e.g. in some deployed Rerun Hub docker images. So we have a carefully crafted compatibility allowlist for known-to-be-ffi-compatible DataFusion releases. """ @@ -64,6 +66,46 @@ class VersionInfo: """The cloud region (e.g. "us-west-2", "eastus"). None if not deployed on cloud.""" +@dataclass(frozen=True) +class BenchmarkResult: + """Result of [`CatalogClient.benchmark`][].""" + + rtt: timedelta + """Round-trip time to the server.""" + + bandwidth: float | None + """ + Estimated download bandwidth from the server, in bytes per second. + + `None` if the bandwidth probe was too small to be measured (e.g. a tiny payload on a fast + loopback connection — the elapsed time is dominated by RTT). + """ + + def __repr__(self) -> str: + return f"BenchmarkResult(rtt={_format_duration(self.rtt)}, bandwidth={_format_bandwidth(self.bandwidth)})" + + +def _format_duration(d: timedelta) -> str: + seconds = d.total_seconds() + if seconds < 1e-3: + return f"{seconds * 1e6:.1f} μs" + if seconds < 1.0: + return f"{seconds * 1e3:.1f} ms" + return f"{seconds:.2f} s" + + +def _format_bandwidth(bps: float | None) -> str: + if bps is None: + return "(too fast to measure)" + units = ("B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s") + value = bps + idx = 0 + while value >= 1024.0 and idx + 1 < len(units): + value /= 1024.0 + idx += 1 + return f"{value:.1f} {units[idx]}" + + class CatalogClient: """ Client for a remote Rerun catalog server. @@ -147,6 +189,36 @@ def version_info(self) -> VersionInfo: version, cloud_provider, cloud_region = self._internal.version_info() return VersionInfo(version=version, cloud_provider=cloud_provider, cloud_region=cloud_region) + @with_tracing("CatalogClient.benchmark") + def benchmark(self, *, num_bytes: int = 16 * 1024 * 1024, num_pings: int = 5) -> BenchmarkResult: + """ + Measure round-trip time and download bandwidth to the server. + + The RTT is estimated as the minimum elapsed time across `num_pings` 1-byte requests + (using the minimum rejects latency spikes from scheduling jitter or transient network + congestion). Bandwidth is measured by downloading `num_bytes` of pseudo-random + (incompressible) bytes, subtracting the RTT from the elapsed time, and dividing by the + payload size. + + Parameters + ---------- + num_bytes + Total payload size to download from the server when measuring bandwidth. + num_pings + How many 1-byte requests to send when estimating RTT. + + Examples + -------- + ```python + client = rr.catalog.CatalogClient("…") + print(client.benchmark()) # BenchmarkResult(rtt=12.0 ms, bandwidth=112.0 MiB/s) + ``` + + """ + rtt_seconds = self._internal.rtt_seconds(num_pings) + bandwidth = self._internal.bandwidth_bytes_per_sec(num_bytes, rtt_seconds) + return BenchmarkResult(rtt=timedelta(seconds=rtt_seconds), bandwidth=bandwidth) + def entries(self, *, include_hidden: bool = False) -> list[DatasetEntry | TableEntry]: """ Returns a list of all entries in the catalog. @@ -154,7 +226,7 @@ def entries(self, *, include_hidden: bool = False) -> list[DatasetEntry | TableE Parameters ---------- include_hidden - If True, include hidden entries (blueprint datasets and system tables like `__entries`). + If True, include hidden entries (blueprint and asset datasets, and system tables like `__entries`). """ return self.datasets(include_hidden=include_hidden) + self.tables(include_hidden=include_hidden) @@ -166,7 +238,7 @@ def datasets(self, *, include_hidden: bool = False) -> list[DatasetEntry]: Parameters ---------- include_hidden - If True, include blueprint datasets. + If True, include hidden datasets (blueprint and asset datasets). """ from . import DatasetEntry @@ -249,7 +321,7 @@ def get_dataset(self, name: str | None = None, *, id: EntryId | str | None = Non """ from . import DatasetEntry - return DatasetEntry(self._internal.get_dataset(self._resolve_name_or_id(id, name))) + return DatasetEntry(self._internal.get_dataset(self._resolve_name_or_id(id, name, entry_kind="dataset"))) @overload def get_table(self, *, id: EntryId | str) -> TableEntry: ... @@ -273,7 +345,7 @@ def get_table(self, name: str | None = None, *, id: EntryId | str | None = None) """ from . import TableEntry - return TableEntry(self._internal.get_table(self._resolve_name_or_id(id, name))) + return TableEntry(self._internal.get_table(self._resolve_name_or_id(id, name, entry_kind="table"))) # --- @@ -349,7 +421,8 @@ def create_table(self, name: str, schema: pa.Schema, url: str | None = None) -> url The URL of the directory for where to store the Lance table. If provided, the table will be stored in a globally unique subdirectory. If not provided, the server will use an automatically generated URL based on - its configured writable storage. + its configured writable storage. On Rerun Hub, custom table URLs are currently not supported: the request + will be rejected unless this parameter is None. """ from . import TableEntry @@ -368,7 +441,13 @@ def ctx(self) -> datafusion.SessionContext: # --- - def _resolve_name_or_id(self, id: EntryId | str | None = None, name: str | None = None) -> EntryId: + def _resolve_name_or_id( + self, + id: EntryId | str | None = None, + name: str | None = None, + *, + entry_kind: str = "entry", + ) -> EntryId: """Helper method to resolve either ID or name. Returns the id or throw an error.""" match id, name: @@ -383,7 +462,10 @@ def _resolve_name_or_id(self, id: EntryId | str | None = None, name: str | None return EntryId(id) case (None, str(name)): - return self._internal._entry_id_from_entry_name(name) + try: + return self._internal._entry_id_from_entry_name(name) + except LookupError: + raise LookupError(f"No {entry_kind} found with name {name!r}") from None case _: raise ValueError("Only one of 'id' or 'name' must be provided.") diff --git a/rerun_py/rerun_sdk/rerun/catalog/_entry.py b/rerun_py/rerun_sdk/rerun/catalog/_entry.py index 23a2c8ea0ca1..51b47a205960 100644 --- a/rerun_py/rerun_sdk/rerun/catalog/_entry.py +++ b/rerun_py/rerun_sdk/rerun/catalog/_entry.py @@ -26,24 +26,19 @@ ) if TYPE_CHECKING: - from datetime import datetime + from datetime import datetime, timedelta import datafusion - from rerun.recording import Recording + from rerun.experimental import LazyStore from . import ( CatalogClient, - ComponentColumnDescriptor, - ComponentColumnSelector, EntryKind, - IndexColumnSelector, - IndexConfig, - IndexingResult, IndexValuesLike, RegistrationHandle, Schema, - VectorDistanceMetric, + UnregistrationHandle, ) @@ -192,11 +187,17 @@ def arrow_schema(self) -> pa.Schema: return self._internal.arrow_schema() - def register_blueprint(self, uri: str, set_default: bool = True) -> None: + def register_blueprint(self, uri: str, set_default: bool = True, *, segment_table: bool = False) -> None: """ Register an existing .rbl visible to the server. By default, also set this blueprint as default. + + Set `segment_table=True` (and `set_default=True`) to register it as this dataset's + default for the segment table blueprint. + + The associated blueprint dataset is owned by this dataset for lifecycle purposes. + Deleting this dataset also deletes the associated blueprint dataset and its storage. """ blueprint_dataset = self.blueprint_dataset() @@ -204,10 +205,15 @@ def register_blueprint(self, uri: str, set_default: bool = True) -> None: if blueprint_dataset is None: raise LookupError("a blueprint dataset is not configured for this dataset") - segment_id = blueprint_dataset.register(uri, on_duplicate=OnDuplicateSegmentLayer.REPLACE).wait().segment_ids[0] + segment_id = ( + blueprint_dataset.register([uri], on_duplicate=OnDuplicateSegmentLayer.REPLACE).wait().segment_ids[0] + ) if set_default: - self.set_default_blueprint(segment_id) + if segment_table: + self.set_default_segment_table_blueprint(segment_id) + else: + self.set_default_blueprint(segment_id) def blueprints(self) -> list[str]: """Lists all blueprints currently registered with this dataset.""" @@ -228,12 +234,107 @@ def default_blueprint(self) -> str | None: return self._internal.default_blueprint_segment_id() + def set_default_segment_table_blueprint(self, blueprint_name: str | None) -> None: + """Set an already-registered blueprint as the default segment table blueprint for this dataset.""" + + return self._internal.set_default_segment_table_blueprint_segment_id(blueprint_name) + + def default_segment_table_blueprint(self) -> str | None: + """Return the name of the currently set segment table blueprint.""" + + return self._internal.default_segment_table_blueprint_segment_id() + def blueprint_dataset(self) -> DatasetEntry | None: - """The associated blueprint dataset, if any.""" + """ + The associated blueprint dataset, if any. + + The associated blueprint dataset is owned by this dataset for lifecycle purposes. + Deleting this dataset also deletes the associated blueprint dataset and its storage. + """ ds = self._internal.blueprint_dataset() return None if ds is None else DatasetEntry(ds) + def assets(self) -> list[str]: + """Lists all assets currently registered with this dataset.""" + + asset_dataset = self.asset_dataset() + if asset_dataset is None: + return [] + else: + return asset_dataset.segment_ids() + + def register_asset(self, uri: str) -> str: + """ + Register an existing .rrd visible to the server as an asset. + + Asset datasets hold a small set of static blobs shared across a dataset's segments, + so they are kept deliberately small. The server enforces a few limits on the .rrd you register: + + * it must contain only static data, temporal chunks are rejected, + * each asset segment must stay under a per-segment size limit, + * the asset dataset may only hold a limited number of segments. + + Parameters + ---------- + uri: + The URI of the .rrd file to register. It must be visible to the server. + + Returns + ------- + str + The segment id of the registered asset. + + """ + + asset_dataset = self.asset_dataset() + + if asset_dataset is None: + # Datasets created before asset datasets were introduced don't have one until their + # entry is next updated, so ask the server to create it. + self._internal._ensure_asset_dataset() + asset_dataset = self.asset_dataset() + + if asset_dataset is None: + raise LookupError("an asset dataset is not configured for this dataset") + + return asset_dataset.register([uri], on_duplicate=OnDuplicateSegmentLayer.REPLACE).wait().segment_ids[0] + + def unregister_asset(self, segment_id: str) -> None: + """ + Unregister a previously registered asset. + + Since assets are shared across all of a dataset's segments, there is no way to scope + an asset to a subset of them, so removing one means unregistering it here. + + Unregistering an asset that doesn't exist is a no-op. + + Parameters + ---------- + segment_id: + The segment id of the asset to unregister, as returned by [`register_asset`][rerun.catalog.DatasetEntry.register_asset]. + + """ + + asset_dataset = self.asset_dataset() + + if asset_dataset is None: + # No asset dataset means no assets were ever registered, so there is nothing to drop. + return + + asset_dataset.unregister(segments_to_drop=[segment_id], layers_to_drop=[]) + + def asset_dataset(self) -> DatasetEntry | None: + """ + The associated asset dataset, if any. + + The associated asset dataset is owned by this dataset for lifecycle purposes. + Deleting this dataset also deletes the associated asset dataset and its storage. + """ + + ds = self._internal.asset_dataset() + return None if ds is None else DatasetEntry(ds) + def schema(self) -> Schema: """Return the schema of the data contained in the dataset.""" from ._schema import Schema @@ -296,6 +397,10 @@ def segment_table( return segment_table_df + @deprecated( + "DatasetEntry.manifest() is deprecated and will be removed in a future release. " + "It was intended for internal and debugging use only." + ) def manifest(self, include_diagnostic_data: bool = False) -> datafusion.DataFrame: """ Return the dataset manifest as a DataFusion DataFrame. @@ -312,6 +417,11 @@ def manifest(self, include_diagnostic_data: bool = False) -> datafusion.DataFram """ + return self._manifest(include_diagnostic_data=include_diagnostic_data) + + def _manifest(self, include_diagnostic_data: bool = False) -> datafusion.DataFrame: + """Return the dataset manifest as a DataFusion DataFrame. Intended for internal and debugging use only.""" + from datafusion import col df = self._internal.manifest() @@ -325,8 +435,8 @@ def segment_url( # noqa: PLR0917 self, segment_id: str, timeline: str | None = None, - start: datetime | int | None = None, - end: datetime | int | None = None, + start: datetime | timedelta | int | None = None, + end: datetime | timedelta | int | None = None, ) -> str: """ Return the URL for the given segment. @@ -339,13 +449,14 @@ def segment_url( # noqa: PLR0917 timeline: str | None The name of the timeline to display. - start: int | datetime | None + start: int | datetime | timedelta | None The start selected time for the segment. - Integer for ticks, or datetime/nanoseconds for timestamps. + Integer for ticks, datetime/nanoseconds for timestamps, or timedelta for durations. - end: int | datetime | None + end: int | datetime | timedelta | None The end selected time for the segment. - Integer for ticks, or datetime/nanoseconds for timestamps. + Integer for ticks, datetime/nanoseconds for timestamps, or timedelta for durations. + If omitted, no time range selection is emitted (only the `#when` cursor). Examples -------- @@ -366,9 +477,11 @@ def segment_url( # noqa: PLR0917 return self._internal.segment_url(segment_id, timeline, start, end) + @with_tracing("DatasetEntry.register") def register( self, - recording_uri: str | Sequence[str], + # NOTE: this can't be Sequence[str], because `str` IS a `Sequence[str]`, and we would thus get no helpful typechecking + recording_uri: list[str], *, layer_name: str | Sequence[str] = "base", on_duplicate: OnDuplicateSegmentLayer = OnDuplicateSegmentLayer.ERROR, @@ -379,10 +492,13 @@ def register( This method initiates the registration of recordings to the dataset, and returns a handle that can be used to wait for completion or iterate over results. + Prefer batching many URIs into a single `register` call rather than calling + `register` repeatedly in a loop, which is much slower. + Parameters ---------- recording_uri: - The URI(s) of the RRD(s) to register. Can be a single URI string or a sequence of URIs. + The URIs of the RRDs to register, as a sequence of strings. layer_name: The layer(s) to which the recordings will be registered to. @@ -400,9 +516,18 @@ def register( A handle to track and wait on the registration tasks. """ + from rerun.error_utils import _send_warning_or_raise + from ._registration_handle import RegistrationHandle if isinstance(recording_uri, str): + _send_warning_or_raise( + "`DatasetEntry.register` was called with a single string for `recording_uri`. " + "This is deprecated: pass a sequence of URIs instead, and prefer batching " + "many URIs into a single call rather than calling `register` in a loop.", + depth_to_user_code=2, + warning_type=DeprecationWarning, + ) recording_uris = [recording_uri] else: recording_uris = list(recording_uri) @@ -418,13 +543,14 @@ def register( self._internal.register(recording_uris, recording_layers=layer_names, on_duplicate=on_duplicate) ) + @with_tracing("DatasetEntry.unregister") def unregister( self, *, segments_to_drop: str | Sequence[str], layers_to_drop: str | Sequence[str], force: bool = False, - ) -> None: + ) -> UnregistrationHandle: """ Unregisters segments and layers from the dataset. @@ -464,7 +590,11 @@ def unregister( else: layers_to_drop = list(layers_to_drop) - self._internal.unregister(segments_to_drop=segments_to_drop, layers_to_drop=layers_to_drop, force=force) + from ._unregistration_handle import UnregistrationHandle + + return UnregistrationHandle( + self._internal.unregister(segments_to_drop=segments_to_drop, layers_to_drop=layers_to_drop, force=force) + ) def register_prefix( self, @@ -506,11 +636,18 @@ def register_prefix( return RegistrationHandle(self._internal.register_prefix(recordings_prefix, layer_name, on_duplicate)) - def download_segment(self, segment_id: str) -> Recording: - """Download a segment from the dataset.""" - from rerun.recording import Recording + def segment_store(self, segment_id: str) -> LazyStore: + """ + Open a remote segment as a [`LazyStore`][rerun.experimental.LazyStore]. - return Recording(self._internal.download_segment(segment_id)) + The manifest is fetched immediately; chunk data is loaded on demand + via [`LazyStore.stream`][rerun.experimental.LazyStore.stream]. To fully + materialize into a [`ChunkStore`][rerun.experimental.ChunkStore], call + `lazy.stream().collect()`. + """ + from rerun.experimental import LazyStore + + return LazyStore(self._internal.segment_store(segment_id)) @with_tracing("DatasetEntry.filter_segments") def filter_segments(self, segment_ids: str | Sequence[str] | datafusion.DataFrame) -> DatasetView: @@ -651,9 +788,45 @@ def reader( fill_latest_at Whether to fill null values with the latest valid data. using_index_values - If provided, specifies the exact index values to sample per segment. - Can be a numpy array (datetime64[ns] or int64), a pyarrow Array, or a sequence. - Use with `fill_latest_at=True` to populate rows with the most recent data. + Index values at which to **resample** data. + + When specified, this argument changes the way rows are returned. Instead + of returning the rows that exist in the data, one row is returned per + `(segment, index_value)` pair you provide. If the segment has no row at + that index value, nulls are returned — or the latest prior value if + `fill_latest_at=True` (which is typically what you want for resampling). + + Don't use this argument for plain index slicing — use a DataFusion filter + on the index column instead. For example: + + ```python + from datafusion import col, lit + + # All rows in a time window. + ds.reader(index="real_time").filter( + (col("real_time") >= lit(t0)) & (col("real_time") <= lit(t1)) + ) + ``` + + This argument accepts the following shapes: + - **plain array**: values are applied only to segments whose index + range covers them (segments outside the range are excluded). + - **dict**: keys are segment IDs, values are per-segment index values + to sample at. + - **DataFrame**: must have `rerun_segment_id` and index columns; + treated as a per-segment value list. + + !!! note + The plain array form requires a scan of the segment table to + map values to the segments whose index range covers them. On + datasets with many segments this can be expensive. Prefer the + dict or DataFrame form when the per-segment values are already + known on the client side. + + !!! note + Unknown segment IDs are silently ignored — they contribute no + rows to the result. Validate client-side if you need to catch + unknown segment IDs. Returns ------- @@ -670,135 +843,6 @@ def reader( using_index_values=using_index_values, ) - @deprecated( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def create_fts_search_index( - self, - *, - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - time_index: IndexColumnSelector, - store_position: bool = False, - base_tokenizer: str = "simple", - ) -> None: - """Create a full-text search index on the given column.""" - - try: - return self._internal.create_fts_search_index( # ty: ignore[deprecated] - column=column, - time_index=time_index, - store_position=store_position, - base_tokenizer=base_tokenizer, - ) - except Exception as err: - raise NotImplementedError( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) from err - - @deprecated( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def create_vector_search_index( - self, - *, - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - time_index: IndexColumnSelector, - target_partition_num_rows: int | None = None, - num_sub_vectors: int = 16, - distance_metric: VectorDistanceMetric | str = "Cosine", - ) -> IndexingResult: - """ - Create a vector index on the given column. - - This will enable indexing and build the vector index over all existing values - in the specified component column. - - Results can be retrieved using the `search_vector` API, which will include - the time-point on the indexed timeline. - - Only one index can be created per component column -- executing this a second - time for the same component column will replace the existing index. - - Parameters - ---------- - column - The component column to create the index on. - time_index - Which timeline this index will map to. - target_partition_num_rows - The target size (in number of rows) for each partition. - The underlying indexer (lance) will pick a default when no value - is specified - today this is 8192. It will also cap the - maximum number of partitions independently of this setting - currently - 4096. - num_sub_vectors - The number of sub-vectors to use when building the index. - distance_metric - The distance metric to use for the index. ("L2", "Cosine", "Dot", "Hamming") - - """ - - try: - return self._internal.create_vector_search_index( # ty: ignore[deprecated] - column=column, - time_index=time_index, - target_partition_num_rows=target_partition_num_rows, - num_sub_vectors=num_sub_vectors, - distance_metric=distance_metric, - ) - except Exception as err: - raise NotImplementedError( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) from err - - def list_search_indexes(self) -> list[IndexingResult]: - """List all user-defined indexes in this dataset.""" - - return self._internal.list_search_indexes() - - def delete_search_indexes( - self, - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - ) -> list[IndexConfig]: - """Deletes all user-defined indexes for the specified column.""" - - return self._internal.delete_search_indexes(column) - - @deprecated( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def search_fts( - self, - query: str, - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - ) -> datafusion.DataFrame: - """Search the dataset using a full-text search query.""" - - try: - return self._internal.search_fts(query, column) # ty: ignore[deprecated] - except Exception as err: - raise NotImplementedError( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) from err - - @deprecated( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def search_vector( - self, - query: Any, # VectorLike - column: str | ComponentColumnSelector | ComponentColumnDescriptor, - top_k: int, - ) -> datafusion.DataFrame: - """Search the dataset using a vector search query.""" - - try: - return self._internal.search_vector(query, column, top_k) # ty: ignore[deprecated] - except Exception as err: - raise NotImplementedError( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) from err - def do_maintenance( # noqa: PLR0917 self, optimize_indexes: bool = False, @@ -987,13 +1031,45 @@ def reader( include_tombstone_columns Whether to include tombstone columns. using_index_values - Index values at which to sample data. - If a plain array is provided, values are applied only to segments - whose index range covers them (segments outside the range are excluded). - If a dict is provided, keys are segment IDs and values are the index values - to sample for that segment (per-segment semantics). - If a DataFrame is provided, it must have 'rerun_segment_id' and index columns. - Use with `fill_latest_at=True` to populate rows with the most recent data. + Index values at which to **resample** data. + + When specified, this argument changes the way rows are returned. Instead + of returning the rows that exist in the data, one row is returned per + `(segment, index_value)` pair you provide. If the segment has no row at + that index value, nulls are returned — or the latest prior value if + `fill_latest_at=True` (which is typically what you want for resampling). + + Don't use this argument for plain index slicing — use a DataFusion filter + on the index column instead. For example: + + ```python + from datafusion import col, lit + + # All rows in a time window. + ds.reader(index="real_time").filter( + (col("real_time") >= lit(t0)) & (col("real_time") <= lit(t1)) + ) + ``` + + This argument accepts the following shapes: + - **plain array**: values are applied only to segments whose index + range covers them (segments outside the range are excluded). + - **dict**: keys are segment IDs, values are per-segment index values + to sample at. + - **DataFrame**: must have `rerun_segment_id` and index columns; + treated as a per-segment value list. + + !!! note + The plain array form requires a scan of the segment table to + map values to the segments whose index range covers them. On + datasets with many segments this can be expensive. Prefer the + dict or DataFrame form when the per-segment values are already + known on the client side. + + !!! note + Unknown segment IDs are silently ignored — they contribute no + rows to the result. Validate client-side if you need to catch + unknown segment IDs. fill_latest_at Whether to fill null values with the latest valid data. @@ -1002,12 +1078,8 @@ def reader( A DataFusion DataFrame. """ - import logging - import datafusion as dfn - available_segments = set() if using_index_values is None else set(self._internal.segment_ids()) - index_values_dict = None match using_index_values: case None: @@ -1027,17 +1099,7 @@ def reader( index_values_dict = self._dataframe_to_index_values_dict(df, index) if index_values_dict is not None: - requested_segments = set(index_values_dict.keys()) - missing_segments = requested_segments - available_segments - - if missing_segments: - logging.warning( - f"Index values for the following inexistent or filtered segments " - f"were ignored: {', '.join(sorted(missing_segments))}" - ) - - valid_segments = requested_segments - missing_segments - view = self._internal.filter_segments([*valid_segments]) + view = self._internal.filter_segments(list(index_values_dict.keys())) else: view = self._internal @@ -1263,6 +1325,79 @@ def arrow_schema(self) -> pa.Schema: return self.reader().schema() + def register_blueprint(self, uri: str, set_default: bool = True) -> None: + """ + Register an existing .rbl visible to the server as this table's blueprint. + + By default, also set this blueprint as default. + + The associated blueprint dataset is owned by this table for lifecycle purposes. + Deleting this table also deletes the associated blueprint dataset and its storage. + + !!! note + ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ + TODO(#12746): Stabilize table blueprint APIs. + """ + + blueprint_dataset = self.blueprint_dataset() + + segment_id = ( + blueprint_dataset.register([uri], on_duplicate=OnDuplicateSegmentLayer.REPLACE).wait().segment_ids[0] + ) + + if set_default: + self.set_default_blueprint(segment_id) + + def blueprints(self) -> list[str]: + """ + Lists all blueprints currently registered with this table. + + !!! note + ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ + TODO(#12746): Stabilize table blueprint APIs. + """ + + return self.blueprint_dataset().segment_ids() + + def set_default_blueprint(self, blueprint_name: str | None) -> None: + """ + Set an already-registered blueprint as default for this table. + + !!! note + ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ + TODO(#12746): Stabilize table blueprint APIs. + """ + + return self._internal.set_default_blueprint_segment_id(blueprint_name) + + def default_blueprint(self) -> str | None: + """ + Return the name currently set blueprint. + + !!! note + ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ + TODO(#12746): Stabilize table blueprint APIs. + """ + + return self._internal.default_blueprint_segment_id() + + def blueprint_dataset(self) -> DatasetEntry: + """ + The associated blueprint dataset. + + Tables get a blueprint dataset automatically when they are created. + For tables created by older servers, this creates the missing blueprint dataset before returning. + + The associated blueprint dataset is owned by this table for lifecycle purposes. + Deleting this table also deletes the associated blueprint dataset and its storage. + + !!! note + ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ + TODO(#12746): Stabilize table blueprint APIs. + """ + + return DatasetEntry(self._internal.blueprint_dataset()) + # --- def append( @@ -1423,10 +1558,7 @@ def _python_objects_to_record_batch(schema: pa.Schema, named_params: dict[str, A ) if pa.types.is_list(field.type) or pa.types.is_large_list(field.type): - error += ( - f" Hint: For single-row list-typed columns, wrap your list in another list: " - f"{name}=[[...]] instead of {name}=[...]" # NOLINT - ) + error += f" Hint: For single-row list-typed columns, wrap your list in another list: {name}=[[…]] instead of {name}=[…]" raise ValueError(error) diff --git a/rerun_py/rerun_sdk/rerun/catalog/_unregistration_handle.py b/rerun_py/rerun_sdk/rerun/catalog/_unregistration_handle.py new file mode 100644 index 000000000000..3304ac3cc2e9 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/catalog/_unregistration_handle.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from rerun_bindings import UnregistrationHandleInternal + + +class UnregistrationHandle: + """Handle to track and wait on segment unregistration tasks.""" + + def __init__(self, internal: UnregistrationHandleInternal) -> None: + self._internal = internal + + def wait(self, timeout_secs: int | None = None) -> None: + """ + Block until the unregistriation completes. + + Parameters + ---------- + timeout_secs + Timeout in seconds. None for blocking. Note that using None doesn't guarantee that a TimeoutError will + never be eventually raised for long-running tasks. + + Raises + ------ + ValueError + If the uregistration fails. + TimeoutError + If the timeout is reached before all tasks complete. + + """ + self._internal.wait(timeout_secs) + + def cancel(self) -> None: + """ + Cancel unrregistration. If the unregistration is already done, this is a noop. + + Raises + ------ + ValueError + If the cancellation fails. + + """ + + self._internal.cancel() diff --git a/rerun_py/rerun_sdk/rerun/components/.gitattributes b/rerun_py/rerun_sdk/rerun/components/.gitattributes index eacfbb74b99b..c58840baa8ad 100644 --- a/rerun_py/rerun_sdk/rerun/components/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/components/.gitattributes @@ -32,6 +32,7 @@ image_format.py linguist-generated=true image_plane_distance.py linguist-generated=true interactive.py linguist-generated=true interpolation_mode.py linguist-generated=true +is_keyframe.py linguist-generated=true key_value_pairs.py linguist-generated=true keypoint_id.py linguist-generated=true lat_lon.py linguist-generated=true @@ -48,6 +49,7 @@ name.py linguist-generated=true opacity.py linguist-generated=true pinhole_projection.py linguist-generated=true plane3d.py linguist-generated=true +point_shading.py linguist-generated=true position2d.py linguist-generated=true position3d.py linguist-generated=true radius.py linguist-generated=true @@ -81,3 +83,6 @@ video_sample.py linguist-generated=true video_timestamp.py linguist-generated=true view_coordinates.py linguist-generated=true visible.py linguist-generated=true +voxel_index.py linguist-generated=true +voxel_size.py linguist-generated=true +voxel_value.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/components/__init__.py b/rerun_py/rerun_sdk/rerun/components/__init__.py index 8b2c2d3437fd..06a81add78e7 100644 --- a/rerun_py/rerun_sdk/rerun/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/components/__init__.py @@ -52,6 +52,7 @@ InterpolationModeBatch, InterpolationModeLike, ) +from .is_keyframe import IsKeyframe, IsKeyframeBatch from .key_value_pairs import KeyValuePairs, KeyValuePairsArrayLike, KeyValuePairsBatch, KeyValuePairsLike from .keypoint_id import KeypointId, KeypointIdBatch from .lat_lon import LatLon, LatLonBatch @@ -78,6 +79,7 @@ from .opacity import Opacity, OpacityBatch from .pinhole_projection import PinholeProjection, PinholeProjectionBatch from .plane3d import Plane3D, Plane3DBatch +from .point_shading import PointShading, PointShadingArrayLike, PointShadingBatch, PointShadingLike from .position2d import Position2D, Position2DBatch from .position3d import Position3D, Position3DBatch from .radius import Radius, RadiusBatch @@ -116,6 +118,9 @@ from .video_timestamp import VideoTimestamp, VideoTimestampBatch from .view_coordinates import ViewCoordinates, ViewCoordinatesBatch from .visible import Visible, VisibleBatch +from .voxel_index import VoxelIndex, VoxelIndexBatch +from .voxel_size import VoxelSize, VoxelSizeBatch +from .voxel_value import VoxelValue, VoxelValueBatch __all__ = [ "AggregationPolicy", @@ -194,6 +199,8 @@ "InterpolationModeArrayLike", "InterpolationModeBatch", "InterpolationModeLike", + "IsKeyframe", + "IsKeyframeBatch", "KeyValuePairs", "KeyValuePairsArrayLike", "KeyValuePairsBatch", @@ -238,6 +245,10 @@ "PinholeProjectionBatch", "Plane3D", "Plane3DBatch", + "PointShading", + "PointShadingArrayLike", + "PointShadingBatch", + "PointShadingLike", "Position2D", "Position2DBatch", "Position3D", @@ -308,4 +319,10 @@ "ViewCoordinatesBatch", "Visible", "VisibleBatch", + "VoxelIndex", + "VoxelIndexBatch", + "VoxelSize", + "VoxelSizeBatch", + "VoxelValue", + "VoxelValueBatch", ] diff --git a/rerun_py/rerun_sdk/rerun/components/is_keyframe.py b/rerun_py/rerun_sdk/rerun/components/is_keyframe.py new file mode 100644 index 000000000000..7e67bce74d40 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/is_keyframe.py @@ -0,0 +1,39 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/components/is_keyframe.fbs". + +# You can extend this class by creating a "IsKeyframeExt" class in "is_keyframe_ext.py". + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["IsKeyframe", "IsKeyframeBatch"] + + +class IsKeyframe(datatypes.Bool, ComponentMixin): + """ + **Component**: Whether a [`components.VideoSample`][rerun.components.VideoSample] contains a keyframe (also known as a sync sample or IDR). + + A keyframe in this sense must be _decoder re-entrant_: a decoder must be able to start + decoding the stream from this sample alone, with no prior decoder state. + Not every intra-coded frame qualifies. Some codecs have intra-only frames that may + still reference existing decoder state and are therefore not valid sync points. + See [`components.VideoCodec`][rerun.components.VideoCodec] for the codec-specific definition of a keyframe. + """ + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of IsKeyframeExt in is_keyframe_ext.py + + # Note: there are no fields here because IsKeyframe delegates to datatypes.Bool + + +class IsKeyframeBatch(datatypes.BoolBatch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.components.IsKeyframe" + + +# This is patched in late to avoid circular dependencies. +IsKeyframe._BATCH_TYPE = IsKeyframeBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/components/point_shading.py b/rerun_py/rerun_sdk/rerun/components/point_shading.py new file mode 100644 index 000000000000..ad97f2ee2ae0 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/point_shading.py @@ -0,0 +1,74 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/components/point_shading.fbs". + +# You can extend this class by creating a "PointShadingExt" class in "point_shading_ext.py". + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Literal + +import pyarrow as pa + +from .._baseclasses import ( + BaseBatch, + ComponentBatchMixin, +) + +__all__ = ["PointShading", "PointShadingArrayLike", "PointShadingBatch", "PointShadingLike"] + + +from enum import Enum + + +class PointShading(Enum): + """**Component**: Defines how points are shaded.""" + + Gradient = 1 + """Radial gradient for a spherical shadow effect.""" + + Flat = 2 + """Flat shading.""" + + @classmethod + def auto(cls, val: str | int | PointShading) -> PointShading: + """Best-effort converter, including a case-insensitive string matcher.""" + if isinstance(val, PointShading): + return val + if isinstance(val, int): + return cls(val) + try: + return cls[val] + except KeyError: + val_lower = val.lower() + for variant in cls: + if variant.name.lower() == val_lower: + return variant + raise ValueError(f"Cannot convert {val} to {cls.__name__}") + + def __str__(self) -> str: + """Returns the variant name.""" + return self.name + + +PointShadingLike = PointShading | Literal["Flat", "Gradient", "flat", "gradient"] | int +"""A type alias for any PointShading-like object.""" + +PointShadingArrayLike = ( + PointShading | Literal["Flat", "Gradient", "flat", "gradient"] | int | Sequence[PointShadingLike] +) +"""A type alias for any PointShading-like array object.""" + + +class PointShadingBatch(BaseBatch[PointShadingArrayLike], ComponentBatchMixin): + _ARROW_DATATYPE = pa.uint8() + _COMPONENT_TYPE: str = "rerun.components.PointShading" + + @staticmethod + def _native_to_pa_array(data: PointShadingArrayLike, data_type: pa.DataType) -> pa.Array: + if isinstance(data, (PointShading, int, str)): + data = [data] + + pa_data = [PointShading.auto(v).value if v is not None else None for v in data] # type: ignore[redundant-expr] # ty: ignore[not-iterable] + + return pa.array(pa_data, type=data_type) diff --git a/rerun_py/rerun_sdk/rerun/components/video_codec.py b/rerun_py/rerun_sdk/rerun/components/video_codec.py index 1b2bbb242e1a..77da955640ee 100644 --- a/rerun_py/rerun_sdk/rerun/components/video_codec.py +++ b/rerun_py/rerun_sdk/rerun/components/video_codec.py @@ -75,6 +75,24 @@ class VideoCodec(Enum): Enum value is the fourcc for 'hev1' (the WebCodec string assigned to this codec) in big endian. """ + VP8 = 0x76703038 + """ + VP8 + + See + + Enum value is the fourcc for 'vp08' (the WebCodec string assigned to this codec) in big endian. + """ + + VP9 = 0x76703039 + """ + VP9 + + See + + Enum value is the fourcc for 'vp09' (the WebCodec string assigned to this codec) in big endian. + """ + @classmethod def auto(cls, val: str | int | VideoCodec) -> VideoCodec: """Best-effort converter, including a case-insensitive string matcher.""" @@ -96,11 +114,14 @@ def __str__(self) -> str: return self.name -VideoCodecLike = VideoCodec | Literal["AV1", "H264", "H265", "av1", "h264", "h265"] | int +VideoCodecLike = VideoCodec | Literal["AV1", "H264", "H265", "VP8", "VP9", "av1", "h264", "h265", "vp8", "vp9"] | int """A type alias for any VideoCodec-like object.""" VideoCodecArrayLike = ( - VideoCodec | Literal["AV1", "H264", "H265", "av1", "h264", "h265"] | int | Sequence[VideoCodecLike] + VideoCodec + | Literal["AV1", "H264", "H265", "VP8", "VP9", "av1", "h264", "h265", "vp8", "vp9"] + | int + | Sequence[VideoCodecLike] ) """A type alias for any VideoCodec-like array object.""" diff --git a/rerun_py/rerun_sdk/rerun/components/voxel_index.py b/rerun_py/rerun_sdk/rerun/components/voxel_index.py new file mode 100644 index 000000000000..6a05bf228822 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/voxel_index.py @@ -0,0 +1,35 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_index.fbs". + +# You can extend this class by creating a "VoxelIndexExt" class in "voxel_index_ext.py". + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["VoxelIndex", "VoxelIndexBatch"] + + +class VoxelIndex(datatypes.IVec3D, ComponentMixin): + """ + **Component**: Integer index of a voxel in a sparse 3D voxel grid. + + The voxel center in local grid coordinates is `(index + 0.5) * voxel_size`. + """ + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of VoxelIndexExt in voxel_index_ext.py + + # Note: there are no fields here because VoxelIndex delegates to datatypes.IVec3D + + +class VoxelIndexBatch(datatypes.IVec3DBatch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.components.VoxelIndex" + + +# This is patched in late to avoid circular dependencies. +VoxelIndex._BATCH_TYPE = VoxelIndexBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/components/voxel_size.py b/rerun_py/rerun_sdk/rerun/components/voxel_size.py new file mode 100644 index 000000000000..30bcb4bb751f --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/voxel_size.py @@ -0,0 +1,36 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_size.fbs". + +# You can extend this class by creating a "VoxelSizeExt" class in "voxel_size_ext.py". + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["VoxelSize", "VoxelSizeBatch"] + + +class VoxelSize(datatypes.Vec3D, ComponentMixin): + """ + **Component**: The scene-unit dimensions of one voxel in a sparse 3D voxel grid. + + Each component is the size of a voxel along the corresponding local grid axis. + All components must be finite and positive. + """ + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of VoxelSizeExt in voxel_size_ext.py + + # Note: there are no fields here because VoxelSize delegates to datatypes.Vec3D + + +class VoxelSizeBatch(datatypes.Vec3DBatch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.components.VoxelSize" + + +# This is patched in late to avoid circular dependencies. +VoxelSize._BATCH_TYPE = VoxelSizeBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/components/voxel_value.py b/rerun_py/rerun_sdk/rerun/components/voxel_value.py new file mode 100644 index 000000000000..8d059bf2c1c2 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/voxel_value.py @@ -0,0 +1,31 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/components/voxel_value.fbs". + +# You can extend this class by creating a "VoxelValueExt" class in "voxel_value_ext.py". + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["VoxelValue", "VoxelValueBatch"] + + +class VoxelValue(datatypes.Float32, ComponentMixin): + """**Component**: Optional scalar occupancy or value associated with a voxel.""" + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of VoxelValueExt in voxel_value_ext.py + + # Note: there are no fields here because VoxelValue delegates to datatypes.Float32 + + +class VoxelValueBatch(datatypes.Float32Batch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.components.VoxelValue" + + +# This is patched in late to avoid circular dependencies. +VoxelValue._BATCH_TYPE = VoxelValueBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/datatypes/.gitattributes b/rerun_py/rerun_sdk/rerun/datatypes/.gitattributes index 526b9248f0b0..d294e4f2e210 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/datatypes/.gitattributes @@ -18,6 +18,7 @@ entity_path.py linguist-generated=true float32.py linguist-generated=true float64.py linguist-generated=true image_format.py linguist-generated=true +ivec3d.py linguist-generated=true keypoint_id.py linguist-generated=true keypoint_pair.py linguist-generated=true mat3x3.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/datatypes/__init__.py b/rerun_py/rerun_sdk/rerun/datatypes/__init__.py index 391c368a85b6..0c8afa496795 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/__init__.py @@ -28,6 +28,7 @@ from .float32 import Float32, Float32ArrayLike, Float32Batch, Float32Like from .float64 import Float64, Float64ArrayLike, Float64Batch, Float64Like from .image_format import ImageFormat, ImageFormatArrayLike, ImageFormatBatch, ImageFormatLike +from .ivec3d import IVec3D, IVec3DArrayLike, IVec3DBatch, IVec3DLike from .keypoint_id import KeypointId, KeypointIdArrayLike, KeypointIdBatch, KeypointIdLike from .keypoint_pair import KeypointPair, KeypointPairArrayLike, KeypointPairBatch, KeypointPairLike from .mat3x3 import Mat3x3, Mat3x3ArrayLike, Mat3x3Batch, Mat3x3Like @@ -143,6 +144,10 @@ "Float64ArrayLike", "Float64Batch", "Float64Like", + "IVec3D", + "IVec3DArrayLike", + "IVec3DBatch", + "IVec3DLike", "ImageFormat", "ImageFormatArrayLike", "ImageFormatBatch", diff --git a/rerun_py/rerun_sdk/rerun/datatypes/ivec3d.py b/rerun_py/rerun_sdk/rerun/datatypes/ivec3d.py new file mode 100644 index 000000000000..f0469493835b --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/datatypes/ivec3d.py @@ -0,0 +1,66 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/datatypes/ivec3d.fbs". + +# You can extend this class by creating a "IVec3DExt" class in "ivec3d_ext.py". + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +import numpy as np +import numpy.typing as npt +import pyarrow as pa +from attrs import define, field + +from .._baseclasses import ( + BaseBatch, +) +from .._converters import ( + to_np_int32, +) +from .._numpy_compatibility import asarray +from .ivec3d_ext import IVec3DExt + +__all__ = ["IVec3D", "IVec3DArrayLike", "IVec3DBatch", "IVec3DLike"] + + +@define(init=False) +class IVec3D(IVec3DExt): + """**Datatype**: An int32 vector in 3D space.""" + + def __init__(self: Any, xyz: IVec3DLike) -> None: + """Create a new instance of the IVec3D datatype.""" + + # You can define your own __init__ function as a member of IVec3DExt in ivec3d_ext.py + self.__attrs_init__(xyz=xyz) + + xyz: npt.NDArray[np.int32] = field(converter=to_np_int32) + + def __array__(self, dtype: npt.DTypeLike = None, copy: bool | None = None) -> npt.NDArray[Any]: + # You can define your own __array__ function as a member of IVec3DExt in ivec3d_ext.py + return asarray(self.xyz, dtype=dtype, copy=copy) + + def __len__(self) -> int: + # You can define your own __len__ function as a member of IVec3DExt in ivec3d_ext.py + return len(self.xyz) + + +if TYPE_CHECKING: + IVec3DLike = IVec3D | npt.NDArray[Any] | npt.ArrayLike | Sequence[int] + """A type alias for any IVec3D-like object.""" +else: + IVec3DLike = Any + +IVec3DArrayLike = ( + IVec3D | Sequence[IVec3DLike] | npt.NDArray[Any] | npt.ArrayLike | Sequence[Sequence[int]] | Sequence[int] +) +"""A type alias for any IVec3D-like array object.""" + + +class IVec3DBatch(BaseBatch[IVec3DArrayLike]): + _ARROW_DATATYPE = pa.list_(pa.field("item", pa.int32(), nullable=False, metadata={}), 3) + + @staticmethod + def _native_to_pa_array(data: IVec3DArrayLike, data_type: pa.DataType) -> pa.Array: + return IVec3DExt.native_to_pa_array_override(data, data_type) diff --git a/rerun_py/rerun_sdk/rerun/datatypes/ivec3d_ext.py b/rerun_py/rerun_sdk/rerun/datatypes/ivec3d_ext.py new file mode 100644 index 000000000000..b57ad8a84c6a --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/datatypes/ivec3d_ext.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pyarrow as pa + +from .._validators import flat_np_int32_array_from_array_like + +if TYPE_CHECKING: + from . import IVec3DArrayLike + + +class IVec3DExt: + """Extension for [IVec3D][rerun.datatypes.IVec3D].""" + + @staticmethod + def native_to_pa_array_override(data: IVec3DArrayLike, data_type: pa.DataType) -> pa.Array: + points = flat_np_int32_array_from_array_like(data, 3) + return pa.FixedSizeListArray.from_arrays(points, type=data_type) diff --git a/rerun_py/rerun_sdk/rerun/datatypes/visible_time_range.py b/rerun_py/rerun_sdk/rerun/datatypes/visible_time_range.py index e47c69aa8cd4..2c9fdc6b35c1 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/visible_time_range.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/visible_time_range.py @@ -28,7 +28,69 @@ def _visible_time_range__timeline__special_field_converter_override(x: datatypes @define(init=False) class VisibleTimeRange(VisibleTimeRangeExt): - """**Datatype**: Visible time range bounds for a specific timeline.""" + """ + **Datatype**: Visible time range bounds for a specific timeline. + + Example + ------- + ### Time-windowed trails (e.g. Trajectories): + ```python + import math + + import rerun as rr + import rerun.blueprint as rrb + + + def point(t: float, phase: float) -> list[float]: + # Sample a point on a helix. + angle = 0.5 * t + phase + return [math.cos(angle), math.sin(angle), 0.1 * t] + + + rr.init("rerun_example_line_strips3d_time_window", spawn=True) + + # Configure the visible time range in the blueprint. + # You can also override this per entity. + rr.send_blueprint( + rrb.Spatial3DView( + origin="/", + time_ranges=rrb.VisibleTimeRange( + "time", + start=rrb.TimeRangeBoundary.cursor_relative(seconds=-5.0), + end=rrb.TimeRangeBoundary.cursor_relative(), + ), + ) + ) + + # Log the line strip increments with timestamps. + for i in range(600): + t0 = i / 30.0 + t1 = (i + 1) / 30.0 + + rr.set_time("time", duration=t1) + rr.log( + "trails", + rr.LineStrips3D( + [ + [point(t0, 0.0), point(t1, 0.0)], + [point(t0, math.pi), point(t1, math.pi)], + ], + colors=[[255, 120, 0], [0, 180, 255]], + radii=0.02, + ), + ) + ``` +
+ + + + + + + +
+ + """ # __init__ can be found in visible_time_range_ext.py diff --git a/rerun_py/rerun_sdk/rerun/experimental/__init__.py b/rerun_py/rerun_sdk/rerun/experimental/__init__.py index 25826392f3f7..22eec47a8c6f 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/__init__.py +++ b/rerun_py/rerun_sdk/rerun/experimental/__init__.py @@ -7,16 +7,27 @@ from __future__ import annotations +from . import video as video from ._chunk import Chunk as Chunk from ._chunk_store import ChunkStore as ChunkStore +from ._hdf5_reader import DatasetInfo as DatasetInfo, Hdf5Reader as Hdf5Reader +from ._index_column import IndexColumn as IndexColumn from ._indexed_reader import IndexedReader as IndexedReader from ._lazy_chunk_stream import LazyChunkStream as LazyChunkStream -from ._lens import Lens as Lens, LensOutput as LensOutput +from ._lazy_store import LazyStore as LazyStore +from ._lens import DeriveLens as DeriveLens, Lens as Lens, MutateLens as MutateLens from ._mcap_reader import McapReader as McapReader -from ._optimization_settings import OptimizationSettings as OptimizationSettings -from ._parquet_reader import ColumnRule as ColumnRule, ParquetReader as ParquetReader +from ._mp4_reader import Mp4Reader as Mp4Reader, Mp4TranscodeOptions as Mp4TranscodeOptions +from ._optimization_profile import OptimizationProfile as OptimizationProfile +from ._parquet_reader import ParquetReader as ParquetReader +from ._query_metrics import ( + MetricsCollector as MetricsCollector, + QueryMetrics as QueryMetrics, + query_metrics as query_metrics, +) from ._rrd_reader import RrdReader as RrdReader from ._selector import Selector as Selector -from ._send_chunk import send_chunk as send_chunk +from ._send_chunks import send_chunks as send_chunks +from ._store_entry import StoreEntry as StoreEntry from ._streaming_reader import StreamingReader as StreamingReader from ._viewer_client import ViewerClient as ViewerClient diff --git a/rerun_py/rerun_sdk/rerun/experimental/_chunk.py b/rerun_py/rerun_sdk/rerun/experimental/_chunk.py index 8e0f11990f16..fd4e220dbba8 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_chunk.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_chunk.py @@ -2,10 +2,15 @@ from typing import TYPE_CHECKING +import pyarrow as pa + +from .._send_dataframe import AUTO_INDEX, _AutoIndex + if TYPE_CHECKING: - from collections.abc import Iterable, Sequence + from collections.abc import Iterable, Iterator, Sequence + from typing import TypeAlias - import pyarrow as pa + import datafusion # Soft dependency, ok for type checking from rerun import ComponentColumn from rerun._baseclasses import ComponentDescriptor @@ -15,6 +20,46 @@ from ._lens import Lens from ._selector import Selector + # Single-schema dataframe sources accepted by `Chunk.from_dataframe`. + # `datafusion` is an optional dependency. + DataframeLike: TypeAlias = "pa.Table | pa.RecordBatch | pa.RecordBatchReader | datafusion.DataFrame" + + +def _resolve_index(index: str | list[str] | None | _AutoIndex) -> tuple[str, list[str]]: + """Map the Python `index` argument to the binding's `(index_mode, index_columns)` arguments.""" + + match index: + case _AutoIndex(): + return "auto", [] + case None: + return "static", [] + case str(): + return "columns", [index] + case _: + return "columns", list(index) + + +def _as_record_batch_reader(dataframe: DataframeLike) -> pa.RecordBatchReader: + """Normalize a single-schema dataframe to a [`pa.RecordBatchReader`][pyarrow.RecordBatchReader].""" + + match dataframe: + case pa.RecordBatchReader(): + return dataframe + case pa.Table(): + return dataframe.to_reader() + case pa.RecordBatch(): + return pa.RecordBatchReader.from_batches(dataframe.schema, [dataframe]) + # Anything implementing the Arrow C stream interface — e.g. a `datafusion.DataFrame` — can + # be streamed lazily. The protocol guarantees a single schema for the whole stream, so we + # don't need to import (or even name) datafusion here. + case _ if hasattr(dataframe, "__arrow_c_stream__"): + return pa.RecordBatchReader.from_stream(dataframe) + case _: + raise TypeError( + "Expected a pyarrow Table, pyarrow RecordBatch, pyarrow RecordBatchReader, or an " + f"Arrow-C-stream object (e.g. a datafusion DataFrame), got {type(dataframe).__name__}", + ) + class Chunk: """A single chunk of data from a recording.""" @@ -25,28 +70,158 @@ def __init__(self, internal: ChunkInternal) -> None: self._internal = internal @classmethod - def from_record_batch(cls, record_batch: pa.RecordBatch) -> Chunk: + def from_record_batch( + cls, + record_batch: pa.RecordBatch, + *, + index: str | list[str] | None | _AutoIndex = AUTO_INDEX, + entity_path: str | None = None, + ) -> list[Chunk]: """ - Create a Chunk from a PyArrow RecordBatch with Rerun schema metadata. + Interpret an Arrow [`RecordBatch`][pyarrow.RecordBatch] as Rerun chunk data. + + Each column of the batch is classified as a row-id column, index (timeline) column, + or a component column. Component columns are then grouped per entity path, and + one chunk per entity path is emitted. + + The `rerun:*` arrow metadata, if it exists, drives the kind of each input column, + as well as the entity/archetype/component type for component columns. - The RecordBatch must have Rerun metadata in its schema, as produced by - `to_record_batch`. This enables round-tripping through PyArrow - transforms. The original chunk ID and row IDs are preserved. + If present, the row id column and chunk id metadata indicate that the batch represents + a fully identified chunk, e.g. as produced by [`Chunk.to_record_batch`][rerun.experimental.Chunk.to_record_batch]. + Both the row ids and chunk id are preserved under the following conditions: + - both are present in the input batch + - `index` is omitted + - `entity_path` is omitted + + If any of these conditions are not met, it means that either the batch is not fully + identified, or that the chunk data is reinterpreted (e.g. entity path rewriting). + In that case, fresh row ids and chunk id are generated and used instead of the input + ones. Parameters ---------- record_batch: - A PyArrow RecordBatch with Rerun schema metadata. + The Arrow record batch to interpret. + Component columns may be either lists (one component batch per row) or plain arrays + (wrapped as single-element lists automatically). + index: + Determines which columns are index (timeline) columns. Each promoted column's + time type is taken from its Arrow datatype: `int64` → sequence, `timestamp(ns)` + → timestamp, `duration(ns)` → duration. + + - Omitted (the default): derive the index columns from the batch's Rerun metadata. + The batch is treated as temporal if it carries index metadata. A batch with no index + metadata is ambiguous and raises an error — unless it is an already-identified chunk + (it carries a row-id column and a chunk id), which round-trips as-is and may therefore + be static. Pass `index=None` to force a static interpretation. + - A column name, or list of column names: treat exactly these columns as timelines. + The remaining (non-row-id) columns become components. + - `None`: produce static chunks (no timeline). Any index metadata or promoted index + column is then a contradiction and is rejected. + + !!! note + Static chunks with multiple rows are legitimate in some cases, but only the last + row is visible from typical latest-at queries. An info-level message is emitted + when this happens — except for an already-identified chunk that is preserved as-is + (see above), which is passed through without this check. + entity_path: + Default entity path for component columns that do not otherwise specify one. + Resolution order per component column is: its `rerun:entity_path` metadata, then the + batch-level `rerun:entity_path` metadata, then the column-name convention (see + *Notes*), then this argument, then the root entity (`/`). + + Returns + ------- + One chunk per distinct entity path described by the batch, in first-seen column order. Raises ------ ValueError - If the RecordBatch lacks required Rerun schema metadata. + In any of the following cases: + + - `index` was omitted and the batch carries no index metadata + (an ambiguous raw batch). Pass `index=` for temporal data or `index=None` + for static data. + - `index=None` was given but the batch also carries index metadata or names an + index column (contradiction). + - `index` names a column that is not present in the batch. + - The batch contains no component columns (there is nothing to log). + - A column promoted to an index contains null values. Time columns must be dense; + static data is expressed with `index=None`, not with null times. + - An index column has an Arrow datatype that is not a supported time type. + - The batch is a fully-identified chunk (it carries both a row-id column and a chunk id) + but resolves to more than one entity path. An identified chunk is preserved as a single + chunk; drop the chunk-id metadata and/or the row-id column to reinterpret it into one + chunk per entity (with freshly-minted ids). + + Notes + ----- + **Column-name convention.** When a component column carries no `rerun:entity_path` / + `rerun:component` metadata, its entity path is read from the column name: if the name starts + with `/` and contains a `:`, the first part of the column name is interpreted as the entity + path and the rest as the component identifier. Example: `/point:Points3D:positions` and + `/metadata:foo`. + + Limitations/Future work + ----------------------- + A batch that mixes static and temporal rows — aka where some index values are `null` — are + rejected. Handling this case requires row-splitting and generating a mix of temporal and + static chunks. + + Recording-property columns (named `property:…`, mapping to the `/__properties` entity) are + not recognized by the column-name convention and are not mapped back to that entity. """ + # NOTE: Agent, keep this in sync with `Chunk.from_record_batch`. from rerun_bindings import ChunkInternal - return cls(ChunkInternal.from_record_batch(record_batch)) + index_mode, index_columns = _resolve_index(index) + return [ + cls(internal) + for internal in ChunkInternal.from_record_batch(record_batch, index_mode, index_columns, entity_path) + ] + + @classmethod + def from_dataframe( + cls, + dataframe: DataframeLike, + *, + index: str | list[str] | None | _AutoIndex = AUTO_INDEX, + entity_path: str | None = None, + ) -> Iterator[Chunk]: + """ + Lazily turn an Arrow-backed dataframe into chunks. + + Accepts a [`Table`][pyarrow.Table], a [`RecordBatch`][pyarrow.RecordBatch], a + [`RecordBatchReader`][pyarrow.RecordBatchReader], or any object implementing the Arrow C + stream interface (`__arrow_c_stream__`) — most notably a `datafusion.DataFrame` (an optional + dependency). + + Yields each chunk of + [`Chunk.from_record_batch`][rerun.experimental.Chunk.from_record_batch] applied to every + record batch in turn. See that method for the `index` and `entity_path` semantics. + + + Raises + ------ + TypeError + If `dataframe` is not a pyarrow `Table`, a pyarrow `RecordBatch`, a pyarrow + `RecordBatchReader`, or an Arrow-C-stream object (such as a `datafusion.DataFrame`). + ValueError + See [`Chunk.from_record_batch`][rerun.experimental.Chunk.from_record_batch]. + + """ + + # Note: by returning a generator instead of _being_ a generator, we ensure that this line is executed at call + # time and not deferred to the first `next()` + reader = _as_record_batch_reader(dataframe) + + def chunks() -> Iterator[Chunk]: + for batch in reader: + yield from cls.from_record_batch(batch, index=index, entity_path=entity_path) + + return chunks() @classmethod def from_columns( @@ -136,6 +311,21 @@ def to_record_batch(self) -> pa.RecordBatch: """Convert this chunk to an Arrow RecordBatch.""" return self._internal.to_record_batch() + def with_entity_path(self, entity_path: str) -> Chunk: + """ + Return a copy of this chunk with a new entity path. + + A fresh chunk ID is generated to avoid aliasing the original chunk in downstream + caches and indices. Row IDs, timelines, and components are preserved as-is. + + Parameters + ---------- + entity_path: + The new entity path for the returned chunk (e.g. `"/left/camera/image"`). + + """ + return Chunk(self._internal.with_entity_path(entity_path)) + def apply_selector( self, source: ComponentDescriptor | str, @@ -147,6 +337,10 @@ def apply_selector( All other columns (timelines, other components) are preserved unchanged. The source component's existing descriptor is preserved. + For better performance, prefer [`MutateLens`][rerun.experimental.MutateLens] + with [`apply_lenses`][rerun.experimental.Chunk.apply_lenses] + which processes multiple transformations in a single pass. + Parameters ---------- source: @@ -177,14 +371,15 @@ def apply_selector( return Chunk(self._internal.apply_selector(source_str, selector._internal)) - def apply_lenses(self, lenses: Sequence[Lens] | Lens) -> list[Chunk]: + def apply_lenses( + self, + lenses: Sequence[Lens] | Lens, + ) -> list[Chunk]: """ Apply one or more lenses to this chunk, returning transformed chunks. Each lens matches by input component. Columns not consumed by any matching lens are forwarded unchanged as a separate chunk. - A single lens with multiple [`LensOutput`][rerun.experimental.LensOutput] groups may produce - multiple output chunks (e.g., with different target entities). If no lens matches the chunk (including when an empty list of lenses is passed), the original chunk is returned unchanged. @@ -192,30 +387,20 @@ def apply_lenses(self, lenses: Sequence[Lens] | Lens) -> list[Chunk]: Parameters ---------- lenses: - Zero or more [`Lens`][rerun.experimental.Lens] objects to apply. + One or more [`Lens`][rerun.experimental.Lens] objects. Returns ------- - A list of [`Chunk`][rerun.experimental.Chunk] objects. Contains the original chunk if no - lens matched, or one or more transformed chunks (optionally - preceded by a chunk with the untouched forwarded columns) - otherwise. - - Raises - ------ - ValueError - If a lens produces a partial result (e.g., a selector fails - to evaluate on the input data, or a lens produces no output - columns). + A list of [`Chunk`][] objects. """ - from ._lens import Lens as LensType + from ._lens import Lens - if isinstance(lenses, LensType): + if isinstance(lenses, Lens): lenses = [lenses] return [Chunk(internal) for internal in self._internal.apply_lenses([lens._internal for lens in lenses])] - def format(self, *, width: int = 240, redact: bool = False) -> str: + def format(self, *, width: int = 240, redact: bool = False, trim_metadata_keys: bool = True) -> str: """ Format this chunk as a human-readable table string. @@ -226,15 +411,18 @@ def format(self, *, width: int = 240, redact: bool = False) -> str: redact: If True, redact non-deterministic values (RowIds, ChunkIds, etc.) for stable snapshot testing. Default: False. + trim_metadata_keys: + If True, trim the `rerun:` / `sorbet:` prefix from metadata keys. + Default: True. """ - return self._internal.format(width=width, redact=redact) + return self._internal.format(width=width, redact=redact, trim_metadata_keys=trim_metadata_keys) def __repr__(self) -> str: return repr(self._internal) def __str__(self) -> str: - return self._internal.format() + return self.format() def __len__(self) -> int: return len(self._internal) diff --git a/rerun_py/rerun_sdk/rerun/experimental/_chunk_store.py b/rerun_py/rerun_sdk/rerun/experimental/_chunk_store.py index 3d5c70e6a0f4..d7a7b95c2959 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_chunk_store.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_chunk_store.py @@ -6,7 +6,9 @@ from collections.abc import Sequence from pathlib import Path - from rerun.catalog import Schema + import datafusion + + from rerun.catalog import ContentFilter, IndexValuesLike, Schema from rerun_bindings import ChunkStoreInternal from ._chunk import Chunk @@ -15,13 +17,13 @@ class ChunkStore: """ - A chunk store. - - TODO(RR-4321): currently, this is fully materialized, in-memory. - - Obtain a ChunkStore from an IndexedReader, e.g.: + A fully-materialized, in-memory chunk store. - store = RrdReader("recording.rrd").store() + Build one from chunks via + [`ChunkStore.from_chunks`][rerun.experimental.ChunkStore.from_chunks], or + fully materialize an [`IndexedReader`][rerun.experimental.IndexedReader] + via `reader.stream().collect()`. + For lazy, on-demand chunk loading, see [`LazyStore`][rerun.experimental.LazyStore]. Use `stream()` to process chunks through the lazy pipeline, or `write_rrd()` to persist to disk. @@ -52,11 +54,9 @@ def summary(self) -> str: Each line describes one chunk: - {entity_path} rows={n} bytes={…} static={True|False} timelines=[…] cols=[…] + {entity_path} rows={n} static={True|False} timelines=[…] cols=[…] Useful for snapshot testing. - - **Important**: For lazily-loaded stores, this forces loading all chunk data from disk. """ return self._internal.summary() @@ -66,6 +66,94 @@ def stream(self) -> LazyChunkStream: return LazyChunkStream(self._internal.stream()) + def reader( + self, + index: str | None, + *, + contents: ContentFilter | str | list[str] | None = None, + include_semantically_empty_columns: bool = False, + include_tombstone_columns: bool = False, + fill_latest_at: bool = False, + using_index_values: IndexValuesLike | None = None, + ctx: datafusion.SessionContext | None = None, + ) -> datafusion.DataFrame: + """ + Build a DataFusion DataFrame over this store. + + The returned DataFrame is data-equivalent to the result of round-tripping + the same chunks through `write_rrd → rr.server.Server → dataset.reader()`, + modulo the `rerun_segment_id` column (absent here because a single + `ChunkStore` has no segment concept). + + Parameters + ---------- + index + The index (timeline) column to use, or `None` for the static-only view. + contents + Entity-path filter. A `ContentFilter` built with the fluent API, a single + entity-path expression, a list of expressions, or `None` for everything. + An empty list returns no rows. + include_semantically_empty_columns + Whether to include columns that are semantically empty. + include_tombstone_columns + Whether to include tombstone columns. + fill_latest_at + Whether to fill null values with the latest valid data. + using_index_values + Index values at which to **resample** data. + + When specified, this argument changes the way rows are returned. Instead + of returning the rows that exist in the data, one row is returned per + `index_value` you provide. If the segment has no row at that index value, + nulls are returned — or the latest prior value if fill_latest_at=True` + (which is typically what you want for resampling). + + Don't use this argument for plain index slicing — use a DataFusion filter + on the index column instead. For example: + + ```python + from datafusion import col, lit + + # All rows in a time window. + store.reader(index="real_time").filter( + (col("real_time") >= lit(t0)) & (col("real_time") <= lit(t1)) + ) + ``` + ctx + DataFusion `SessionContext` to register the table into. When `None`, + uses `datafusion.SessionContext.global_ctx()` — the process-wide + default. + Pass an explicit `ctx` for isolation or a custom `SessionConfig`. + + """ + import datafusion + + from rerun.catalog._content_filter import ContentFilter + + contents_list: list[str] | None + match contents: + case ContentFilter(): + contents_list = contents.to_exprs() + case str(): + contents_list = [contents] + case None: + contents_list = None + case _: + contents_list = list(contents) + + table = self._internal.reader( + index=index, + contents=contents_list, + include_semantically_empty_columns=include_semantically_empty_columns, + include_tombstone_columns=include_tombstone_columns, + fill_latest_at=fill_latest_at, + using_index_values=using_index_values, + ) + if ctx is None: + # TODO(RR-4795): we should use a SDK-provided context (with pre-populated UDF, etc.) instead of global_ctx + ctx = datafusion.SessionContext.global_ctx() + return ctx.read_table(table) + def write_rrd( self, path: str | Path, diff --git a/rerun_py/rerun_sdk/rerun/experimental/_hdf5_reader.py b/rerun_py/rerun_sdk/rerun/experimental/_hdf5_reader.py new file mode 100644 index 000000000000..e985a177271c --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/_hdf5_reader.py @@ -0,0 +1,236 @@ +"""Experimental HDF5 reader mapping groups to entities and datasets to components.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from rerun_bindings import Hdf5ReaderInternal + +from ._lazy_chunk_stream import LazyChunkStream + +if TYPE_CHECKING: + from pathlib import Path + + from ._index_column import IndexColumn + + +@dataclass(frozen=True) +class DatasetInfo: + """ + Structural metadata for a single HDF5 dataset. + + Attributes + ---------- + path: + Full path of the dataset within the file (e.g. `/observations/qpos`). + shape: + Dataset dimensions (e.g. `(272, 128, 128, 3)`). + dtype: + Element type name (e.g. `"uint8"`, `"float64"`). + + """ + + path: str + shape: tuple[int, ...] + dtype: str + + +class Hdf5Reader: + """ + Read chunks from an HDF5 file. + + The reader is a lightweight handle over the file: inspect the raw structure + with `groups()`, `datasets()`, and `attributes()`, and produce chunks with + `stream(...)`. All loading options live on `stream()`, so one reader can drive + several differently-configured streams over the same file. + + Each HDF5 group is mapped to a Rerun entity, and the group's leaf datasets + become the columns of that entity. The file root maps to the entity `/`, and + nested groups map to nested entity paths (`/observations/images` becomes the + entity `/observations/images`). See `stream()` for how datasets, timelines, and + attributes are turned into chunks. + + Parameters + ---------- + path: + Path to the `.hdf5` / `.h5` file. + + Raises + ------ + FileNotFoundError + If `path` does not exist. + + """ + + _internal: Hdf5ReaderInternal + + def __init__(self, path: str | Path) -> None: + self._internal = Hdf5ReaderInternal(str(path)) + + def stream( + self, + *, + entity_path_prefix: str | None = None, + index_column: IndexColumn | None = None, + ignore_datasets: list[str] | None = None, + use_structs: bool = True, + ) -> LazyChunkStream: + """ + Return a lazy stream over all chunks in the HDF5 file. + + Each call is independent: the same reader can be streamed several times with + different configurations. + + Datasets are loaded according to their dimensionality: + + - A 0-D (scalar) dataset is loaded as **static** data — a single value with + no timeline. + - A 1-D dataset `[N]` becomes a column of `N` scalar rows. + - A 2-D dataset `[N, K]` becomes a column of `N` rows, each a fixed-size list + of `K` elements. + - A 3-D-or-higher dataset `[N, d1, …, dk]` becomes a column of `N` rows, each + a single blob of the matching type (an Arrow `List`) holding + the row's raw row-major values. The original per-row shape is not recorded + in the emitted data; recover it via [`datasets`][rerun.experimental.Hdf5Reader.datasets]. + + For 1-D and higher-dimensional datasets the **leading** dimension is always + the row axis. Element types are mapped to their natural Arrow equivalents + (signed and unsigned integers, floats, and strings); no semantic + interpretation is applied. + + HDF5 attributes are emitted as **static** chunks under a dedicated + `__hdf5_properties` entity, mirroring the source layout: root attributes land + on `__hdf5_properties`, and attributes on object `/a/b` on + `__hdf5_properties/a/b`. Each attribute becomes one static component named + after it, typed with the same mapping as datasets. This keeps the general + `__properties` entity free for user-defined property layers. + + Row alignment + ------------- + Every loaded, non-ignored, non-scalar dataset is aligned positionally to the + file-wide timeline and must therefore share the same number of rows (scalar + datasets are static and exempt): + + - With an `index_column`, that shared count is the index dataset's length. + - Without one, the datasets must all agree on a single row count, which + becomes the length of the generated `row_index` timeline. + + A dataset that violates this raises unless it is listed in `ignore_datasets`; + nothing is dropped automatically to satisfy alignment. + + Parameters + ---------- + entity_path_prefix: + Optional prefix prepended to every entity path (for example `"/world"`). + index_column: + Dataset to use as the file-wide timeline index, built with + [`IndexColumn`][rerun.experimental.IndexColumn], e.g. + `IndexColumn.timestamp("/time", input_unit="s")` or + `IndexColumn.sequence("/frame_id")`. + + The referenced dataset must be 1-dimensional. When omitted, a single + `row_index` sequence timeline (0, 1, …) is generated for the whole file + and every loaded dataset must align to it (see Row alignment). + ignore_datasets: + Datasets or groups to exclude entirely. Each entry is a dataset path or + a group path (which excludes the whole subtree). Ignored datasets are + neither loaded nor considered for row alignment. + use_structs: + When `True` (default), all columns of an entity are packed into a single + Arrow `Struct` component, with one field per dataset named after that + dataset. When `False`, each dataset becomes a separate component on the + same entity. A group holding a single dataset always emits that dataset + as a bare component, never as a one-field struct. + + Raises + ------ + ValueError + If a loaded, non-ignored, non-scalar dataset cannot be aligned to the + applicable row count (the index length, or the file's shared row count + when no `index_column` is set). Resolve by adding the offending dataset + to `ignore_datasets` or by choosing a compatible `index_column`. + + Also raised when the file exists but cannot be parsed as HDF5: the + layout is validated eagerly here, so such failures surface at + `stream()` rather than lazily mid-iteration. + + """ + return LazyChunkStream( + self._internal.stream( + entity_path_prefix=entity_path_prefix, + index_column=index_column._as_internal_tuple() if index_column is not None else None, + ignore_datasets=ignore_datasets, + use_structs=use_structs, + ) + ) + + def groups(self, path: str = "/") -> list[str]: + """ + List the group paths under `path`, recursively. + + Metadata only — no dataset values are read; reflects the raw file. + + Parameters + ---------- + path: + Group under which to list. Defaults to the root group `/`, i.e. the + whole file. + + """ + return self._internal.groups(path) + + def datasets(self, path: str = "/") -> list[DatasetInfo]: + """ + List the datasets under `path`, recursively, with their shape and dtype. + + Metadata only — no dataset values are read; reflects the raw file. + + Parameters + ---------- + path: + Group under which to list. Defaults to the root group `/`, i.e. the + whole file. + + """ + return [ + DatasetInfo(path=dataset_path, shape=tuple(shape), dtype=dtype) + for (dataset_path, shape, dtype) in self._internal.datasets(path) + ] + + def attributes(self, path: str = "/") -> dict[str, int | float | str | bytes | list[int | float | str]]: + """ + Read the HDF5 attributes attached to an object as a typed Python dict. + + This is a convenience accessor for the same attributes that `stream()` + emits under `__hdf5_properties` (see the class docstring). It reads the + raw file directly. + + Parameters + ---------- + path: + Path to the object whose attributes are read. Defaults to the root + group `/`, i.e. the file-level (global) attributes. May reference + any group or dataset. + + Returns + ------- + A mapping from attribute name to value. Scalar attributes are returned + as Python scalars (`int`, `float`, `str`, `bytes`); array-valued + attributes are returned as lists. Empty if the object has no attributes. + + Raises + ------ + KeyError + If `path` does not exist in the file. + + """ + return self._internal.attributes(path) + + @property + def path(self) -> Path: + """The file path of the HDF5 file.""" + return self._internal.path + + def __repr__(self) -> str: + return f"Hdf5Reader({self._internal.path})" diff --git a/rerun_py/rerun_sdk/rerun/experimental/_index_column.py b/rerun_py/rerun_sdk/rerun/experimental/_index_column.py new file mode 100644 index 000000000000..b2ca25302def --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/_index_column.py @@ -0,0 +1,58 @@ +"""A typed timeline-index specification shared by the experimental readers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +TimeUnit = Literal["ns", "us", "ms", "s"] +"""What the raw index values represent — not a desired output unit.""" + + +@dataclass(frozen=True) +class IndexColumn: + """ + A dataset/column to use as a timeline index, and how to interpret it. + + Construct one with [`timestamp`][rerun.experimental.IndexColumn.timestamp], + [`duration`][rerun.experimental.IndexColumn.duration], or + [`sequence`][rerun.experimental.IndexColumn.sequence] — the timeline kind is + the constructor you pick, so there is nothing to mistype: + + ```python + IndexColumn.timestamp("/time", input_unit="s") + IndexColumn.duration("/elapsed", input_unit="us") + IndexColumn.sequence("/frame_id") + ``` + """ + + path: str + """Path of the 1-D dataset (HDF5) or name of the column (Parquet) to use as the index.""" + + kind: Literal["timestamp", "duration", "sequence"] + """The timeline kind: time since epoch, elapsed time, or an ordinal integer index.""" + + input_unit: TimeUnit | None = None + """ + What the raw integer/float values represent (**not** a desired output unit); + values are scaled to nanoseconds internally. `None` for `sequence`. + """ + + @classmethod + def timestamp(cls, path: str, *, input_unit: TimeUnit = "ns") -> IndexColumn: + """A time-since-epoch timeline. `input_unit` describes the raw values (default `"ns"`).""" + return cls(path=path, kind="timestamp", input_unit=input_unit) + + @classmethod + def duration(cls, path: str, *, input_unit: TimeUnit = "ns") -> IndexColumn: + """An elapsed-time timeline. `input_unit` describes the raw values (default `"ns"`).""" + return cls(path=path, kind="duration", input_unit=input_unit) + + @classmethod + def sequence(cls, path: str) -> IndexColumn: + """An ordinal integer-index timeline. No unit applies.""" + return cls(path=path, kind="sequence", input_unit=None) + + def _as_internal_tuple(self) -> tuple[str, str, str | None]: + """The `(path, kind, unit)` triple the internal bindings expect.""" + return (self.path, self.kind, self.input_unit) diff --git a/rerun_py/rerun_sdk/rerun/experimental/_indexed_reader.py b/rerun_py/rerun_sdk/rerun/experimental/_indexed_reader.py index ebbd0ebb0f68..44920d183b50 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_indexed_reader.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_indexed_reader.py @@ -5,18 +5,23 @@ from ._streaming_reader import StreamingReader if TYPE_CHECKING: - from ._chunk_store import ChunkStore + from ._lazy_store import LazyStore @runtime_checkable class IndexedReader(StreamingReader, Protocol): """ - Protocol for readers that can produce a fully materialized ChunkStore. + Protocol for readers backed by an index/manifest. Extends `StreamingReader`: every `IndexedReader` also supports `stream() -> LazyChunkStream` for pure-streaming processing. + + Indexed readers expose a [`LazyStore`][rerun.experimental.LazyStore] view + over the source via `store()` — the manifest is read up-front; chunks load + on demand. To fully materialize into a + [`ChunkStore`][rerun.experimental.ChunkStore], call `stream().collect()`. """ - def store(self) -> ChunkStore: - """Return a fully materialized ChunkStore from this source.""" + def store(self) -> LazyStore: + """Return a [`LazyStore`][rerun.experimental.LazyStore] view of this source.""" ... diff --git a/rerun_py/rerun_sdk/rerun/experimental/_lazy_chunk_stream.py b/rerun_py/rerun_sdk/rerun/experimental/_lazy_chunk_stream.py index f46278313fd3..94dea183a22b 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_lazy_chunk_stream.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_lazy_chunk_stream.py @@ -15,20 +15,21 @@ from rerun_bindings import ChunkInternal, ComponentDescriptor from ._chunk_store import ChunkStore - from ._optimization_settings import OptimizationSettings + from ._optimization_profile import OptimizationProfile class LazyChunkStream: """ A lazy, composable pipeline over chunks. - Builder methods (`filter`, `drop`, `split`, `merge`) **consume** the input stream(s) - and return new stream(s). A consumed stream cannot be used as a builder input again; attempting - to do so raises a `ValueError`. This prevents accidental reuse that would result in duplicate - use of the same stream in a pipeline. + Builder methods (`filter`, `drop`, `split`, `map`, `flat_map`, `lenses`, `merge`) + **consume** the input stream(s) and return new stream(s). A consumed stream cannot be + used again; attempting to do so raises a `ValueError`. This prevents accidental reuse + that would result in duplicate use of the same stream in a pipeline. - Terminal methods (`collect`, `write_rrd`, `__iter__`) do **not** consume the stream and - may be called repeatedly. Each call creates a fresh execution of the pipeline. + Terminal methods (`to_chunks`, `__iter__`, `collect`, `write_rrd`) do **not** consume + the stream — they run the pipeline and leave the stream usable. Each call creates a + fresh execution. """ _internal: LazyChunkStreamInternal @@ -126,7 +127,7 @@ def drop( def map(self, fn: Callable[[Chunk], Chunk]) -> LazyChunkStream: """ - Apply a Python function to each chunk, producing exactly one output chunk. + Apply a Python function to each chunk, producing exactly one output chunk. Consumes this stream. Runs in Python (GIL-bound, sequential). For transforms that may produce zero or many chunks, use `flat_map` instead. @@ -139,7 +140,7 @@ def _wrapper(internal: ChunkInternal) -> ChunkInternal: def flat_map(self, fn: Callable[[Chunk], Iterable[Chunk]]) -> LazyChunkStream: """ - Apply a Python function to each chunk, producing zero or more output chunks. + Apply a Python function to each chunk, producing zero or more output chunks. Consumes this stream. Runs in Python (GIL-bound, sequential). """ @@ -167,7 +168,7 @@ def lenses( Parameters ---------- lenses: - One or more [`Lens`][rerun.experimental.Lens] objects describing the transformations. + One or more [`Lens`][rerun.experimental.Lens] objects. output_mode: How to handle unmatched chunks: @@ -257,7 +258,7 @@ def write_rrd( recording_id: str, ) -> None: """ - Consume the stream and write all chunks to an RRD file. + Run the pipeline and write all chunks to an RRD file. The caller must provide application_id and recording_id explicitly. """ @@ -270,16 +271,16 @@ def write_rrd( def collect( self, *, - optimize: OptimizationSettings | None = None, + optimize: OptimizationProfile | None = None, ) -> ChunkStore: """ - Consume the stream and materialize all chunks into a ChunkStore. + Run the pipeline and materialize all chunks into a ChunkStore. By default, only the single-pass compaction that happens naturally - during chunk insertion is applied. Pass `optimize=OptimizationSettings(...)` - to run additional optimization (extra convergence passes, video GoP - rebatching); the defaults for [`OptimizationSettings`][rerun.experimental.OptimizationSettings] - mirror those of the `rerun rrd optimize` CLI. + during chunk insertion is applied. Pass `optimize=OptimizationProfile.LIVE` + or `optimize=OptimizationProfile.OBJECT_STORE` to run additional + optimization (extra convergence passes, video GoP rebatching) tuned for + the chosen target. Parameters ---------- @@ -287,14 +288,14 @@ def collect( If `None` (default), no extra optimization is performed beyond the single pass that happens on insert. - Otherwise, apply the given settings after insertion. + Otherwise, apply the given profile after insertion. Examples -------- - Run optimization with default settings (matches `rerun rrd optimize`): + Run with the object-store-tuned profile: ```python - store = reader.stream().collect(optimize=OptimizationSettings()) + store = reader.stream().collect(optimize=OptimizationProfile.OBJECT_STORE) ``` """ @@ -310,11 +311,12 @@ def collect( extra_passes=optimize.extra_passes, gop_batching=optimize.gop_batching, split_size_ratio=optimize.split_size_ratio, + fix_keyframe=optimize.fix_keyframe, ), ) def to_chunks(self) -> list[Chunk]: - """Consume the stream and return all chunks as a list.""" + """Run the pipeline and return all chunks as a list.""" return [Chunk(internal) for internal in self._internal.to_chunks()] def __iter__(self) -> Iterator[Chunk]: diff --git a/rerun_py/rerun_sdk/rerun/experimental/_lazy_store.py b/rerun_py/rerun_sdk/rerun/experimental/_lazy_store.py new file mode 100644 index 000000000000..fd4650276a7e --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/_lazy_store.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + + from rerun.catalog import Schema + from rerun_bindings import LazyStoreInternal + + from ._lazy_chunk_stream import LazyChunkStream + + +class LazyStore: + """ + Index-based, on-demand chunk store. + + The manifest is held in memory (so `schema()`, `summary()`, and `__len__` + work without loading any chunks), but chunk data is loaded only when + requested. + + Example: + lazy = RrdReader("recording.rrd").store() + + Use `stream()` to process chunks through the lazy pipeline, or `write_rrd()` + to persist to disk. To fully materialize into a + [`ChunkStore`][rerun.experimental.ChunkStore], call `lazy.stream().collect()`. + + """ + + _internal: LazyStoreInternal + + def __init__(self, internal: LazyStoreInternal) -> None: + self._internal = internal + + def schema(self) -> Schema: + """The schema describing all columns in this store, derived from the manifest.""" + from rerun.catalog import Schema + + return Schema(self._internal.schema()) + + def summary(self) -> str: + """ + Compact, deterministic summary of every chunk in the store. + + Built from the manifest; no chunk data is loaded. Each line describes one chunk: + + {entity_path} rows={n} static={True|False} timelines=[…] cols=[…] + + Useful for snapshot testing. + """ + return self._internal.summary() + + def stream(self) -> LazyChunkStream: + """Return a lazy stream over all chunks in this store.""" + from ._lazy_chunk_stream import LazyChunkStream + + return LazyChunkStream(self._internal.stream()) + + @property + def _chunks_loaded(self) -> int: + """ + Monotonic count of chunks physically loaded from this store since it was opened. + + For test purposes. + """ + return self._internal._chunks_loaded + + def write_rrd( + self, + path: str | Path, + *, + application_id: str, + recording_id: str, + ) -> None: + """ + Write all chunks to an RRD file. + + The caller must provide application_id and recording_id explicitly. + """ + self.stream().write_rrd( + path, + application_id=application_id, + recording_id=recording_id, + ) + + def __len__(self) -> int: + """Return the number of chunks described by the manifest.""" + return self._internal.num_chunks() + + def __repr__(self) -> str: + return f"LazyStore({len(self)} chunks)" diff --git a/rerun_py/rerun_sdk/rerun/experimental/_lens.py b/rerun_py/rerun_sdk/rerun/experimental/_lens.py index bd7eddd5517a..0a08c2e0165c 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_lens.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_lens.py @@ -1,43 +1,77 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, TypeAlias from rerun._baseclasses import ComponentDescriptor -from rerun_bindings import LensInternal, LensOutputInternal +from rerun_bindings import DeriveLensInternal, MutateLensInternal from ._selector import Selector if TYPE_CHECKING: - from collections.abc import Mapping + import pyarrow as pa -class LensOutput: +class DeriveLens: """ - Describes one output group of a lens. + A derive lens that creates new component/time columns from an input component. - Each input row produces exactly one output row (1:1 mapping). - Times are inherited from the input chunk unchanged. + Derive lenses extract fields from a component and produce new columns, + optionally at a different entity and/or with new time columns. + + Pass `scatter=True` to enable 1:N row mapping (exploding lists). Example usage:: - output = ( - LensOutput() - .to_component("rerun.components.TextDocument:text", Selector(".")) + lens = ( + DeriveLens("Imu:accel") + .to_component(rr.Scalars.descriptor_scalars(), Selector(".x")) + ) + + To write to an explicit target entity:: + + lens = ( + DeriveLens("Imu:accel", output_entity="/out/x") + .to_component(rr.Scalars.descriptor_scalars(), Selector(".x")) ) """ - _internal: LensOutputInternal + _internal: DeriveLensInternal + + def __init__( + self, + input_component: str, + *, + output_entity: str | None = None, + scatter: bool = False, + ) -> None: + """ + Create a new derive lens. + + Parameters + ---------- + input_component: + The component identifier to match (e.g. `"Imu:accel"`). + output_entity: + Optional target entity path. When set, output is written + to this entity instead of the input entity. + scatter: + When `True`, use 1:N row mapping (explode lists). - def __init__(self) -> None: - """Create a new output group.""" - self._internal = LensOutputInternal() + """ + self._internal = DeriveLensInternal( + input_component, + output_entity=output_entity, + scatter=scatter, + ) def to_component( self, component: ComponentDescriptor | str, selector: Selector | str, - ) -> LensOutput: + *, + cast_to: pa.DataType | Literal["auto"] | None = None, + ) -> DeriveLens: """ Add a component output column. @@ -46,22 +80,25 @@ def to_component( component: A `ComponentDescriptor` or a component identifier string for the output column (e.g. `"Scalars:scalars"`). - Using a full `ComponentDescriptor` preserves archetype and - component type metadata in the output. selector: A [`Selector`][rerun.experimental.Selector] or selector query string to apply to the input column. + cast_to: + How to cast the produced column to match the target component. By default + (`None`) the column is emitted as-is. Pass `"auto"` to cast it to the + component's canonical Arrow datatype, or an explicit pyarrow `DataType` to + cast it to that type. Casting errors if the conversion is unsupported. Returns ------- - A new [`LensOutput`][rerun.experimental.LensOutput] with the component added. + A new [`DeriveLens`][rerun.experimental.DeriveLens] with the component added. """ sel = _normalize_selector(selector) - new = LensOutput.__new__(LensOutput) + new = DeriveLens.__new__(DeriveLens) if isinstance(component, str): component = ComponentDescriptor(component) - new._internal = self._internal.to_component(component, sel._internal) + new._internal = self._internal.to_component(component, sel._internal, cast_to=cast_to) return new def to_timeline( @@ -69,7 +106,7 @@ def to_timeline( timeline_name: str, timeline_type: Literal["sequence", "duration_ns", "timestamp_ns"], selector: Selector | str, - ) -> LensOutput: + ) -> DeriveLens: """ Add a time extraction column. @@ -86,80 +123,242 @@ def to_timeline( Returns ------- - A new [`LensOutput`][rerun.experimental.LensOutput] with the time column added. + A new [`DeriveLens`][rerun.experimental.DeriveLens] with the time column added. """ sel = _normalize_selector(selector) - new = LensOutput.__new__(LensOutput) + new = DeriveLens.__new__(DeriveLens) new._internal = self._internal.to_timeline(timeline_name, timeline_type, sel._internal) return new + def to_packed_component( + self, component: ComponentDescriptor | str, *fields: str, cast_to: pa.DataType | Literal["auto"] | None = "auto" + ) -> DeriveLens: + """ + Add a component output column by packing the provided fields in a fixed-size list. -class Lens: - """ - A lens that transforms component data from one form to another. + Parameters + ---------- + component: + A `ComponentDescriptor` or a component identifier string for the output column + (e.g. `"Points3D:positions"`). + *fields: + Names of the struct fields to pack, in order. They must all resolve to the same + datatype. At least one field is required. + cast_to: + How to cast the packed column to match the target component. Defaults to `"auto"`, + which casts to the component's canonical Arrow datatype (e.g. the `f64` columns + a parquet file typically holds → the `f32` a `Transform3D:translation` expects). + Pass an explicit pyarrow `DataType` to cast to that type, or `None` to emit the + packed list as-is. - Lenses extract, transform, and restructure component data. They are - applied to chunks whose entity path matches the content filter and - that contain the specified input component. - Example usage:: + Returns + ------- + A new [`DeriveLens`][rerun.experimental.DeriveLens] with the packed component added. - lens = Lens( - "example:Instruction:text", - LensOutput() - .to_component("rerun.components.TextDocument:text", Selector(".")), - ) + """ + if not fields: + raise ValueError("to_packed_component requires at least one field") + selector = f"pack({', '.join(f'.{field}!' for field in fields)})" + return self.to_component(component, selector, cast_to=cast_to) + + # TODO(RR-5007): this should ideally be codegened + def to_translation(self, x: str, y: str, z: str) -> DeriveLens: + """ + Add a `Transform3D:translation` component from the provided paths. + + Parameters + ---------- + x, y, z: + Paths of the struct fields holding the translation components. + + Returns + ------- + A new [`DeriveLens`][rerun.experimental.DeriveLens] with the translation added. + + """ + from rerun.archetypes import Transform3D + + return self.to_packed_component(Transform3D.descriptor_translation(), x, y, z, cast_to="auto") + + # TODO(RR-5007): this should ideally be codegened + def to_quaternion(self, x: str, y: str, z: str, w: str) -> DeriveLens: # noqa: PLR0917 + """ + Add a `Transform3D:quaternion` component from the provided paths. + + Parameters + ---------- + x, y, z, w: + Paths of the struct fields holding the quaternion components, in `xyzw` order. + + Returns + ------- + A new [`DeriveLens`][rerun.experimental.DeriveLens] with the quaternion added. - To write to explicit target entities:: + """ + from rerun.archetypes import Transform3D + + return self.to_packed_component(Transform3D.descriptor_quaternion(), x, y, z, w, cast_to="auto") + + # TODO(RR-5007): this should ideally be codegened + def to_scale(self, x: str, y: str, z: str) -> DeriveLens: + """ + Add a `Transform3D:scale` component from the provided paths. + + Parameters + ---------- + x, y, z: + Paths of the struct fields holding the per-axis scale factors. + + Returns + ------- + A new [`DeriveLens`][rerun.experimental.DeriveLens] with the scale added. + + """ + from rerun.archetypes import Transform3D - lens = Lens( - "Imu:accel", - to_entity={ - "/out/x": LensOutput().to_component(desc, ".x"), - "/out/y": LensOutput().to_component(desc, ".y"), - }, + return self.to_packed_component(Transform3D.descriptor_scale(), x, y, z, cast_to="auto") + + # TODO(RR-5007): this should ideally be codegened + def to_rotation_axis_angle(self, axis_x: str, axis_y: str, axis_z: str, angle: str) -> DeriveLens: # noqa: PLR0917 + """ + Add a `Transform3D:rotation_axis_angle` component from the provided paths. + + Parameters + ---------- + axis_x, axis_y, axis_z: + Paths of the struct fields holding the rotation axis components. + angle: + Path of the struct field holding the rotation angle, in radians. + + Returns + ------- + A new [`DeriveLens`][rerun.experimental.DeriveLens] with the rotation added. + + """ + from rerun.archetypes import Transform3D + + # TODO(RR-4999): drop the .pipe and use a struct-constructing selector once structs are supported + selector = Selector(".").pipe( + lambda struct: _build_rotation_axis_angle_struct(struct, axis_x, axis_y, axis_z, angle) ) + return self.to_component(Transform3D.descriptor_rotation_axis_angle(), selector) + + # TODO(RR-5007): this should ideally be codegened + def to_scalars(self, *fields: str) -> DeriveLens: + """ + Add a `Scalars:scalars` component from the provided path(s). + + Each path becomes one scalar instance per row, so a single path yields one series and + multiple paths yield one series each at the same entity. + + Parameters + ---------- + *fields: + Paths of the struct fields to read as scalars, in order. At least one is required. + + Returns + ------- + A new [`DeriveLens`][rerun.experimental.DeriveLens] with the scalars added. + + """ + from rerun.archetypes import Scalars + + if not fields: + raise ValueError("to_scalars requires at least one field") + if len(fields) == 1: + # A single scalar must stay a plain value, not a 1-element fixed-size list. + return self.to_component(Scalars.descriptor_scalars(), f".{fields[0]}", cast_to=None) + # Pack into a fixed-size list, then flatten with `.[]` so the values land as N + # instances in the per-row list (`List`) rather than a nested fixed-size list. + packed = ", ".join(f".{field}!" for field in fields) + return self.to_component(Scalars.descriptor_scalars(), f"pack({packed}) | .[]", cast_to=None) + - To restrict which entities a lens applies to, use - `stream.filter(content=...)` before `.lenses()`. +class MutateLens: + """ + A mutate lens that modifies the input component in-place. + + Mutate lenses apply a selector transformation to the input component, + replacing it in the chunk. By default, new row IDs are generated. + Pass `keep_row_ids=True` to preserve original row IDs. + + Example usage:: + + lens = MutateLens("Imu:accel", Selector(".x")) """ - _internal: LensInternal + _internal: MutateLensInternal def __init__( self, input_component: str, - output: LensOutput | None = None, + selector: Selector | str, *, - to_entity: Mapping[str, LensOutput] | None = None, + keep_row_ids: bool = False, ) -> None: """ - Create a new lens. + Create a new mutate lens. Parameters ---------- input_component: - The component identifier to match in input chunks. - output: - A [`LensOutput`][rerun.experimental.LensOutput] for the same entity as the input. - At most one is allowed. - to_entity: - A dict mapping entity paths to [`LensOutput`][rerun.experimental.LensOutput] objects - for writing to explicit target entities. + The component identifier to modify in-place. + selector: + A [`Selector`][rerun.experimental.Selector] or selector query string to apply. + keep_row_ids: + When `True`, preserve the original row IDs. """ - target_internals = {k: v._internal for k, v in to_entity.items()} if to_entity else None - self._internal = LensInternal( + sel = _normalize_selector(selector) + self._internal = MutateLensInternal( input_component, - output._internal if output is not None else None, - to_entity=target_internals, + sel._internal, + keep_row_ids=keep_row_ids, ) +Lens: TypeAlias = DeriveLens | MutateLens +"""Union of all lens types.""" + + def _normalize_selector(selector: Selector | str) -> Selector: """Normalize a selector argument to a Selector object.""" if isinstance(selector, str): return Selector(selector) return selector + + +def _interleave_to_fsl(arrays: list[pa.Array], dtype: pa.DataType) -> pa.FixedSizeListArray: + """Interleave same-length arrays row-wise into a `FixedSizeList(len(arrays), dtype)` with non-null items.""" + import numpy as np + import pyarrow as pa + import pyarrow.compute as pc + + columns = [pc.cast(array, dtype).to_numpy(zero_copy_only=False) for array in arrays] + flat = pa.array(np.stack(columns, axis=1).reshape(-1), type=dtype) + return pa.FixedSizeListArray.from_arrays(flat, type=pa.list_(pa.field("item", dtype, nullable=False), len(arrays))) + + +def _build_rotation_axis_angle_struct( # noqa: PLR0917 + struct: pa.StructArray, + axis_x: str, + axis_y: str, + axis_z: str, + angle: str, +) -> pa.StructArray: + """Build the exact `Struct{axis: FixedSizeList[3], angle: f32}` a `RotationAxisAngle` expects.""" + import pyarrow as pa + import pyarrow.compute as pc + + axis = _interleave_to_fsl([struct.field(axis_x), struct.field(axis_y), struct.field(axis_z)], pa.float32()) + angle_arr = pc.cast(struct.field(angle), pa.float32()) + return pa.StructArray.from_arrays( + [axis, angle_arr], + fields=[ + pa.field("axis", axis.type, nullable=False), + pa.field("angle", pa.float32(), nullable=False), + ], + ) diff --git a/rerun_py/rerun_sdk/rerun/experimental/_mcap_reader.py b/rerun_py/rerun_sdk/rerun/experimental/_mcap_reader.py index 2d23ed273e9a..f50b635d3cca 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_mcap_reader.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_mcap_reader.py @@ -27,6 +27,9 @@ def __init__( decoders: Sequence[str] | None = None, include_topic_regex: Sequence[str] | None = None, exclude_topic_regex: Sequence[str] | None = None, + start_time_ns: int | None = None, + end_time_ns: int | None = None, + recover: bool = False, ) -> None: """ Construct a new MCAP reader. @@ -42,7 +45,8 @@ def __init__( Optional offset in nanoseconds to add to all `TimestampNs` time columns. decoders: Optional list of MCAP decoder identifiers to enable. If omitted, all - available decoders are enabled. Use [`McapReader.available_decoders`][] + available decoders are enabled. Use + [`McapReader.available_decoders`][rerun.experimental.McapReader.available_decoders] to enumerate them. include_topic_regex: Optional list of regex patterns. If provided, only topics matching at @@ -51,6 +55,21 @@ def __init__( exclude_topic_regex: Optional list of regex patterns. Topics matching any pattern are skipped. Applied after includes. Same syntax as `include_topic_regex`. + start_time_ns: + Optional inclusive lower bound on the raw MCAP `log_time` (nanoseconds). + Messages before this time are skipped. `None` leaves the range open at the start. + end_time_ns: + Optional exclusive upper bound on the raw MCAP `log_time` (nanoseconds). + Messages at or after this time are skipped. `None` leaves the range open + at the end. + recover: + Whether to recover a missing or invalid MCAP summary in memory. Our reader normally + requires the summary + chunk index that live at the end of the file, so an interrupted + recording (valid start, truncated tail, no footer/summary) fails to read. When `recover` + is set, the summary is reconstructed from a front-to-back scan instead: the incomplete + tail chunk/record is dropped with a warning, and any channel declared only in the + dropped tail is lost. The recovered statistics only count the channels and messages that + could be recovered. Healthy files are unaffected. """ self._internal = McapReaderInternal( @@ -60,11 +79,33 @@ def __init__( decoders=list(decoders) if decoders is not None else None, include_topic_regex=list(include_topic_regex) if include_topic_regex is not None else None, exclude_topic_regex=list(exclude_topic_regex) if exclude_topic_regex is not None else None, + start_time_ns=start_time_ns, + end_time_ns=end_time_ns, + recover=recover, ) - def stream(self) -> LazyChunkStream: - """Return a lazy stream over all chunks in the MCAP file.""" - return LazyChunkStream(self._internal.stream()) + def stream( + self, + *, + start_time_ns: int | None = None, + end_time_ns: int | None = None, + ) -> LazyChunkStream: + """ + Return a lazy stream over the chunks in the MCAP file. + + `start_time_ns` and `end_time_ns` override the values passed to the constructor, for this + scan only. If either `start_time_ns` or `end_time_ns` are provided both are reset. + """ + return LazyChunkStream( + self._internal.stream( + start_time_ns=start_time_ns, + end_time_ns=end_time_ns, + ) + ) + + def time_bounds(self) -> tuple[int, int]: + """Return the `(min, max)` MCAP `log_time` bounds (nanoseconds, inclusive).""" + return self._internal.time_bounds() @property def path(self) -> Path: diff --git a/rerun_py/rerun_sdk/rerun/experimental/_mp4_reader.py b/rerun_py/rerun_sdk/rerun/experimental/_mp4_reader.py new file mode 100644 index 000000000000..cbe52b58edd5 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/_mp4_reader.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Literal, overload + +from rerun_bindings import Mp4ReaderInternal, Mp4TranscodeOptionsInternal + +from ..components import VideoCodec +from ._lazy_chunk_stream import LazyChunkStream + + +@dataclass(frozen=True, kw_only=True) +class Mp4TranscodeOptions: + """How to transcode an mp4.""" + + output_codec: VideoCodec | None = None + """ + Re-encode to this [`VideoCodec`][rerun.components.VideoCodec] instead of keeping + the source codec; the emitted `VideoStream` codec follows it. `None` (default) + keeps the source codec. + """ + + gop_size: int | None = None + """ + Force a keyframe every `gop_size` frames in the transcoded output. Requesting it + triggers a re-encode. `None` (default) keeps the encoder's default GOP. + """ + + try_gpu: bool = False + """ + Try to use a hardware (GPU) encoder if the local FFmpeg provides one for the + output codec, otherwise fall back to software (best-effort). GPU encoding is + drawn from the NVENC and VideoToolbox families only, so it realistically applies + to H264/H265 and, on newer NVIDIA hardware, AV1. VP8/VP9 always fall back to + software — their only GPU encoders are Intel QSV/VAAPI, which are not yet used. + Has no effect unless a transcode is already happening. + """ + + ffmpeg_override: str | Path | None = None + """ + Override the `ffmpeg` executable used to transcode. When `None` (default), + `ffmpeg` is looked up on `PATH`. Ignored when no transcode happens. + """ + + def __post_init__(self) -> None: + if self.output_codec is not None and not isinstance(self.output_codec, VideoCodec): + raise TypeError( + f"output_codec must be a rerun.components.VideoCodec, got {type(self.output_codec).__name__}" + ) + + def _to_internal(self) -> Mp4TranscodeOptionsInternal: + """Lower the validated options onto the Rust binding (codec → its fourcc value).""" + return Mp4TranscodeOptionsInternal( + gop_size=self.gop_size, + output_codec=None if self.output_codec is None else self.output_codec.value, + try_gpu=self.try_gpu, + ffmpeg_override=None if self.ffmpeg_override is None else Path(self.ffmpeg_override).absolute(), + ) + + +class Mp4Reader: + """Read chunks from an MP4 file.""" + + _internal: Mp4ReaderInternal + + @overload + def __init__( + self, + path: str | Path, + *, + mode: Literal["stream"] = "stream", + chunk_by_gop: bool = True, + timeline_name: str = "video", + timeline_type: Literal["duration", "timestamp"] = "duration", + transcode: Mp4TranscodeOptions | None = None, + entity_path: str | None = None, + ) -> None: ... + + @overload + def __init__( + self, + path: str | Path, + *, + mode: Literal["asset"], + timeline_name: str = "video", + timeline_type: Literal["duration", "timestamp"] = "duration", + entity_path: str | None = None, + ) -> None: ... + + def __init__( + self, + path: str | Path, + *, + mode: Literal["asset", "stream"] = "stream", + chunk_by_gop: bool = True, + timeline_name: str = "video", + timeline_type: Literal["duration", "timestamp"] = "duration", + transcode: Mp4TranscodeOptions | None = None, + entity_path: str | None = None, + ) -> None: + """ + Construct a new MP4 reader. + + Parameters + ---------- + path: + Path to the `.mp4` file to read. + mode: + How to convert the mp4 into chunks. + + - `"stream"` (default): emit a static `VideoStream(codec=…)` chunk + followed by per-GOP (or per-sample) `VideoSample` chunks. The mp4 + must use a codec representable as + [`VideoCodec`][rerun.components.VideoCodec]. + A source containing B-frames — or any source for which a + transform is requested via `transcode` — is transcoded with FFmpeg + into an equivalent B-frame-free stream before emission, which + requires an `ffmpeg` executable. + - `"asset"`: emit an `AssetVideo` blob chunk plus a + `VideoFrameReference` index chunk, matching the behavior of + `rerun video.mp4`. + chunk_by_gop: + Only meaningful when `mode="stream"`. When `True` (default), each + emitted Rerun chunk contains a keyframe plus all dependent samples + up to (but not including) the next keyframe. When `False`, each + sample becomes its own one-row Rerun chunk. + + Passing `chunk_by_gop=False` together with `mode="asset"` raises + `ValueError`. + timeline_name: + Name of the timeline used for stream-mode samples and for the + `VideoFrameReference` index chunk in asset mode. Defaults to + `"video"`. + timeline_type: + How to interpret the timeline values. + + The emitted values are the mp4 PTS (nanoseconds since the start of + the video) only the declared Arrow type changes: + + - `"duration"` (default): the values are typed as a duration, the + natural mp4 PTS interpretation. + - `"timestamp"`: the same PTS values, typed as nanoseconds since the + Unix epoch. The reader does not shift them, so until you retag them + — via a downstream `.map(...)` on the chunk stream with + caller-supplied wall-clock times (e.g. from a trajectory file) — + they render as timestamps near 1970. + transcode: + Only meaningful when `mode="stream"`. An + [`Mp4TranscodeOptions`][rerun.experimental.Mp4TranscodeOptions] + describing an optional re-encode. + entity_path: + Entity path under which chunks are emitted. When `None` (default), + the entity path is derived from the absolute file path (e.g. + `foo/video.mp4` run from `/data` becomes `/data/foo/video.mp4`). The + path is resolved to absolute up front, so the result is independent + of any later change to the working directory. + + """ + if mode == "asset" and transcode is not None: + raise ValueError('`transcode` is only valid with `mode="stream"`') + + self._internal = Mp4ReaderInternal( + Path(path).absolute(), + mode=mode, + chunk_by_gop=chunk_by_gop, + timeline_name=timeline_name, + timeline_type=timeline_type, + transcode=None if transcode is None else transcode._to_internal(), + entity_path=entity_path, + ) + + def stream(self) -> LazyChunkStream: + """Return a lazy stream over all chunks in the MP4 file.""" + return LazyChunkStream(self._internal.stream()) + + @property + def path(self) -> Path: + """The file path of the MP4 file.""" + return self._internal.path + + @property + def entity_path(self) -> str: + """The entity path under which chunks are emitted.""" + return self._internal.entity_path + + def __repr__(self) -> str: + return f"Mp4Reader({self._internal.path})" diff --git a/rerun_py/rerun_sdk/rerun/experimental/_optimization_profile.py b/rerun_py/rerun_sdk/rerun/experimental/_optimization_profile.py new file mode 100644 index 000000000000..fabcdec706fa --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/_optimization_profile.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import ClassVar + + +@dataclass(frozen=True, kw_only=True) +class OptimizationProfile: + """ + Named optimization profile passed to `LazyChunkStream.collect(optimize=...)`. + + Two presets: + + - `OptimizationProfile.LIVE`: small chunks tuned for the live Viewer workflow. + - `OptimizationProfile.OBJECT_STORE`: large chunks tuned for object-store-backed + query and streaming (e.g. a catalog server). + + The presets are *fully concrete*: every field has a value. Custom profiles + built by calling `OptimizationProfile(...)` directly may pass `None` on the + threshold fields to fall back to the SDK's internal default + (`OptimizationProfile.LIVE`'s thresholds). + """ + + LIVE: ClassVar[OptimizationProfile] + """ + Optimized for the live Viewer workflow: small chunks for low-latency + rendering and fine-grained time-panel precision. + """ + + OBJECT_STORE: ClassVar[OptimizationProfile] + """ + Optimized for object-store-backed storage (e.g. a catalog server): + larger chunks tuned for query throughput and streaming over the network. + """ + + max_bytes: int | None = None + """Chunk size threshold in bytes. ``None`` means use `LIVE`'s default.""" + + max_rows: int | None = None + """Maximum rows per sorted chunk. ``None`` means use `LIVE`'s default.""" + + max_rows_if_unsorted: int | None = None + """Maximum rows per unsorted chunk. ``None`` means use `LIVE`'s default.""" + + extra_passes: int = 50 + """Number of extra convergence passes run after the initial insert.""" + + gop_batching: bool = True + """ + If `True` (default), video stream chunks are rebatched to align with GoP + (keyframe) boundaries after normal compaction. + + GoP rebatching never splits a GoP across chunks, so streams with long + keyframe intervals can produce chunks much larger than `max_bytes`. + """ + + split_size_ratio: float | None = None + """ + If set, split chunks so no two archetype groups sharing a chunk differ in + byte size by more than this factor. Values should be `>= 1`; at `1.0`, + every archetype is forced into its own chunk. + + This keeps large columns (images, videos, blobs) out of the same chunk as + small columns (scalars, transforms, text), so the viewer can fetch just + the small columns without dragging along the large payload. Components + belonging to the same archetype are always kept together. + + A good starting value is `10.0`. If `None` (default), no splitting is + performed. + """ + + fix_keyframe: bool = False + """ + If `True`, any user-supplied `VideoStream:is_keyframe` data is dropped and + re-derived from the encoded samples during video rebatching. + """ + + +OptimizationProfile.LIVE = OptimizationProfile( + max_bytes=12 * 8 * 4096, + max_rows=4096, + max_rows_if_unsorted=1024, + extra_passes=50, + gop_batching=True, + split_size_ratio=None, + fix_keyframe=False, +) + +OptimizationProfile.OBJECT_STORE = OptimizationProfile( + max_bytes=2 * 1024 * 1024, + max_rows=65_536, + max_rows_if_unsorted=8_192, + extra_passes=50, + gop_batching=True, + split_size_ratio=10.0, + fix_keyframe=False, +) diff --git a/rerun_py/rerun_sdk/rerun/experimental/_optimization_settings.py b/rerun_py/rerun_sdk/rerun/experimental/_optimization_settings.py deleted file mode 100644 index de4b4ec47c1a..000000000000 --- a/rerun_py/rerun_sdk/rerun/experimental/_optimization_settings.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass(frozen=True, kw_only=True) -class OptimizationSettings: - """ - Settings for optimizing a ChunkStore via `LazyChunkStream.collect(optimize=...)`. - - Defaults mirror those of the `rerun rrd optimize` CLI. `None` on a threshold - field means using the default internal value. - """ - - max_bytes: int | None = None - """Chunk size threshold in bytes. ``None`` means use the default.""" - - max_rows: int | None = None - """Maximum rows per sorted chunk. ``None`` means use the default.""" - - max_rows_if_unsorted: int | None = None - """Maximum rows per unsorted chunk. ``None`` means use the default.""" - - extra_passes: int = 50 - """Number of extra convergence passes run after the initial insert.""" - - gop_batching: bool = True - """ - If `True` (default), video stream chunks are rebatched to align with GoP - (keyframe) boundaries after normal compaction. - - GoP rebatching never splits a GoP across chunks, so streams with long keyframe - intervals can produce chunks much larger than `max_bytes`. - """ - - split_size_ratio: float | None = None - """ - If set, split chunks so no two archetype groups sharing a chunk differ in - byte size by more than this factor. Values should be `>= 1`; at `1.0`, - every archetype is forced into its own chunk. - - This keeps large columns (images, videos, blobs) out of the same chunk as - small columns (scalars, transforms, text), so the viewer can fetch just the - small columns without dragging along the large payload. Components belonging - to the same archetype are always kept together. - - A good starting value is `10.0`. If `None` (default), no splitting is - performed. - """ diff --git a/rerun_py/rerun_sdk/rerun/experimental/_parquet_reader.py b/rerun_py/rerun_sdk/rerun/experimental/_parquet_reader.py index e8bc84bfee8e..400857f56a7a 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_parquet_reader.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_parquet_reader.py @@ -1,8 +1,7 @@ -"""Experimental parquet reader with configurable column grouping and column rules.""" +"""Experimental parquet reader with configurable column grouping.""" from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING from rerun_bindings import ParquetReaderInternal @@ -12,99 +11,40 @@ if TYPE_CHECKING: from pathlib import Path + from ._index_column import IndexColumn -@dataclass(frozen=True) -class ColumnRule: - """ - Rule for combining columns with matching suffixes into a Rerun component. - - Use the factory methods to create rules: - - `translation3d()` — 3 columns → `Translation3D` - - `rotation_quat()` — 4 columns → `RotationQuat` - - `rotation_axis_angle()` — 4 columns → `RotationAxisAngle` - - `scale3d()` — 3 columns → `Scale3D` - - `scalars()` — N columns → `Scalars` with named series - - `transform()` — 3 + 4 columns → `Transform3D` (translation + rotation) +class ParquetReader: """ - - suffixes: list[str] - target: str - names: list[str] | None = None - field_name_override: str | None = None - rotation_suffixes: list[str] | None = None - - @classmethod - def translation3d(cls, suffixes: list[str], *, field_name_override: str | None = None) -> ColumnRule: - """Create a rule that combines 3 columns into a `Translation3D` component.""" - if len(suffixes) != 3: - raise ValueError("Translation3D requires exactly 3 suffixes") - return cls(suffixes, "Translation3D", field_name_override=field_name_override) - - @classmethod - def rotation_quat(cls, suffixes: list[str], *, field_name_override: str | None = None) -> ColumnRule: - """Create a rule that combines 4 columns into a `RotationQuat` component.""" - if len(suffixes) != 4: - raise ValueError("RotationQuat requires exactly 4 suffixes") - return cls(suffixes, "RotationQuat", field_name_override=field_name_override) - - @classmethod - def rotation_axis_angle(cls, suffixes: list[str], *, field_name_override: str | None = None) -> ColumnRule: - """Create a rule that combines 4 columns into a `RotationAxisAngle` component (3 axis + 1 angle).""" - if len(suffixes) != 4: - raise ValueError("RotationAxisAngle requires exactly 4 suffixes (3 axis + 1 angle)") - return cls(suffixes, "RotationAxisAngle", field_name_override=field_name_override) - - @classmethod - def scale3d(cls, suffixes: list[str], *, field_name_override: str | None = None) -> ColumnRule: - """Create a rule that combines 3 columns into a `Scale3D` component.""" - if len(suffixes) != 3: - raise ValueError("Scale3D requires exactly 3 suffixes") - return cls(suffixes, "Scale3D", field_name_override=field_name_override) - - @classmethod - def scalars( - cls, - suffixes: list[str], - *, - names: list[str], - field_name_override: str | None = None, - ) -> ColumnRule: - """Create a rule that combines N columns into a `Scalars` component with named series.""" - if len(suffixes) != len(names): - raise ValueError("suffixes and names must have the same length") - return cls(suffixes, "Scalars", names=names, field_name_override=field_name_override) - - @classmethod - def transform( - cls, - translation_suffixes: list[str], - rotation_suffixes: list[str], - *, - field_name_override: str | None = None, - ) -> ColumnRule: - """ - Create a rule that combines 3 translation + 4 rotation columns into a `Transform3D`. - - Both suffix sets must match with the same sub-prefix for columns to be - combined. In struct mode, produces a nested struct with `translation` - and `quaternion` fields. In flat mode, emits both components at the - same entity path. - """ - if len(translation_suffixes) != 3: - raise ValueError("Transform requires exactly 3 translation suffixes") - if len(rotation_suffixes) != 4: - raise ValueError("Transform requires exactly 4 rotation suffixes") - return cls( - translation_suffixes, - "Transform", - field_name_override=field_name_override, - rotation_suffixes=rotation_suffixes, + Read chunks from a Parquet file. + + The reader turns raw parquet columns into grouped, time-indexed + [`Chunk`][rerun.experimental.Chunk]s of struct/scalar components. To map those + struct fields into Rerun archetypes (translation, rotation, scalars, …), apply + lenses to the resulting `.stream()` — see + [`DeriveLens`][rerun.experimental.DeriveLens]: + + Example + ------- + ```python + from rerun.experimental import ParquetReader, DeriveLens, IndexColumn + + store = ( + ParquetReader(path, index_columns=[IndexColumn.sequence("frame_index")]) + .stream() + .lenses( + [ + DeriveLens("data", output_entity="/pose") + .to_translation("pos_x", "pos_y", "pos_z") + .to_quaternion("quat_x", "quat_y", "quat_z", "quat_w") + ], + content="/transform", ) + .collect() + ) + ``` - -class ParquetReader: - """Read chunks from a Parquet file.""" + """ _internal: ParquetReaderInternal @@ -118,11 +58,10 @@ def __init__( prefixes: list[str] | None = None, use_structs: bool = True, static_columns: list[str] | None = None, - index_columns: list[tuple[str, str] | tuple[str, str, str]] | None = None, - column_rules: list[ColumnRule] | None = None, + index_columns: list[IndexColumn] | None = None, ) -> None: """ - Load a parquet file with configurable column grouping and column rules. + Load a parquet file with configurable column grouping. Parameters ---------- @@ -154,44 +93,15 @@ def __init__( emitted once as timeless/static data. An error is raised if a listed column contains varying values. index_columns: - List of columns to use as timeline indices. Each entry is a tuple: - `(name, type)` or `(name, type, unit)`. - - The `type` specifies the timeline kind: - - - `"timestamp"`: time since epoch - - `"duration"`: elapsed time - - `"sequence"`: ordinal integer index - - The `unit` describes what the raw integer values in the column - represent (not a desired output unit). Rerun stores all timestamps - in nanoseconds internally, so values are scaled accordingly. - Supported: `"ns"` (default), `"us"`, `"ms"`, `"s"`. - Ignored for `"sequence"` type. + Columns to use as timeline indices, each built with + [`IndexColumn`][rerun.experimental.IndexColumn], e.g. + `IndexColumn.timestamp("ts", input_unit="ms")` or + `IndexColumn.sequence("frame_index")`. When omitted, a synthetic `row_index` sequence timeline is generated automatically (one entry per row). - column_rules: - Rules for combining columns with matching suffixes into typed - Rerun components. Each rule is a `ColumnRule` created via - factory methods. Rules are processed in list order; the first rule - whose suffixes match wins. Put specific rules before broad - catch-all rules. - - Example:: - - column_rules=[ - ColumnRule.translation3d(["_pos_x", "_pos_y", "_pos_z"], field_name_override="_pos"), - ColumnRule.rotation_quat(["_quat_x", "_quat_y", "_quat_z", "_quat_w"], field_name_override="_quat"), - ColumnRule.scalars(["_x", "_y", "_z"], names=["x", "y", "z"]), - ] """ - # Normalize index_columns: pad 2-tuples to 3-tuples with None for the unit - normalized_index = ( - [(t[0], t[1], t[2] if len(t) > 2 else None) for t in index_columns] if index_columns is not None else None - ) - self._internal = ParquetReaderInternal( str(path), entity_path_prefix=entity_path_prefix, @@ -200,8 +110,7 @@ def __init__( prefixes=prefixes, use_structs=use_structs, static_columns=static_columns, - index_columns=normalized_index, - column_rules=column_rules, + index_columns=([ic._as_internal_tuple() for ic in index_columns] if index_columns is not None else None), ) def stream(self) -> LazyChunkStream: diff --git a/rerun_py/rerun_sdk/rerun/experimental/_query_metrics.py b/rerun_py/rerun_sdk/rerun/experimental/_query_metrics.py new file mode 100644 index 000000000000..dc40477e285c --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/_query_metrics.py @@ -0,0 +1,309 @@ +""" +Programmatic capture of DataFusion query metrics from Python. + +`re_datafusion` records plan-time and per-partition fetch metrics on every +dataset query (`query_chunks`, `filters_pushed_down`, `fetch_grpc_bytes`, …). +On the Rust side these surface in `EXPLAIN ANALYZE`; from Python they +*should* surface via `df.explain(analyze=True)`, but a bug in +`datafusion-python` / `datafusion_ffi` currently strips the metrics when the +plan crosses the FFI capsule. A fix is in flight upstream. + +In the meantime, this module exposes [`query_metrics`][] — a context manager +that captures the same metrics directly from the Rust side, bypassing +DataFusion's FFI: + +```python +from rerun.experimental import query_metrics + +with query_metrics() as m: + df = dataset.reader(index="time_1").limit(100) + df.collect() + print(m.last_query()) +``` + +Each query that runs inside the `with` block produces one +[`QueryMetrics`][] record (built when the last per-partition stream +finishes). Mid-scope reads via `m.queries` or `m.last_query()` are +non-destructive; on `__exit__` any remaining snapshots are drained into the +collector and the scope is unbound. + +`query_metrics()` is part of `rerun.experimental` — once the upstream +DataFusion FFI fix lands, `df.explain(analyze=True)` starts working and this +API may evolve (or be removed) without going through the standard +deprecation cycle. +""" + +from __future__ import annotations + +import contextlib +import logging +from contextvars import ContextVar +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import datetime + from collections.abc import Iterator + +logger = logging.getLogger("rerun") + + +# Stack of currently-active `_MetricsCollectorHandle`s, scoped to the current +# `contextvars.Context`. The Rust side reads this in +# `rerun_py/src/catalog/dataset_view.rs::reader()` to bind metrics capture to +# the queries built inside an active scope — and nothing else. +# +# Stored as a tuple (immutable, cheap to copy on push) so each +# `_active_collectors.set(…)` produces a fresh value that the ContextVar +# token can reset cleanly. The name "object" rather than the concrete +# `_MetricsCollectorHandle` keeps this module importable when the catalog +# bindings aren't available in the local build. +_active_collectors: ContextVar[tuple[object, ...]] = ContextVar("rerun_query_metrics_collectors", default=()) # NOLINT + + +@dataclass(frozen=True) +class QueryMetrics: + """ + One query's metrics, captured at the moment its last per-partition stream finished. + + Mirrors the Rust-side `re_datafusion::QuerySnapshot`. The same numbers are + produced via three transports: this dataclass (Python), DataFusion's + `EXPLAIN ANALYZE`, and the PostHog analytics OTLP span. Field naming + differs across the three: + + - Timing fields here are `datetime.timedelta` (`total_duration`, + `time_to_first_chunk`, …). `EXPLAIN ANALYZE` uses DataFusion `Time` + metrics, which print their own units. The OTLP analytics attributes + keep an explicit `_us` suffix and carry integer microseconds + (`total_duration_us`, `time_to_first_chunk_us`, …) because OTLP + attribute values are scalar (`i64` / `f64` / `bool` / `string`) and + can't carry a duration natively. + - `query_chunks_per_segment_mean` is a `float` and does not appear in + `EXPLAIN ANALYZE`, since DataFusion `Count` metrics are integer-only. + The corresponding `_min` / `_max` integer fields are surfaced in all + three transports. + + `fetch_direct_max_attempt` is the true maximum attempt number across all + partitions. + """ + + # Plan-time + dataset_id: str + query_chunks: int + query_segments: int + query_layers: int + query_columns: int + query_entities: int + query_bytes: int + query_chunks_per_segment_min: int + query_chunks_per_segment_max: int + query_chunks_per_segment_mean: float + query_type: str + primary_index_name: str | None + time_to_first_chunk_info: datetime.timedelta | None + filters_pushed_down: int + filters_applied_client_side: int + entity_path_narrowing_applied: bool + + # Execution-time + total_duration: datetime.timedelta + time_to_first_chunk: datetime.timedelta | None + error_kind: str | None + direct_terminal_reason: str | None + + # Fetch counters (summed across partitions) + fetch_grpc_requests: int + fetch_grpc_bytes: int + fetch_direct_requests: int + fetch_direct_bytes: int + fetch_direct_retries: int + fetch_direct_requests_retried: int + fetch_direct_retry_sleep: datetime.timedelta + fetch_direct_max_attempt: int + fetch_direct_original_ranges: int + fetch_direct_merged_ranges: int + + # Scheduling and admission counters + planned_fetch_batches: int + planned_segment_waves: int + segment_admission_limit: int + max_segments_per_fetch_batch: int + max_segments_per_wave: int + peak_active_segments: int + pipeline_budget_bytes: int + pipeline_peak_decoded_bytes: int + pipeline_byte_waits: int + segment_admission_waits: int + pipeline_stall_breaker_activations: int + + @property + def fetch_requests(self) -> int: + """Total fetch requests across both gRPC and direct transports.""" + return self.fetch_grpc_requests + self.fetch_direct_requests + + @property + def fetch_bytes(self) -> int: + """Total bytes fetched across both gRPC and direct transports.""" + return self.fetch_grpc_bytes + self.fetch_direct_bytes + + +def _from_rust(m: object) -> QueryMetrics: + """Build a `QueryMetrics` from a Rust-side `_QueryMetrics` PyO3 instance.""" + return QueryMetrics( + dataset_id=m.dataset_id, # type: ignore[attr-defined] + query_chunks=m.query_chunks, # type: ignore[attr-defined] + query_segments=m.query_segments, # type: ignore[attr-defined] + query_layers=m.query_layers, # type: ignore[attr-defined] + query_columns=m.query_columns, # type: ignore[attr-defined] + query_entities=m.query_entities, # type: ignore[attr-defined] + query_bytes=m.query_bytes, # type: ignore[attr-defined] + query_chunks_per_segment_min=m.query_chunks_per_segment_min, # type: ignore[attr-defined] + query_chunks_per_segment_max=m.query_chunks_per_segment_max, # type: ignore[attr-defined] + query_chunks_per_segment_mean=m.query_chunks_per_segment_mean, # type: ignore[attr-defined] + query_type=m.query_type, # type: ignore[attr-defined] + primary_index_name=m.primary_index_name, # type: ignore[attr-defined] + time_to_first_chunk_info=m.time_to_first_chunk_info, # type: ignore[attr-defined] + filters_pushed_down=m.filters_pushed_down, # type: ignore[attr-defined] + filters_applied_client_side=m.filters_applied_client_side, # type: ignore[attr-defined] + entity_path_narrowing_applied=m.entity_path_narrowing_applied, # type: ignore[attr-defined] + total_duration=m.total_duration, # type: ignore[attr-defined] + time_to_first_chunk=m.time_to_first_chunk, # type: ignore[attr-defined] + error_kind=m.error_kind, # type: ignore[attr-defined] + direct_terminal_reason=m.direct_terminal_reason, # type: ignore[attr-defined] + fetch_grpc_requests=m.fetch_grpc_requests, # type: ignore[attr-defined] + fetch_grpc_bytes=m.fetch_grpc_bytes, # type: ignore[attr-defined] + fetch_direct_requests=m.fetch_direct_requests, # type: ignore[attr-defined] + fetch_direct_bytes=m.fetch_direct_bytes, # type: ignore[attr-defined] + fetch_direct_retries=m.fetch_direct_retries, # type: ignore[attr-defined] + fetch_direct_requests_retried=m.fetch_direct_requests_retried, # type: ignore[attr-defined] + fetch_direct_retry_sleep=m.fetch_direct_retry_sleep, # type: ignore[attr-defined] + fetch_direct_max_attempt=m.fetch_direct_max_attempt, # type: ignore[attr-defined] + fetch_direct_original_ranges=m.fetch_direct_original_ranges, # type: ignore[attr-defined] + fetch_direct_merged_ranges=m.fetch_direct_merged_ranges, # type: ignore[attr-defined] + planned_fetch_batches=m.planned_fetch_batches, # type: ignore[attr-defined] + planned_segment_waves=m.planned_segment_waves, # type: ignore[attr-defined] + segment_admission_limit=m.segment_admission_limit, # type: ignore[attr-defined] + max_segments_per_fetch_batch=m.max_segments_per_fetch_batch, # type: ignore[attr-defined] + max_segments_per_wave=m.max_segments_per_wave, # type: ignore[attr-defined] + peak_active_segments=m.peak_active_segments, # type: ignore[attr-defined] + pipeline_budget_bytes=m.pipeline_budget_bytes, # type: ignore[attr-defined] + pipeline_peak_decoded_bytes=m.pipeline_peak_decoded_bytes, # type: ignore[attr-defined] + pipeline_byte_waits=m.pipeline_byte_waits, # type: ignore[attr-defined] + segment_admission_waits=m.segment_admission_waits, # type: ignore[attr-defined] + pipeline_stall_breaker_activations=m.pipeline_stall_breaker_activations, # type: ignore[attr-defined] + ) + + +class MetricsCollector: + """ + Accumulator yielded by [`query_metrics`][rerun.experimental.query_metrics] on `__enter__`. + + Use `last_query()` / `queries` to read snapshots accumulated so far; both + are non-destructive. On context-manager exit any remaining snapshots are + drained into this collector and the scope is unbound from the + `ContextVar`, so the collector is still readable after the scope ends. + """ + + def __init__(self, handle: object | None) -> None: + # `handle` is the Rust `_MetricsCollectorHandle`. `None` means + # allocation failed and this collector is inert — every operation + # returns the current `_finalized` snapshot list (empty by default). + self._handle = handle + self._finalized: list[QueryMetrics] = [] + + @property + def queries(self) -> list[QueryMetrics]: + """Non-destructive snapshot of all queries captured so far.""" + if self._handle is None: + return list(self._finalized) + live = [_from_rust(m) for m in self._handle.snapshot()] # type: ignore[attr-defined] + # After scope exit the Rust handle still works, but `_finalize` has + # already moved the buffer into `_finalized`. Combine both so the + # collector is fully readable post-`with` block. + return self._finalized + live + + def last_query(self) -> QueryMetrics | None: + """Most recently captured query, or `None` if none yet.""" + qs = self.queries + return qs[-1] if qs else None + + def clear(self) -> None: + """Drop all captured snapshots from both the Rust buffer and this collector.""" + self._finalized.clear() + if self._handle is not None: + self._handle.drain() # type: ignore[attr-defined] + + def _finalize(self) -> None: + """Move any remaining Rust-side snapshots into `_finalized`. Called on `__exit__`.""" + if self._handle is None: + return + drained = [_from_rust(m) for m in self._handle.drain()] # type: ignore[attr-defined] + self._finalized.extend(drained) + + +@contextlib.contextmanager +def query_metrics() -> Iterator[MetricsCollector]: + """ + Capture DataFusion query metrics for every query that runs inside the `with` block. + + Yields a [`MetricsCollector`][rerun.experimental.MetricsCollector]; read `.last_query()` or + `.queries` mid-scope or after the scope exits. + + The scope is bound to the current `contextvars.Context`: every + `re_datafusion` query built from `dataset.reader(…)` while this scope + is open contributes a `QueryMetrics` record. Nested `query_metrics()` + scopes each see queries built inside them. Queries built in another + thread or `asyncio` task that did **not** inherit this context (e.g. a + raw `threading.Thread` rather than one started via + `contextvars.copy_context()`) are *not* captured. + + The collectors are bound to a query at `reader()` time, so a `df` built + inside the `with` block whose `.collect()` runs after `__exit__` still + flows to the collector; a `df` built outside but executed inside does + not. + + Examples + -------- + ```python + import rerun as rr + from rerun.experimental import query_metrics + + client = rr.catalog.CatalogClient("rerun://…") + dataset = client.get_dataset(name="…") + + with query_metrics() as m: + df = dataset.reader(index="time_1").limit(100) + df.collect() + print(m.last_query()) + ``` + + """ + try: + from rerun_bindings import _new_metrics_collector + except ImportError: + # The PyO3 bindings haven't been built with the catalog feature; the + # bridge isn't available. Yield an inert collector so user code still + # runs. + logger.warning( + "rerun.experimental.query_metrics() is a no-op: the catalog " + "bindings are not available in this build of rerun.", + ) + yield MetricsCollector(handle=None) + return + + try: + handle = _new_metrics_collector() + except Exception: # pragma: no cover — defensive; PyO3 allocation shouldn't fail + logger.exception("Failed to allocate query_metrics collector; yielding inert collector.") + yield MetricsCollector(handle=None) + return + + token = _active_collectors.set((*_active_collectors.get(), handle)) + collector = MetricsCollector(handle=handle) + try: + yield collector + finally: + try: + collector._finalize() + finally: + _active_collectors.reset(token) diff --git a/rerun_py/rerun_sdk/rerun/experimental/_rrd_reader.py b/rerun_py/rerun_sdk/rerun/experimental/_rrd_reader.py index 38155c18b1f7..846bf8d0735f 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_rrd_reader.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_rrd_reader.py @@ -4,8 +4,9 @@ from rerun_bindings import RrdReaderInternal -from ._chunk_store import ChunkStore from ._lazy_chunk_stream import LazyChunkStream +from ._lazy_store import LazyStore +from ._store_entry import StoreEntry if TYPE_CHECKING: from pathlib import Path @@ -13,36 +14,67 @@ class RrdReader: """ - Read chunks from an RRD file (streaming, sequential). + Read chunks from an RRD file. - Currently, the first Recording store is streamed. Blueprint stores and subsequent recording stores are ignored + Use `recordings()` or `blueprints()` to discover what stores exist in the file, + then `stream()` or `store()` to access a specific one. When no store is + specified, the first recording store is used. """ - # TODO(RR-4263): we eventually need to address the above limitation and provide better control to the user. - _internal: RrdReaderInternal def __init__(self, path: str | Path) -> None: self._internal = RrdReaderInternal(str(path)) - def stream(self) -> LazyChunkStream: - """Return a lazy stream over all chunks in the RRD file.""" - # TODO(RR-4321): this should probably be self.store().stream() instead, when `ChunkStore` is lazily loaded - return LazyChunkStream(self._internal.stream()) - - def store(self) -> ChunkStore: - """Load the entire RRD into a fully materialized ChunkStore.""" - return ChunkStore(self._internal.store()) - - @property - def application_id(self) -> str | None: - """Application ID from the RRD's StoreInfo, if present.""" - return self._internal.application_id - - @property - def recording_id(self) -> str | None: - """Recording ID from the RRD's StoreInfo, if present.""" - return self._internal.recording_id + def recordings(self) -> list[StoreEntry]: + """List the recording entries in this RRD file.""" + return [StoreEntry(s) for s in self._internal.store_entries() if s.kind == "recording"] + + def blueprints(self) -> list[StoreEntry]: + """List the blueprint entries in this RRD file.""" + return [StoreEntry(s) for s in self._internal.store_entries() if s.kind == "blueprint"] + + def stream(self, *, store: StoreEntry | None = None) -> LazyChunkStream: + """ + Return a lazy stream over chunks from a store. + + Parameters + ---------- + store: + Which store to stream. If `None`, uses the first recording store. + + Raises + ------ + ValueError + If the specified store is not in this RRD file, or `None` was passed + and the file contains no recording stores. + + """ + internal_store = store._internal if store is not None else None + return LazyChunkStream(self._internal.stream(store=internal_store)) + + def store(self, *, store: StoreEntry | None = None) -> LazyStore: + """ + Open a specific store as a [`LazyStore`][rerun.experimental.LazyStore]. + + Reads the manifest immediately; chunk data is loaded on demand. + Legacy RRDs without a footer/manifest are not supported here — use + `RrdReader(...).stream().collect()` for those. + + Parameters + ---------- + store: + Which store to load. If `None`, uses the first recording store. + + Raises + ------ + ValueError + If the specified store is not in this RRD file, or `None` was passed + and the file contains no recording stores. + + """ + internal_store = store._internal if store is not None else None + return LazyStore(self._internal.store(store=internal_store)) @property def path(self) -> Path: diff --git a/rerun_py/rerun_sdk/rerun/experimental/_selector.py b/rerun_py/rerun_sdk/rerun/experimental/_selector.py index 1ebeaee0ef7e..70158084949d 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_selector.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_selector.py @@ -115,3 +115,12 @@ def __repr__(self) -> str: def __str__(self) -> str: return str(self._internal) + + def __reduce__(self) -> tuple[type[Selector], tuple[str]]: + query = self._internal.try_to_string() + if query is None: + raise TypeError( + "Cannot pickle Selector containing a Python callable from .pipe(); " + "pass a Selector to .pipe() instead, or use a pure-string selector.", + ) + return (type(self), (query,)) diff --git a/rerun_py/rerun_sdk/rerun/experimental/_send_chunk.py b/rerun_py/rerun_sdk/rerun/experimental/_send_chunk.py deleted file mode 100644 index 10693f630759..000000000000 --- a/rerun_py/rerun_sdk/rerun/experimental/_send_chunk.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import rerun_bindings as bindings - -if TYPE_CHECKING: - from rerun.recording_stream import RecordingStream - - from ._chunk import Chunk - - -def send_chunk( - chunk: Chunk, - *, - recording: RecordingStream | None = None, -) -> None: - """ - Send a pre-built [`Chunk`][rerun.experimental.Chunk] to a recording stream. - - Parameters - ---------- - chunk: - The chunk to send. - recording: - Specifies the [`rerun.RecordingStream`][] to use. - If left unspecified, defaults to the current active data recording. - - """ - bindings.send_chunk( - chunk=chunk._internal, - recording=recording.to_native() if recording is not None else None, - ) diff --git a/rerun_py/rerun_sdk/rerun/experimental/_send_chunks.py b/rerun_py/rerun_sdk/rerun/experimental/_send_chunks.py new file mode 100644 index 000000000000..844275cbd877 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/_send_chunks.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import rerun_bindings as bindings + +from ._chunk import Chunk +from ._chunk_store import ChunkStore +from ._lazy_chunk_stream import LazyChunkStream +from ._lazy_store import LazyStore + +if TYPE_CHECKING: + from collections.abc import Iterable + + from rerun.recording_stream import RecordingStream + from rerun_bindings import ChunkInternal + + +def _unwrap(c: object) -> ChunkInternal: + """Validate-then-unwrap a single chunk for the bindings iterable arm.""" + if not isinstance(c, Chunk): + raise TypeError( + f"send_chunks expects Chunk objects in the iterable, got {type(c).__name__!r}", + ) + return c._internal + + +def send_chunks( + chunks: Chunk | LazyChunkStream | LazyStore | ChunkStore | Iterable[Chunk], + *, + recording: RecordingStream | None = None, +) -> None: + """ + Send chunks to a recording stream. Blocks until every chunk has been queued. + + !!! note + For a `LazyChunkStream` and `LazyStore` inputs, this call triggers execution + and/or loading and will block for the duration of this process. + + Parameters + ---------- + chunks: + One of: + + - A single [`Chunk`][rerun.experimental.Chunk]. + - A [`LazyChunkStream`][rerun.experimental.LazyChunkStream] — consume + the stream and forward all chunks to the recording stream. + - A [`LazyStore`][rerun.experimental.LazyStore] — send all chunks to the + recording stream. This triggers loading all chunks from the source. + - A [`ChunkStore`][rerun.experimental.ChunkStore] — send all chunks to + the recording stream (fast since all chunks are already loaded). + - Any iterable of `Chunk` objects. + + Source store identity (`application_id`, `recording_id`) is **not** + preserved: chunks adopt the destination recording's identity. + recording: + Recording stream to send into. Defaults to the current active recording. + + """ + native = recording.to_native() if recording is not None else None + + match chunks: + case LazyStore() | ChunkStore(): + chunks.stream()._internal.send_to_recording(native) + case LazyChunkStream(): + chunks._internal.send_to_recording(native) + case Chunk(): + bindings.send_chunks(chunks._internal, recording=native) + case _: # Iterable[Chunk] + bindings.send_chunks((_unwrap(c) for c in chunks), recording=native) diff --git a/rerun_py/rerun_sdk/rerun/experimental/_store_entry.py b/rerun_py/rerun_sdk/rerun/experimental/_store_entry.py new file mode 100644 index 000000000000..062dfc264199 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/_store_entry.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + from rerun_bindings import StoreEntryInternal + + +class StoreEntry: + """Describes a store found in an RRD file.""" + + _internal: StoreEntryInternal + + def __init__(self, internal: StoreEntryInternal) -> None: + self._internal = internal + + @property + def kind(self) -> Literal["recording", "blueprint"]: + """Store kind: `"recording"` or `"blueprint"`.""" + return self._internal.kind + + @property + def application_id(self) -> str: + """The application ID of the store.""" + return self._internal.application_id + + @property + def recording_id(self) -> str: + """The recording ID of the store.""" + return self._internal.recording_id + + def __repr__(self) -> str: + return ( + f"StoreEntry(kind={self.kind!r}, " + f"application_id={self.application_id!r}, " + f"recording_id={self.recording_id!r})" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, StoreEntry): + return NotImplemented + return self._internal == other._internal + + def __hash__(self) -> int: + return self._internal.__hash__() diff --git a/rerun_py/rerun_sdk/rerun/experimental/_streaming_reader.py b/rerun_py/rerun_sdk/rerun/experimental/_streaming_reader.py index 0601227c91de..188f5aa00199 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_streaming_reader.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_streaming_reader.py @@ -12,8 +12,9 @@ class StreamingReader(Protocol): Protocol for readers that produce a sequential stream of chunks. All readers provide `stream() -> LazyChunkStream`. Readers for indexable - formats will additionally satisfy `IndexedReader` (future) and provide - `store() -> ChunkStore`. + formats will additionally satisfy + [`IndexedReader`][rerun.experimental.IndexedReader], which adds + `store() -> LazyStore` and `load() -> ChunkStore`. """ def stream(self) -> LazyChunkStream: diff --git a/rerun_py/rerun_sdk/rerun/experimental/_viewer_client.py b/rerun_py/rerun_sdk/rerun/experimental/_viewer_client.py index bcc89db52ccd..bb8bd8f2946b 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/_viewer_client.py +++ b/rerun_py/rerun_sdk/rerun/experimental/_viewer_client.py @@ -1,10 +1,15 @@ from __future__ import annotations +import os +import signal +import subprocess +import warnings from typing import TYPE_CHECKING from rerun._arrow import to_record_batch if TYPE_CHECKING: + from types import TracebackType from uuid import UUID import datafusion @@ -13,28 +18,179 @@ from rerun_bindings import ViewerClientInternal +_DEFAULT_URL = "rerun+http://127.0.0.1:9876/proxy" + + class ViewerClient: """ A connection to an instance of a Rerun viewer. + Use the [`connect`][rerun.experimental.ViewerClient.connect] classmethod + to attach to an already-running viewer, or + [`spawn`][rerun.experimental.ViewerClient.spawn] to start a fresh one + (e.g. in headless mode for CI screenshots). + + Spawned-viewer teardown: + + - Explicit [`close`][rerun.experimental.ViewerClient.close] always + terminates the spawned viewer. + - For an attached viewer (`detach_process=False`), exiting a `with` block + or garbage-collecting the client also terminates the viewer. + - A detached viewer keeps running through `with` exits and garbage + collection. Only an explicit `close()` shuts it down. + !!! warning This API is experimental and may change or be removed in future versions. """ - def __init__(self, addr: str = "127.0.0.1:9876") -> None: + def __init__( + self, + url: str = _DEFAULT_URL, + *, + _pid: int | None = None, + _kill_on_exit: bool = False, + ) -> None: """ - Create a new viewer client connection. + Low-level constructor. + + Prefer + [`ViewerClient.connect`][rerun.experimental.ViewerClient.connect] or + [`ViewerClient.spawn`][rerun.experimental.ViewerClient.spawn]. Parameters ---------- - addr: - The address of the viewer to connect to, in the format "host:port". - Defaults to "127.0.0.1:9876" for a local viewer. + url: + The URL to connect to. The scheme must be one of `rerun://`, + `rerun+http://`, or `rerun+https://`, and the pathname must be + `/proxy` — the same form accepted by [`rerun.connect_grpc`][]. + Defaults to `rerun+http://127.0.0.1:9876/proxy`. + _pid: + Internal — set by `spawn()` to the pid of the launched viewer so + that `close()` can terminate it. + _kill_on_exit: + Internal — set by `spawn()` to indicate that implicit teardown + (`__exit__`, `__del__`) should call `close()`. See the class + docstring for the full teardown rules. """ from rerun_bindings import ViewerClientInternal - self._internal: ViewerClientInternal = ViewerClientInternal(addr) + # `close()` kills the spawned viewer when `_pid` is set. Implicit + # teardown via `__exit__` or `__del__` is additionally gated on + # `_kill_on_exit`: a detached viewer is meant to survive both. + self._pid: int | None = _pid + self._kill_on_exit: bool = _kill_on_exit + self._url: str = url + self._internal: ViewerClientInternal = ViewerClientInternal(url) + + @classmethod + def connect(cls, url: str = _DEFAULT_URL) -> ViewerClient: + """ + Connect to an already-running viewer. + + Parameters + ---------- + url: + The URL to connect to. The scheme must be one of `rerun://`, + `rerun+http://`, or `rerun+https://`, and the pathname must be + `/proxy` — the same form accepted by [`rerun.connect_grpc`][]. + Defaults to `rerun+http://127.0.0.1:9876/proxy`. + + """ + return cls(url) + + @classmethod + def spawn( + cls, + *, + headless: bool = False, + port: int = 9876, + memory_limit: str = "75%", + server_memory_limit: str = "1GiB", + hide_welcome_screen: bool = False, + detach_process: bool | None = None, + executable_name: str = "rerun", + executable_path: str | None = None, + ) -> ViewerClient: + """ + Spawn a fresh viewer process and connect to it. + + Parameters + ---------- + headless: + Run the spawned viewer in headless mode (no OS window). + The viewer still listens for gRPC connections, so the SDK can keep + logging data and request screenshots via + [`save_screenshot`][rerun.experimental.ViewerClient.save_screenshot]. + + A working graphics stack must be present — either a real GPU/driver or a + software rasterizer like Mesa's `lavapipe`. In a bare CI + container with no Vulkan adapter, the viewer panics on + startup with "No graphics adapter was found". + port: + The port to listen on. + memory_limit: + An upper limit on how much memory the Rerun Viewer should use. + When this limit is reached, Rerun will drop the oldest data. + Example: `16GB` or `50%` (of system total). + server_memory_limit: + An upper limit on how much memory the gRPC server running + in the same process as the Rerun Viewer should use. + When this limit is reached, Rerun will drop the oldest data. + Example: `16GB` or `50%` (of system total). + + Defaults to `1GiB`. + hide_welcome_screen: + Hide the normal Rerun welcome screen. + detach_process: + Detach the spawned viewer from this Python process. + + A detached viewer survives unexpected parent termination + (e.g. crashes or terminal hang-up), `with` block exits, and + garbage collection — to take it down you must call + [`close`][rerun.experimental.ViewerClient.close] explicitly. + An attached viewer is killed by all of those. + + Defaults to `True` for a regular GUI viewer and `False` when + `headless=True`, since a leftover invisible viewer is rarely what + you want. + executable_name: + Specifies the name of the Rerun executable. + You can omit the `.exe` suffix on Windows. + + Defaults to `rerun`. + executable_path: + Enforce a specific executable to use instead of searching + through PATH for `executable_name`. + + Unspecified by default. + + """ + from rerun._spawn import _spawn_viewer + + if detach_process is None: + detach_process = not headless + + pid = _spawn_viewer( + port=port, + memory_limit=memory_limit, + server_memory_limit=server_memory_limit, + hide_welcome_screen=hide_welcome_screen, + detach_process=detach_process, + executable_name=executable_name, + executable_path=executable_path, + headless=headless, + ) + return cls( + f"rerun+http://127.0.0.1:{port}/proxy", + _pid=pid, + _kill_on_exit=not detach_process, + ) + + @property + def url(self) -> str: + """The `rerun+http://…/proxy` URL of the viewer this client is connected to.""" + return self._url def send_table(self, name: str, table: pa.RecordBatch | list[pa.RecordBatch] | datafusion.DataFrame) -> None: """ @@ -82,3 +238,77 @@ def save_screenshot(self, file_path: str, view_id: str | UUID | None = None) -> """ view_id_str = str(view_id) if view_id is not None else None self._internal.save_screenshot(file_path, view_id_str) + + def close(self) -> None: + """ + Close the client, terminating the spawned viewer. + + Emits a `UserWarning` and is a no-op if there is no spawned viewer to + terminate (either the client never spawned one, or it has already + been closed). Safe to call multiple times — only the first call has + an effect. + """ + pid = self._pid + self._pid = None + if pid is None: + warnings.warn( + "ViewerClient.close() called with no viewer to terminate " + "(the client was constructed via ViewerClient.connect(), or close() was already called).", + UserWarning, + stacklevel=2, + ) + return + + try: + # The python `rerun` command is a shim (see `rerun_cli/__main__.py`) that spawns the + # rust cli binary as a child process. Killing only the shim pid would orphan that child + # and leak the viewer (along with the port it holds), so we must take down the whole + # process tree. + if os.name != "posix": + # Windows has no POSIX process groups. `taskkill /T` walks the parent → child + # relationship Windows records for the `subprocess.call` in the shim and kills the + # native viewer too. `/F` is required because the GUI viewer has no console to + # receive a graceful signal (`os.kill`/SIGTERM maps to `TerminateProcess` anyway). + subprocess.run( + ["taskkill", "/PID", str(pid), "/T", "/F"], + check=True, + capture_output=True, + ) + else: + # On unix the shim is launched in its own process group (see `spawn.rs`), and the + # viewer child inherits it, so we can kill both cleanly with a single `killpg`. + os.killpg(pid, signal.SIGTERM) + except (OSError, subprocess.CalledProcessError) as err: + warnings.warn( + f"ViewerClient.close() could not close pid {pid}: {err}", + UserWarning, + stacklevel=2, + ) + + def __enter__(self) -> ViewerClient: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + # Only attached viewers are torn down on `with` exit. Skip when + # there's nothing to kill so we don't trip close()'s warning if the + # user already closed manually inside the block. + if self._kill_on_exit and self._pid is not None: + self.close() + + def __del__(self) -> None: + # Try stopping the viewer if it wasn't detached. Skip when there's + # nothing to kill — both because there's no work to do and to avoid + # tripping close()'s warning during GC. + try: + if not getattr(self, "_kill_on_exit", False): + return + if getattr(self, "_pid", None) is None: + return + self.close() + except Exception: + pass diff --git a/rerun_py/rerun_sdk/rerun/experimental/dataloader/__init__.py b/rerun_py/rerun_sdk/rerun/experimental/dataloader/__init__.py index e1f3a677671c..6a2f252b89a5 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/dataloader/__init__.py +++ b/rerun_py/rerun_sdk/rerun/experimental/dataloader/__init__.py @@ -2,22 +2,26 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Any + from rerun._tracing import tracing_scope, with_tracing -from ._config import Column, DataSource -from ._decoders import ColumnDecoder, ImageDecoder, NumericDecoder, VideoFrameDecoder -from ._iterable_dataset import RerunIterableDataset -from ._map_dataset import RerunMapDataset +from ._config import DataSource, Field from ._sample_index import ( FixedRateSampling, SampleIndex, SegmentMetadata, ) +if TYPE_CHECKING: + from ._decoders import ColumnDecoder, ImageDecoder, NumericDecoder, VideoFrameDecoder + from ._iterable_dataset import RerunIterableDataset + from ._map_dataset import RerunMapDataset + __all__ = [ - "Column", "ColumnDecoder", "DataSource", + "Field", "FixedRateSampling", "ImageDecoder", "NumericDecoder", @@ -29,3 +33,28 @@ "tracing_scope", "with_tracing", ] + +# These names require the optional `dataloader` extra (torch, av, torchvision, +# pillow); they are imported lazily (PEP 562) so the package imports without the +# extra, and decoding pulls it in only on first use. +_LAZY_SUBMODULES = { + "ColumnDecoder": "._decoders", + "ImageDecoder": "._decoders", + "NumericDecoder": "._decoders", + "VideoFrameDecoder": "._decoders", + "RerunIterableDataset": "._iterable_dataset", + "RerunMapDataset": "._map_dataset", +} + + +def __getattr__(name: str) -> Any: + submodule = _LAZY_SUBMODULES.get(name) + if submodule is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + from importlib import import_module + + return getattr(import_module(submodule, __name__), name) + + +def __dir__() -> list[str]: + return sorted(__all__) diff --git a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_config.py b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_config.py index a888394a2e6f..0d0a464189b6 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_config.py +++ b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_config.py @@ -1,4 +1,4 @@ -"""User-facing configuration dataclasses for Rerun Data Platform-backed Torch datasets.""" +"""User-facing configuration dataclasses for catalog-server-backed Torch datasets.""" from __future__ import annotations @@ -7,32 +7,56 @@ if TYPE_CHECKING: from rerun.catalog._entry import DatasetEntry + from rerun.experimental._selector import Selector from ._decoders import ColumnDecoder @dataclass(frozen=True) -class Column: +class Field: """ - Declarative column definition for a training sample. + Declarative spec for one field of a training sample. + + !!! note + This API is provisional and will be improved, expect the surface to change. Parameters ---------- path - Entity path + component in the Rerun store, - e.g. `"/camera:EncodedImage:blob"`. + `entity_path:Archetype:component` triple identifying the source + column (e.g. `"/camera:EncodedImage:blob"`). decode - A [`ColumnDecoder`][rerun.experimental.dataloader.ColumnDecoder] instance that converts raw Arrow data - into a tensor (e.g. `NumericDecoder()` or `ImageDecoder()`). + A [`ColumnDecoder`][rerun.experimental.dataloader.ColumnDecoder] + that turns the Arrow column into a tensor. + select + Optional jq-like [`Selector`][rerun.experimental.Selector] applied + client-side to the Arrow column before `decode`. Used for nested + struct/list access. The server-side projection is unaffected. + + ```python + Field( + path="/agent:ListOfStructs:animals", + select=Selector(".[0].dog"), + decode=NumericDecoder(), + ) + ``` window - Optional `(start_offset, end_offset)` inclusive range relative - to the current index value. `(0, 99)` means "current frame - plus the next 99". + Optional `(start_offset, end_offset)` range, inclusive on both + ends and added to the current index value. The field then yields + the slice of values across that window instead of a single + sample. Offsets are in the index timeline's native unit: + integer steps for integer-indexed timelines, nanoseconds for + timestamp timelines (use multiples of the + [`FixedRateSampling`][rerun.experimental.dataloader.FixedRateSampling] + period to align with the sampling grid). For example, `(1, 50)` + on an integer timeline fetches the next 50 values after the + current sample. """ path: str decode: ColumnDecoder + select: Selector | None = None window: tuple[int, int] | None = None diff --git a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_decoders.py b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_decoders.py index 25d283280e91..434c8bd6f05d 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_decoders.py +++ b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_decoders.py @@ -2,7 +2,8 @@ import io from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, cast +from collections import OrderedDict +from typing import Any, cast import av import numpy as np @@ -13,10 +14,9 @@ from rerun._tracing import with_tracing -from ._sample_index import _ns_to_datetime64 - -if TYPE_CHECKING: - from collections.abc import Iterator +from ...components import VideoCodec +from ..video import detect_gop_start, is_annex_b, length_prefixed_to_annex_b +from ._sample_index import _ns_to_datetime64, _ns_to_timedelta64 # AV1 through ``libdav1d`` is faster. _CODEC_TO_DECODER = { @@ -26,8 +26,18 @@ "hevc": "hevc", } -_ANNEX_B_START_CODE = b"\x00\x00\x00\x01" -_ANNEX_B_START_CODE_SHORT = b"\x00\x00\x01" +_CODEC_NAME_ALIASES = {"avc": "h264", "hevc": "h265"} + + +def _to_video_codec(codec: str) -> VideoCodec | None: + """ + Map a codec string to [`VideoCodec`][rerun.components.VideoCodec]. + + Returns `None` for codecs Rerun doesn't know; every known codec has a + keyframe detector in `rerun.experimental.video.detect_gop_start`. + """ + name = _CODEC_NAME_ALIASES.get(codec.lower(), codec.lower()) + return getattr(VideoCodec, name.upper(), None) class ColumnDecoder(ABC): @@ -44,16 +54,16 @@ class ColumnDecoder(ABC): def decode( self, raw: pa.ChunkedArray, - index_value: int | np.datetime64, + index_value: int | np.datetime64 | np.timedelta64, segment_id: str, - ) -> torch.Tensor: - """Decode *raw* Arrow data into a tensor.""" + ) -> torch.Tensor | None: + """Decode *raw* Arrow data into a tensor, or return `None` to signal data missing.""" ... def context_range( self, - index_value: int | np.datetime64, - ) -> tuple[int | np.datetime64, int | np.datetime64] | None: + index_value: int | np.datetime64 | np.timedelta64, + ) -> tuple[int | np.datetime64 | np.timedelta64, int | np.datetime64 | np.timedelta64] | None: """ Extra index-value range needed to decode *index_value*. @@ -63,6 +73,31 @@ def context_range( del index_value return None + def prior_keyframe_path(self, field_path: str) -> str | None: + """ + Sibling column whose non-null rows mark a re-entrant keyframe, or `None`. + + Override on decoders that need the prefetch window anchored at the prior + keyframe (compressed video). Default returns `None`. + """ + del field_path + return None + + @property + def fill_latest_at(self) -> bool: + """ + Whether this column's prefetch read latest-at-fills empty grid slots. + + `True` for stateless columns (images, scalars): each grid slot wants the + most recent value snapped from the real rows. Compressed video keeps it + `True` too (consecutive duplicates from a dense grid are dropped at + decode time), but a decoder reading frame-indexed data where the grid + lands 1:1 on real samples can override to `False` for exact, fill-free + packet reads. The read is partitioned by this flag so it stays a global + query argument per group rather than a per-column one. + """ + return True + def __repr__(self) -> str: return f"{type(self).__name__}()" @@ -71,7 +106,12 @@ class ImageDecoder(ColumnDecoder): """Decode a single encoded-image blob (JPEG/PNG) to a `[C, H, W]` uint8 tensor.""" @with_tracing("ImageDecoder.decode") - def decode(self, raw: pa.ChunkedArray, index_value: int | np.datetime64, segment_id: str) -> torch.Tensor: + def decode( + self, + raw: pa.ChunkedArray, + index_value: int | np.datetime64 | np.timedelta64, + segment_id: str, + ) -> torch.Tensor: del index_value, segment_id combined = raw.combine_chunks() blob_bytes = bytes(_flatten_blob(combined, 0)) @@ -83,7 +123,12 @@ class NumericDecoder(ColumnDecoder): """Decode Arrow numeric / list-of-numeric columns to a tensor.""" @with_tracing("NumericDecoder.decode") - def decode(self, raw: pa.ChunkedArray, index_value: int | np.datetime64, segment_id: str) -> torch.Tensor: + def decode( + self, + raw: pa.ChunkedArray, + index_value: int | np.datetime64 | np.timedelta64, + segment_id: str, + ) -> torch.Tensor: del index_value, segment_id return torch.as_tensor(_unwrap_to_numpy(raw.combine_chunks())) @@ -113,7 +158,7 @@ def _is_list_type(t: pa.DataType) -> bool: def _flatten_blob(arr: pa.Array, row: int) -> np.ndarray: - """Extract row *row* bytes from a `list>` or `list` array.""" + """Extract row *row* bytes from a `list>` or `list` array.""" outer_offsets = arr.offsets.to_numpy() lo, hi = int(outer_offsets[row]), int(outer_offsets[row + 1]) inner = arr.values.slice(lo, hi - lo) @@ -130,57 +175,54 @@ def _flatten_blob(arr: pa.Array, row: int) -> np.ndarray: return np.frombuffer(inner.buffers()[2], dtype=np.uint8, offset=start, count=end - start) -def _avcc_to_annex_b(data: bytes, nal_length_size: int = 4) -> bytes: - """Convert AVCC/AVC1 (length-prefixed) NAL units to Annex B (start-code-prefixed).""" - result = bytearray() - pos = 0 - while pos + nal_length_size <= len(data): - nal_length = int.from_bytes(data[pos : pos + nal_length_size], "big") - pos += nal_length_size - if nal_length <= 0 or pos + nal_length > len(data): - break - result.extend(_ANNEX_B_START_CODE) - result.extend(data[pos : pos + nal_length]) - pos += nal_length - return bytes(result) +class _DecoderSession: + """An open codec context reused across `decode` calls that extend the same GOP.""" + __slots__ = ("context", "fed_samples", "frames_emitted", "last_frame") -def _is_annex_b(data: bytes) -> bool: - """Check if data starts with an Annex B start code.""" - return data[:4] == _ANNEX_B_START_CODE or data[:3] == _ANNEX_B_START_CODE_SHORT + def __init__(self, context: av.VideoCodecContext) -> None: + self.context = context + self.fed_samples: list[bytes] = [] + self.frames_emitted = 0 + self.last_frame: av.VideoFrame | None = None -def _is_av1_keyframe_packet(sample: bytes) -> bool: - """ - True if *sample* starts with an AV1 OBU that begins a random-access point. - - A keyframe packet's first OBU is either `OBU_SEQUENCE_HEADER` (type 1) - or `OBU_TEMPORAL_DELIMITER` (type 2); non-keyframe packets start with - `OBU_FRAME` (type 6) or `OBU_FRAME_HEADER` (type 3). - """ - if not sample: - return False - obu_type = (sample[0] >> 3) & 0xF - return obu_type in (1, 2) +def _starts_with(samples: list[bytes], prefix: list[bytes]) -> bool: + """True if *samples* begins with *prefix*.""" + return len(samples) >= len(prefix) and samples[: len(prefix)] == prefix class VideoFrameDecoder(ColumnDecoder): """ Compressed video random access via context-aware fetching. - Strategy: - - - `context_range(N)` returns `(N - keyframe_interval, N)`, telling - the prefetcher to fetch a few extra frames before the target. - - `decode()` receives the context data (keyframe through target), - decodes sequentially, returns only the final frame. - - The *keyframe_interval* is a conservative estimate. Fetching a few - extra frames beyond the actual keyframe is cheap (small Arrow rows). - Under-estimating means a decode failure -> fallback to wider fetch. - - Samples may be raw H.264 AVC1/AVCC (length-prefixed NAL units) or - Annex B; the format is detected automatically per sample. + Anchors the decode window at the prior keyframe by consulting the sibling + `is_keyframe` component on the `VideoStream` archetype, derived from + `Field.path` (e.g. `/cam:VideoStream:sample` pairs with + `/cam:VideoStream:is_keyframe`). The marker is populated by the user or by + `LazyChunkStream.collect(optimize=…)`, and lives in dedicated chunks + separate from the video sample, so the lookup is cheap. + + When the column is missing from the schema, or has no row at or before + the target, the decoder falls back to a fixed-size window: the previous + `keyframe_interval` samples (counted directly for integer indices, + converted to `keyframe_interval / fps_estimate` seconds for timestamp + indices). `keyframe_interval` must be at least the actual GOP length, and + for timestamp indices `fps_estimate` must be close to the true frame rate. + + Samples may be raw H.264 AVC1/AVCC (length-prefixed NAL units) or Annex B; + the format is detected automatically per sample. + + A call whose window extends the previous call's (same GOP) reuses an open + codec context and decodes only the new packets. + + Returns `None` when the resolved window contains no decodable keyframe: + the target precedes the entity's first frame in a multi-modal segment, + the fallback `keyframe_interval` under-estimates the true GOP length, or + the anchored row was user-logged `is_keyframe=true` on a sample that + isn't actually a codec keyframe (run optimize with `fix_keyframe=True` to + re-derive markers from the encoded samples). Consumers must filter these + out in their collate function before stacking. """ def __init__( @@ -189,24 +231,45 @@ def __init__( keyframe_interval: int = 30, fps_estimate: float = 30.0, codec: str = "h264", + max_decoder_sessions: int = 8, ) -> None: self.codec = codec - self._decoder_name = _CODEC_TO_DECODER.get(codec, codec) + # Cached: read per sample in the decode loop. + self._video_codec = _to_video_codec(codec) self._keyframe_interval = keyframe_interval - self._keyframe_duration_ns = int(keyframe_interval / fps_estimate * 1e9) + self._fps_estimate = fps_estimate + self._max_decoder_sessions = max_decoder_sessions + + # LRU of live decode sessions, keyed by `(segment_id, keyframe sample)`. + self._sessions: OrderedDict[tuple[str, bytes], _DecoderSession] = OrderedDict() def __repr__(self) -> str: return f"VideoFrameDecoder(codec={self.codec!r})" + def __getstate__(self) -> dict[str, Any]: + """Drop the sessions: open codec contexts cannot be pickled.""" + state = self.__dict__.copy() + state["_sessions"] = OrderedDict() + return state + + def prior_keyframe_path(self, field_path: str) -> str | None: + prefix, sep, _ = field_path.rpartition(":") + if not sep: + return None + return f"{prefix}:is_keyframe" + def context_range( self, - index_value: int | np.datetime64, - ) -> tuple[int | np.datetime64, int | np.datetime64] | None: + index_value: int | np.datetime64 | np.timedelta64, + ) -> tuple[int | np.datetime64 | np.timedelta64, int | np.datetime64 | np.timedelta64] | None: """Need frames from estimated keyframe position to target.""" + keyframe_duration_ns = int(self._keyframe_interval / self._fps_estimate * 1e9) if isinstance(index_value, np.datetime64): iv = int(np.int64(index_value)) - lo = _ns_to_datetime64(iv - self._keyframe_duration_ns) - return (lo, index_value) + return (_ns_to_datetime64(iv - keyframe_duration_ns), index_value) + if isinstance(index_value, np.timedelta64): + iv = int(np.int64(index_value)) + return (_ns_to_timedelta64(iv - keyframe_duration_ns), index_value) iv = int(index_value) return (max(0, iv - self._keyframe_interval), iv) @@ -214,27 +277,25 @@ def context_range( def decode( self, raw: pa.ChunkedArray, - index_value: int | np.datetime64, + index_value: int | np.datetime64 | np.timedelta64, segment_id: str, - ) -> torch.Tensor: - """Decode the target frame from the context samples in *raw*.""" + ) -> torch.Tensor | None: + """Decode the target frame from the context samples in *raw*, or `None` if no keyframe is available.""" return self._decode_to_target(raw, index_value, segment_id) def _decode_to_target( self, raw_context: pa.ChunkedArray, - target_idx: int | np.datetime64, + target_idx: int | np.datetime64 | np.timedelta64, segment_id: str, - ) -> torch.Tensor: + ) -> torch.Tensor | None: """ Decode context through *target_idx* and return the final frame. - `context_range` ends exactly at *target_idx*, so the target is - always the last decoded frame. Earlier frames (prior to the - target) are not cached: for sequence indices we'd need to know - how many encoded samples were dropped by the codec before the - first keyframe, and for timestamp indices we'd need per-sample - timestamps we don't have here. + `context_range` ends exactly at *target_idx*, so the target is always + the last decoded frame. For delay-free streams (one frame out per + packet in) the codec context is kept open, and a later call whose + window extends this one decodes only the new packets. """ combined = raw_context.combine_chunks() num_rows = len(combined) @@ -244,36 +305,99 @@ def _decode_to_target( sample_bytes = bytes(_flatten_blob(combined, i)) if not sample_bytes: continue - if self.codec == "h264" and not _is_annex_b(sample_bytes): - sample_bytes = _avcc_to_annex_b(sample_bytes) + if self._video_codec is VideoCodec.H264 and not is_annex_b(sample_bytes): + sample_bytes = length_prefixed_to_annex_b(sample_bytes) + # `fill_latest_at` repeats the previous frame's bytes for grid slots + # with no source frame, so the window can hold consecutive duplicate + # samples. Re-feeding a duplicate packet corrupts the decoder's + # reference state, so skip them. + # TODO(RR-4751): we should measure whether we can optimize this by doing precise queries when `VideoStream::is_keyframe` is present. + if samples and sample_bytes == samples[-1]: + continue samples.append(sample_bytes) - # libdav1d rejects a non-keyframe as the first packet. - if self.codec == "av1": - drop = 0 - while drop < len(samples) and not _is_av1_keyframe_packet(samples[drop]): - drop += 1 - samples = samples[drop:] - - target_tensor = None - for frame in self._decode_packets(samples): - target_tensor = self._frame_to_tensor(frame) - - if target_tensor is None: + # No bootstrap context: target precedes the first keyframe in the + # prefetched range. See class docstring. + if not self._has_keyframe(samples): + return None + + # For codecs we recognize, drop leading non-keyframe samples so the decoder sees a + # bootstrap packet first (libdav1d rejects a non-keyframe outright; + # H.264/HEVC need SPS/PPS, plus VPS for HEVC, before any non-IDR/IRAP slice). + # For codecs without a detector, `_is_keyframe` returns None and the loop is a no-op. + drop = 0 + while drop < len(samples): + is_keyframe = self._is_keyframe(samples[drop]) + if is_keyframe is None or is_keyframe: + break + drop += 1 + samples = samples[drop:] + + # `samples[0]` is the window's keyframe, distinguishing GOPs within a segment. + session_key = (segment_id, samples[0]) + session = self._sessions.pop(session_key, None) + if session is None or not _starts_with(samples, session.fed_samples): + session = _DecoderSession(self._create_context()) + + # The session stays popped while feeding, so a raising packet can't + # leave a corrupt context behind. + for sample in samples[len(session.fed_samples) :]: + for frame in session.context.decode(av.Packet(sample)): + session.frames_emitted += 1 + session.last_frame = frame + session.fed_samples = samples + + if session.frames_emitted == len(samples) and session.last_frame is not None: + # Delay-free stream: the last emitted frame is the target; keep the context open. + self._sessions[session_key] = session + while len(self._sessions) > self._max_decoder_sessions: + self._sessions.popitem(last=False) + return self._frame_to_tensor(session.last_frame) + + # Delayed frames (B-frames or pipelining): flush. A flushed context + # cannot be re-fed, so no session is kept. + target_frame = session.last_frame + for frame in session.context.decode(None): + target_frame = frame + + if target_frame is None: raise RuntimeError( - f"Failed to decode target frame {target_idx} from {num_rows} context samples for segment {segment_id}" + f"Failed to decode target frame {target_idx} for segment {segment_id}: " + f"{len(samples)} context samples included a keyframe but the decoder " + "produced no frame." ) - return target_tensor + return self._frame_to_tensor(target_frame) + + def _is_keyframe(self, sample: bytes) -> bool | None: + """Whether *sample* can boot the decoder, or `None` if we have no detector for this codec.""" + if self._video_codec is None: + return None + try: + return detect_gop_start(sample, self._video_codec) + except ValueError: + # Malformed GOP-start candidate (e.g. unparsable SPS): can't bootstrap from it. + return False - def _decode_packets(self, samples: list[bytes]) -> Iterator[av.VideoFrame]: - """Decode raw packet bytes directly via a per-call CodecContext — no container.""" - ctx = cast("av.VideoCodecContext", av.CodecContext.create(self._decoder_name, "r")) + def _has_keyframe(self, samples: list[bytes]) -> bool: + """True if *samples* has a known-codec keyframe, or this codec has no detector (then we trust the decoder).""" for sample in samples: - for frame in ctx.decode(av.Packet(sample)): - yield frame - for frame in ctx.decode(None): - yield frame + is_keyframe = self._is_keyframe(sample) + if is_keyframe is None: + return True + if is_keyframe: + return True + return False + + def _create_context(self) -> av.VideoCodecContext: + """A fresh raw-packet CodecContext (no container).""" + decoder_name = _CODEC_TO_DECODER.get(self.codec, self.codec) + context = cast("av.VideoCodecContext", av.CodecContext.create(decoder_name, "r")) + if decoder_name == "libdav1d": + # dav1d delays output for pipelining by default; the session fast + # path needs one frame out per packet in. + context.options = {"max_frame_delay": "1"} + return context def _frame_to_tensor(self, frame: av.VideoFrame) -> torch.Tensor: """Convert a PyAV VideoFrame to a `(3, H, W)` uint8 tensor.""" diff --git a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_iterable_dataset.py b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_iterable_dataset.py index bbb1520374de..ad311d5c5275 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_iterable_dataset.py +++ b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_iterable_dataset.py @@ -1,4 +1,4 @@ -"""IterableDataset backed by the Rerun Data Platform.""" +"""IterableDataset backed by a catalog server.""" from __future__ import annotations @@ -13,25 +13,28 @@ from rerun._tracing import tracing_scope from ._sample_index import FixedRateSampling, SampleIndex -from ._utils import Target, _decode_iter, _fetch_arrow, _WorkerConnection +from ._utils import Target, _decode_iter, _fetch_arrow, _warn_if_fork_unsafe, _WorkerConnection if TYPE_CHECKING: from collections.abc import Iterator import pyarrow as pa - from ._config import Column, DataSource + from ._config import DataSource, Field -class RerunIterableDataset(torch.utils.data.IterableDataset[dict[str, torch.Tensor]]): +class RerunIterableDataset(torch.utils.data.IterableDataset[dict[str, torch.Tensor | None]]): """ - Iterable dataset backed by the Rerun Data Platform. + Iterable dataset backed by a catalog server. - Internally fetches data in large chunks (`fetch_size` samples per server query) and yields individual samples. - This amortizes the fixed per-query overhead over many samples while letting the `DataLoader` control the training batch size independently. + Fetches `fetch_size` samples per server query and yields individual + samples, so per-query overhead is amortized across many samples while + the `DataLoader` controls the training batch size independently. - Shuffling is handled internally: each epoch shuffles the full index list, then partitions it across workers. - Use `set_epoch` to re-seed the shuffle between epochs. + The index list is partitioned across DDP ranks and DataLoader workers + internally. With `shuffle=True` (default), the full list is shuffled + once per epoch before partitioning; call `set_epoch` to re-seed + between epochs. Parameters ---------- @@ -39,12 +42,12 @@ class RerunIterableDataset(torch.utils.data.IterableDataset[dict[str, torch.Tens The dataset to read from (with optional segment filter). index Timeline to iterate (e.g. `"frame_nr"`). - columns - Output fields, keyed by output name. + fields + Sample fields, keyed by output name. timeline_sampling Required when `index` is a timestamp timeline; ignored for - integer indices. Pass [`FixedRateSampling`][rerun.experimental.dataloader.FixedRateSampling] to sample on - a fixed grid (e.g. 30 Hz). + integer indices. Pass [`FixedRateSampling`][rerun.experimental.dataloader.FixedRateSampling] + to sample on a fixed grid (e.g. 30 Hz). fetch_size Number of samples to fetch per server query. Larger values amortize network overhead but use more memory. Defaults to 128. @@ -57,7 +60,7 @@ def __init__( self, source: DataSource, index: str, - columns: dict[str, Column], + fields: dict[str, Field], *, timeline_sampling: FixedRateSampling | None = None, fetch_size: int = 128, @@ -65,7 +68,9 @@ def __init__( ) -> None: super().__init__() - self._columns = columns + _warn_if_fork_unsafe(stacklevel=3) + + self._fields = fields self._index = index self._fetch_size = fetch_size self._shuffle = shuffle @@ -74,19 +79,19 @@ def __init__( self._sample_index = SampleIndex.build( source, index, - self._columns, + self._fields, timeline_sampling=timeline_sampling, ) self._connection = _WorkerConnection( catalog_url=source.dataset.catalog.url, dataset_name=source.dataset.name, - columns=columns, + fields=fields, ) @property def sample_index(self) -> SampleIndex: - """The underlying [`SampleIndex`][rerun.experimental.dataloader.SampleIndex] — useful for diagnostics.""" + """The underlying [`SampleIndex`][rerun.experimental.dataloader.SampleIndex].""" return self._sample_index def __len__(self) -> int: @@ -97,13 +102,12 @@ def set_epoch(self, epoch: int) -> None: """Set the epoch for shuffling (like `DistributedSampler.set_epoch`).""" self._epoch = epoch - def __iter__(self) -> Iterator[dict[str, torch.Tensor]]: + def __iter__(self) -> Iterator[dict[str, torch.Tensor | None]]: """ - Yield individual samples as they're decoded. + Yield individual samples as they are decoded. - Pipeline: the arrow fetch for chunk N+1 runs on a background - thread while chunk N is being decoded and yielded, so samples - stream out during decode instead of waiting for the full chunk. + The arrow fetch for chunk N+1 runs on a background thread while + chunk N is being decoded, so samples stream out during decode. """ with tracing_scope("RerunIterableDataset.__iter__"): view, decoders = self._connection.ensure() @@ -116,17 +120,17 @@ def __iter__(self) -> Iterator[dict[str, torch.Tensor]]: executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="rerun-fetch") - def submit_fetch(chunk: np.ndarray) -> Future[tuple[list[Target], dict[str, pa.Table]]]: + def submit_fetch(chunk: np.ndarray) -> Future[tuple[list[Target], dict[str, dict[str, pa.Table]]]]: # Copy the calling thread's contextvars so _fetch_arrow's span is # parented under the current OTel context instead of appearing as a root trace. ctx = contextvars.copy_context() - def fetch() -> tuple[list[Target], dict[str, pa.Table]]: + def fetch() -> tuple[list[Target], dict[str, dict[str, pa.Table]]]: return ctx.run( _fetch_arrow, view=view, index=self._index, - columns=self._columns, + fields=self._fields, decoders=decoders, sample_index=self._sample_index, indices=chunk, @@ -135,7 +139,7 @@ def fetch() -> tuple[list[Target], dict[str, pa.Table]]: return executor.submit(fetch) try: - pending: Future[tuple[list[Target], dict[str, pa.Table]]] | None = submit_fetch(chunks[0]) + pending: Future[tuple[list[Target], dict[str, dict[str, pa.Table]]]] | None = submit_fetch(chunks[0]) for i, _ in enumerate(chunks): assert pending is not None targets, seg_tables = pending.result() @@ -144,7 +148,7 @@ def fetch() -> tuple[list[Target], dict[str, pa.Table]]: targets=targets, seg_tables=seg_tables, index=self._index, - columns=self._columns, + fields=self._fields, decoders=decoders, ) finally: @@ -160,9 +164,8 @@ def _worker_indices(self) -> np.ndarray: rng.shuffle(all_indices) # Partition across distributed ranks first (DDP), then across - # DataLoader workers within this rank. Contiguous blocks (not - # interleaved) so workers hit their fetch boundaries at different - # times. + # DataLoader workers within this rank. Contiguous (not interleaved) + # so each worker touches a smaller set of segments per fetch chunk. if torch.distributed.is_available() and torch.distributed.is_initialized(): all_indices = _contiguous_shard( all_indices, diff --git a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_map_dataset.py b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_map_dataset.py index 6efc7fc115a5..84001a27d332 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_map_dataset.py +++ b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_map_dataset.py @@ -1,4 +1,4 @@ -"""Map-style Dataset backed by the Rerun Data Platform.""" +"""Map-style Dataset backed by a catalog server.""" from __future__ import annotations @@ -10,31 +10,32 @@ from rerun._tracing import with_tracing from ._sample_index import FixedRateSampling, SampleIndex -from ._utils import _decode_iter, _fetch_arrow, _WorkerConnection +from ._utils import _decode_iter, _fetch_arrow, _warn_if_fork_unsafe, _WorkerConnection if TYPE_CHECKING: - from ._config import Column, DataSource + from ._config import DataSource, Field -class RerunMapDataset(torch.utils.data.Dataset[dict[str, torch.Tensor]]): +class RerunMapDataset(torch.utils.data.Dataset[dict[str, torch.Tensor | None]]): """ - Map-style dataset backed by the Rerun Data Platform. + Map-style dataset backed by a catalog server. - Supports random access by global index, making it compatible with - PyTorch's sampler ecosystem (`DistributedSampler`, `WeightedRandomSampler`, `SubsetRandomSampler`, …). + Supports random access by global index, so it works with PyTorch's + sampler ecosystem (`DistributedSampler`, `WeightedRandomSampler`, + `SubsetRandomSampler`, ...). Shuffling and cross-worker partitioning + are driven by the `DataLoader`'s sampler. - Shuffling and cross-worker partitioning are driven by the `DataLoader`'s sampler. - - For simple in-order streaming with internal shuffling, use [`RerunIterableDataset`][rerun.experimental.dataloader.RerunIterableDataset] instead. + For streaming iteration with internal shuffling, use + [`RerunIterableDataset`][rerun.experimental.dataloader.RerunIterableDataset] instead. Parameters ---------- source The dataset to read from (with optional segment filter). index - Timeline to iterate (e.g. `"frame_nr"`). - columns - Output fields, keyed by output name. + Timeline column to use as the sample index (e.g. `"frame_nr"`). + fields + Sample fields, keyed by output name. timeline_sampling Required when `index` is a timestamp timeline; ignored for integer indices. Pass [`FixedRateSampling`][rerun.experimental.dataloader.FixedRateSampling] to sample on @@ -46,7 +47,7 @@ class RerunMapDataset(torch.utils.data.Dataset[dict[str, torch.Tensor]]): dataset = RerunMapDataset( source, "frame_nr", - {"image": Column("/camera:Image:blob", decode=ImageDecoder())}, + {"image": Field("/camera:Image:blob", decode=ImageDecoder())}, ) sampler = DistributedSampler(dataset) loader = DataLoader(dataset, batch_size=8, sampler=sampler, num_workers=4) @@ -60,54 +61,56 @@ def __init__( self, source: DataSource, index: str, - columns: dict[str, Column], + fields: dict[str, Field], *, timeline_sampling: FixedRateSampling | None = None, ) -> None: super().__init__() - self._columns = columns + _warn_if_fork_unsafe(stacklevel=3) + + self._fields = fields self._index = index self._sample_index = SampleIndex.build( source, index, - self._columns, + self._fields, timeline_sampling=timeline_sampling, ) self._connection = _WorkerConnection( catalog_url=source.dataset.catalog.url, dataset_name=source.dataset.name, - columns=columns, + fields=fields, ) @property def sample_index(self) -> SampleIndex: - """The underlying [`SampleIndex`][rerun.experimental.dataloader.SampleIndex] — useful for diagnostics.""" + """The underlying [`SampleIndex`][rerun.experimental.dataloader.SampleIndex].""" return self._sample_index def __len__(self) -> int: """Total number of samples across all segments.""" return self._sample_index.total_samples - def __getitem__(self, idx: int) -> dict[str, torch.Tensor]: # ty: ignore[invalid-method-override] + def __getitem__(self, idx: int) -> dict[str, torch.Tensor | None]: # ty: ignore[invalid-method-override] """Fetch a single sample by global index (one server query).""" return self.__getitems__([idx])[0] @with_tracing("RerunMapDataset.__getitems__") - def __getitems__(self, indices: list[int]) -> list[dict[str, torch.Tensor]]: + def __getitems__(self, indices: list[int]) -> list[dict[str, torch.Tensor | None]]: """ Fetch multiple samples by global index in a single server query. - PyTorch's `DataLoader` calls this automatically when available, - so each training batch round-trips only once. + PyTorch's `DataLoader` calls this automatically when present, so + each batch round-trips once. """ view, decoders = self._connection.ensure() targets, seg_tables = _fetch_arrow( view=view, index=self._index, - columns=self._columns, + fields=self._fields, decoders=decoders, sample_index=self._sample_index, indices=indices, @@ -117,7 +120,7 @@ def __getitems__(self, indices: list[int]) -> list[dict[str, torch.Tensor]]: targets=targets, seg_tables=seg_tables, index=self._index, - columns=self._columns, + fields=self._fields, decoders=decoders, ), ) diff --git a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_sample_index.py b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_sample_index.py index 968eb187d796..9381384c68c1 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_sample_index.py +++ b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_sample_index.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from collections.abc import Iterable - from ._config import Column, DataSource + from ._config import DataSource, Field def _ns_to_datetime64(ns: int) -> np.datetime64: @@ -22,10 +22,24 @@ def _ns_to_datetime64(ns: int) -> np.datetime64: return np.datetime64(ns, "ns") +def _ns_to_timedelta64(ns: int) -> np.timedelta64: + """Convert a nanosecond count to a `timedelta64[ns]` scalar.""" + return np.timedelta64(ns, "ns") + + +def _ns_to_dtype(ns: int, ns_dtype: str | None) -> int | np.datetime64 | np.timedelta64: + """Convert a nanosecond count to the index-typed scalar (`int`, `datetime64`, or `timedelta64`).""" + if ns_dtype == "datetime64[ns]": + return _ns_to_datetime64(ns) + if ns_dtype == "timedelta64[ns]": + return _ns_to_timedelta64(ns) + return ns + + @dataclass(frozen=True) class FixedRateSampling: """ - Sample timestamp timelines at a fixed nominal rate. + Sample timestamp or duration timelines at a fixed nominal rate. Indices are drawn on an algebraic grid `seg.index_start + k * ns_per_sample`. The server's @@ -60,9 +74,10 @@ class SampleIndex: ns_per_sample For [`FixedRateSampling`][rerun.experimental.dataloader.FixedRateSampling]: nanoseconds between grid points. `None` for integer indices. - is_timestamp - True when the index is a timestamp timeline. Controls output - dtype of `indices_in_range`. + ns_dtype + Numpy dtype string used when materializing index values: + `"datetime64[ns]"` for timestamp timelines, `"timedelta64[ns]"` + for duration timelines, or `None` for plain integer indices. """ @@ -71,11 +86,11 @@ def __init__( segments: list[SegmentMetadata], *, ns_per_sample: int | None = None, - is_timestamp: bool = False, + ns_dtype: str | None = None, ) -> None: self._segments = segments self._ns_per_sample = ns_per_sample - self._is_timestamp = is_timestamp + self._ns_dtype = ns_dtype seg_sizes = np.array([s.num_samples for s in segments], dtype=np.int64) self._cumulative_sizes = np.concatenate([[0], np.cumsum(seg_sizes)]) @@ -85,10 +100,20 @@ def segments(self) -> list[SegmentMetadata]: """Per-segment metadata list.""" return self._segments + @property + def ns_dtype(self) -> str | None: + """Numpy dtype for materialized index values, or ``None`` for integer indices.""" + return self._ns_dtype + @property def is_timestamp(self) -> bool: """Whether the index is a timestamp timeline.""" - return self._is_timestamp + return self._ns_dtype == "datetime64[ns]" + + @property + def is_duration(self) -> bool: + """Whether the index is a duration timeline.""" + return self._ns_dtype == "timedelta64[ns]" @property def ns_per_sample(self) -> int | None: @@ -100,12 +125,13 @@ def total_samples(self) -> int: """Total number of samples across all segments.""" return int(self._cumulative_sizes[-1]) - def global_to_local(self, idx: int) -> tuple[SegmentMetadata, int | np.datetime64]: + def global_to_local(self, idx: int) -> tuple[SegmentMetadata, int | np.datetime64 | np.timedelta64]: """ Map a global index `[0, total_samples)` to `(segment, concrete_idx_value)`. - The returned index value is a plain `int` for integer timelines - and a `datetime64[ns]` for timestamp timelines. + The returned index value is a plain `int` for integer timelines, + a `datetime64[ns]` for timestamp timelines, and a + `timedelta64[ns]` for duration timelines. """ total = int(self._cumulative_sizes[-1]) if idx < 0 or idx >= total: @@ -115,25 +141,29 @@ def global_to_local(self, idx: int) -> tuple[SegmentMetadata, int | np.datetime6 seg = self._segments[seg_idx] return seg, self.resolve_local_index(seg, pos) - def resolve_local_index(self, seg: SegmentMetadata, pos: int) -> int | np.datetime64: + def resolve_local_index(self, seg: SegmentMetadata, pos: int) -> int | np.datetime64 | np.timedelta64: """ Convert a positional index within `seg` to a concrete index value. `pos` is in `[0, seg.num_samples)`. Returns `datetime64[ns]` - for timestamp timelines, a plain `int` for integer indices. + for timestamp timelines, `timedelta64[ns]` for duration + timelines, and a plain `int` for integer indices. """ if self._ns_per_sample is not None: ns = seg.index_start + int(pos) * self._ns_per_sample - return _ns_to_datetime64(ns) + return _ns_to_dtype(ns, self._ns_dtype) return int(seg.index_start) + int(pos) - def indices_in_range(self, seg: SegmentMetadata, lo: int, hi: int) -> Iterable[int]: # noqa: ARG002 + def indices_in_range(self, lo: int, hi: int) -> Iterable[int]: """ - Enumerate valid index values in `[lo, hi]` for `seg`. - - Returned values are plain `int` (ns-since-epoch for timestamp - indices). The caller casts the aggregated set to the right - `numpy` dtype. + Enumerate valid index values in `[lo, hi]`. + + For fixed-rate timelines the returned values walk down from `hi` + in `ns_per_sample` steps (so they remain on the grid as long as + `hi` is). For integer timelines, every value in `[lo, hi]` is + returned. Values are plain `int` (ns-since-epoch for timestamp + indices, ns count for duration indices); the caller casts the + aggregated set to the right `numpy` dtype. """ if hi < lo: return () @@ -148,7 +178,7 @@ def indices_in_range(self, seg: SegmentMetadata, lo: int, hi: int) -> Iterable[i def build( source: DataSource, index: str, - columns: dict[str, Column], + fields: dict[str, Field], *, timeline_sampling: FixedRateSampling | None = None, ) -> SampleIndex: @@ -161,14 +191,14 @@ def build( Data source to build from. index Name of the index timeline column. - columns - Column definitions for window-trim calculation. + fields + Field definitions, used for window-trim calculation. timeline_sampling - Required for timestamp indices; ignored for integer indices. + Required for timestamp and duration indices; ignored for integer indices. Pass [`FixedRateSampling`][rerun.experimental.dataloader.FixedRateSampling] for a regular grid. """ - return _build(source, index, columns, timeline_sampling=timeline_sampling) + return _build(source, index, fields, timeline_sampling=timeline_sampling) def _ns_per_sample(rate_hz: float) -> int: @@ -182,7 +212,7 @@ def _ns_per_sample(rate_hz: float) -> int: class _RangesCtx: """Parameters shared across the per-segment build loop.""" - columns: dict[str, Column] + fields: dict[str, Field] ranges_table: pa.Table start_col: str end_col: str @@ -211,21 +241,27 @@ def pick(keywords: tuple[str, ...], side: str) -> str: return pick(("start", "min"), "start"), pick(("end", "max"), "end") -def _window_trims_ns(columns: dict[str, Column]) -> tuple[int, int]: - """(trim_start, trim_end) from column window offsets (native units).""" +def _window_trims_ns(fields: dict[str, Field]) -> tuple[int, int]: + """ + Largest `(-window[0], window[1])` across all fields, floored at 0. + + Used to shrink the iterable range so windowed lookups stay inside + each segment. Only called for timestamp or duration timelines, where + `field.window` is interpreted as nanoseconds (hence the `_ns` suffix). + """ trim_start = 0 trim_end = 0 - for col in columns.values(): - if col.window is not None: - trim_start = max(trim_start, -col.window[0]) - trim_end = max(trim_end, col.window[1]) + for field in fields.values(): + if field.window is not None: + trim_start = max(trim_start, -field.window[0]) + trim_end = max(trim_end, field.window[1]) return trim_start, trim_end def _build( source: DataSource, index: str, - columns: dict[str, Column], + fields: dict[str, Field], *, timeline_sampling: FixedRateSampling | None, ) -> SampleIndex: @@ -249,7 +285,7 @@ def _build( start_col, end_col = _find_range_columns(ranges_table, index) ctx = _RangesCtx( - columns=columns, + fields=fields, ranges_table=ranges_table, start_col=start_col, end_col=end_col, @@ -257,19 +293,22 @@ def _build( start_type = ranges_table.schema.field(start_col).type is_timestamp = pa.types.is_timestamp(start_type) + is_duration = pa.types.is_duration(start_type) - if is_timestamp: + if is_timestamp or is_duration: + kind = "timestamp" if is_timestamp else "duration" if timeline_sampling is None: raise TypeError( - f"Index {index!r} is a timestamp timeline; you must pass " + f"Index {index!r} is a {kind} timeline; you must pass " "timeline_sampling=FixedRateSampling(rate_hz=…) so the " "dataloader knows how to draw sample indices." ) - return _build_fixed_rate(ctx, _ns_per_sample(timeline_sampling.rate_hz)) + ns_dtype = "datetime64[ns]" if is_timestamp else "timedelta64[ns]" + return _build_fixed_rate(ctx, _ns_per_sample(timeline_sampling.rate_hz), ns_dtype=ns_dtype) if timeline_sampling is not None: warnings.warn( - f"timeline_sampling={timeline_sampling!r} ignored: index {index!r} is not a timestamp timeline", + f"timeline_sampling={timeline_sampling!r} ignored: index {index!r} is not a timestamp or duration timeline", stacklevel=3, ) return _build_integer(ctx) @@ -279,10 +318,10 @@ def _build_integer(ctx: _RangesCtx) -> SampleIndex: """Build SampleIndex for integer-indexed data.""" min_window_start = 0 max_window_end = 0 - for col in ctx.columns.values(): - if col.window is not None: - min_window_start = min(min_window_start, col.window[0]) - max_window_end = max(max_window_end, col.window[1]) + for field in ctx.fields.values(): + if field.window is not None: + min_window_start = min(min_window_start, field.window[0]) + max_window_end = max(max_window_end, field.window[1]) seg_col = ctx.ranges_table.column("rerun_segment_id").to_pylist() min_vals = ctx.ranges_table.column(ctx.start_col).to_pylist() @@ -307,12 +346,12 @@ def _build_integer(ctx: _RangesCtx) -> SampleIndex: ) ) - return SampleIndex(segments, ns_per_sample=None, is_timestamp=False) + return SampleIndex(segments, ns_per_sample=None, ns_dtype=None) -def _build_fixed_rate(ctx: _RangesCtx, ns_per_sample: int) -> SampleIndex: +def _build_fixed_rate(ctx: _RangesCtx, ns_per_sample: int, *, ns_dtype: str) -> SampleIndex: """ - Build SampleIndex for a timestamp timeline sampled at a fixed rate. + Build SampleIndex for a timestamp or duration timeline sampled at a fixed rate. With a user-provided rate we compute `num_samples` and draw sample timestamps algebraically on a grid -- no server query for @@ -320,10 +359,12 @@ def _build_fixed_rate(ctx: _RangesCtx, ns_per_sample: int) -> SampleIndex: is absorbed by `fill_latest_at` on the server. """ seg_col = ctx.ranges_table.column("rerun_segment_id").to_pylist() - min_vals = ctx.ranges_table.column(ctx.start_col).to_numpy() - max_vals = ctx.ranges_table.column(ctx.end_col).to_numpy() + # Cast through the underlying ns integer so the math below works + # for both timestamp("ns") and duration("ns") columns. + min_vals = ctx.ranges_table.column(ctx.start_col).to_numpy().astype("int64") + max_vals = ctx.ranges_table.column(ctx.end_col).to_numpy().astype("int64") - trim_start_ns, trim_end_ns = _window_trims_ns(ctx.columns) + trim_start_ns, trim_end_ns = _window_trims_ns(ctx.fields) segments: list[SegmentMetadata] = [] for seg_id, seg_min, seg_max in zip(seg_col, min_vals, max_vals, strict=False): @@ -345,4 +386,4 @@ def _build_fixed_rate(ctx: _RangesCtx, ns_per_sample: int) -> SampleIndex: ) ) - return SampleIndex(segments, ns_per_sample=ns_per_sample, is_timestamp=True) + return SampleIndex(segments, ns_per_sample=ns_per_sample, ns_dtype=ns_dtype) diff --git a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_utils.py b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_utils.py index f38ab2fca0c6..e5a7176ed33e 100644 --- a/rerun_py/rerun_sdk/rerun/experimental/dataloader/_utils.py +++ b/rerun_py/rerun_sdk/rerun/experimental/dataloader/_utils.py @@ -2,43 +2,87 @@ from __future__ import annotations +import contextvars +import multiprocessing import os +import sys +import warnings from collections import defaultdict +from concurrent.futures import Future, ThreadPoolExecutor +from dataclasses import dataclass from typing import TYPE_CHECKING, Any import numpy as np +import pyarrow as pa import pyarrow.compute as pc +from datafusion import col from rerun._tracing import attach_parent_carrier, current_trace_carrier, tracing_scope, with_tracing from rerun.catalog import CatalogClient +from ._sample_index import _ns_to_datetime64, _ns_to_timedelta64 + if TYPE_CHECKING: from collections.abc import Iterator - import pyarrow as pa import torch - from ._config import Column + from rerun.experimental._selector import Selector + + from ._config import Field from ._decoders import ColumnDecoder from ._sample_index import SampleIndex, SegmentMetadata -#: (segment_metadata, index_value) pair identifying one sample to produce. -Target = tuple["SegmentMetadata", "int | np.datetime64"] + +@dataclass(frozen=True, slots=True) +class Target: + """One sample to produce.""" + + segment: SegmentMetadata + index_value: int | np.datetime64 | np.timedelta64 + anchors: dict[str, int] + + +def _warn_if_fork_unsafe(stacklevel: int) -> None: + """ + Warn when DataLoader workers will be started with `fork`. + + Rerun's `rerun_bindings` extension uses a process-global tokio runtime. + `fork` only carries the calling thread into the child, so the runtime's + worker threads vanish and the first catalog call from a DataLoader + worker deadlocks. Only `spawn` (and `forkserver`) are currently safe. + """ + method = multiprocessing.get_start_method(allow_none=True) + will_be_fork = method == "fork" or (method is None and sys.platform.startswith("linux")) + if not will_be_fork: + return + warnings.warn( + "The default multiprocessing start method is 'fork'. The Rerun " + "dataloader needs 'spawn' or 'forkserver' for DataLoader workers " + "(num_workers > 0). Forked workers will deadlock on their first " + "catalog call. Pass " + "`multiprocessing_context=multiprocessing.get_context('spawn')` to " + "your DataLoader, or call " + "`torch.multiprocessing.set_start_method('spawn')` before creating " + "workers. You can ignore this warning if you use num_workers=0.", + RuntimeWarning, + stacklevel=stacklevel, + ) class _WorkerConnection: - """Lazily-initialized per-worker catalog connection, view, and decoders.""" + """Per-worker catalog connection, view, and decoders, built lazily.""" def __init__( self, *, catalog_url: str, dataset_name: str, - columns: dict[str, Column], + fields: dict[str, Field], ) -> None: self._catalog_url = catalog_url self._dataset_name = dataset_name - self._columns = columns + self._fields = fields self._initialized: bool = False self._init_pid: int = -1 self._view: Any = None @@ -53,16 +97,21 @@ def ensure(self) -> tuple[Any, dict[str, ColumnDecoder]]: client = CatalogClient(self._catalog_url) dataset = client.get_dataset(self._dataset_name) - self._decoders = {k: col.decode for k, col in self._columns.items()} - self._view = dataset.filter_contents(_derive_content_filter(self._columns)) + self._decoders = {k: f.decode for k, f in self._fields.items()} + # Leave the dataset unscoped here: each read group narrows contents to its own + # entities at query time (`_fetch_group`, `_fetch_prior_keyframes`). A shared + # union filter here would defeat that, since `filter_contents` only ever widens, + # so a group could never exclude the other groups' (heavy video) entities. + self._view = dataset self._initialized = True self._init_pid = pid return self._view, self._decoders def __getstate__(self) -> dict[str, Any]: - """Strip the unpicklable catalog view so DataLoader can send us to workers.""" + """Drop the cached view so the worker rebuilds its own connection via `ensure()`.""" state = self.__dict__.copy() state["_view"] = None + state["_initialized"] = False # Capture the parent's OTel context so worker spans are linked to it. state["_parent_trace_carrier"] = current_trace_carrier() return state @@ -77,129 +126,416 @@ def _fetch_arrow( *, view: Any, index: str, - columns: dict[str, Column], + fields: dict[str, Field], decoders: dict[str, ColumnDecoder], sample_index: SampleIndex, indices: np.ndarray | list[int], -) -> tuple[list[Target], dict[str, pa.Table]]: - """Run the server query for `indices` and return `(targets, per-segment tables)`.""" - targets: list[Target] = [sample_index.global_to_local(int(idx)) for idx in indices] - query_indices = _build_query_indices( - targets, - columns, - decoders, +) -> tuple[list[Target], dict[str, dict[str, pa.Table]]]: + """ + Run the server queries for `indices` and return `(targets, per-field tables)`. + + Fields are partitioned into read groups so each group queries only its own + index values: a heavy keyframe-anchored column (video) is fetched over its + `[keyframe, target]` window alone, not the union with every other field's + window. The returned mapping is `field_key -> {segment_id -> table}`. + """ + located = [sample_index.global_to_local(int(idx)) for idx in indices] + keyframes = _fetch_prior_keyframes( + view=view, + index=index, + fields=fields, + decoders=decoders, + located=located, sample_index=sample_index, ) - - reader = view.reader( + targets: list[Target] = [] + for seg, idx_val in located: + iv = int(idx_val) + anchors: dict[str, int] = {} + for key, by_seg in keyframes.items(): + kf = _prior_keyframe(by_seg.get(seg.segment_id), iv) + if kf is not None: + anchors[key] = kf + targets.append(Target(segment=seg, index_value=idx_val, anchors=anchors)) + + groups = _read_groups(fields, decoders) + group_results = _fetch_groups_parallel( + groups, + view=view, index=index, - using_index_values=query_indices, - fill_latest_at=True, + decoders=decoders, + sample_index=sample_index, + targets=targets, ) - arrow_table = reader.to_arrow_table() - seg_tables = _split_by_segment(arrow_table) + + seg_tables: dict[str, dict[str, pa.Table]] = {} + for group_fields, group_tables in group_results: + for key in group_fields: + seg_tables[key] = group_tables return targets, seg_tables +def _fetch_groups_parallel( + groups: list[tuple[bool, dict[str, Field]]], + *, + view: Any, + index: str, + decoders: dict[str, ColumnDecoder], + sample_index: SampleIndex, + targets: list[Target], +) -> list[tuple[dict[str, Field], dict[str, pa.Table]]]: + """ + Fetch every read group, overlapping them when there is more than one. + + Each group is an independent server round-trip, so a thread per group lets + them run concurrently instead of back-to-back: the catalog query releases the + GIL while it waits on the server. Each thread runs under a copy of the + caller's context so its `_fetch_group` tracing spans stay nested under + `_fetch_arrow`. + """ + + def fetch(fill_latest_at: bool, group_fields: dict[str, Field]) -> dict[str, pa.Table]: + return _fetch_group( + view=view, + index=index, + fields=group_fields, + decoders=decoders, + sample_index=sample_index, + targets=targets, + fill_latest_at=fill_latest_at, + ) + + if len(groups) == 1: + fill_latest_at, group_fields = groups[0] + return [(group_fields, fetch(fill_latest_at, group_fields))] + + with ThreadPoolExecutor(max_workers=len(groups), thread_name_prefix="rerun-fetch-group") as executor: + futures: list[tuple[dict[str, Field], Future[dict[str, pa.Table]]]] = [ + (group_fields, executor.submit(contextvars.copy_context().run, fetch, fill_latest_at, group_fields)) + for fill_latest_at, group_fields in groups + ] + return [(group_fields, future.result()) for group_fields, future in futures] + + +def _read_groups( + fields: dict[str, Field], + decoders: dict[str, ColumnDecoder], +) -> list[tuple[bool, dict[str, Field]]]: + """ + Partition `fields` into read groups, each fetched by one server query. + + Grouped by `(ColumnDecoder.fill_latest_at, is keyframe-anchored)`, since + `fill_latest_at` is a per-query argument and anchored fields need their own + `[keyframe, target]` index values rather than the shared window union. + Returns `(fill_latest_at, group_fields)` pairs. + """ + groups: dict[tuple[bool, bool], dict[str, Field]] = defaultdict(dict) + for key, field in fields.items(): + decoder = decoders[key] + anchored = decoder.prior_keyframe_path(field.path) is not None + groups[(decoder.fill_latest_at, anchored)][key] = field + return [(fill_latest_at, group_fields) for (fill_latest_at, _anchored), group_fields in groups.items()] + + +def _fetch_group( + *, + view: Any, + index: str, + fields: dict[str, Field], + decoders: dict[str, ColumnDecoder], + sample_index: SampleIndex, + targets: list[Target], + fill_latest_at: bool, +) -> dict[str, pa.Table]: + """Run one server query over the index values one read group needs, split per segment.""" + anchored = any(decoders[key].prior_keyframe_path(field.path) is not None for key, field in fields.items()) + group = f"{'anchored' if anchored else 'windowed'},{'fill' if fill_latest_at else 'exact'}" + with tracing_scope(f"RerunDataset._fetch_group[{group}]"): + query_indices = _build_query_indices(targets, fields, decoders, sample_index=sample_index) + + # Scope the query to just this group's entities. Otherwise it fetches (then + # discards at projection) chunks for every other group's entities too: a scalar + # group would drag in the heavy `VideoStream:sample` chunks of the video group. + # The server's projection-based entity narrowing is disabled under `fill_latest_at`, + # so narrow explicitly here. `using_index_values` pins the row set, so restricting + # entities cannot change the returned rows or their latest-at fills. + df = ( + view + .filter_contents(_derive_content_filter(fields)) + .filter_segments(list(query_indices.keys())) + .reader( + index=index, + using_index_values=query_indices, + fill_latest_at=fill_latest_at, + ) + ) + + # `index` and `rerun_segment_id` are preserved because `_decode_iter` and `_split_by_segment` read them. + select_exprs = [col(index), col("rerun_segment_id")] + select_exprs.extend(col(field.path).alias(key) for key, field in fields.items()) + + with tracing_scope(f"RerunDataset._fetch_group.to_arrow_table[{group}]"): + arrow_table = df.select(*select_exprs).to_arrow_table() + + return _split_by_segment(arrow_table) + + def _decode_iter( *, targets: list[Target], - seg_tables: dict[str, pa.Table], + seg_tables: dict[str, dict[str, pa.Table]], index: str, - columns: dict[str, Column], + fields: dict[str, Field], decoders: dict[str, ColumnDecoder], -) -> Iterator[dict[str, torch.Tensor]]: - """Yield decoded samples one at a time from a pre-fetched arrow chunk.""" +) -> Iterator[dict[str, torch.Tensor | None]]: + """Yield decoded samples one at a time from the pre-fetched per-field arrow chunks.""" with tracing_scope("RerunDataset._decode_chunk"): - for seg_meta, idx_val in targets: + for target in targets: with tracing_scope("RerunDataset._decode_sample"): - seg_table = seg_tables.get(seg_meta.segment_id) - if seg_table is None: - raise RuntimeError(f"No rows returned for segment {seg_meta.segment_id!r} at index {idx_val!r}") - sample: dict[str, torch.Tensor] = {} - index_array = seg_table[index] - for key, col in columns.items(): + segment_id = target.segment.segment_id + sample: dict[str, torch.Tensor | None] = {} + for key, field in fields.items(): decoder = decoders[key] - lo, hi = _column_index_range(idx_val, col, decoder) or (idx_val, idx_val) + seg_table = seg_tables[key].get(segment_id) + if seg_table is None: + raise RuntimeError( + f"No rows returned for field {key!r} in segment {segment_id!r} at index {target.index_value!r}" + ) + index_array = seg_table[index] + lo, hi = _field_index_range( + target.index_value, field, decoder, prior_keyframe=target.anchors.get(key) + ) or (target.index_value, target.index_value) mask = pc.and_( pc.greater_equal(index_array, lo), pc.less_equal(index_array, hi), ) - raw = seg_table.filter(mask).column(col.path) - sample[key] = decoder.decode(raw, idx_val, seg_meta.segment_id) + raw = seg_table.filter(mask).column(key) + if field.select is not None: + raw = _apply_selector(field.select, raw) + sample[key] = decoder.decode(raw, target.index_value, segment_id) yield sample -def _column_index_range( - idx_val: int | np.datetime64, - col: Column, +def _field_index_range( + idx_val: int | np.datetime64 | np.timedelta64, + field: Field, decoder: ColumnDecoder, + *, + prior_keyframe: int | None = None, ) -> tuple[Any, Any] | None: """ - Inclusive `(lo, hi)` range of index values needed for one column at `idx_val`, or `None` if only `idx_val` itself is needed. + Inclusive `(lo, hi)` range of index values needed for one field at `idx_val`, or `None` if only `idx_val` is needed. - Window (e.g. action windows) takes precedence over decoder context - (e.g. video keyframe prefetch). + Precedence: `Field.window` > `prior_keyframe` > `ColumnDecoder.context_range`. """ - if col.window is not None: - return idx_val + col.window[0], idx_val + col.window[1] + if field.window is not None: + return idx_val + field.window[0], idx_val + field.window[1] + if prior_keyframe is not None: + # `lo` must match `idx_val`'s type, or the pyarrow window mask in + # `_decode_iter` has no kernel (e.g. `greater_equal(duration, int64)`). + if isinstance(idx_val, np.datetime64): + lo: Any = _ns_to_datetime64(prior_keyframe) + elif isinstance(idx_val, np.timedelta64): + lo = _ns_to_timedelta64(prior_keyframe) + else: + lo = prior_keyframe + return lo, idx_val return decoder.context_range(idx_val) def _build_query_indices( targets: list[Target], - columns: dict[str, Column], + fields: dict[str, Field], decoders: dict[str, ColumnDecoder], *, sample_index: SampleIndex, -) -> dict[str, np.ndarray]: +) -> dict[str, np.ndarray | pa.Array]: """ - Group targets by segment and expand with window + decoder context. - - Returns a `{segment_id: ndarray_of_index_values}` dict ready for - `reader(using_index_values=…)`. Values are `int64` for integer - indices and `datetime64[ns]` for timestamp timelines. + Group `targets` by segment, expanded with each field's window and decoder context. + + Returns a `{segment_id: index_values}` dict ready for + `reader(using_index_values=...)`. Values are an `int64` ndarray for + integer indices, a `pa.timestamp("ns")` array for timestamp + timelines, and a `pa.duration("ns")` array for duration timelines. + The Rust `IndexValuesLike` binding only accepts `datetime64` + ndarrays among the temporal numpy dtypes, so temporal values cross + the binding as pyarrow arrays — matching the convention used by + `TimeColumn` in `_send_columns.py`. """ - is_timestamp = sample_index.is_timestamp + ns_dtype = sample_index.ns_dtype groups: dict[str, set[int]] = defaultdict(set) - for seg_meta, idx_val in targets: - segment_id = seg_meta.segment_id + for target in targets: + segment_id = target.segment.segment_id - groups[segment_id].add(int(idx_val)) + groups[segment_id].add(int(target.index_value)) - for key, col in columns.items(): - rng = _column_index_range(idx_val, col, decoders[key]) + for key, field in fields.items(): + anchor = target.anchors.get(key) + rng = _field_index_range(target.index_value, field, decoders[key], prior_keyframe=anchor) if rng is None: continue lo, hi = rng - for val in sample_index.indices_in_range(seg_meta, int(lo), int(hi)): + for val in sample_index.indices_in_range(int(lo), int(hi)): groups[segment_id].add(int(val)) + # The keyframe's exact index value is unlikely to land on a fixed-rate + # grid; ensure the main fetch returns its row regardless. + if anchor is not None: + groups[segment_id].add(anchor) - result: dict[str, np.ndarray] = {} + result: dict[str, np.ndarray | pa.Array] = {} for segment_id, vals in groups.items(): arr = np.array(sorted(vals), dtype=np.int64) - if is_timestamp: - arr = arr.view("datetime64[ns]") - result[segment_id] = arr + if ns_dtype == "datetime64[ns]": + result[segment_id] = pa.array(arr, type=pa.timestamp("ns")) + elif ns_dtype == "timedelta64[ns]": + result[segment_id] = pa.array(arr, type=pa.duration("ns")) + else: + result[segment_id] = arr return result +@with_tracing("RerunDataset._fetch_prior_keyframes") +def _fetch_prior_keyframes( + *, + view: Any, + index: str, + fields: dict[str, Field], + decoders: dict[str, ColumnDecoder], + located: list[tuple[SegmentMetadata, int | np.datetime64 | np.timedelta64]], + sample_index: SampleIndex, +) -> dict[str, dict[str, np.ndarray]]: + """ + Per-field sorted keyframe index values, grouped by segment. + + Skips fields with `Field.window` set, decoders whose `prior_keyframe_path` + returns `None`, and anchor paths absent from the live schema. Returns `{}` + when no field needs an anchor, so non-video datasets pay no query overhead. + + Queries `is_keyframe` rows at or before each segment's max target. + Works whether `is_keyframe` is logged sparsely (only `true` on keyframes) + or densely (`true`/`false` on every row). The result maps + `field_key -> {segment_id: sorted_int64_keyframes}`; values are `int` + (ns-since-epoch for timestamp timelines, ns count for duration timelines). + The caller bisects via + [`_prior_keyframe`][rerun.experimental.dataloader._utils._prior_keyframe]. + """ + keyframe_fields: dict[str, str] = {} + for key, field in fields.items(): + if field.window is not None: + continue + path = decoders[key].prior_keyframe_path(field.path) + if path is not None: + keyframe_fields[key] = path + if not keyframe_fields or not located: + return {} + + # Anchor columns may not exist in the schema (e.g. pre-optimize data with no user-logged `is_keyframe`) + # drop those fields so the caller falls back to the decoder heuristic + schema_columns = set(view.schema().column_names()) + keyframe_fields = {k: p for k, p in keyframe_fields.items() if p in schema_columns} + if not keyframe_fields: + return {} + + # Per-segment max target across all anchor-using fields. + max_per_segment: dict[str, int] = {} + for seg, idx_val in located: + sid = seg.segment_id + iv = int(idx_val) + max_per_segment[sid] = max(iv, max_per_segment.get(sid, iv)) + + unique_paths = list(dict.fromkeys(keyframe_fields.values())) + + def idx_lit(value: int) -> Any: + # The literal must match the index column type, or DataFusion fails to + # coerce the comparison (`Duration(ns) <= Int64`). + if sample_index.is_timestamp: + return np.datetime64(value, "ns") + if sample_index.is_duration: + return np.timedelta64(value, "ns") + return value + + # Filter to keyframes at or before the largest target across all segments, in a + # single predicate. A per-segment OR (`(seg==A & idx<=tA) | (seg==B & idx<=tB) | …`) + # is expanded server-side into one `QueryDataset` request per segment, each planned + # serially, so the cost scales with segment count. Using the global max instead + # collapses that to a single request; segments whose own target is lower over-fetch + # a few extra keyframe rows (sparse, tiny), and the client-side `_prior_keyframe` + # bisect still selects the correct keyframe per segment per target. Segments are + # already restricted by `filter_segments` below. + global_max = max(max_per_segment.values()) + index_filter = col(index) <= idx_lit(global_max) + + # `is_keyframe` is `List` in Arrow. Datafusion can't coerce that to + # `Bool`, so `is_not_null()` is a coarse server-side pre-filter. The actual + # value check happens client-side in the `by_path` loop below. + # TODO(isaac): Will be able to do check server side with upcoming DF changes. + path_filter = col(unique_paths[0]).is_not_null() + for p in unique_paths[1:]: + path_filter = path_filter | col(p).is_not_null() + + # Selecting only the `is_keyframe` columns (a strict subset of the entity's + # components) under the default `fill_latest_at=False` lets the server push this + # projection into each query's `fuzzy_descriptors` and skip chunks for the heavy + # `VideoStream:sample` sibling. Keep the select narrow and do not pass + # `fill_latest_at=True`, or the push-down (gated on `SparseFillStrategy::None`) + # falls back to fetching every component on the entity. + # Scope to just the anchor entities (the `is_keyframe` siblings live on the same + # entities as the video samples), so this query never touches unrelated entities. + anchor_contents = sorted({f"{p.split(':')[0]}/**" for p in unique_paths}) + + with tracing_scope("RerunDataset._fetch_prior_keyframes.to_arrow_table"): + table = ( + view + .filter_contents(anchor_contents) + .filter_segments(list(max_per_segment.keys())) + .reader(index=index) + .filter(index_filter & path_filter) + .select(col(index), col("rerun_segment_id"), *[col(p) for p in unique_paths]) + .to_arrow_table() + ) + + # Per-path: sorted int64 arrays of keyframe index values, grouped by segment. + # `int(scalar)` on a `datetime64[ns]` element returns its nanoseconds-since-epoch + # representation, so this works uniformly for int64 and timestamp timelines. + by_path: dict[str, dict[str, np.ndarray]] = {} + for path in unique_paths: + mask = pc.list_element(table.column(path), 0) + sub = table.filter(mask) + sub_segs = sub.column("rerun_segment_id").to_pylist() + sub_idx = sub.column(index).to_numpy(zero_copy_only=False) + by_seg: dict[str, list[int]] = defaultdict(list) + for s, v in zip(sub_segs, sub_idx, strict=True): + by_seg[s].append(int(v)) + by_path[path] = {s: np.sort(np.array(vs, dtype=np.int64)) for s, vs in by_seg.items()} + + return {key: by_path[path] for key, path in keyframe_fields.items()} + + +def _prior_keyframe(sorted_kfs: np.ndarray | None, target: int) -> int | None: + """Largest value in *sorted_kfs* that is `<=` *target*, or `None` if none exists.""" + if sorted_kfs is None or len(sorted_kfs) == 0: + return None + pos = int(np.searchsorted(sorted_kfs, target, side="right")) - 1 + return None if pos < 0 else int(sorted_kfs[pos]) + + def _split_by_segment(table: pa.Table) -> dict[str, pa.Table]: """Split a combined table into per-segment tables.""" seg_col = table.column("rerun_segment_id") return {segment_id.as_py(): table.filter(pc.equal(seg_col, segment_id)) for segment_id in pc.unique(seg_col)} -def _derive_content_filter(columns: dict[str, Column]) -> list[str]: - """ - Build content-filter patterns from column paths. +def _apply_selector(selector: Selector, raw: pa.ChunkedArray) -> pa.ChunkedArray: + """Combine `raw` into a single Arrow array, run the selector on it, and re-wrap the output as a `ChunkedArray`.""" + combined = raw.combine_chunks() + out = selector.execute(combined) + if out is None: + return pa.chunked_array([], type=combined.type) + return pa.chunked_array([out]) - `"/camera:EncodedImage:blob"` → `"/camera/**"` - """ - paths: set[str] = set() - for col in columns.values(): - entity = col.path.split(":")[0] - paths.add(f"{entity}/**") - return sorted(paths) + +def _derive_content_filter(fields: dict[str, Field]) -> list[str]: + """Build entity content-filter patterns from field paths (`"/camera:EncodedImage:blob"` -> `"/camera/**"`).""" + return sorted({f"{f.path.split(':')[0]}/**" for f in fields.values()}) diff --git a/rerun_py/rerun_sdk/rerun/experimental/video.py b/rerun_py/rerun_sdk/rerun/experimental/video.py new file mode 100644 index 000000000000..48c471289804 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/experimental/video.py @@ -0,0 +1,33 @@ +"""Utilities for working with encoded video sample streams.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from rerun_bindings import ( + video_detect_gop_start, + video_length_prefixed_to_annex_b as length_prefixed_to_annex_b, +) + +if TYPE_CHECKING: + from ..components import VideoCodec + +__all__ = [ + "detect_gop_start", + "is_annex_b", + "length_prefixed_to_annex_b", +] + + +def is_annex_b(sample: bytes) -> bool: + """Whether the sample starts with an Annex B start code (`00 00 01` or `00 00 00 01`).""" + return sample.startswith((b"\x00\x00\x00\x01", b"\x00\x00\x01")) + + +def detect_gop_start(sample: bytes, codec: VideoCodec) -> bool: + """ + Detect whether a video sample starts a group of pictures, i.e. is a keyframe. + + H.264/H.265 samples must be in Annex B format. + """ + return video_detect_gop_start(sample, codec.value) diff --git a/rerun_py/rerun_sdk/rerun/notebook.py b/rerun_py/rerun_sdk/rerun/notebook.py index 1470a61784d9..e32525fc7f05 100644 --- a/rerun_py/rerun_sdk/rerun/notebook.py +++ b/rerun_py/rerun_sdk/rerun/notebook.py @@ -41,6 +41,22 @@ ) from .recording_stream import RecordingStream, get_data_recording +__all__ = [ + "ContainerSelectionItem", + "EntitySelectionItem", + "PauseEvent", + "PlayEvent", + "RecordingOpenEvent", + "SelectionChangeEvent", + "SelectionItem", + "TimeUpdateEvent", + "TimelineChangeEvent", + "ViewSelectionItem", + "Viewer", + "ViewerEvent", + "set_default_size", +] + HAS_NOTEBOOK = True try: from ipywidgets import HTML as _HTML, VBox as _VBox @@ -404,7 +420,7 @@ def update_panels( Parameters ---------- top: str - State of the panel, positioned on the top of the viewer. + State of the top panel of the viewer. blueprint: str State of the blueprint panel, positioned on the left side of the viewer. selection: str diff --git a/rerun_py/rerun_sdk/rerun/recording/__init__.py b/rerun_py/rerun_sdk/rerun/recording/__init__.py deleted file mode 100644 index c4820ad534d3..000000000000 --- a/rerun_py/rerun_sdk/rerun/recording/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from rerun_bindings import ( - load_archive as _load_archive, - load_recording as _load_recording, -) - -from ._recording import Recording as Recording, RRDArchive as RRDArchive - -if TYPE_CHECKING: - from pathlib import Path - - -def load_recording(path_to_rrd: str | Path) -> Recording: - """ - Load a single recording from an RRD file. - - Will raise a `ValueError` if the file does not contain exactly one recording. - - Parameters - ---------- - path_to_rrd: - The path to the file to load. - - Returns - ------- - Recording - The loaded recording. - - """ - return Recording(_load_recording(path_to_rrd)) - - -def load_archive(path_to_rrd: str | Path) -> RRDArchive: - """ - Load a rerun archive from an RRD file. - - Parameters - ---------- - path_to_rrd: - The path to the file to load. - - Returns - ------- - RRDArchive - The loaded archive. - - """ - return RRDArchive(_load_archive(path_to_rrd)) diff --git a/rerun_py/rerun_sdk/rerun/recording/_recording.py b/rerun_py/rerun_sdk/rerun/recording/_recording.py deleted file mode 100644 index 4c83e703a65b..000000000000 --- a/rerun_py/rerun_sdk/rerun/recording/_recording.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from rerun.experimental import Chunk -from rerun_bindings import recording_from_chunks - -if TYPE_CHECKING: - from collections.abc import Generator, Iterable - from pathlib import Path - - from rerun.catalog import Schema - from rerun_bindings import RecordingInternal, RRDArchiveInternal - - -class Recording: - """ - A single Rerun recording. - - This can be loaded from an RRD file using [`load_recording()`][rerun.recording.load_recording]. - - A recording is a collection of data that was logged to Rerun. This data is organized - as a column for each index (timeline) and each entity/component pair that was logged. - - You can examine the [`.schema()`][rerun.recording.Recording.schema] of the recording to see - what data is available. - """ - - _internal: RecordingInternal - - def __init__(self, inner: RecordingInternal) -> None: - self._internal = inner - - def schema(self) -> Schema: - """The schema describing all the columns available in the recording.""" - from rerun.catalog import Schema - - return Schema(self._internal.schema()) - - def recording_id(self) -> str: - """The recording ID of the recording.""" - return self._internal.recording_id() - - def application_id(self) -> str: - """The application ID of the recording.""" - return self._internal.application_id() - - def chunks(self) -> Generator[Chunk, None, None]: - """Iterate over all physical chunks in this recording.""" - - for chunk_internal in self._internal.chunks(): - yield Chunk(chunk_internal) - - @staticmethod - def from_chunks(chunks: Iterable[Chunk], application_id: str, recording_id: str) -> Recording: - """ - Create a new recording from an iterable of chunks. - - Parameters - ---------- - chunks: - An iterable of chunks to include in the recording. - application_id: - The application ID for the new recording. - recording_id: - The recording ID for the new recording. - - Returns - ------- - Recording - The newly created recording. - - """ - - return Recording(recording_from_chunks((c._internal for c in chunks), application_id, recording_id)) - - def save(self, path: str | Path) -> None: - """Save this recording to an RRD file.""" - self._internal.save(str(path)) - - -class RRDArchive: - """ - An archive loaded from an RRD. - - RRD archives may include 1 or more recordings or blueprints. - """ - - _internal: RRDArchiveInternal - - def __init__(self, inner: RRDArchiveInternal) -> None: - self._internal = inner - - def num_recordings(self) -> int: - """The number of recordings in the archive.""" - return self._internal.num_recordings() - - def all_recordings(self) -> list[Recording]: - """All the recordings in the archive.""" - return [Recording(r) for r in self._internal.all_recordings()] diff --git a/rerun_py/rerun_sdk/rerun/recording_stream.py b/rerun_py/rerun_sdk/rerun/recording_stream.py index 12fc8d5e1c3b..72520ae296b9 100644 --- a/rerun_py/rerun_sdk/rerun/recording_stream.py +++ b/rerun_py/rerun_sdk/rerun/recording_stream.py @@ -10,10 +10,11 @@ from typing_extensions import Self -import rerun as rr from rerun import bindings from rerun_bindings import ChunkBatcherConfig as ChunkBatcherConfig # noqa: TC001 +from ._send_dataframe import AUTO_INDEX, _AutoIndex + if TYPE_CHECKING: from collections.abc import Iterable from datetime import datetime, timedelta @@ -25,11 +26,12 @@ from rerun import AsComponents, BlueprintLike, ComponentColumn, DescribedComponentBatch as DescribedComponentBatch from rerun._memory import MemoryRecording + from rerun.experimental import Chunk, ChunkStore, LazyChunkStream, LazyStore + from rerun.experimental._chunk import DataframeLike from rerun.sinks import LogSinkLike from ._send_columns import TimeColumnLike as TimeColumnLike - # TODO(#3793): defaulting recording_id to authkey should be opt-in active_recording_stream: contextvars.ContextVar[RecordingStream] = contextvars.ContextVar("active_recording_stream") @@ -179,6 +181,14 @@ class RecordingStream: has been recorded and (if applicable) flushed to the underlying OS-managed file descriptor, but other threads may still have data in flight. + On context manager exit, file-like sinks (e.g. those created by [`rerun.RecordingStream.save`][]) + are also finalized so the resulting `.rrd` is consumable immediately — without this, the file's + footer would only be written when the `RecordingStream` is garbage-collected. Streaming sinks + (e.g. [`rerun.RecordingStream.connect_grpc`][], [`rerun.RecordingStream.serve_grpc`][]) are left + intact and continue to receive data after the `with`-block exits. After a file-like sink has + been finalized this way, subsequent log calls on the same `RecordingStream` go to a buffered + sink until a new sink is attached. + See also: [`rerun.get_data_recording`][], [`rerun.get_global_data_recording`][], [`rerun.get_thread_local_data_recording`][]. @@ -361,6 +371,10 @@ def __exit__( ) -> None: self.flush() + # Finalize file-like sinks (e.g. `save()`) so the resulting `.rrd` is consumable as soon as + # the `with`-block exits. Streaming sinks like gRPC are left untouched. + bindings.finalize_deferred_sinks(recording=self.to_native()) + current_recording = active_recording_stream.get(None) # Restore the context state @@ -515,7 +529,13 @@ def connect_grpc( connect_grpc(url, default_blueprint=default_blueprint, recording=self) - def save(self, path: str | Path, default_blueprint: BlueprintLike | None = None) -> None: + def save( + self, + path: str | Path, + default_blueprint: BlueprintLike | None = None, + *, + write_footer: bool = True, + ) -> None: """ Stream all log-data to a file. @@ -534,14 +554,26 @@ def save(self, path: str | Path, default_blueprint: BlueprintLike | None = None) already has an active blueprint, the new blueprint won't become active until the user clicks the "reset blueprint" button. If you want to activate the new blueprint immediately, instead use the [`rerun.send_blueprint`][] API. + write_footer: + Whether to emit a complete RRD footer (including a manifest of every chunk) at the + end of the stream. Defaults to `True`. See [`rerun.save`][] for details and + trade-offs (notably memory usage in long-running streaming sessions). + + *Warning*: lack of footer will significantly hurt random-access performance and some + tools (e.g. LazyStore) may not work properly. """ from .sinks import save - save(path, default_blueprint, recording=self) + save(path, default_blueprint, recording=self, write_footer=write_footer) - def stdout(self, default_blueprint: BlueprintLike | None = None) -> None: + def stdout( + self, + default_blueprint: BlueprintLike | None = None, + *, + write_footer: bool = True, + ) -> None: """ Stream all log-data to stdout. @@ -559,12 +591,18 @@ def stdout(self, default_blueprint: BlueprintLike | None = None) -> None: already has an active blueprint, the new blueprint won't become active until the user clicks the "reset blueprint" button. If you want to activate the new blueprint immediately, instead use the [`rerun.send_blueprint`][] API. + write_footer: + Whether to emit a complete RRD footer (including a manifest of every chunk) at the + end of the stream. Defaults to `True`. See [`rerun.save`][] for details. + + *Warning*: lack of footer will significantly hurt random-access performance and some + tools (e.g. LazyStore) may not work properly. """ from .sinks import stdout - stdout(default_blueprint, recording=self) + stdout(default_blueprint, recording=self, write_footer=write_footer) def memory_recording(self) -> MemoryRecording: """ @@ -689,24 +727,6 @@ def send_blueprint( send_blueprint(blueprint=blueprint, make_active=make_active, make_default=make_default, recording=self) - def send_recording(self, recording: rr.recording.Recording) -> None: - """ - Send a `Recording` loaded from a `.rrd` to the `RecordingStream`. - - !!! Warning - ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ - - Parameters - ---------- - recording: - A `Recording` loaded from a `.rrd`. - - """ - - from .sinks import send_recording - - send_recording(rrd=recording, recording=self) - def spawn( self, *, @@ -949,6 +969,38 @@ def reset_time(self) -> None: bindings.reset_time(recording=self.to_native()) + def set_log_tick_enabled(self, enabled: bool) -> None: + """ + Enable or disable automatic injection of the `log_tick` timeline into logged data. + + `log_tick` is a per-recording counter that increments on every logging call. + It is **disabled** by default (it can also be controlled via the `RERUN_LOG_TICK` environment variable). + + Parameters + ---------- + enabled: + Whether to inject the `log_tick` timeline. + + """ + + bindings.set_log_tick_enabled(enabled, recording=self.to_native()) + + def set_log_time_enabled(self, enabled: bool) -> None: + """ + Enable or disable automatic injection of the `log_time` timeline into logged data. + + `log_time` is the wall-clock time at which data was logged. + It is **enabled** by default (it can also be controlled via the `RERUN_LOG_TIME` environment variable). + + Parameters + ---------- + enabled: + Whether to inject the `log_time` timeline. + + """ + + bindings.set_log_time_enabled(enabled, recording=self.to_native()) + def log( self, entity_path: str | list[object], @@ -1015,7 +1067,7 @@ def log( Static data has no time associated with it, exists on all timelines, and unconditionally shadows any temporal data of the same type. - Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). Additional timelines set by [`rerun.RecordingStream.set_time`][] will also be included. strict: @@ -1064,7 +1116,7 @@ def log_file_from_contents( Static data has no time associated with it, exists on all timelines, and unconditionally shadows any temporal data of the same type. - Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). Additional timelines set by [`rerun.RecordingStream.set_time`][] will also be included. """ @@ -1110,7 +1162,7 @@ def log_file_from_path( Static data has no time associated with it, exists on all timelines, and unconditionally shadows any temporal data of the same type. - Otherwise, the data will be timestamped automatically with `log_time` and `log_tick`. + Otherwise, the data will be timestamped automatically with `log_time` (and `log_tick`, if enabled). Additional timelines set by [`rerun.RecordingStream.set_time`][] will also be included. """ @@ -1169,27 +1221,103 @@ def send_columns( send_columns(entity_path=entity_path, indexes=indexes, columns=columns, strict=strict, recording=self) - def send_chunk(self, chunk: rr.experimental.Chunk) -> None: - """Send a pre-built [`Chunk`][rerun.experimental.Chunk] to this recording stream.""" + def send_chunks( + self, + chunks: Chunk | LazyChunkStream | LazyStore | ChunkStore | Iterable[Chunk], + ) -> None: + """ + Send chunks to this recording stream. Blocks until every chunk has been queued. - from .experimental._send_chunk import send_chunk + See [`rerun.experimental.send_chunks`][]. - send_chunk(chunk, recording=self) + !!! note + For a `LazyChunkStream` and `LazyStore` inputs, this call triggers execution + and/or loading and will block for the duration of this process. - def send_record_batch(self, batch: pa.RecordBatch) -> None: - """Coerce a single pyarrow `RecordBatch` to Rerun structure.""" + Parameters + ---------- + chunks: + One of: + + - A single [`Chunk`][rerun.experimental.Chunk]. + - A [`LazyChunkStream`][rerun.experimental.LazyChunkStream] — consume + the stream and forward all chunks to this recording stream. + - A [`LazyStore`][rerun.experimental.LazyStore] — send all chunks to this + recording stream. This triggers loading all chunks from the source. + - A [`ChunkStore`][rerun.experimental.ChunkStore] — send all chunks to + this recording stream (fast since all chunks are already loaded). + - Any iterable of `Chunk` objects. + + Source store identity (`application_id`, `recording_id`) is **not** + preserved: chunks adopt this recording's identity. + + """ + from .experimental._send_chunks import send_chunks + send_chunks(chunks, recording=self) + + def send_record_batch( + self, + batch: pa.RecordBatch, + *, + index: str | list[str] | None | _AutoIndex = AUTO_INDEX, + entity_path: str | None = None, + ) -> None: + """ + Coerce a single pyarrow `RecordBatch` to Rerun structure and log it. + + A thin wrapper over [`Chunk.from_record_batch`][rerun.experimental.Chunk.from_record_batch] + followed by [`send_chunks`][rerun.experimental.send_chunks]. See `Chunk.from_record_batch` for + the full column-classification semantics and the conditions under which a `ValueError` is raised. + + Parameters + ---------- + batch: + The Arrow record batch to interpret. + index: + Determines which columns are index (timeline) columns. See + [`Chunk.from_record_batch`][rerun.experimental.Chunk.from_record_batch] for the full + semantics. Defaults to deriving the index from the batch's Rerun metadata. + entity_path: + Default entity path for component columns that do not otherwise specify one. + + """ from ._send_dataframe import send_record_batch - send_record_batch(batch, recording=self) + send_record_batch(batch, recording=self, index=index, entity_path=entity_path) + + def send_dataframe( + self, + df: DataframeLike, + *, + index: str | list[str] | None | _AutoIndex = AUTO_INDEX, + entity_path: str | None = None, + ) -> None: + """ + Coerce a pyarrow `Table` / `RecordBatch` / `RecordBatchReader`, or a datafusion `DataFrame`, to Rerun structure and log it. - # TODO(RR-3198): this should accept a `datafusion.DataFrame` as a soft dependency - def send_dataframe(self, df: pa.RecordBatchReader | pa.Table) -> None: - """Coerce a pyarrow `RecordBatchReader` or `Table` to Rerun structure.""" + A thin wrapper over [`Chunk.from_dataframe`][rerun.experimental.Chunk.from_dataframe] followed by + [`send_chunks`][rerun.experimental.send_chunks]. See `Chunk.from_dataframe` for the accepted input + types, and `Chunk.from_record_batch` for the full column-classification semantics and the + conditions under which a `ValueError` is raised. + Parameters + ---------- + df: + The dataframe to interpret. Must be a pyarrow `Table`, pyarrow `RecordBatch`, pyarrow + `RecordBatchReader`, or datafusion `DataFrame` (an optional dependency) — each has a + single fixed schema. + index: + Determines which columns are index (timeline) columns. See + [`Chunk.from_record_batch`][rerun.experimental.Chunk.from_record_batch] for the full + semantics. Defaults to deriving the index from the dataframe's Rerun metadata. + entity_path: + Default entity path for component columns that do not otherwise specify one. + + """ from ._send_dataframe import send_dataframe - send_dataframe(df, recording=self) + send_dataframe(df, recording=self, index=index, entity_path=entity_path) def __str__(self) -> str: return str(self.inner) diff --git a/rerun_py/rerun_sdk/rerun/server.py b/rerun_py/rerun_sdk/rerun/server.py index fedd7c7f76b9..8c83e05bd61c 100644 --- a/rerun_py/rerun_sdk/rerun/server.py +++ b/rerun_py/rerun_sdk/rerun/server.py @@ -16,6 +16,8 @@ from collections.abc import Sequence from types import TracebackType +__all__ = ["Server"] + class Server: """ diff --git a/rerun_py/rerun_sdk/rerun/sinks.py b/rerun_py/rerun_sdk/rerun/sinks.py index c39ff4ae5aee..3ecadea69752 100644 --- a/rerun_py/rerun_sdk/rerun/sinks.py +++ b/rerun_py/rerun_sdk/rerun/sinks.py @@ -16,7 +16,6 @@ if TYPE_CHECKING: import pathlib - from rerun.recording import Recording from rerun.recording_stream import RecordingStream @@ -176,6 +175,8 @@ def save( path: str | pathlib.Path, default_blueprint: BlueprintLike | None = None, recording: RecordingStream | None = None, + *, + write_footer: bool = True, ) -> None: """ Stream all log-data to a file. @@ -199,6 +200,17 @@ def save( Specifies the [`rerun.RecordingStream`][] to use. If left unspecified, defaults to the current active data recording, if there is one. See also: [`rerun.init`][], [`rerun.set_global_data_recording`][]. + write_footer: + Whether to emit a complete RRD footer (including a manifest of every chunk) at the + end of the stream. Defaults to `True`. + + Producing a footer keeps per-chunk metadata in memory for the lifetime of the sink, + which grows linearly with the number of chunks logged. Pass `write_footer=False` for + long-running streaming sessions; the resulting file is still a valid RRD and a footer + can be added after the fact via `rerun rrd optimize`. + + *Warning*: lack of footer will significantly hurt random-access performance and some + tools (e.g. LazyStore) may not work properly. """ @@ -226,10 +238,16 @@ def save( path=str(path), default_blueprint=blueprint_storage, recording=recording.to_native() if recording is not None else None, + write_footer=write_footer, ) -def stdout(default_blueprint: BlueprintLike | None = None, recording: RecordingStream | None = None) -> None: +def stdout( + default_blueprint: BlueprintLike | None = None, + recording: RecordingStream | None = None, + *, + write_footer: bool = True, +) -> None: """ Stream all log-data to stdout. @@ -251,6 +269,12 @@ def stdout(default_blueprint: BlueprintLike | None = None, recording: RecordingS Specifies the [`rerun.RecordingStream`][] to use. If left unspecified, defaults to the current active data recording, if there is one. See also: [`rerun.init`][], [`rerun.set_global_data_recording`][]. + write_footer: + Whether to emit a complete RRD footer (including a manifest of every chunk) at the + end of the stream. Defaults to `True`. See [`rerun.save`][] for details and trade-offs. + + *Warning*: lack of footer will significantly hurt random-access performance and some + tools (e.g. LazyStore) may not work properly. """ @@ -277,6 +301,7 @@ def stdout(default_blueprint: BlueprintLike | None = None, recording: RecordingS bindings.stdout( default_blueprint=blueprint_storage, recording=recording.to_native() if recording is not None else None, + write_footer=write_footer, ) @@ -435,34 +460,6 @@ def send_blueprint( ) -def send_recording(rrd: Recording, recording: RecordingStream | None = None) -> None: - """ - Send a `Recording` loaded from a `.rrd` to the `RecordingStream`. - - !!! Warning - ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ - - Parameters - ---------- - rrd: - A recording loaded from a `.rrd` file. - recording: - Specifies the [`rerun.RecordingStream`][] to use. - If left unspecified, defaults to the current active data recording, if there is one. - See also: [`rerun.init`][], [`rerun.set_global_data_recording`][]. - - """ - application_id = get_application_id(recording=recording) # NOLINT - - if application_id is None: - raise ValueError("No application id found. You must call rerun.init before sending a recording.") - - bindings.send_recording( - rrd._internal, - recording=recording.to_native() if recording is not None else None, - ) - - def spawn( *, port: int = 9876, diff --git a/rerun_py/rerun_sdk/rerun/time.py b/rerun_py/rerun_sdk/rerun/time.py index 4882bdf77d15..92840e87b91c 100644 --- a/rerun_py/rerun_sdk/rerun/time.py +++ b/rerun_py/rerun_sdk/rerun/time.py @@ -202,3 +202,51 @@ def reset_time(recording: RecordingStream | None = None) -> None: bindings.reset_time( recording=recording.to_native() if recording is not None else None, ) + + +def set_log_tick_enabled(enabled: bool, recording: RecordingStream | None = None) -> None: + """ + Enable or disable automatic injection of the `log_tick` timeline into logged data. + + `log_tick` is a per-recording counter that increments on every logging call. + It is **disabled** by default (it can also be controlled via the `RERUN_LOG_TICK` environment variable). + + Parameters + ---------- + enabled: + Whether to inject the `log_tick` timeline. + recording: + Specifies the [`rerun.RecordingStream`][] to use. + If left unspecified, defaults to the current active data recording, if there is one. + See also: [`rerun.init`][], [`rerun.set_global_data_recording`][]. + + """ + + bindings.set_log_tick_enabled( + enabled, + recording=recording.to_native() if recording is not None else None, + ) + + +def set_log_time_enabled(enabled: bool, recording: RecordingStream | None = None) -> None: + """ + Enable or disable automatic injection of the `log_time` timeline into logged data. + + `log_time` is the wall-clock time at which data was logged. + It is **enabled** by default (it can also be controlled via the `RERUN_LOG_TIME` environment variable). + + Parameters + ---------- + enabled: + Whether to inject the `log_time` timeline. + recording: + Specifies the [`rerun.RecordingStream`][] to use. + If left unspecified, defaults to the current active data recording, if there is one. + See also: [`rerun.init`][], [`rerun.set_global_data_recording`][]. + + """ + + bindings.set_log_time_enabled( + enabled, + recording=recording.to_native() if recording is not None else None, + ) diff --git a/rerun_py/rerun_sdk/rerun/urdf.py b/rerun_py/rerun_sdk/rerun/urdf.py index 04109f069021..1b60595e0a48 100644 --- a/rerun_py/rerun_sdk/rerun/urdf.py +++ b/rerun_py/rerun_sdk/rerun/urdf.py @@ -9,6 +9,8 @@ from collections.abc import Sequence from pathlib import Path + import pyarrow as pa + from . import Transform3D from ._baseclasses import ComponentColumnList from .experimental import LazyChunkStream @@ -373,5 +375,41 @@ def stream(self, *, include_joint_transforms: bool = True) -> LazyChunkStream: return LazyChunkStream(self._inner.stream(include_joint_transforms=include_joint_transforms)) + def compute_joint_transform_batches( + self, + names: pa.Array, + values: pa.Array, + *, + clamp: bool = False, + ) -> pa.Array: + """ + Compute batches of 3D transform components from Arrow list arrays containing joint names and values. + + `names` must be a `ListArray` with `Utf8` values. + `values` must be a `ListArray` with values castable to `Float64`. + + The output is a `ListArray` with `translation`, `quaternion`, `parent_frame`, and + `child_frame` fields and the same outer row count as the inputs. + + Note: this is intended as a helper for lens pipelines, where you would usually pipe this output + through an additional lens that scatters each batch into final `Transform3D` component rows. + + Parameters + ---------- + names: + Joint names for each row. + values: + Joint values for each row. + clamp: + Whether to clamp & warn about values outside joint limits. + + Returns + ------- + pa.Array + Transform batches with one outer row for each input row. + + """ + return self._inner.compute_joint_transform_batches(names, values, clamp=clamp) + def __repr__(self) -> str: return self._inner.__repr__() diff --git a/rerun_py/rerun_sdk/rerun_cli/__main__.py b/rerun_py/rerun_sdk/rerun_cli/__main__.py index 2e56aaa5096d..8ffdb2ec271b 100644 --- a/rerun_py/rerun_sdk/rerun_cli/__main__.py +++ b/rerun_py/rerun_sdk/rerun_cli/__main__.py @@ -23,6 +23,10 @@ def main() -> int: if "RERUN_CLI_PATH" in os.environ: print(f"Using overridden RERUN_CLI_PATH={os.environ['RERUN_CLI_PATH']}", file=sys.stderr) target_path = os.environ["RERUN_CLI_PATH"] + elif sys.platform == "darwin": + bundled = os.path.join(os.path.dirname(__file__), "Rerun.app", "Contents", "MacOS", "Rerun") + bare = os.path.join(os.path.dirname(__file__), "rerun") + target_path = bundled if os.path.exists(bundled) else bare else: target_path = os.path.join(os.path.dirname(__file__), "rerun") diff --git a/rerun_py/src/arrow.rs b/rerun_py/src/arrow.rs index 46d9a3194eff..302ae55ec450 100644 --- a/rerun_py/src/arrow.rs +++ b/rerun_py/src/arrow.rs @@ -8,14 +8,16 @@ use arrow::array::{ use arrow::buffer::OffsetBuffer as ArrowOffsetBuffer; use arrow::datatypes::Field as ArrowField; use arrow::pyarrow::PyArrowType; -use pyo3::exceptions::PyRuntimeError; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::types::{PyAnyMethods as _, PyDict, PyDictMethods as _, PyString}; use pyo3::{Bound, PyAny, PyResult}; use re_arrow_util::ArrowArrayDowncastRef as _; use re_chunk::{Chunk, ChunkError, ChunkId, PendingRow, RowId, TimeColumn, TimelineName}; use re_log_types::TimePoint; use re_sdk::external::nohash_hasher::IntMap; -use re_sdk::{ComponentDescriptor, EntityPath, Timeline}; +use re_sdk::{ + ArchetypeName, ComponentDescriptor, ComponentIdentifier, ComponentType, EntityPath, Timeline, +}; /// Perform Python-to-Rust conversion for a `ComponentDescriptor`. pub fn descriptor_to_rust(component_descr: &Bound<'_, PyAny>) -> PyResult { @@ -39,9 +41,10 @@ pub fn descriptor_to_rust(component_descr: &Bound<'_, PyAny>) -> PyResult = component.extract()?; let descr = ComponentDescriptor { - archetype: archetype.map(|s| s.as_ref().into()), - component: component.as_ref().into(), - component_type: component_type.map(|s| s.as_ref().into()), + archetype: archetype.and_then(|s| ArchetypeName::try_new(s.as_ref()).ok()), + component: ComponentIdentifier::try_new(component.as_ref()) + .map_err(|err| PyRuntimeError::new_err(err.to_string()))?, + component_type: component_type.and_then(|s| ComponentType::try_new(s.as_ref()).ok()), }; descr.sanity_check(); Ok(descr) @@ -92,17 +95,16 @@ pub fn build_chunk_from_components( let (arrays, timeline_names): (Vec, Vec) = itertools::process_results( timelines.iter().map(|(name, array)| { - let py_name = name.downcast::()?; + let py_name = name.cast::()?; let name: std::borrow::Cow<'_, str> = py_name.extract()?; - let timeline_name: TimelineName = name.as_ref().into(); + let timeline_name = TimelineName::try_new(name.as_ref()) + .map_err(|err| PyValueError::new_err(err.to_string()))?; array_to_rust(&array).map(|array| (array, timeline_name)) }), |iter| iter.unzip(), )?; - let timelines: Result, ChunkError> = arrays - .into_iter() - .zip(timeline_names) + let timelines: Result, ChunkError> = std::iter::zip(arrays, timeline_names) .map(|(array, timeline_name)| { let time_type = re_log_types::TimeType::from_arrow_datatype(array.data_type()) .ok_or_else(|| ChunkError::Malformed { @@ -135,26 +137,26 @@ pub fn build_chunk_from_components( |iter| iter.unzip(), )?; - let components: Result, ChunkError> = arrays - .into_iter() - .zip(component_descrs) - .map(|(list_array, descr)| { - let batch = if let Some(batch) = list_array.downcast_array_ref::() { - batch.clone() - } else { - let offsets = - ArrowOffsetBuffer::from_lengths(std::iter::repeat_n(1, list_array.len())); - let field = ArrowField::new("item", list_array.data_type().clone(), true).into(); - ArrowListArray::try_new(field, offsets, list_array, None).map_err(|err| { - ChunkError::Malformed { - reason: format!("Failed to wrap in List array: {err}"), - } - })? - }; - - Ok((descr, batch)) - }) - .collect(); + let components: Result, ChunkError> = + std::iter::zip(arrays, component_descrs) + .map(|(list_array, descr)| { + let batch = if let Some(batch) = list_array.downcast_array_ref::() { + batch.clone() + } else { + let offsets = + ArrowOffsetBuffer::from_lengths(std::iter::repeat_n(1, list_array.len())); + let field = + ArrowField::new("item", list_array.data_type().clone(), true).into(); + ArrowListArray::try_new(field, offsets, list_array, None).map_err(|err| { + ChunkError::Malformed { + reason: format!("Failed to wrap in List array: {err}"), + } + })? + }; + + Ok((descr, batch)) + }) + .collect(); let components = components .map_err(|err| PyRuntimeError::new_err(format!("Error converting component data: {err}")))? diff --git a/rerun_py/src/catalog/catalog_client.rs b/rerun_py/src/catalog/catalog_client.rs index 4f7bcbd1750a..a56644bcdec9 100644 --- a/rerun_py/src/catalog/catalog_client.rs +++ b/rerun_py/src/catalog/catalog_client.rs @@ -1,43 +1,20 @@ -use std::collections::HashSet; -use std::sync::{Arc, LazyLock}; - -static RERUN_SDK_NUM_CPUS: LazyLock> = LazyLock::new(|| { - let physical_cpus = std::thread::available_parallelism() - .map(|n| n.get() as u64) - .unwrap_or(2); - - std::env::var("RERUN_SDK_NUM_CPUS").ok().map(|val| { - // DataFusion's target_partitions requires a positive integer. - // If a fractional value is provided, truncate it. Clamp to - // [1, physical_cpus] so we never exceed the machine's actual - // core count (guards against infinity / huge values). - if let Ok(f) = val.trim().parse::() { - (f as u64).clamp(1, physical_cpus) - } else { - re_log::warn_once!( - "Failed to parse RERUN_SDK_NUM_CPUS={val:?}, defaulting to {physical_cpus}" - ); - physical_cpus - } - }) -}); +use std::sync::Arc; use arrow::datatypes::Schema; use arrow::pyarrow::PyArrowType; use pyo3::exceptions::{PyLookupError, PyRuntimeError, PyValueError}; use pyo3::types::{PyAnyMethods as _, PyDict}; use pyo3::{Py, PyAny, PyErr, PyResult, Python, pyclass, pymethods}; -use re_datafusion::{DEFAULT_CATALOG_NAME, get_all_catalog_names}; use re_log_types::EntryName; use re_protos::cloud::v1alpha1::{EntryFilter, EntryKind}; -use crate::catalog::datafusion_catalog::PyDataFusionCatalogProvider; +use crate::catalog::datafusion_catalog::PyDataFusionCatalogProviderList; use crate::catalog::{ ConnectionHandle, PyDatasetEntryInternal, PyEntryId, PyRerunHtmlTable, PyTableEntryInternal, to_py_err, }; use crate::trace_context::read_trace_context_from_python; -use crate::utils::{get_tokio_runtime, wait_for_future}; +use crate::utils::wait_for_future; /// Client for a remote Rerun catalog server. #[pyclass( @@ -66,7 +43,7 @@ fn setup_datafusion_context(py: Python<'_>) -> PyResult> { let config_options = PyDict::new(py); config_options.set_item("datafusion.execution.coalesce_batches", "false")?; - if let Some(cores) = *RERUN_SDK_NUM_CPUS { + if let Some(cores) = re_datafusion::rerun_sdk_num_cpus() { config_options.set_item("datafusion.execution.target_partitions", cores.to_string())?; } @@ -130,7 +107,7 @@ impl PyCatalogClientInternal { datafusion_ctx, }; - ret.update_catalog_providers(py, true)?; + ret.register_catalog_provider_list(py)?; Ok(ret) } @@ -152,6 +129,26 @@ impl PyCatalogClientInternal { Ok((info.version, info.cloud_provider, info.cloud_region)) } + fn rtt_seconds(self_: Py, py: Python<'_>, num_pings: usize) -> PyResult { + let _span = read_trace_context_from_python(py, "CatalogClient.rtt_seconds").entered(); + let connection = self_.borrow(py).connection.clone(); + Ok(connection.rtt(py, num_pings)?.as_secs_f64()) + } + + fn bandwidth_bytes_per_sec( + self_: Py, + py: Python<'_>, + num_bytes: u64, + rtt_seconds: f64, + ) -> PyResult> { + let _span = + read_trace_context_from_python(py, "CatalogClient.bandwidth_bytes_per_sec").entered(); + let connection = self_.borrow(py).connection.clone(); + let rtt = std::time::Duration::try_from_secs_f64(rtt_seconds) + .map_err(|err| PyValueError::new_err(format!("invalid rtt_seconds: {err}")))?; + connection.bandwidth_bytes_per_sec(py, num_bytes, rtt) + } + /// Get a list of all dataset entries in the catalog. fn datasets( self_: Py, @@ -161,18 +158,34 @@ impl PyCatalogClientInternal { let _span = read_trace_context_from_python(py, "CatalogClient.datasets").entered(); let connection = self_.borrow(py).connection.clone(); - let mut entry_details = - connection.find_entries(py, EntryFilter::new().with_entry_kind(EntryKind::Dataset))?; - - if include_hidden { - entry_details.extend(connection.find_entries( - py, - EntryFilter::new().with_entry_kind(EntryKind::BlueprintDataset), - )?); - } + let entry_details = connection.find_entries( + py, + EntryFilter { + id: None, + name: None, + // Passing the deprecated `entry_kind` as None for + // compatibility with older Rerun Hub versions. + // + // With this setting legacy Rerun Hub versions will return + // return all known entry kinds, which we'll need to filter below. + // See RR-5186. + entry_kind: None, + entry_kinds: vec![ + EntryKind::Dataset as i32, + EntryKind::BlueprintDataset as i32, + EntryKind::AssetDataset as i32, + ], + }, + )?; entry_details .into_iter() + .filter(|details| { + matches!( + details.kind, + EntryKind::Dataset | EntryKind::BlueprintDataset | EntryKind::AssetDataset + ) && (include_hidden || !details.name.is_hidden()) + }) .map(|details| { let dataset_entry = connection.read_dataset(py, details.id)?; Py::new( @@ -192,8 +205,15 @@ impl PyCatalogClientInternal { let _span = read_trace_context_from_python(py, "CatalogClient.tables").entered(); let connection = self_.borrow(py).connection.clone(); - let entry_details = - connection.find_entries(py, EntryFilter::new().with_entry_kind(EntryKind::Table))?; + // `with_entry_kind` is deprecated and kept for compatibility with Rerun Hub + // older than 0.15. Drop when all customers are on 0.15 or newer. + #[expect(deprecated)] + let entry_details = connection.find_entries( + py, + EntryFilter::new() + .with_entry_kind(EntryKind::Table) + .with_entry_kinds([EntryKind::Table]), + )?; entry_details .into_iter() @@ -277,8 +297,6 @@ impl PyCatalogClientInternal { let name = EntryName::new(name).map_err(|err| PyValueError::new_err(err.to_string()))?; let table_entry = connection.register_table(py, name, url)?; - self_.borrow(py).update_catalog_providers(py, false)?; - Py::new( py, PyTableEntryInternal::new(self_.clone_ref(py), table_entry), @@ -330,8 +348,6 @@ impl PyCatalogClientInternal { let name = EntryName::new(name).map_err(|err| PyValueError::new_err(err.to_string()))?; let table_entry = connection.create_table_entry(py, &name, schema, url)?; - self_.borrow(py).update_catalog_providers(py, false)?; - Py::new( py, PyTableEntryInternal::new(self_.clone_ref(py), table_entry), @@ -390,41 +406,20 @@ impl PyCatalogClientInternal { } impl PyCatalogClientInternal { - fn update_catalog_providers(&self, py: Python<'_>, force_register: bool) -> Result<(), PyErr> { - let client = wait_for_future(py, self.connection.client())?; - let runtime = get_tokio_runtime().handle(); - - let provider_names = get_all_catalog_names(&client, runtime).map_err(to_py_err)?; - let mut providers = provider_names - .iter() - .map(|p| p.as_str()) - .collect::>(); - if !providers.contains(&DEFAULT_CATALOG_NAME) { - providers.push(DEFAULT_CATALOG_NAME); - } - - if let Some(ctx) = self.datafusion_ctx.as_ref() { - let existing_catalogs: HashSet = - ctx.call_method0(py, "catalog_names")?.extract(py)?; - - for provider_name in providers { - if !force_register && existing_catalogs.contains(provider_name) { - continue; - } - - let catalog_provider = PyDataFusionCatalogProvider::new( - Some(provider_name.to_owned()), - client.clone(), - ); + /// Install a single lazy [`PyDataFusionCatalogProviderList`] on the session context. The + /// list resolves catalogs on demand, so this call performs no gRPC; subsequent SQL + /// planning never fans out to wildcard `FindEntries` either. + #[tracing::instrument(skip_all)] + fn register_catalog_provider_list(&self, py: Python<'_>) -> Result<(), PyErr> { + let Some(ctx) = self.datafusion_ctx.as_ref() else { + return Ok(()); + }; - ctx.call_method1( - py, - "register_catalog_provider", - (provider_name, catalog_provider), - )?; - } - } + let connection = wait_for_future(py, self.connection.connection())?; + let provider_list = + PyDataFusionCatalogProviderList::new(connection.client, connection.analytics); + ctx.call_method1(py, "register_catalog_provider_list", (provider_list,))?; Ok(()) } } diff --git a/rerun_py/src/catalog/component_columns.rs b/rerun_py/src/catalog/component_columns.rs index f4dbdbb6a9be..cfe546b310f2 100644 --- a/rerun_py/src/catalog/component_columns.rs +++ b/rerun_py/src/catalog/component_columns.rs @@ -10,6 +10,7 @@ use re_sorbet::{ComponentColumnDescriptor, ComponentColumnSelector}; /// column, use [`ComponentColumnSelector`][rerun.catalog.ComponentColumnSelector]. #[pyclass( frozen, + from_py_object, hash, eq, name = "ComponentColumnDescriptor", @@ -124,6 +125,7 @@ impl From for ComponentColumnDescriptor { /// The component to select #[pyclass( frozen, + from_py_object, eq, name = "ComponentColumnSelector", module = "rerun_bindings.rerun_bindings" diff --git a/rerun_py/src/catalog/connection_handle.rs b/rerun_py/src/catalog/connection_handle.rs index 6debe083fbff..a008b307196f 100644 --- a/rerun_py/src/catalog/connection_handle.rs +++ b/rerun_py/src/catalog/connection_handle.rs @@ -4,23 +4,26 @@ use arrow::array::{RecordBatch, RecordBatchIterator, RecordBatchReader}; use arrow::datatypes::{Schema as ArrowSchema, SchemaRef}; use arrow::ffi_stream::ArrowArrayStreamReader; use arrow::pyarrow::PyArrowType; +use itertools::Itertools as _; use pyo3::exceptions::PyValueError; -use pyo3::{PyErr, PyResult, Python}; -use re_arrow_util::ArrowArrayDowncastRef as _; -use re_chunk_store::QueryExpression; +use pyo3::{PyResult, Python}; +use re_chunk_store::{QueryExpression, SparseFillStrategy}; use re_datafusion::query_from_query_expression; use re_log::external::log::warn; use re_log_types::{EntryId, EntryName}; +use re_protos::cloud::v1alpha1::ext as cloud_ext; use re_protos::cloud::v1alpha1::ext::{ - DataSource, DatasetDetails, DatasetEntry, EntryDetails, QueryDatasetRequest, - RegisterWithDatasetTaskDescriptor, TableEntry, VersionResponse, + DataSource, DatasetDetails, DatasetEntry, EntryDetails, QueryDatasetDataframe, + QueryDatasetRequest, QueryTasksDataframe, RegisterWithDatasetTaskDescriptor, TableDetails, + TableEntry, VersionResponse, }; -use re_protos::cloud::v1alpha1::{EntryFilter, QueryDatasetResponse, QueryTasksResponse}; +use re_protos::cloud::v1alpha1::{EntryFilter, QueryTasksResponse}; use re_protos::common::v1alpha1::TaskId; -use re_protos::common::v1alpha1::ext::{IfDuplicateBehavior, ScanParameters}; +use re_protos::common::v1alpha1::ext::{IfDuplicateBehavior, ScanParameters, SegmentId}; use re_protos::headers::RerunHeadersInjectorExt as _; -use re_protos::{invalid_schema, missing_field}; -use re_redap_client::{ApiError, ConnectionClient, ConnectionRegistryHandle}; +use re_protos::missing_field; +use re_redap_client::{ApiError, Connection, ConnectionClient, ConnectionRegistryHandle, TraceId}; +use re_types_core::LayerName; use crate::catalog::table_entry::PyTableInsertModeInternal; use crate::catalog::to_py_err; @@ -42,13 +45,17 @@ impl ConnectionHandle { } } - pub async fn client(&self) -> PyResult { + pub async fn connection(&self) -> PyResult { self.connection_registry - .client(self.origin.clone()) + .connection(self.origin.clone()) .await .map_err(to_py_err) } + pub async fn client(&self) -> PyResult { + Ok(self.connection().await?.client) + } + pub fn origin(&self) -> &re_uri::Origin { &self.origin } @@ -66,6 +73,29 @@ impl ConnectionHandle { }) } + #[tracing::instrument(level = "info", skip_all)] + pub fn rtt(&self, py: Python<'_>, num_pings: usize) -> PyResult { + wait_for_future(py, async { + self.client().await?.rtt(num_pings).await.map_err(to_py_err) + }) + } + + #[tracing::instrument(level = "info", skip_all)] + pub fn bandwidth_bytes_per_sec( + &self, + py: Python<'_>, + num_bytes: u64, + rtt: std::time::Duration, + ) -> PyResult> { + wait_for_future(py, async { + self.client() + .await? + .bandwidth_bytes_per_sec(num_bytes, rtt) + .await + .map_err(to_py_err) + }) + } + #[tracing::instrument(level = "info", skip_all)] pub fn find_entries(&self, py: Python<'_>, filter: EntryFilter) -> PyResult> { wait_for_future(py, async { @@ -93,8 +123,8 @@ impl ConnectionHandle { &self, py: Python<'_>, entry_id: EntryId, - entry_details_update: re_protos::cloud::v1alpha1::ext::EntryDetailsUpdate, - ) -> PyResult { + entry_details_update: cloud_ext::EntryDetailsUpdate, + ) -> PyResult { wait_for_future(py, async { self.client() .await? @@ -156,7 +186,7 @@ impl ConnectionHandle { .await .map_err(to_py_err)? .iter() - .map(|id| id.id.clone()) + .map(|id| id.to_string()) .collect::>()) }) } @@ -207,6 +237,22 @@ impl ConnectionHandle { }) } + #[tracing::instrument(level = "info", skip_all)] + pub fn update_table( + &self, + py: Python<'_>, + entry_id: EntryId, + table_details: TableDetails, + ) -> PyResult { + wait_for_future(py, async { + self.client() + .await? + .update_table_entry(entry_id, table_details) + .await + .map_err(to_py_err) + }) + } + #[tracing::instrument(level = "info", skip_all)] pub fn write_table( &self, @@ -263,24 +309,24 @@ impl ConnectionHandle { py: Python<'_>, dataset_id: EntryId, recording_uris: Vec, - recording_layers: Vec, + recording_layers: Vec, on_duplicate: IfDuplicateBehavior, - ) -> PyResult> { + ) -> PyResult<(Option, Vec)> { let last_layer = recording_layers .last() .cloned() - .unwrap_or_else(|| DataSource::DEFAULT_LAYER.to_owned()); - - let data_sources = recording_uris - .iter() - .zip( - recording_layers - .into_iter() - .chain(std::iter::repeat_with(|| last_layer.clone())), - ) - .map(|(url, layer)| DataSource::new_rrd_layer(layer, url)) - .collect::, _>>() - .map_err(to_py_err)?; + .unwrap_or_else(LayerName::base); + + let data_sources = std::iter::zip( + &recording_uris, + std::iter::chain( + recording_layers, + std::iter::repeat_with(|| last_layer.clone()), + ), + ) + .map(|(url, layer)| DataSource::new_rrd_layer(layer, url)) + .try_collect() + .map_err(to_py_err)?; wait_for_future(py, async { self.client() @@ -293,11 +339,7 @@ impl ConnectionHandle { /// Unregisters segments and layers from the dataset. /// - /// Excluding IO errors, this will always succeed as long the target dataset exists. - /// Corollary: unregistering data that doesn't exist is a no-op. - /// - /// This always returns a subset of the data from `ScanDatasetManifest`, and therefore the data will - /// also follow the schema returned by [`Self::get_dataset_manifest_schema`]. + /// This is an asynchronous operation, and returns a list of task ids. /// /// This method acts as a *product* filter: /// * empty `segments_to_drop` + empty `layers_to_drop`: invalid argument error @@ -314,10 +356,10 @@ impl ConnectionHandle { &self, py: Python<'_>, dataset_id: EntryId, - segments_to_drop: Vec, - layers_to_drop: Vec, + segments_to_drop: Vec, + layers_to_drop: Vec, force: bool, - ) -> PyResult> { + ) -> PyResult<(Option, Vec)> { wait_for_future(py, async { self.client() .await? @@ -338,9 +380,9 @@ impl ConnectionHandle { py: Python<'_>, dataset_id: EntryId, recordings_prefix: String, - recordings_layer: String, + recordings_layer: LayerName, on_duplicate: IfDuplicateBehavior, - ) -> PyResult> { + ) -> PyResult<(Option, Vec)> { let data_source = DataSource::new_rrd_layer_prefix(recordings_layer, recordings_prefix) .map_err(to_py_err)?; let data_sources = vec![data_source]; @@ -355,7 +397,7 @@ impl ConnectionHandle { } #[tracing::instrument(level = "info", skip_all)] - #[expect(clippy::fn_params_excessive_bools, clippy::too_many_arguments)] + #[expect(clippy::fn_params_excessive_bools)] pub fn do_maintenance( &self, py: Python<'_>, @@ -450,65 +492,38 @@ impl ConnectionHandle { .try_into() .map_err(to_py_err)?; - // TODO(andrea): all this column unwrapping is a bit hideous. Maybe the idea of returning a dataframe rather - // than a nicely typed object should be revisited. - - let schema = item.schema(); - if !schema.contains(&QueryTasksResponse::schema()) { - let err = invalid_schema!(QueryTasksResponse); - let err = ApiError::deserialization_with_source( - trace_id, - err, - "failed waiting for tasks done: received item with invalid schema", - ); - return Err(to_py_err(err)); - } - - let col_indices = [ - QueryTasksResponse::FIELD_TASK_ID, - QueryTasksResponse::FIELD_EXEC_STATUS, - QueryTasksResponse::FIELD_MSGS, - ] - .iter() - .map(|name| schema.index_of(name)) - .collect::, _>>() - .map_err(|err| { + let on_err = |err| { to_py_err(ApiError::deserialization_with_source( trace_id, err, - "failed waiting for tasks done: missing column on item", + "failed waiting for tasks done: received item with invalid schema", )) - })?; - - let projected = item.project(&col_indices).map_err(to_py_err)?; - - let (task_ids, statuses, msgs) = { - ( - projected - .column(0) - .try_downcast_array_ref::() - .map_err(to_py_err)?, - projected - .column(1) - .try_downcast_array_ref::() - .map_err(to_py_err)?, - projected - .column(2) - .try_downcast_array_ref::() - .map_err(to_py_err)?, - ) }; - for i in 0..projected.num_rows() { - if statuses.value(i) != "success" { - let err = format!("task {}: {}", task_ids.value(i), msgs.value(i)); - errors.push(err); + let task_ids = QueryTasksDataframe::COLUMN_TASK_ID + .extract(&item) + .map_err(&on_err)?; + let statuses = QueryTasksDataframe::COLUMN_EXEC_STATUS + .extract(&item) + .map_err(&on_err)?; + let msgs = QueryTasksDataframe::COLUMN_MSGS + .extract(&item) + .map_err(&on_err)?; + + for (task_id, status, msg) in itertools::izip!(&task_ids, &statuses, &msgs) { + if status != "success" { + errors.push(format!("task {task_id}: {}", msg.unwrap_or_default())); } } } if !errors.is_empty() { + // Put the trace-id early, before the (potentially long) list of errors. + let trace_id_line = match trace_id { + Some(trace_id) => format!("\nTask-completion query trace-id: {trace_id}"), + None => String::new(), + }; let msg = format!( - "all tasks completed, but the following errors occurred:\n{}", + "All tasks completed, but the following errors occurred.{trace_id_line}\n\n{}", errors.join("\n") ); Err(PyValueError::new_err(msg)) @@ -555,7 +570,10 @@ impl ConnectionHandle { .map(|ident| ident.to_string()) .collect(); - let query = query_from_query_expression(query_expression); + let query = query_from_query_expression( + query_expression, + query_expression.sparse_fill_strategy != SparseFillStrategy::None, + ); let request = QueryDatasetRequest { segment_ids: segment_ids @@ -571,8 +589,8 @@ impl ConnectionHandle { query: Some(query), scan_parameters: Some(ScanParameters { columns: vec![ - QueryDatasetResponse::FIELD_CHUNK_SEGMENT_ID.to_owned(), - QueryDatasetResponse::FIELD_CHUNK_ID.to_owned(), + QueryDatasetDataframe::COLUMN_CHUNK_SEGMENT_ID_NAME.to_owned(), + QueryDatasetDataframe::COLUMN_CHUNK_ID_NAME.to_owned(), ], ..Default::default() }), @@ -584,26 +602,20 @@ impl ConnectionHandle { .client() .await? .inner() - .query_dataset( - tonic::Request::new(request.into()) - .with_entry_id(dataset_id) - .map_err(to_py_err)?, - ) + .query_dataset(tonic::Request::new(request.into()).with_entry_id(dataset_id)) .await .map_err(to_py_err)? .into_inner(); // TODO(jleibs): Make this streaming - let record_batches: Result, PyErr> = response_stream + let record_batches: Vec = response_stream .collect::, _>>() .await .map_err(to_py_err)? .into_iter() .filter_map(|response| response.data) .map(|dataframe_part| dataframe_part.try_into().map_err(to_py_err)) - .collect(); - - let record_batches = record_batches?; + .try_collect()?; // TODO(jleibs): Still need a better pattern for getting these schemas let first = record_batches diff --git a/rerun_py/src/catalog/dataframe_rendering.rs b/rerun_py/src/catalog/dataframe_rendering.rs index affa3397dffa..f38cb003067d 100644 --- a/rerun_py/src/catalog/dataframe_rendering.rs +++ b/rerun_py/src/catalog/dataframe_rendering.rs @@ -5,7 +5,12 @@ use comfy_table::Table; use pyo3::{Bound, PyAny, PyResult, pyclass, pymethods}; use re_arrow_util::{RecordBatchFormatOpts, format_record_batch_opts}; -#[pyclass(eq, name = "RerunHtmlTable", module = "rerun_bindings.rerun_bindings")] +#[pyclass( + eq, + from_py_object, + name = "RerunHtmlTable", + module = "rerun_bindings.rerun_bindings" +)] #[derive(Clone, PartialEq, Eq)] pub struct PyRerunHtmlTable { max_width: Option, diff --git a/rerun_py/src/catalog/datafusion_catalog.rs b/rerun_py/src/catalog/datafusion_catalog.rs index ead9220549c4..c44ede4384e0 100644 --- a/rerun_py/src/catalog/datafusion_catalog.rs +++ b/rerun_py/src/catalog/datafusion_catalog.rs @@ -2,51 +2,53 @@ use std::sync::Arc; use crate::catalog::table_provider_adapter::ffi_logical_codec_from_pycapsule; use crate::utils::get_tokio_runtime; -use datafusion::catalog::CatalogProvider; -use datafusion_ffi::catalog_provider::FFI_CatalogProvider; +use datafusion::catalog::CatalogProviderList; +use datafusion_ffi::catalog_provider_list::FFI_CatalogProviderList; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult, pyclass, pymethods}; -use re_datafusion::RedapCatalogProvider; -use re_redap_client::ConnectionClient; +use re_datafusion::RedapCatalogProviderList; +use re_redap_client::{ConnectionAnalyticsExporter, ConnectionClient}; +/// PyO3 wrapper exposing a [`RedapCatalogProviderList`] to a Python `datafusion.SessionContext` +/// via `register_catalog_provider_list(...)`. #[pyclass( frozen, eq, - name = "DataFusionCatalog", + name = "DataFusionCatalogList", module = "rerun_bindings.rerun_bindings" )] -pub(crate) struct PyDataFusionCatalogProvider { - pub provider: Arc, +pub(crate) struct PyDataFusionCatalogProviderList { + pub provider: Arc, } -impl PartialEq for PyDataFusionCatalogProvider { +impl PartialEq for PyDataFusionCatalogProviderList { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.provider, &other.provider) } } -impl PyDataFusionCatalogProvider { - pub fn new(name: Option, client: ConnectionClient) -> Self { +impl PyDataFusionCatalogProviderList { + pub fn new(client: ConnectionClient, analytics: Option) -> Self { let runtime = get_tokio_runtime().handle().clone(); - let provider = Arc::new(RedapCatalogProvider::new(name.as_deref(), client, runtime)); + let provider = Arc::new(RedapCatalogProviderList::new(client, runtime, analytics)); Self { provider } } } #[pymethods] // NOLINT: ignore[py-mthd-str] -impl PyDataFusionCatalogProvider { - /// Returns a DataFusion catalog provider capsule. - fn __datafusion_catalog_provider__<'py>( +impl PyDataFusionCatalogProviderList { + /// Returns a DataFusion catalog provider list capsule. + fn __datafusion_catalog_provider_list__<'py>( &self, session: &Bound<'py, PyAny>, ) -> PyResult> { - let capsule_name = cr"datafusion_catalog_provider".into(); + let capsule_name = cr"datafusion_catalog_provider_list".into(); - let provider = Arc::clone(&self.provider) as Arc; + let provider = Arc::clone(&self.provider) as Arc; let runtime = get_tokio_runtime().handle().clone(); let codec = ffi_logical_codec_from_pycapsule(session)?; - let provider = FFI_CatalogProvider::new_with_ffi_codec(provider, Some(runtime), codec); + let provider = FFI_CatalogProviderList::new_with_ffi_codec(provider, Some(runtime), codec); PyCapsule::new(session.py(), provider, Some(capsule_name)) } diff --git a/rerun_py/src/catalog/dataset_entry.rs b/rerun_py/src/catalog/dataset_entry.rs index 1e8053d31012..fc06bcbeee46 100644 --- a/rerun_py/src/catalog/dataset_entry.rs +++ b/rerun_py/src/catalog/dataset_entry.rs @@ -1,36 +1,25 @@ use std::sync::Arc; -use arrow::array::{RecordBatch, RecordBatchOptions, StringArray}; -use arrow::datatypes::{Field, Schema as ArrowSchema}; +use arrow::datatypes::Schema as ArrowSchema; use arrow::pyarrow::PyArrowType; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::exceptions::{PyOverflowError, PyRuntimeError, PyValueError}; use pyo3::types::PyAnyMethods as _; -use pyo3::{Bound, Py, PyAny, PyErr, PyRef, PyRefMut, PyResult, Python, pyclass, pymethods}; -use re_chunk_store::{ChunkStore, ChunkStoreHandle}; -use re_datafusion::{DatasetManifestProvider, SearchResultsTableProvider, SegmentTableProvider}; -use re_log_types::{EntryId, StoreId, StoreKind}; -use re_protos::cloud::v1alpha1::ext::{ - DatasetDetails, DatasetEntry, EntryDetails, IndexProperties, -}; -use re_protos::cloud::v1alpha1::{ - CreateIndexRequest, DeleteIndexesRequest, IndexConfig, IndexQueryProperties, - InvertedIndexQuery, ListIndexesRequest, SearchDatasetRequest, VectorIndexQuery, - index_query_properties, -}; -use re_protos::common::v1alpha1::ext::{DatasetHandle, IfDuplicateBehavior}; -use re_protos::headers::RerunHeadersInjectorExt as _; -use re_redap_client::fetch_chunks_response_to_chunk_and_segment_id; -use re_sorbet::{SorbetColumnDescriptors, TimeColumnSelector}; -use tokio_stream::StreamExt as _; +use pyo3::{Bound, Py, PyAny, PyRef, PyRefMut, PyResult, Python, pyclass, pymethods}; +use re_chunk_store::LazyStore; +use re_datafusion::{DatasetManifestProvider, SegmentTableProvider}; +use re_log_types::EntryId; +use re_protos::cloud::v1alpha1::ext::{DatasetDetails, DatasetEntry, EntryDetails}; +use re_protos::common::v1alpha1::ext::{DatasetHandle, IfDuplicateBehavior, SegmentId}; +use re_redap_client::SegmentChunkProvider; +use re_sorbet::SorbetColumnDescriptors; +use re_types_core::LayerName; use super::registration_handle::PyRegistrationHandleInternal; -use super::{ - PyCatalogClientInternal, PyEntryDetails, PyIndexConfig, PyIndexingResult, - PyTableProviderAdapterInternal, VectorDistanceMetricLike, VectorLike, to_py_err, -}; +use super::{PyCatalogClientInternal, PyEntryDetails, PyTableProviderAdapterInternal, to_py_err}; +use crate::catalog::PySchemaInternal; use crate::catalog::entry::set_entry_name; -use crate::catalog::{AnyComponentColumn, PyIndexColumnSelector, PySchemaInternal}; -use crate::recording::PyRecordingInternal; +use crate::catalog::unregistration_handle::PyUnregistrationHandleInternal; +use crate::chunk_stream::lazy_store::PyLazyStoreInternal; use crate::trace_context::read_trace_context_from_python; use crate::utils::wait_for_future; @@ -126,6 +115,42 @@ impl PyDatasetEntryInternal { Some(Py::new(py, Self::new(client, dataset_entry))).transpose() } + /// The associated asset dataset, if any. + fn asset_dataset(self_: PyRef<'_, Self>, py: Python<'_>) -> PyResult>> { + let _span = read_trace_context_from_python(py, "DatasetEntry.asset_dataset").entered(); + let Some(asset_dataset_entry_id) = self_.dataset_details.asset_dataset else { + return Ok(None); + }; + + let client = self_.client.clone_ref(py); + let connection = self_.client.borrow(py).connection().clone(); + + let dataset_entry = connection.read_dataset(py, asset_dataset_entry_id)?; + + Some(Py::new(py, Self::new(client, dataset_entry))).transpose() + } + + /// Ask the server to create a missing asset dataset. + /// + /// Datasets created before asset datasets were introduced don't have one until their entry + /// is next updated, so send the server an update carrying the current details. + fn _ensure_asset_dataset(mut self_: PyRefMut<'_, Self>, py: Python<'_>) -> PyResult<()> { + let _span = + read_trace_context_from_python(py, "DatasetEntry._ensure_asset_dataset").entered(); + if self_.dataset_details.asset_dataset.is_some() { + return Ok(()); + } + + let connection = self_.client.borrow(py).connection().clone(); + + let result = + connection.update_dataset(py, self_.entry_details.id, self_.dataset_details.clone())?; + + self_.dataset_details = result.dataset_details; + + Ok(()) + } + /// The default blueprint segment ID for this dataset, if any. fn default_blueprint_segment_id(self_: PyRef<'_, Self>) -> Option { self_ @@ -159,6 +184,41 @@ impl PyDatasetEntryInternal { Ok(()) } + /// The default segment table blueprint segment ID for this dataset, if any. + fn default_segment_table_blueprint_segment_id(self_: PyRef<'_, Self>) -> Option { + self_ + .dataset_details + .default_segment_table_blueprint_segment + .as_ref() + .map(ToString::to_string) + } + + /// Set the default segment table blueprint segment ID for this dataset. + /// + /// Pass `None` to clear the blueprint. This fails if the change cannot be made to the remote server. + #[pyo3(signature = (segment_id))] + fn set_default_segment_table_blueprint_segment_id( + mut self_: PyRefMut<'_, Self>, + py: Python<'_>, + segment_id: Option, + ) -> PyResult<()> { + let _span = read_trace_context_from_python( + py, + "DatasetEntry.set_default_segment_table_blueprint_segment_id", + ) + .entered(); + let connection = self_.client.borrow(py).connection().clone(); + + let mut dataset_details = self_.dataset_details.clone(); + dataset_details.default_segment_table_blueprint_segment = segment_id.map(Into::into); + + let result = connection.update_dataset(py, self_.entry_details.id, dataset_details)?; + + self_.dataset_details = result.dataset_details; + + Ok(()) + } + /// Return the schema of the data contained in the dataset. fn schema(self_: PyRef<'_, Self>) -> PyResult { let _span = read_trace_context_from_python(self_.py(), "DatasetEntry.schema").entered(); @@ -232,13 +292,14 @@ impl PyDatasetEntryInternal { /// timeline: str | None /// The name of the timeline to display. /// - /// start: int | datetime | None + /// start: int | datetime | timedelta | None /// The start selected time for the segment. - /// Integer for ticks, or datetime/nanoseconds for timestamps. + /// Integer for ticks, datetime/nanoseconds for timestamps, or timedelta for durations. /// - /// end: int | datetime | None + /// end: int | datetime | timedelta | None /// The end selected time for the segment. - /// Integer for ticks, or datetime/nanoseconds for timestamps. + /// Integer for ticks, datetime/nanoseconds for timestamps, or timedelta for durations. + /// If omitted, no time range selection is emitted (only the `#when` cursor). /// /// Examples /// -------- @@ -274,42 +335,46 @@ impl PyDatasetEntryInternal { )); } - // Convert Python objects to i64 - let start_i64 = start + // Convert Python objects to typed time cells (int → sequence, datetime → timestamp) + let start_cell = start .as_ref() - .map(|s| py_object_to_i64(py, s)) + .map(|s| py_object_to_time_cell(py, s)) + .transpose()?; + let end_cell = end + .as_ref() + .map(|e| py_object_to_time_cell(py, e)) + .transpose()?; + + let timeline = timeline + .map(|timeline| { + re_chunk::TimelineName::try_new(timeline) + .map_err(|err| PyValueError::new_err(err.to_string())) + }) .transpose()?; - let end_i64 = end.as_ref().map(|e| py_object_to_i64(py, e)).transpose()?; Ok(re_uri::DatasetSegmentUri { origin: connection.origin().clone(), dataset_id: self_.entry_details.id.id, - segment_id, - - //TODO(ab): add support for this + segment_id: SegmentId::from(segment_id), fragment: re_uri::Fragment { selection: None, when: timeline.map(|timeline| { ( - re_chunk::TimelineName::new(timeline), - re_sdk::TimeCell::new( - re_log_types::TimeType::TimestampNs, - start_i64 - .map(|start| start.try_into().expect("start time must be valid")) - .unwrap_or(re_log_types::NonMinI64::MIN), - ), + timeline, + start_cell.unwrap_or_else(|| { + re_sdk::TimeCell::new( + re_log_types::TimeType::TimestampNs, + re_log_types::NonMinI64::MIN, + ) + }), ) }), - time_selection: timeline.map(|timeline| re_uri::TimeSelection { - timeline: re_chunk::Timeline::new_timestamp(timeline), - range: re_log_types::AbsoluteTimeRange::new( - start_i64 - .map(|start| start.try_into().expect("start time must be valid")) - .unwrap_or(re_log_types::NonMinI64::MIN), - end_i64 - .map(|end| end.try_into().expect("end time must be valid")) - .unwrap_or(re_log_types::NonMinI64::MAX), - ), + time_selection: Option::zip(end_cell, timeline).map(|(end, timeline)| { + let start = start_cell.unwrap_or(end); + re_uri::TimeSelection { + timeline: re_chunk::Timeline::new(timeline, start.typ()), + range: re_log_types::AbsoluteTimeRange::new(start.value, end.value), + } }), }, } @@ -345,7 +410,12 @@ impl PyDatasetEntryInternal { let on_duplicate = parse_on_duplicate(on_duplicate)?; let _span = read_trace_context_from_python(py, "DatasetEntry.register").entered(); - let results = connection.register_with_dataset( + let recording_layers = recording_layers + .into_iter() + .map(LayerName::try_new) + .collect::, _>>() + .map_err(to_py_err)?; + let (request_trace_id, results) = connection.register_with_dataset( py, self_.entry_details.id, recording_uris, @@ -356,6 +426,7 @@ impl PyDatasetEntryInternal { Ok(PyRegistrationHandleInternal::new( self_.client.clone_ref(py), results, + request_trace_id, )) } @@ -394,12 +465,18 @@ impl PyDatasetEntryInternal { segments_to_drop: Vec, layers_to_drop: Vec, force: bool, - ) -> PyResult<()> { + ) -> PyResult { let py = self_.py(); let _span = read_trace_context_from_python(py, "DatasetEntry.unregister").entered(); let connection = self_.client.borrow(py).connection().clone(); - let _results = connection.unregister_from_dataset( + let segments_to_drop = segments_to_drop.into_iter().map(SegmentId::new).collect(); + let layers_to_drop = layers_to_drop + .into_iter() + .map(LayerName::try_new) + .collect::, _>>() + .map_err(to_py_err)?; + let (request_trace_id, task_ids) = connection.unregister_from_dataset( py, self_.entry_details.id, segments_to_drop, @@ -407,7 +484,11 @@ impl PyDatasetEntryInternal { force, )?; - Ok(()) + Ok(PyUnregistrationHandleInternal::new( + self_.client.clone_ref(py), + task_ids, + request_trace_id, + )) } /// Register all RRDs under a given prefix to the dataset and return a handle to the tasks. @@ -442,429 +523,45 @@ impl PyDatasetEntryInternal { let connection = self_.client.borrow(py).connection().clone(); let on_duplicate = parse_on_duplicate(on_duplicate)?; - let results = connection.register_with_dataset_prefix( + let (request_trace_id, results) = connection.register_with_dataset_prefix( py, self_.entry_details.id, recordings_prefix, - layer_name, + LayerName::try_new(layer_name).map_err(to_py_err)?, on_duplicate, )?; Ok(PyRegistrationHandleInternal::new( self_.client.clone_ref(py), results, + request_trace_id, )) } - /// Download a segment from the dataset. - fn download_segment( - self_: PyRef<'_, Self>, - segment_id: String, - ) -> PyResult { - let _span = - read_trace_context_from_python(self_.py(), "DatasetEntry.download_segment").entered(); - let catalog_client = self_.client.borrow(self_.py()); - let connection = catalog_client.connection(); - let dataset_id = self_.entry_details.id; - let dataset_name = self_.entry_details.name.clone(); - - let store: PyResult = wait_for_future(self_.py(), async move { - let mut client = connection.client().await?; - let response_stream = client - .fetch_segment_chunks_by_query(re_redap_client::SegmentQueryParams { - dataset_id, - segment_id: segment_id.clone().into(), - include_static_data: true, - include_temporal_data: true, - query: None, - generate_direct_urls: false, - }) - .await - .map_err(to_py_err)?; - - let mut chunks_stream = fetch_chunks_response_to_chunk_and_segment_id(response_stream); - - let store_id = StoreId::new( - StoreKind::Recording, - dataset_name.to_string(), - segment_id.clone(), - ); - let mut store = ChunkStore::new(store_id, Default::default()); - - while let Some(chunks) = chunks_stream.next().await { - for chunk in chunks.map_err(to_py_err)? { - let (chunk, chunk_segment_id) = chunk; - - if Some(&segment_id) != chunk_segment_id.as_ref() { - re_log::warn!( - expected = segment_id, - got = chunk_segment_id, - "unexpected segment ID in chunk stream, this is a bug" - ); - } - store - .insert_chunk(&std::sync::Arc::new(chunk)) - .map_err(to_py_err)?; - } - } - - Ok(store) - }); - - let handle = ChunkStoreHandle::new(store?); - - Ok(PyRecordingInternal { - store: handle, - store_info: None, - }) - } - - // TODO(RR-2824): we should have a generic `create_index(PyIndexConfig)` - - /// Create a full-text search index on the given column. - #[pyo3(signature = ( - *, - column, - time_index, - store_position = false, - base_tokenizer = "simple", - ))] - fn create_fts_search_index( - self_: PyRef<'_, Self>, - column: AnyComponentColumn, - time_index: PyIndexColumnSelector, - store_position: bool, - base_tokenizer: &str, - ) -> PyResult<()> { - let _span = - read_trace_context_from_python(self_.py(), "DatasetEntry.create_fts_search_index") - .entered(); - let connection = self_.client.borrow(self_.py()).connection().clone(); - let dataset_id = self_.entry_details.id; - let time_selector: TimeColumnSelector = time_index.into(); - - let schema = Self::fetch_schema(&self_)?; - let component_descriptor = schema.column_for_selector(column)?; - - let properties = IndexProperties::Inverted { - store_position, - base_tokenizer: base_tokenizer.into(), - }; - - let request = CreateIndexRequest { - config: Some(IndexConfig { - properties: Some(properties.into()), - column: Some(component_descriptor.0.into()), - time_index: Some(time_selector.timeline.into()), - }), - }; - - wait_for_future(self_.py(), async { - connection - .client() - .await? - .inner() - .create_index( - tonic::Request::new(request) - .with_entry_id(dataset_id) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?, - ) - .await - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - - Ok(()) - }) - } - - /// Create a vector index on the given column. - /// - /// This will enable indexing and build the vector index over all existing values - /// in the specified component column. - /// - /// Results can be retrieved using the `search_vector` API, which will include - /// the time-point on the indexed timeline. + /// Open a remote segment as a [`LazyStore`][rerun.experimental.LazyStore]. /// - /// Only one index can be created per component column -- executing this a second - /// time for the same component column will replace the existing index. - /// - /// Parameters - /// ---------- - /// column : AnyComponentColumn - /// The component column to create the index on. - /// time_index : IndexColumnSelector - /// Which timeline this index will map to. - /// target_partition_num_rows : int | None - /// The target size (in number of rows) for each partition. - /// The underlying indexer (lance) will pick a default when no value - /// is specified - today this is 8192. It will also cap the - /// maximum number of partitions independently of this setting - currently - /// 4096. - /// num_sub_vectors : int - /// The number of sub-vectors to use when building the index. - /// distance_metric : VectorDistanceMetricLike - /// The distance metric to use for the index. ("L2", "Cosine", "Dot", "Hamming") - #[pyo3(signature = ( - *, - column, - time_index, - target_partition_num_rows = None, - num_sub_vectors = 16, - distance_metric = VectorDistanceMetricLike::VectorDistanceMetric(crate::catalog::PyVectorDistanceMetric::Cosine), - ))] - fn create_vector_search_index( - self_: PyRef<'_, Self>, - column: AnyComponentColumn, - time_index: PyIndexColumnSelector, - target_partition_num_rows: Option, - num_sub_vectors: u32, - distance_metric: VectorDistanceMetricLike, - ) -> PyResult { - let _span = - read_trace_context_from_python(self_.py(), "DatasetEntry.create_vector_search_index") - .entered(); - let connection = self_.client.borrow(self_.py()).connection().clone(); - let dataset_id = self_.entry_details.id; - - let time_selector: TimeColumnSelector = time_index.into(); - - let schema = Self::fetch_schema(&self_)?; - let component_descriptor = schema.column_for_selector(column)?; - - let distance_metric: re_protos::cloud::v1alpha1::VectorDistanceMetric = - distance_metric.try_into()?; - - let properties = IndexProperties::VectorIvfPq { - target_partition_num_rows, - num_sub_vectors, - metric: distance_metric, - }; - - let config = re_protos::cloud::v1alpha1::ext::IndexConfig { - time_index: time_selector.timeline, - column: component_descriptor.0.clone().into(), - properties: properties.clone(), - }; - - let request = CreateIndexRequest { - config: Some(IndexConfig { - properties: Some(properties.into()), - column: Some(component_descriptor.0.into()), - time_index: Some(time_selector.timeline.into()), - }), - }; - - wait_for_future(self_.py(), async { - let result = connection - .client() - .await? - .inner() - .create_index( - tonic::Request::new(request) - .with_entry_id(dataset_id) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?, - ) - .await - .map_err(|err| PyRuntimeError::new_err(err.to_string()))? - .into_inner(); - - Ok(PyIndexingResult { - index: config.into(), - statistics_json: result.statistics_json, - debug_info: result.debug_info, - }) - }) - } - - /// List all user-defined indexes in this dataset. - fn list_search_indexes(self_: PyRef<'_, Self>) -> PyResult> { - let _span = read_trace_context_from_python(self_.py(), "DatasetEntry.list_search_indexes") - .entered(); - let connection = self_.client.borrow(self_.py()).connection().clone(); - let dataset_id = self_.entry_details.id; - - let request = ListIndexesRequest {}; - - wait_for_future(self_.py(), async { - let result = connection - .client() - .await? - .inner() - .list_indexes( - tonic::Request::new(request) - .with_entry_id(dataset_id) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?, - ) - .await - .map_err(|err| PyRuntimeError::new_err(err.to_string()))? - .into_inner(); - - let indexes: Result, PyErr> = result - .indexes - .into_iter() - .map(|index| { - let index = re_protos::cloud::v1alpha1::ext::IndexConfig::try_from(index)?; - Ok(PyIndexConfig::from(index)) - }) - .collect(); - - Ok(itertools::izip!(indexes?, result.statistics_json) - .map(|(index, statistics_json)| PyIndexingResult { - index, - statistics_json, - debug_info: None, - }) - .collect()) - }) - } - - /// Deletes all user-defined indexes for the specified column. - // - // TODO(RR-2824): this should also be capable of accepting a `PyIndexConfig` directly. - fn delete_search_indexes( - self_: PyRef<'_, Self>, - column: AnyComponentColumn, - ) -> PyResult> { - let _span = - read_trace_context_from_python(self_.py(), "DatasetEntry.delete_search_indexes") - .entered(); - let connection = self_.client.borrow(self_.py()).connection().clone(); - let dataset_id = self_.entry_details.id; - - let schema = Self::fetch_schema(&self_)?; - let component_descriptor = schema.column_for_selector(column)?; - - let request = DeleteIndexesRequest { - column: Some(component_descriptor.0.into()), - }; - - wait_for_future(self_.py(), async { - let result = connection - .client() - .await? - .inner() - .delete_indexes( - tonic::Request::new(request) - .with_entry_id(dataset_id) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?, - ) - .await - .map_err(|err| PyRuntimeError::new_err(err.to_string()))? - .into_inner(); - - let indexes: Result, PyErr> = result - .indexes - .into_iter() - .map(|index| { - let index = re_protos::cloud::v1alpha1::ext::IndexConfig::try_from(index)?; - Ok(PyIndexConfig::from(index)) - }) - .collect(); - - indexes - }) - } - - /// Search the dataset using a full-text search query. - fn search_fts( - self_: PyRef<'_, Self>, - query: String, - column: AnyComponentColumn, - ) -> PyResult> { - let py = self_.py(); - let _span = read_trace_context_from_python(py, "DatasetEntry.search_fts").entered(); - let connection = self_.client.borrow(py).connection().clone(); - let dataset_id = self_.entry_details.id; - - let schema = Self::fetch_schema(&self_)?; - let component_descriptor = schema.column_for_selector(column)?; - - let schema = arrow::datatypes::Schema::new_with_metadata( - vec![Field::new("items", arrow::datatypes::DataType::Utf8, false)], - Default::default(), - ); - - let query = RecordBatch::try_new_with_options( - Arc::new(schema), - vec![Arc::new(StringArray::from_iter_values([query]))], - &RecordBatchOptions::default().with_row_count(Some(1)), - ) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - - let request = SearchDatasetRequest { - column: Some(component_descriptor.0.into()), - properties: Some(IndexQueryProperties { - props: Some( - re_protos::cloud::v1alpha1::index_query_properties::Props::Inverted( - InvertedIndexQuery {}, - ), - ), - }), - query: Some(query.into()), - scan_parameters: None, - }; - - let provider = wait_for_future(py, async move { - SearchResultsTableProvider::new(connection.client().await?, dataset_id, request) - .map_err(to_py_err)? - .into_provider() - .await - .map_err(to_py_err) - })?; - - let table = PyTableProviderAdapterInternal::new(provider, false); - - let client = self_.client.borrow(py); - let ctx = client.ctx(py)?; - let ctx = ctx.bind(py); - drop(client); - - ctx.call_method1("read_table", (table,)) - } - - /// Search the dataset using a vector search query. - fn search_vector<'py>( - self_: PyRef<'py, Self>, - query: VectorLike<'_>, - column: AnyComponentColumn, - top_k: u32, - ) -> PyResult> { + /// One round-trip on construction (the manifest); chunks are fetched on + /// demand. + fn segment_store(self_: PyRef<'_, Self>, segment_id: String) -> PyResult { let py = self_.py(); - let _span = read_trace_context_from_python(py, "DatasetEntry.search_vector").entered(); + let _span = read_trace_context_from_python(py, "DatasetEntry.segment_store").entered(); let connection = self_.client.borrow(py).connection().clone(); let dataset_id = self_.entry_details.id; - - let schema = Self::fetch_schema(&self_)?; - let component_descriptor = schema.column_for_selector(column)?; - - let query = query.to_record_batch()?; - - let request = SearchDatasetRequest { - column: Some(component_descriptor.0.into()), - properties: Some(IndexQueryProperties { - props: Some(index_query_properties::Props::Vector(VectorIndexQuery { - top_k: Some(top_k), - })), - }), - query: Some(query.into()), - scan_parameters: None, - }; - - let provider = wait_for_future(py, async move { - SearchResultsTableProvider::new(connection.client().await?, dataset_id, request) - .map_err(to_py_err)? - .into_provider() - .await - .map_err(to_py_err) + let segment_id = SegmentId::from(segment_id); + + let provider = wait_for_future(py, async { + SegmentChunkProvider::try_new( + connection.connection_registry().clone(), + connection.origin().clone(), + dataset_id, + segment_id, + ) + .await + .map_err(to_py_err) })?; - let table = PyTableProviderAdapterInternal::new(provider, false); - - let client = self_.client.borrow(py); - let ctx = client.ctx(py)?; - let ctx = ctx.bind(py); - drop(client); - - ctx.call_method1("read_table", (table,)) + let lazy = LazyStore::new(Arc::new(provider)); + Ok(PyLazyStoreInternal::new(lazy)) } /// Perform maintenance tasks on the datasets. @@ -1029,3 +726,27 @@ fn py_object_to_i64(py: Python<'_>, obj: &Bound<'_, PyAny>) -> PyResult { let converted = int_builtin.call1((obj,))?; converted.extract::() } + +/// Convert a Python object to a [`re_sdk::TimeCell`], inferring the time type from the Python type. +/// +/// Plain `int` → [`TimeType::Sequence`]; `datetime.timedelta` → [`TimeType::DurationNs`]; +/// anything else (datetime, `numpy.datetime64`, …) → [`TimeType::TimestampNs`] via +/// [`py_object_to_i64`]. +fn py_object_to_time_cell(py: Python<'_>, obj: &Bound<'_, PyAny>) -> PyResult { + use re_log_types::TimeType; + + if let Ok(value) = obj.extract::() { + return Ok(re_sdk::TimeCell::new(TimeType::Sequence, value)); + } + + if let Ok(duration) = obj.extract::() { + let nanos = duration.num_nanoseconds().ok_or_else(|| { + PyOverflowError::new_err("datetime.timedelta is out of nanosecond range") + })?; + + return Ok(re_sdk::TimeCell::new(TimeType::DurationNs, nanos)); + } + + let nanos = py_object_to_i64(py, obj)?; + Ok(re_sdk::TimeCell::new(TimeType::TimestampNs, nanos)) +} diff --git a/rerun_py/src/catalog/dataset_view.rs b/rerun_py/src/catalog/dataset_view.rs index 5dfc59f3fd7b..a34dba521516 100644 --- a/rerun_py/src/catalog/dataset_view.rs +++ b/rerun_py/src/catalog/dataset_view.rs @@ -1,18 +1,20 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::sync::Arc; +#[cfg(feature = "perf_telemetry")] +use crate::trace_context::extract_trace_context_from_contextvar; use arrow::datatypes::Schema as ArrowSchema; use arrow::pyarrow::PyArrowType; use datafusion::catalog::TableProvider; +use itertools::Itertools as _; use pyo3::exceptions::PyValueError; use pyo3::prelude::PyAnyMethods as _; use pyo3::{Bound, Py, PyAny, PyRef, PyResult, Python, pyclass, pymethods}; use re_chunk_store::{QueryExpression, SparseFillStrategy, TimeInt, ViewContentsSelector}; use re_datafusion::DataframeQueryTableProvider; use re_log_types::{EntityPathFilter, ResolvedEntityPathFilter}; -#[cfg(feature = "perf_telemetry")] -use re_perf_telemetry::extract_trace_context_from_contextvar; use re_sorbet::{ColumnDescriptor, SorbetColumnDescriptors}; +use re_types_core::SegmentId; use crate::catalog::{ IndexValuesLike, PyDatasetEntryInternal, PySchemaInternal, PyTableProviderAdapterInternal, @@ -243,8 +245,8 @@ impl PyDatasetViewInternal { .map(|values_map| { values_map .into_iter() - .map(|(k, v)| v.to_index_values().map(|v| (k, v))) - .collect::, _>>() + .map(|(k, v)| v.to_index_values().map(|v| (SegmentId::from(k), v))) + .try_collect() }) .transpose()?; @@ -327,7 +329,7 @@ fn build_view_contents( } /// Build a table provider for dataframe queries with the given parameters. -#[expect(clippy::too_many_arguments, clippy::fn_params_excessive_bools)] +#[expect(clippy::fn_params_excessive_bools)] fn build_dataframe_query_table_provider( py: Python<'_>, dataset: &Py, @@ -337,7 +339,7 @@ fn build_dataframe_query_table_provider( include_semantically_empty_columns: bool, include_tombstone_columns: bool, fill_latest_at: bool, - using_index_values: Option>>, + using_index_values: Option>>, ) -> PyResult> { let dataset_ref = dataset.borrow(py); let dataset_id = dataset_ref.entry_id(); @@ -378,7 +380,12 @@ fn build_dataframe_query_table_provider( } else { re_chunk_store::StaticColumnSelection::Both }, - filtered_index: index.map(Into::into), + filtered_index: index + .map(|index| { + re_chunk::TimelineName::try_new(index) + .map_err(|err| PyValueError::new_err(err.to_string())) + }) + .transpose()?, filtered_index_range: None, filtered_index_values: None, using_index_values: None, @@ -407,6 +414,14 @@ fn build_dataframe_query_table_provider( let index_values = using_index_values.map(Arc::new); // Reuse the already-fetched schema so the provider skips its own `GetDatasetSchema` RPC. let arrow_schema = Some(schema); + + // Bind any active `query_metrics()` collectors to this query at plan + // construction time. Empty when no scope is open; the read traverses the + // Python `ContextVar` so only collectors from this thread/task are + // captured. Concurrent or unrelated queries elsewhere in the process are + // not affected. + let metrics_collectors = crate::query_metrics::active_metrics_collectors(py); + wait_for_future(py, async move { DataframeQueryTableProvider::new( connection.origin().clone(), @@ -418,6 +433,7 @@ fn build_dataframe_query_table_provider( arrow_schema, #[cfg(not(target_arch = "wasm32"))] trace_headers_opt, + metrics_collectors, ) .await }) diff --git a/rerun_py/src/catalog/entry.rs b/rerun_py/src/catalog/entry.rs index 40dc86f3b7c5..809d33f07d3c 100644 --- a/rerun_py/src/catalog/entry.rs +++ b/rerun_py/src/catalog/entry.rs @@ -4,12 +4,18 @@ use pyo3::exceptions::PyTypeError; use pyo3::{Py, PyErr, PyResult, Python, pyclass, pymethods}; use re_log_types::EntryId; use re_protos::cloud::v1alpha1::EntryKind; +use re_protos::cloud::v1alpha1::ext; use re_protos::cloud::v1alpha1::ext::EntryDetails; use crate::catalog::PyCatalogClientInternal; /// A unique identifier for an entry in the catalog. -#[pyclass(eq, name = "EntryId", module = "rerun_bindings.rerun_bindings")] +#[pyclass( + eq, + from_py_object, + name = "EntryId", + module = "rerun_bindings.rerun_bindings" +)] #[derive(Clone, PartialEq, Eq)] pub struct PyEntryId { pub id: EntryId, @@ -50,6 +56,7 @@ impl From for PyEntryId { /// The kinds of entries that can be stored in the catalog. #[pyclass( name = "EntryKind", + from_py_object, eq, eq_int, module = "rerun_bindings.rerun_bindings" @@ -70,6 +77,9 @@ pub enum PyEntryKind { #[pyo3(name = "BLUEPRINT_DATASET")] BlueprintDataset = 5, + + #[pyo3(name = "ASSET_DATASET")] + AssetDataset = 6, } // Enums don't need str @@ -97,6 +107,7 @@ impl TryFrom for PyEntryKind { EntryKind::Table => Ok(Self::Table), EntryKind::TableView => Ok(Self::TableView), EntryKind::BlueprintDataset => Ok(Self::BlueprintDataset), + EntryKind::AssetDataset => Ok(Self::AssetDataset), } } } @@ -109,6 +120,7 @@ impl From for EntryKind { PyEntryKind::Table => Self::Table, PyEntryKind::TableView => Self::TableView, PyEntryKind::BlueprintDataset => Self::BlueprintDataset, + PyEntryKind::AssetDataset => Self::AssetDataset, } } } @@ -173,7 +185,7 @@ pub fn set_entry_name( let entry_name = re_protos::EntryName::new(name) .map_err(|err| pyo3::exceptions::PyValueError::new_err(err.to_string()))?; - let entry_details_update = re_protos::cloud::v1alpha1::ext::EntryDetailsUpdate { + let entry_details_update = ext::EntryDetailsUpdate { name: Some(entry_name), }; diff --git a/rerun_py/src/catalog/errors.rs b/rerun_py/src/catalog/errors.rs index 618094770e37..8cb63a8a142f 100644 --- a/rerun_py/src/catalog/errors.rs +++ b/rerun_py/src/catalog/errors.rs @@ -84,6 +84,9 @@ enum ExternalError { #[error(transparent)] TokenError(#[from] re_auth::TokenError), + + #[error(transparent)] + InvalidLayerNameError(#[from] re_types_core::InvalidLayerNameError), } const _: () = assert!( @@ -189,9 +192,9 @@ impl From for PyErr { ApiErrorKind::NotFound => NotFoundError::new_err(err.to_string()), ApiErrorKind::AlreadyExists => AlreadyExistsError::new_err(err.to_string()), ApiErrorKind::Timeout => PyTimeoutError::new_err(err.to_string()), - ApiErrorKind::Unimplemented | ApiErrorKind::Internal => { - PyRuntimeError::new_err(err.to_string()) - } + ApiErrorKind::Unimplemented + | ApiErrorKind::Internal + | ApiErrorKind::FailedPrecondition => PyRuntimeError::new_err(err.to_string()), }, ExternalError::ArrowError(err) => PyValueError::new_err(format!("Arrow error: {err}")), @@ -219,6 +222,8 @@ impl From for PyErr { ExternalError::TokenError(err) => { PyPermissionError::new_err(format!("Invalid token: {err}")) } + + ExternalError::InvalidLayerNameError(err) => PyValueError::new_err(err.to_string()), } } } diff --git a/rerun_py/src/catalog/index_columns.rs b/rerun_py/src/catalog/index_columns.rs index 98c732d68563..21b03a0b1bac 100644 --- a/rerun_py/src/catalog/index_columns.rs +++ b/rerun_py/src/catalog/index_columns.rs @@ -1,4 +1,5 @@ -use pyo3::{pyclass, pymethods}; +use pyo3::exceptions::PyValueError; +use pyo3::{PyResult, pyclass, pymethods}; use re_sorbet::{IndexColumnDescriptor, TimeColumnSelector}; /// The descriptor of an index column. @@ -11,6 +12,7 @@ use re_sorbet::{IndexColumnDescriptor, TimeColumnSelector}; /// column, use [`IndexColumnSelector`][rerun.catalog.IndexColumnSelector]. #[pyclass( frozen, + from_py_object, eq, hash, name = "IndexColumnDescriptor", @@ -58,6 +60,7 @@ impl From for PyIndexColumnDescriptor { /// The name of the index to select. Usually the name of a timeline. #[pyclass( frozen, + from_py_object, eq, name = "IndexColumnSelector", module = "rerun_bindings.rerun_bindings" @@ -79,8 +82,10 @@ impl PyIndexColumnSelector { // Note: the `Parameters` section goes into the class docstring. #[new] #[pyo3(text_signature = "(self, index)")] - fn new(index: &str) -> Self { - Self(TimeColumnSelector::from(index)) + fn new(index: &str) -> PyResult { + let timeline = re_chunk::TimelineName::try_new(index) + .map_err(|err| PyValueError::new_err(err.to_string()))?; + Ok(Self(TimeColumnSelector::from(timeline))) } fn __repr__(&self) -> String { diff --git a/rerun_py/src/catalog/indexes.rs b/rerun_py/src/catalog/indexes.rs deleted file mode 100644 index 00f7f09694f7..000000000000 --- a/rerun_py/src/catalog/indexes.rs +++ /dev/null @@ -1,291 +0,0 @@ -use std::sync::Arc; - -use arrow::array::{Float32Array, RecordBatch, RecordBatchOptions}; -use arrow::datatypes::Field; -use pyo3::exceptions::PyRuntimeError; -use pyo3::prelude::*; -use pyo3::types::PyDict; -use pyo3::{FromPyObject, PyErr, PyResult, pyclass, pymethods}; -use re_protos::cloud::v1alpha1::ext::IndexProperties; -use re_sorbet::ComponentColumnSelector; - -use crate::catalog::{PyComponentColumnSelector, PyIndexColumnSelector, to_py_err}; - -// --- - -/// The result returned from an indexing operation. -#[pyclass(name = "IndexingResult", module = "rerun_bindings.rerun_bindings")] // NOLINT: ignore[py-cls-eq] non-trivial implementation -pub struct PyIndexingResult { - pub index: PyIndexConfig, - pub statistics_json: bytes::Bytes, - pub debug_info: Option, -} - -#[pymethods] -impl PyIndexingResult { - /// Returns configuration information and properties about the newly created index. - #[getter] - pub fn properties(&self) -> PyIndexConfig { - self.index.clone() - } - - /// Returns the component column that this index was created on. - #[getter] - pub fn column(&self) -> PyComponentColumnSelector { - self.index.component_column() - } - - /// Returns best-effort backend-specific statistics about the newly created index. - // - // TODO(RR-2824): should this deserialize and return a native dict? - #[getter] - pub fn statistics(&self) -> String { - String::from_utf8_lossy(&self.statistics_json).to_string() - } - - /// Get debug information about the indexing operation. - /// - /// The exact contents of debug information may vary depending on the indexing operation performed - /// and the server implementation. - /// - /// Returns - /// ------- - /// Optional[dict] - /// A dictionary containing debug information, or `None` if no debug information is available - #[allow(clippy::allow_attributes, rustdoc::broken_intra_doc_links)] - fn debug_info(&self, py: Python<'_>) -> PyResult>> { - match &self.debug_info { - Some(debug_info) => { - let dict = PyDict::new(py); - - if let Some(memory_used) = debug_info.memory_used { - dict.set_item("memory_used", memory_used)?; - } - - Ok(Some(dict.into())) - } - None => Ok(None), - } - } - - pub fn __repr__(&self) -> String { - // Technically not a repr, but nice to printout when this is in a list. - format!("IndexingResult(index={})", self.index) - } -} - -// --- - -/// The complete description of a user-defined index. -#[pyclass(eq, name = "IndexConfig", module = "rerun_bindings.rerun_bindings")] -#[derive(Clone, PartialEq, Eq)] -pub struct PyIndexConfig { - pub time_index: PyIndexColumnSelector, - pub column: PyComponentColumnSelector, - pub properties: PyIndexProperties, -} - -impl std::fmt::Display for PyIndexConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { - time_index, - column, - properties, - } = self; - - f.write_fmt(format_args!("'{column}' on '{time_index}': {properties}")) - } -} - -// TODO(RR-2824): this should probably expose quite a bit more than that. -#[pymethods] -impl PyIndexConfig { - pub fn __str__(&self) -> String { - self.to_string() - } - - pub fn __repr__(&self) -> String { - format!("IndexConfig({self})") - } - - /// Returns the time column that this index applies to. - #[getter] - pub fn time_column(&self) -> PyIndexColumnSelector { - self.time_index.clone() - } - - /// Returns the component column that this index applies to. - #[getter] - pub fn component_column(&self) -> PyComponentColumnSelector { - self.column.clone() - } - - /// Returns the properties/configuration of the index. - #[getter] - pub fn properties(&self) -> PyIndexProperties { - self.properties.clone() - } -} - -impl From for PyIndexConfig { - fn from(value: re_protos::cloud::v1alpha1::ext::IndexConfig) -> Self { - Self { - time_index: PyIndexColumnSelector(value.time_index.into()), - column: PyComponentColumnSelector(ComponentColumnSelector::from_descriptor( - value.column.entity_path, - &value.column.descriptor, - )), - properties: PyIndexProperties { - props: value.properties, - }, - } - } -} - -// --- - -/// The properties and configuration of a user-defined index. -#[pyclass(eq, name = "IndexProperties", module = "rerun_bindings.rerun_bindings")] -#[derive(Clone, PartialEq, Eq)] -pub struct PyIndexProperties { - pub props: IndexProperties, -} - -impl std::fmt::Display for PyIndexProperties { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { props } = self; - - f.write_fmt(format_args!("{props}")) - } -} - -// TODO(RR-2824): this should probably expose quite a bit more than that; for now this is only -// really useful for printing. -#[pymethods] -impl PyIndexProperties { - pub fn __str__(&self) -> String { - self.to_string() - } - - pub fn __repr__(&self) -> String { - format!("IndexProperties({self})") - } -} - -impl From for PyIndexProperties { - fn from(props: IndexProperties) -> Self { - Self { props } - } -} - -// --- - -/// The type of distance metric to use for vector index and search. -#[pyclass( - name = "VectorDistanceMetric", - eq, - eq_int, - module = "rerun_bindings.rerun_bindings" -)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum PyVectorDistanceMetric { - L2, - Cosine, - Dot, - Hamming, -} - -impl From for re_protos::cloud::v1alpha1::VectorDistanceMetric { - fn from(metric: PyVectorDistanceMetric) -> Self { - match metric { - PyVectorDistanceMetric::L2 => Self::L2, - PyVectorDistanceMetric::Cosine => Self::Cosine, - PyVectorDistanceMetric::Dot => Self::Dot, - PyVectorDistanceMetric::Hamming => Self::Hamming, - } - } -} - -/// A type alias for either a `VectorDistanceMetric` enum or a string literal. -#[derive(FromPyObject)] -pub enum VectorDistanceMetricLike { - #[pyo3(transparent, annotation = "enum")] - VectorDistanceMetric(PyVectorDistanceMetric), - - #[pyo3(transparent, annotation = "literal")] - CatchAll(String), -} - -impl TryFrom for re_protos::cloud::v1alpha1::VectorDistanceMetric { - type Error = PyErr; - - fn try_from(metric: VectorDistanceMetricLike) -> Result { - match metric { - VectorDistanceMetricLike::VectorDistanceMetric(metric) => Ok(metric.into()), - VectorDistanceMetricLike::CatchAll(metric) => match metric.to_lowercase().as_str() { - "l2" => Ok(PyVectorDistanceMetric::L2.into()), - "cosine" => Ok(PyVectorDistanceMetric::Cosine.into()), - "dot" => Ok(PyVectorDistanceMetric::Dot.into()), - "hamming" => Ok(PyVectorDistanceMetric::Hamming.into()), - _ => Err(pyo3::exceptions::PyValueError::new_err(format!( - "Unknown vector distance metric: {metric}" - ))), - }, - } - } -} - -impl From for i32 { - fn from(metric: PyVectorDistanceMetric) -> Self { - let proto_typed = re_protos::cloud::v1alpha1::VectorDistanceMetric::from(metric); - - proto_typed as Self - } -} - -// --- - -/// A type alias for a vector (vector search input data). -#[derive(FromPyObject)] -pub enum VectorLike<'py> { - NumPy(numpy::PyArrayLike1<'py, f32>), - Vector(Vec), -} - -impl VectorLike<'_> { - pub fn to_record_batch(&self) -> PyResult { - let schema = arrow::datatypes::Schema::new_with_metadata( - vec![Field::new( - "items", - arrow::datatypes::DataType::Float32, - false, - )], - Default::default(), - ); - - match self { - VectorLike::NumPy(array) => { - let floats: Vec = array - .as_array() - .as_slice() - .ok_or_else(|| { - PyRuntimeError::new_err("Failed to convert numpy array to slice".to_owned()) - })? - .to_vec(); - - RecordBatch::try_new_with_options( - Arc::new(schema), - vec![Arc::new(Float32Array::from(floats))], - &RecordBatchOptions::default(), - ) - .map_err(to_py_err) - } - VectorLike::Vector(floats) => RecordBatch::try_new_with_options( - Arc::new(schema), - vec![Arc::new(Float32Array::from(floats.clone()))], - &RecordBatchOptions::default(), - ) - .map_err(to_py_err), - } - } -} diff --git a/rerun_py/src/catalog/mod.rs b/rerun_py/src/catalog/mod.rs index 38bc729f8169..bb59bf82ec10 100644 --- a/rerun_py/src/catalog/mod.rs +++ b/rerun_py/src/catalog/mod.rs @@ -10,13 +10,13 @@ mod dataset_view; mod entry; mod errors; mod index_columns; -mod indexes; mod registration_handle; mod schema; mod segment_url_udf; mod table_entry; mod table_provider_adapter; mod type_aliases; +mod unregistration_handle; use errors::{AlreadyExistsError, NotFoundError}; use pyo3::prelude::*; @@ -31,16 +31,13 @@ pub use self::dataset_view::PyDatasetViewInternal; pub use self::entry::{PyEntryDetails, PyEntryId, PyEntryKind}; pub use self::errors::to_py_err; pub use self::index_columns::{PyIndexColumnDescriptor, PyIndexColumnSelector}; -pub use self::indexes::{ - PyIndexConfig, PyIndexProperties, PyIndexingResult, PyVectorDistanceMetric, - VectorDistanceMetricLike, VectorLike, -}; pub use self::registration_handle::{PyRegistrationHandleInternal, PyRegistrationIterator}; pub use self::schema::PySchemaInternal; pub use self::segment_url_udf::PySegmentUrlUdfInternal; pub use self::table_entry::{PyTableEntryInternal, PyTableInsertModeInternal}; pub use self::table_provider_adapter::PyTableProviderAdapterInternal; pub use self::type_aliases::{AnyComponentColumn, IndexValuesLike, PyIndexValuesLikeInternal}; +pub use self::unregistration_handle::PyUnregistrationHandleInternal; /// Register the `rerun.catalog` module. pub(crate) fn register(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -53,6 +50,7 @@ pub(crate) fn register(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -66,12 +64,6 @@ pub(crate) fn register(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> m.add_class::()?; m.add_class::()?; - // indexing - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - // testing m.add_class::()?; diff --git a/rerun_py/src/catalog/registration_handle.rs b/rerun_py/src/catalog/registration_handle.rs index a8a0b3ce2d6c..03973dd09fe7 100644 --- a/rerun_py/src/catalog/registration_handle.rs +++ b/rerun_py/src/catalog/registration_handle.rs @@ -4,13 +4,14 @@ use std::sync::Arc; use futures::StreamExt as _; use parking_lot::Mutex; use pyo3::exceptions::{PyStopIteration, PyValueError}; -use pyo3::{Py, PyRef, PyRefMut, PyResult, Python, pyclass, pymethods}; -use re_arrow_util::{ArrowArrayDowncastRef as _, RecordBatchExt as _}; +use pyo3::{Py, PyErr, PyRef, PyRefMut, PyResult, Python, pyclass, pymethods}; use re_protos::{ - cloud::v1alpha1::QueryTasksResponse, - cloud::v1alpha1::ext::{QueryTasksOnCompletionResponse, RegisterWithDatasetTaskDescriptor}, + cloud::v1alpha1::ext::{ + QueryTasksDataframe, QueryTasksOnCompletionResponse, RegisterWithDatasetTaskDescriptor, + }, common::v1alpha1::TaskId, }; +use re_redap_client::TraceId; use tokio::sync::mpsc; use tracing::Instrument as _; @@ -45,6 +46,13 @@ pub struct PyRegistrationHandleInternal { /// Note: using vec index here is ok because this struct is essentially immutable, so /// out-of-bound errors are unlikely. task_id_to_indices: HashMap>, + + /// Trace-id of the request that created this handle. + /// + /// A registration task is long-running, and the initial request may succeed + /// even though the registration itself ultimately fails. + /// In that case we want to show the trace-id of the original request to the user. + request_trace_id: Option, } impl PyRegistrationHandleInternal { @@ -52,6 +60,7 @@ impl PyRegistrationHandleInternal { pub fn new( client: Py, descriptors: Vec, + request_trace_id: Option, ) -> Self { let mut task_id_to_indices: HashMap> = HashMap::new(); for (idx, desc) in descriptors.iter().enumerate() { @@ -65,6 +74,7 @@ impl PyRegistrationHandleInternal { client, descriptors, task_id_to_indices, + request_trace_id, } } @@ -92,13 +102,18 @@ impl PyRegistrationHandleInternal { let (tx, rx) = mpsc::channel::>>(32 * 1024); let descriptors = self.descriptors.clone(); let task_id_to_indices = self.task_id_to_indices.clone(); + let request_trace_id = self.request_trace_id; let runtime = get_tokio_runtime(); runtime.spawn( async move { + // The query trace-id is already part of any `to_py_err` error message (it lives + // on `ApiError`); here we additionally surface the original request trace-id. + let with_trace_id = |err| prepend_request_trace_id(err, request_trace_id.as_ref()); + let mut client = match connection.client().await { Ok(c) => c, Err(err) => { - tx.send(Err(err)).await.ok(); + tx.send(Err(with_trace_id(err))).await.ok(); return; } }; @@ -107,7 +122,7 @@ impl PyRegistrationHandleInternal { match client.query_tasks_on_completion(task_ids, timeout).await { Ok(stream) => stream, Err(err) => { - tx.send(Err(to_py_err(err))).await.ok(); + tx.send(Err(with_trace_id(to_py_err(err)))).await.ok(); return; } }; @@ -131,7 +146,7 @@ impl PyRegistrationHandleInternal { } Err(err) => { - tx.send(Err(err)).await.ok(); + tx.send(Err(with_trace_id(err))).await.ok(); break; } } @@ -160,6 +175,10 @@ impl PyRegistrationHandleInternal { // exception. let descriptors = self.descriptors.clone(); let task_id_to_indices = self.task_id_to_indices.clone(); + + // Trace-id of the original registration request. + let request_trace_id = self.request_trace_id; + wait_for_future( py, async move { @@ -170,6 +189,9 @@ impl PyRegistrationHandleInternal { .await .map_err(to_py_err)?; + // Trace-id of this completion query. + let query_trace_id = response_stream.trace_id(); + // Collect unique error messages (deduplicated across descriptors // that share the same task). let mut unique_errors: Vec = Vec::new(); @@ -192,14 +214,15 @@ impl PyRegistrationHandleInternal { // Check for any errors if !unique_errors.is_empty() { return Err(PyValueError::new_err(format!( - "Registration failed while processing the following segments:\n{}", - unique_errors.join("\n") + "Registration failed.{}\n\nThe following segments failed:\n{}", + format_trace_ids(request_trace_id.as_ref(), query_trace_id.as_ref()), + unique_errors.join("\n"), ))); } Ok(descriptors .iter() - .map(|d| d.segment_id.id.clone()) + .map(|d| d.segment_id.to_string()) .collect()) } .instrument(span), @@ -232,6 +255,43 @@ impl PyRegistrationHandleInternal { } } +/// Prepend the original request trace-id to an error surfaced to Python. +/// +/// The trace-id goes first so it stays visible ahead of the (potentially long and +/// private) error details. Returns the error unchanged when no trace-id is known. +fn prepend_request_trace_id(err: PyErr, request_trace_id: Option<&TraceId>) -> PyErr { + match request_trace_id { + Some(trace_id) => { + PyValueError::new_err(format!("Registration request trace-id: {trace_id}\n{err}")) + } + None => err, + } +} + +/// Format a leading trace-id section for a registration error message. +/// +/// Trace-ids go *early* in the error — before the (potentially long and private) +/// list of failed segments — so they stay visible even when the rest is truncated. +/// Returns an empty string when no trace-ids are known, so callers can splice it +/// in unconditionally. +pub(super) fn format_trace_ids( + request_trace_id: Option<&TraceId>, + query_trace_id: Option<&TraceId>, +) -> String { + let mut lines = Vec::new(); + if let Some(trace_id) = request_trace_id { + lines.push(format!("Registration request trace-id: {trace_id}")); + } + if let Some(trace_id) = query_trace_id { + lines.push(format!("Task-completion query trace-id: {trace_id}")); + } + if lines.is_empty() { + String::new() + } else { + format!("\n{}", lines.join("\n")) + } +} + /// Process a single response from the task completion stream. fn process_task_response( response: QueryTasksOnCompletionResponse, @@ -240,44 +300,28 @@ fn process_task_response( ) -> PyResult> { let item = response.data; - let projected = item - .project_columns( - [ - QueryTasksResponse::FIELD_TASK_ID, - QueryTasksResponse::FIELD_EXEC_STATUS, - QueryTasksResponse::FIELD_MSGS, - ] - .into_iter(), - ) - .map_err(to_py_err)?; - - let (task_ids_col, statuses, msgs) = ( - projected - .column(0) - .try_downcast_array_ref::() - .map_err(to_py_err)?, - projected - .column(1) - .try_downcast_array_ref::() - .map_err(to_py_err)?, - projected - .column(2) - .try_downcast_array_ref::() - .map_err(to_py_err)?, - ); + let on_err = + |err| PyValueError::new_err(format!("invalid QueryTasks response dataframe: {err}")); + let task_ids = QueryTasksDataframe::COLUMN_TASK_ID + .extract(&item) + .map_err(on_err)?; + let statuses = QueryTasksDataframe::COLUMN_EXEC_STATUS + .extract(&item) + .map_err(on_err)?; + let msgs = QueryTasksDataframe::COLUMN_MSGS + .extract(&item) + .map_err(on_err)?; let mut results = Vec::new(); - for i in 0..projected.num_rows() { - let task_id = task_ids_col.value(i); - let status = statuses.value(i); - let msg = msgs.value(i); + for (task_id, status, msg) in itertools::izip!(&task_ids, &statuses, &msgs) { + let msg = msg.unwrap_or_default(); if let Some(indices) = task_id_to_indices.get(task_id) { for &idx in indices { let desc = &descriptors[idx]; - let segment_id = desc.segment_id.id.clone(); + let segment_id = desc.segment_id.to_string(); let error = match status { "success" => None, "cancelled" => Some("registration was cancelled".to_owned()), diff --git a/rerun_py/src/catalog/schema.rs b/rerun_py/src/catalog/schema.rs index c349c2aca773..774901ca5d20 100644 --- a/rerun_py/src/catalog/schema.rs +++ b/rerun_py/src/catalog/schema.rs @@ -15,6 +15,7 @@ use crate::catalog::{AnyComponentColumn, to_py_err}; #[pyclass( frozen, + from_py_object, eq, name = "SchemaInternal", module = "rerun_bindings.rerun_bindings" diff --git a/rerun_py/src/catalog/segment_url_udf.rs b/rerun_py/src/catalog/segment_url_udf.rs index be4c1eb3401e..2a2cf2cda3c3 100644 --- a/rerun_py/src/catalog/segment_url_udf.rs +++ b/rerun_py/src/catalog/segment_url_udf.rs @@ -14,11 +14,12 @@ use datafusion_ffi::udf::FFI_ScalarUDF; use pyo3::types::PyCapsule; use pyo3::{Bound, PyResult, Python, pyclass, pymethods}; +use re_log::ResultExt as _; use re_log_types::{ AbsoluteTimeRange, DataPath, NonMinI64, TimeCell, TimeType, Timeline, TimelineName, }; use re_tuid::Tuid; -use re_types_core::Loggable as _; +use re_types_core::{Loggable as _, SegmentId}; use re_uri::{DatasetSegmentUri, Fragment, Origin, TimeSelection}; #[derive(Debug)] @@ -269,7 +270,7 @@ impl ScalarUDFImpl for SegmentUrlUdf { continue; } - let segment_id = segment_ids.value(row).to_owned(); + let segment_id = SegmentId::from(segment_ids.value(row).to_owned()); let when = time_info.as_ref().and_then(|(time_type, ts_array)| { if ts_array.is_null(row) { @@ -280,7 +281,8 @@ impl ScalarUDFImpl for SegmentUrlUdf { .value(row); let time_cell = TimeCell::new(*time_type, NonMinI64::try_from(i64_val).ok()?); let tl_name = timeline_name.as_deref()?; - Some((TimelineName::new(tl_name), time_cell)) + let tl_name = TimelineName::try_new(tl_name).ok_or_log_error_once()?; + Some((tl_name, time_cell)) }); let time_selection = @@ -299,6 +301,7 @@ impl ScalarUDFImpl for SegmentUrlUdf { let start = NonMinI64::try_from(start_val).ok()?; let end = NonMinI64::try_from(end_val).ok()?; let tl_name = timeline_name.as_deref()?; + let tl_name = TimelineName::try_new(tl_name).ok_or_log_error_once()?; let timeline = Timeline::new(tl_name, *time_type); let range = AbsoluteTimeRange::new(start, end); Some(TimeSelection { timeline, range }) diff --git a/rerun_py/src/catalog/table_entry.rs b/rerun_py/src/catalog/table_entry.rs index 8bfcaeef0791..828fe02ec9c9 100644 --- a/rerun_py/src/catalog/table_entry.rs +++ b/rerun_py/src/catalog/table_entry.rs @@ -8,11 +8,13 @@ use pyo3::exceptions::PyRuntimeError; use pyo3::types::{PyAnyMethods as _, PyCapsule}; use pyo3::{Bound, Py, PyAny, PyRef, PyRefMut, PyResult, Python, pyclass, pymethods}; use re_datafusion::TableEntryTableProvider; -use re_protos::cloud::v1alpha1::ext::{EntryDetails, ProviderDetails, TableEntry, TableInsertMode}; +use re_protos::cloud::v1alpha1::ext::{ + EntryDetails, ProviderDetails, TableDetails, TableEntry, TableInsertMode, +}; use crate::catalog::entry::set_entry_name; use crate::catalog::table_provider_adapter::ffi_logical_codec_from_pycapsule; -use crate::catalog::{PyCatalogClientInternal, PyEntryDetails, to_py_err}; +use crate::catalog::{PyCatalogClientInternal, PyDatasetEntryInternal, PyEntryDetails, to_py_err}; use crate::trace_context::read_trace_context_from_python; use crate::utils::{get_tokio_runtime, wait_for_future}; @@ -24,6 +26,7 @@ use crate::utils::{get_tokio_runtime, wait_for_future}; pub struct PyTableEntryInternal { client: Py, entry_details: EntryDetails, + table_details: TableDetails, lazy_provider: Option>, url: Option, } @@ -58,6 +61,70 @@ impl PyTableEntryInternal { // Table entry methods // + /// The associated blueprint dataset. + fn blueprint_dataset( + mut self_: PyRefMut<'_, Self>, + py: Python<'_>, + ) -> PyResult> { + let _span = read_trace_context_from_python(py, "TableEntry.blueprint_dataset").entered(); + + let client = self_.client.clone_ref(py); + let connection = self_.client.borrow(py).connection().clone(); + + if self_.table_details.blueprint_dataset.is_none() { + let table_entry = + connection.update_table(py, self_.entry_details.id, self_.table_details.clone())?; + self_.table_details = table_entry.table_details; + } + + let blueprint_dataset_entry_id = self_ + .table_details + .blueprint_dataset + .ok_or_else(|| PyRuntimeError::new_err("missing table blueprint dataset"))?; + let dataset_entry = connection.read_dataset(py, blueprint_dataset_entry_id)?; + + Py::new(py, PyDatasetEntryInternal::new(client, dataset_entry)) + } + + /// The default blueprint segment ID for this table, if any. + /// + /// ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ + /// TODO(#12746): Stabilize table blueprint APIs. + fn default_blueprint_segment_id(self_: PyRef<'_, Self>) -> Option { + self_ + .table_details + .default_blueprint_segment + .as_ref() + .map(ToString::to_string) + } + + /// Set the default blueprint segment ID for this table. + /// + /// Pass `None` to clear the blueprint. This fails if the change cannot be made to the remote server. + /// + /// ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ + /// TODO(#12746): Stabilize table blueprint APIs. + #[pyo3(signature = (segment_id))] + fn set_default_blueprint_segment_id( + mut self_: PyRefMut<'_, Self>, + py: Python<'_>, + segment_id: Option, + ) -> PyResult<()> { + let _span = + read_trace_context_from_python(py, "TableEntry.set_default_blueprint_segment_id") + .entered(); + let connection = self_.client.borrow(py).connection().clone(); + + let mut table_details = self_.table_details.clone(); + table_details.default_blueprint_segment = segment_id.map(Into::into); + + let result = connection.update_table(py, self_.entry_details.id, table_details)?; + + self_.table_details = result.table_details; + + Ok(()) + } + /// Returns a DataFusion table provider capsule. fn __datafusion_table_provider__<'py>( self_: PyRefMut<'py, Self>, @@ -148,6 +215,7 @@ impl PyTableEntryInternal { Self { client, entry_details: table_entry.details, + table_details: table_entry.table_details, lazy_provider: None, url, } @@ -188,6 +256,7 @@ impl PyTableEntryInternal { #[pyclass( name = "TableInsertModeInternal", + from_py_object, eq, eq_int, module = "rerun_bindings.rerun_bindings" diff --git a/rerun_py/src/catalog/table_provider_adapter.rs b/rerun_py/src/catalog/table_provider_adapter.rs index 9fda65ca2ad3..f576cd61b8a0 100644 --- a/rerun_py/src/catalog/table_provider_adapter.rs +++ b/rerun_py/src/catalog/table_provider_adapter.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use crate::utils::get_tokio_runtime; use datafusion::catalog::TableProvider; use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec; use datafusion_ffi::table_provider::FFI_TableProvider; @@ -8,6 +7,8 @@ use pyo3::prelude::{PyAnyMethods as _, PyCapsuleMethods as _}; use pyo3::types::PyCapsule; use pyo3::{Bound, PyAny, PyResult, pyclass, pymethods}; +use crate::utils::get_tokio_runtime; + /// Adapter to expose a [`TableProvider`] to the Python side via the DataFusion FFI capsule protocol. #[pyclass( frozen, @@ -60,9 +61,16 @@ pub(crate) fn ffi_logical_codec_from_pycapsule( obj.to_owned() }; - let capsule = capsule.downcast::()?; - // Safety: If we cannot downcast this then there is something very wrong with datafusion-python - let codec = unsafe { capsule.reference::() }; + let capsule = capsule.cast::()?; + let codec_ptr = capsule + .pointer_checked(Some(c"datafusion_logical_extension_codec"))? + .cast::(); + // Safety: `pointer_checked` has verified the capsule name matches + // `datafusion_logical_extension_codec` and that the pointer is non-null. We trust + // datafusion-python to have stored a valid, initialized `FFI_LogicalExtensionCodec` + // behind a capsule of that name; if it hasn't, something is very wrong with + // datafusion-python. + let codec = unsafe { codec_ptr.as_ref() }; Ok(codec.clone()) } diff --git a/rerun_py/src/catalog/type_aliases.rs b/rerun_py/src/catalog/type_aliases.rs index 5ae318997ab6..2b255f21e7c9 100644 --- a/rerun_py/src/catalog/type_aliases.rs +++ b/rerun_py/src/catalog/type_aliases.rs @@ -8,7 +8,7 @@ use arrow::pyarrow::PyArrowType; use numpy::PyArrayMethods as _; use pyo3::exceptions::{PyTypeError, PyValueError}; use pyo3::prelude::PyAnyMethods as _; -use pyo3::{Bound, FromPyObject, PyAny, PyResult, pyclass, pymethods}; +use pyo3::{Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, pyclass, pymethods}; use re_arrow_util::ArrowArrayDowncastRef as _; use re_sorbet::ComponentColumnSelector; @@ -53,8 +53,10 @@ pub enum IndexValuesLike<'py> { CatchAll(Bound<'py, PyAny>), } -impl<'py> FromPyObject<'py> for IndexValuesLike<'py> { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for IndexValuesLike<'py> { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> PyResult { // Try PyArrow first if let Ok(pyarrow) = obj.extract::>() { return Ok(Self::PyArrow(pyarrow)); @@ -86,7 +88,7 @@ impl<'py> FromPyObject<'py> for IndexValuesLike<'py> { } // Fall back to catch all - Ok(Self::CatchAll(obj.clone())) + Ok(Self::CatchAll(obj.to_owned())) } } @@ -214,6 +216,7 @@ impl IndexValuesLike<'_> { /// `TimeInt` values. #[pyclass( frozen, + from_py_object, name = "_IndexValuesLikeInternal", module = "rerun_bindings.rerun_bindings", hash, @@ -236,7 +239,7 @@ impl PyIndexValuesLikeInternal { #[new] #[pyo3(text_signature = "(self, values)")] fn new(values: Bound<'_, PyAny>) -> PyResult { - let index_values_like = IndexValuesLike::extract_bound(&values)?; + let index_values_like = IndexValuesLike::extract(values.as_borrowed())?; let values = index_values_like.to_index_values()?; Ok(Self { values }) } diff --git a/rerun_py/src/catalog/unregistration_handle.rs b/rerun_py/src/catalog/unregistration_handle.rs new file mode 100644 index 000000000000..2cc4d77af93a --- /dev/null +++ b/rerun_py/src/catalog/unregistration_handle.rs @@ -0,0 +1,156 @@ +// This whole module should really be shared with `registration_handle`, +// but there are result handling differences that make it impossible currently. +use pyo3::{Py, PyResult, Python, exceptions::PyValueError, pyclass, pymethods}; +use re_protos::{ + cloud::v1alpha1::ext::{QueryTasksDataframe, QueryTasksOnCompletionResponse}, + common::v1alpha1::TaskId, +}; +use re_redap_client::TraceId; +use tokio_stream::StreamExt as _; +use tracing::Instrument as _; + +use crate::{ + catalog::{PyCatalogClientInternal, registration_handle::format_trace_ids, to_py_err}, + trace_context::read_trace_context_from_python, + utils::wait_for_future, +}; + +const DEFAULT_TIMEOUT_SECS: u64 = 60 * 60; + +/// Internal handle exposed to Python for tracking unregistration tasks. +#[pyclass( + name = "UnregistrationHandleInternal", + module = "rerun_bindings.rerun_bindings" +)] +pub struct PyUnregistrationHandleInternal { + client: Py, + tasks: Vec, + + /// Trace-id of the request that created this handle. + request_trace_id: Option, +} + +impl PyUnregistrationHandleInternal { + /// Create a new unregistration handle from task descriptors. + pub fn new( + client: Py, + tasks: Vec, + request_trace_id: Option, + ) -> Self { + Self { + client, + tasks, + request_trace_id, + } + } +} + +#[pymethods] +impl PyUnregistrationHandleInternal { + /// Wait for all tasks to complete. + /// Raises an error if the unregistration fails. + #[pyo3(signature = (timeout_secs=None))] + fn wait(&self, py: Python<'_>, timeout_secs: Option) -> PyResult<()> { + let span = read_trace_context_from_python(py, "UnregistrationHandle.wait"); + + // This happens when running the SDK against an old server, which does not have asynchronous unregistration. + if self.tasks.is_empty() { + return Ok(()); + } + + let connection = self.client.borrow(py).connection().clone(); + let task_ids = self.tasks.clone(); + let timeout = std::time::Duration::from_secs(timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS)); + + // Trace-id of the original registration request. + let request_trace_id = self.request_trace_id; + + wait_for_future( + py, + async move { + let mut response_stream = connection + .client() + .await? + .query_tasks_on_completion(task_ids, timeout) + .await + .map_err(to_py_err)?; + + // Trace-id of this completion query. + let query_trace_id = response_stream.trace_id(); + + let mut errors = Vec::new(); + + while let Some(response) = response_stream.next().await { + let response: QueryTasksOnCompletionResponse = + response.map_err(to_py_err)?.try_into().map_err(to_py_err)?; + + let on_err = |err| { + PyValueError::new_err(format!( + "invalid QueryTasks response dataframe: {err}" + )) + }; + let task_ids = QueryTasksDataframe::COLUMN_TASK_ID + .extract(&response.data) + .map_err(on_err)?; + let statuses = QueryTasksDataframe::COLUMN_EXEC_STATUS + .extract(&response.data) + .map_err(on_err)?; + let msgs = QueryTasksDataframe::COLUMN_MSGS + .extract(&response.data) + .map_err(on_err)?; + + for (task_id, status, msg) in itertools::izip!(&task_ids, &statuses, &msgs) { + let msg = msg.unwrap_or_default(); + + let error = match status { + "success" => None, + "cancelled" => Some("unregistration was cancelled".to_owned()), + _ => Some(msg.to_owned()), + }; + + if let Some(err) = error { + errors.push(format!("Unregistration task '{task_id}' failed: {err}")); + } + } + } + + // Check for any errors + if !errors.is_empty() { + return Err(PyValueError::new_err(format!( + "Unregistration failed.{}\n\nThe following segments failed:\n{}", + format_trace_ids(request_trace_id.as_ref(), query_trace_id.as_ref()), + errors.join("\n"), + ))); + } + + Ok(()) + } + .instrument(span), + ) + } + + /// Cancel unregistration. + /// If the unregistration is already done, this is a noop. + #[pyo3(signature = ())] + fn cancel(&self, py: Python<'_>) -> PyResult<()> { + let span = read_trace_context_from_python(py, "cancel"); + + let connection = self.client.borrow(py).connection().clone(); + let task_ids = self.tasks.clone(); + + wait_for_future( + py, + async move { + connection + .client() + .await? + .cancel_tasks(task_ids) + .await + .map_err(to_py_err)?; + + Ok(()) + } + .instrument(span), + ) + } +} diff --git a/rerun_py/src/chunk/mod.rs b/rerun_py/src/chunk/mod.rs index c5a27256422f..b86db60224ea 100644 --- a/rerun_py/src/chunk/mod.rs +++ b/rerun_py/src/chunk/mod.rs @@ -1,16 +1,13 @@ mod types; use pyo3::types::{PyModule, PyModuleMethods as _}; -use pyo3::{Bound, PyResult, wrap_pyfunction}; +use pyo3::{Bound, PyResult}; -pub use self::types::{PyChunkInternal, PyChunkIterator}; +pub use self::types::PyChunkInternal; /// Register the `rerun.chunk` module. pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; - m.add_class::()?; - - m.add_function(wrap_pyfunction!(types::recording_from_chunks, m)?)?; Ok(()) } diff --git a/rerun_py/src/chunk/types.rs b/rerun_py/src/chunk/types.rs index b165782ae5ad..347c1addb439 100644 --- a/rerun_py/src/chunk/types.rs +++ b/rerun_py/src/chunk/types.rs @@ -1,9 +1,6 @@ use std::sync::Arc; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; use pyo3::exceptions::PyRuntimeError; -use pyo3::exceptions::PyStopIteration; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyDict; @@ -11,10 +8,7 @@ use pyo3::types::PyDict; use arrow::array::RecordBatch as ArrowRecordBatch; use arrow::pyarrow::{PyArrowType, ToPyArrow as _}; use re_chunk::Chunk; -use re_chunk_store::{ChunkStore, ChunkStoreConfig, ChunkStoreHandle}; -use re_log_types::{EntityPath, StoreId, StoreInfo, StoreSource}; - -use crate::recording::PyRecordingInternal; +use re_log_types::EntityPath; /// A single chunk of data from a recording. #[pyclass( @@ -93,16 +87,64 @@ impl PyChunkInternal { Ok(batch.to_pyarrow(py)?.unbind()) } - /// Create a Chunk from a PyArrow RecordBatch with Rerun schema metadata. + /// Interpret a PyArrow RecordBatch as Rerun chunk data, one chunk per entity path. + /// + /// `index_mode` is one of `"auto"`, `"static"`, or `"columns"`; when it is `"columns"`, + /// `index_columns` names the columns to promote to timelines. `entity_path` is the default + /// entity path for un-located component columns. /// - /// The RecordBatch must have been produced by `to_record_batch()` or have - /// equivalent Rerun metadata in its schema. + /// All conversion errors (both `SorbetError` and `ChunkError`) are mapped to `ValueError`, so + /// the documented `Raises` contract of `Chunk.from_record_batch` holds. #[staticmethod] - #[expect(clippy::needless_pass_by_value)] // PyO3 requires owned PyArrowType for #[staticmethod] - fn from_record_batch(record_batch: PyArrowType) -> PyResult { - let chunk = Chunk::from_record_batch(&record_batch.0) - .map_err(|err| PyValueError::new_err(err.to_string()))?; - Ok(Self::new(Arc::new(chunk))) + #[pyo3(signature = (record_batch, index_mode, index_columns, entity_path))] + #[expect(clippy::needless_pass_by_value)] // PyO3 requires owned arguments for #[staticmethod] + fn from_record_batch( + record_batch: PyArrowType, + index_mode: &str, + index_columns: Vec, + entity_path: Option, + ) -> PyResult> { + use re_log_types::TimelineName; + use re_sorbet::DataframeIndex; + + let index = match index_mode { + "auto" => DataframeIndex::Auto, + "static" => DataframeIndex::Static, + "columns" => DataframeIndex::Columns( + index_columns + .iter() + .map(|s| { + TimelineName::try_new(s.as_str()) + .map_err(|err| PyValueError::new_err(err.to_string())) + }) + .collect::>>()?, + ), + _ => { + return Err(PyValueError::new_err(format!( + "Invalid index mode {index_mode:?}; expected \"auto\", \"static\", or \"columns\"." + ))); + } + }; + let entity_path = entity_path.map(|p| EntityPath::parse_forgiving(&p)); + + let chunks = + Chunk::from_dataframe_record_batch(&record_batch.0, &index, entity_path.as_ref()) + .map_err(|err| PyValueError::new_err(err.to_string()))?; + + Ok(chunks + .into_iter() + .map(|chunk| Self::new(Arc::new(chunk))) + .collect()) + } + + /// Return a copy of this chunk with a new entity path. + /// + /// A fresh chunk ID is generated to avoid aliasing the original chunk in downstream + /// caches and indices. Row IDs, timelines, and components are preserved as-is. + fn with_entity_path(&self, entity_path: &str) -> Self { + let entity_path = EntityPath::parse_forgiving(entity_path); + let chunk = self.chunk.clone_with_new_entity_path(entity_path); + Self::new(Arc::new(chunk)) } /// Create a Chunk from an entity path, timeline arrays, and component arrays. @@ -124,12 +166,19 @@ impl PyChunkInternal { #[pyo3(signature = (lenses))] fn apply_lenses( &self, - lenses: Vec>, + py: Python<'_>, + lenses: Vec, ) -> PyResult> { use re_lenses_core::ChunkExt as _; - let lenses: Vec<_> = lenses.iter().map(|l| l.inner().clone()).collect(); - match self.chunk.apply_lenses(&lenses) { + let lenses: Vec<_> = lenses + .iter() + .map(|l| l.build(py)) + .collect::>>()?; + match self + .chunk + .apply_lenses(&lenses, &re_lenses::default_runtime()) + { Ok(chunks) => Ok(chunks .into_iter() .map(|chunk| Self { @@ -157,28 +206,36 @@ impl PyChunkInternal { use re_lenses_core::ChunkExt as _; use re_types_core::ComponentIdentifier; - let source_id = ComponentIdentifier::from(source); + let source_id = ComponentIdentifier::try_new(source) + .map_err(|err| PyValueError::new_err(err.to_string()))?; let new_chunk = self .chunk - .apply_selector(source_id, selector.selector()) + .apply_selector( + source_id, + selector.selector(), + &re_lenses::default_runtime(), + ) .map_err(|err| PyValueError::new_err(err.to_string()))?; Ok(Self::new(Arc::new(new_chunk))) } - /// Format this chunk as a human-readable table string. - /// - /// Args: - /// width: Fixed width for the table (default: 240). - /// redact: If true, redact non-deterministic values like RowIds (default: false). - #[pyo3(signature = (*, width=240, redact=false))] - fn format(&self, width: usize, redact: bool) -> PyResult { + /// Format this chunk as a human-readable table string. Internal: the user-facing wrapper sets defaults. + #[pyo3(signature = (*, width, redact, trim_metadata_keys))] + #[expect(clippy::fn_params_excessive_bools)] // Named keyword args in Python. + fn format(&self, width: usize, redact: bool, trim_metadata_keys: bool) -> PyResult { let batch = self .chunk .to_record_batch() .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - Ok(re_arrow_util::format_record_batch_with_width(&batch, Some(width), redact).to_string()) + let opts = re_arrow_util::RecordBatchFormatOpts { + width: Some(width), + redact_non_deterministic: redact, + trim_metadata_keys, + ..Default::default() + }; + Ok(re_arrow_util::format_record_batch_opts(&batch, &opts).to_string()) } fn __repr__(&self) -> String { @@ -195,70 +252,3 @@ impl PyChunkInternal { self.chunk.num_rows() } } - -/// An iterator over chunks in a recording. -// TODO(RR-4126): currently, the stores we can iterate from are fully loaded in memory, so the -// `Vec>` is an acceptable shortcut. In the future, this iterator should be streaming and -// only load chunks (from file/remote segment) to pipeline over larger-than-ram data. -#[pyclass(name = "ChunkIterator", module = "rerun_bindings.rerun_bindings")] // NOLINT: ignore[py-cls-eq] -pub struct PyChunkIterator { - chunks: Vec>, - index: AtomicUsize, -} - -impl PyChunkIterator { - pub fn new(chunks: Vec>) -> Self { - Self { - chunks, - index: AtomicUsize::new(0), - } - } -} - -#[pymethods] // NOLINT: ignore[py-mthd-str] -impl PyChunkIterator { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - - fn __next__(&self) -> PyResult { - let idx = self.index.fetch_add(1, Ordering::Relaxed); - if idx < self.chunks.len() { - Ok(PyChunkInternal::new(self.chunks[idx].clone())) - } else { - Err(PyStopIteration::new_err("")) - } - } -} - -/// Create a new recording from an iterable of chunks. -#[pyfunction] -#[expect(clippy::needless_pass_by_value)] -pub fn recording_from_chunks( - py: Python<'_>, - chunks: &Bound<'_, PyAny>, - application_id: String, - recording_id: String, -) -> PyResult { - let store_id = StoreId::recording(application_id.as_str(), recording_id.as_str()); - - let mut store = ChunkStore::new(store_id.clone(), ChunkStoreConfig::DEFAULT); - - let iter = chunks.try_iter()?; - for item in iter { - let item: Bound<'_, PyAny> = item?; - let chunk_internal: PyRef<'_, PyChunkInternal> = item.extract()?; - store - .insert_chunk(chunk_internal.inner()) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - } - - let info = StoreInfo::new(store_id, StoreSource::Other("rerun-sdk-python".into())); - - let _ = py; - - Ok(PyRecordingInternal { - store: ChunkStoreHandle::new(store), - store_info: Some(info), - }) -} diff --git a/rerun_py/src/chunk_stream/chunk_store.rs b/rerun_py/src/chunk_stream/chunk_store.rs index e3606e85af76..de34c5005c56 100644 --- a/rerun_py/src/chunk_stream/chunk_store.rs +++ b/rerun_py/src/chunk_stream/chunk_store.rs @@ -3,61 +3,44 @@ use std::sync::Arc; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -use re_byte_size::SizeBytes as _; use re_chunk::Chunk; -use re_chunk_store::{ChunkStore, ChunkStoreConfig, ChunkStoreHandle, LazyRrdStore}; -use re_log_types::{StoreId, StoreKind}; -use re_sorbet::ChunkColumnDescriptors; +use re_chunk_store::{ + ChunkStore, ChunkStoreConfig, ChunkStoreHandle, QueryExpression, SparseFillStrategy, + StaticColumnSelection, ViewContentsSelector, +}; +use re_datafusion::LocalChunkStoreTableProvider; +use re_log_types::{EntityPathFilter, StoreId, StoreKind}; use super::error::ChunkPipelineError; use super::py_stream::PyLazyChunkStreamInternal; use super::stream::LazyChunkStream; +use super::summary::{SummaryRow, format_summary}; use super::{ChunkStream, ChunkStreamFactory}; -use crate::catalog::PySchemaInternal; +use crate::catalog::{ + IndexValuesLike, PySchemaInternal, PyTableProviderAdapterInternal, to_py_err, +}; use crate::chunk::PyChunkInternal; -/// A chunk store, either fully materialized or lazily backed by an RRD file. -/// -/// This is a newtype around [`ChunkStoreInternal`] because PyO3 cannot derive -/// `#[pyclass]` on enums whose variants hold non-PyO3 types. +/// A fully-materialized, in-memory chunk store. /// /// Implements [`ChunkStreamFactory`] so `stream()` can hand `self.clone()` /// straight to [`LazyChunkStream::from_factory`] -- no intermediate wrapper. #[pyclass( frozen, + from_py_object, name = "ChunkStoreInternal", module = "rerun_bindings.rerun_bindings" )] #[derive(Clone)] -pub struct PyChunkStoreInternal(ChunkStoreInternal); - -/// Fully materialized or lazily-backed chunk store. -//TODO(RR-4341): this is a temporary thing until we have a more general `ChunkProvider` abstraction. -#[derive(Clone)] -enum ChunkStoreInternal { - /// All chunks are in memory. - InMemory(ChunkStoreHandle), - - /// Index loaded from RRD footer, chunks loaded on demand. - IndexedRrd(Arc), -} - -impl ChunkStoreInternal { - fn schema(&self) -> ChunkColumnDescriptors { - match self { - Self::InMemory(handle) => handle.read().schema().chunk_column_descriptors(), - Self::IndexedRrd(lazy) => lazy.schema().chunk_column_descriptors(), - } - } +pub struct PyChunkStoreInternal { + handle: ChunkStoreHandle, } impl PyChunkStoreInternal { - pub fn in_memory(store: ChunkStore) -> Self { - Self(ChunkStoreInternal::InMemory(ChunkStoreHandle::new(store))) - } - - pub fn indexed_rrd(lazy: LazyRrdStore) -> Self { - Self(ChunkStoreInternal::IndexedRrd(Arc::new(lazy))) + pub fn new(store: ChunkStore) -> Self { + Self { + handle: ChunkStoreHandle::new(store), + } } } @@ -74,39 +57,64 @@ impl PyChunkStoreInternal { .insert_chunk(py_chunk.inner()) .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; } - Ok(Self::in_memory(store)) + Ok(Self::new(store)) } /// The schema describing all columns in this store. fn schema(&self) -> PySchemaInternal { PySchemaInternal { - columns: self.0.schema().into(), + columns: self + .handle + .read() + .schema() + .chunk_column_descriptors() + .into(), metadata: Default::default(), } } /// The total number of chunks in this store (virtual and physical). fn num_chunks(&self) -> usize { - match &self.0 { - ChunkStoreInternal::InMemory(handle) => handle.read().num_physical_chunks(), - ChunkStoreInternal::IndexedRrd(lazy) => lazy.manifest().num_chunks(), - } + self.handle.read().num_physical_chunks() } /// Compact, deterministic summary of every chunk in the store for snapshot testing. /// /// Each line describes one chunk: - /// `{entity_path} rows={n} bytes={…} static={bool} timelines=[…] cols=[…]` + /// `{entity_path} rows={n} static={bool} timelines=[…] cols=[…]` /// /// Chunks are sorted by `(entity_path, !is_static)`. The `cols` list - /// combines timeline and component column names (sorted), excluding - /// `rerun.controls` columns. - /// - /// For lazily-loaded stores, this forces loading all chunk data from disk. - //TODO(ab): should that be implemented on `re_chunk_store::ChunkStore` directly? - fn summary(&self) -> PyResult { - let chunks = self.collect_all_chunks()?; - Ok(summary_from_chunks(&chunks)) + /// combines timeline and component column names (sorted). + fn summary(&self) -> String { + let store = self.handle.read(); + let chunks: Vec> = store.iter_physical_chunks().cloned().collect(); + let rows = chunks.iter().map(|chunk| { + let mut timelines: Vec = chunk + .timelines() + .keys() + .map(|t| t.as_str().to_owned()) + .collect(); + timelines.sort(); + + let mut cols: Vec = std::iter::chain( + chunk.timelines().keys().map(|t| t.as_str().to_owned()), + chunk + .components() + .component_descriptors() + .map(|d| d.display_name().to_owned()), + ) + .collect(); + cols.sort(); + + SummaryRow { + entity_path: chunk.entity_path().to_string(), + num_rows: chunk.num_rows() as u64, + is_static: chunk.is_static(), + timelines, + cols, + } + }); + format_summary(rows) } /// Return a lazy stream over all chunks in this store. @@ -114,91 +122,109 @@ impl PyChunkStoreInternal { // Each compile() snapshots the store's current physical chunks. PyLazyChunkStreamInternal::new(LazyChunkStream::from_factory(self.clone())) } -} -impl PyChunkStoreInternal { - /// Collect all chunks from either variant, loading lazily if needed. - fn collect_all_chunks(&self) -> PyResult>> { - match &self.0 { - ChunkStoreInternal::InMemory(handle) => { - Ok(handle.read().iter_physical_chunks().cloned().collect()) - } + /// Build a `TableProvider` for an in-process DataFusion query over this store. + /// + /// All keyword arguments are required at this internal layer; defaults + /// live in the public `ChunkStore.reader()` wrapper. + #[expect(clippy::fn_params_excessive_bools)] + #[expect(clippy::needless_pass_by_value)] // PyO3 extraction yields owned values + #[pyo3(signature = ( + *, + index, + contents, + include_semantically_empty_columns, + include_tombstone_columns, + fill_latest_at, + using_index_values, + ))] + fn reader( + &self, + index: Option, + contents: Option>, + include_semantically_empty_columns: bool, + include_tombstone_columns: bool, + fill_latest_at: bool, + using_index_values: Option>, + ) -> PyResult { + // `LocalChunkStoreTableProvider::try_new` validates `index` against the + // store schema and returns an error if it doesn't exist. + let view_contents = + build_view_contents_from_filters(&self.handle.read(), contents.as_deref()); + let using_index_values = using_index_values + .map(|v| v.to_index_values()) + .transpose()?; + + let static_only = index.is_none(); + let query = QueryExpression { + view_contents: Some(view_contents), + include_semantically_empty_columns, + include_tombstone_columns, + include_static_columns: if static_only { + StaticColumnSelection::StaticOnly + } else { + StaticColumnSelection::Both + }, + filtered_index: index + .map(|index| { + re_chunk::TimelineName::try_new(index) + .map_err(|err| pyo3::exceptions::PyValueError::new_err(err.to_string())) + }) + .transpose()?, + filtered_index_range: None, + filtered_index_values: None, + using_index_values, + filtered_is_not_null: None, + sparse_fill_strategy: if fill_latest_at { + SparseFillStrategy::LatestAtGlobal + } else { + SparseFillStrategy::None + }, + selection: None, + }; - ChunkStoreInternal::IndexedRrd(lazy) => lazy - .collect_physical_chunks() - .map_err(|err| PyRuntimeError::new_err(err.to_string())), - } + let provider = + LocalChunkStoreTableProvider::try_new(self.handle.clone(), query).map_err(to_py_err)?; + Ok(PyTableProviderAdapterInternal::new( + Arc::new(provider), + /* streaming= */ true, + )) } } +/// Build a `ViewContentsSelector` from `contents` expressions, applied +/// against the store's currently known entities. +/// +/// Semantics mirror [`PyDatasetViewInternal::filter_contents`]: +/// * `None` → everything (`/**`) +/// * `Some([])` → nothing (`-/**`) +/// * `Some(exprs)` → join `exprs` with spaces and parse as a filter. +fn build_view_contents_from_filters( + store: &ChunkStore, + contents: Option<&[String]>, +) -> ViewContentsSelector { + let filter = match contents { + None => EntityPathFilter::parse_forgiving("/**").resolve_without_substitutions(), + Some([]) => EntityPathFilter::parse_forgiving("-/**").resolve_without_substitutions(), + Some(exprs) => { + EntityPathFilter::parse_forgiving(exprs.join(" ")).resolve_without_substitutions() + } + }; + store + .all_entities() + .into_iter() + .filter(|ep| filter.matches(ep)) + .map(|ep| (ep, None)) + .collect() +} + impl ChunkStreamFactory for PyChunkStoreInternal { fn create(&self) -> Result, ChunkPipelineError> { - let chunks = match &self.0 { - ChunkStoreInternal::InMemory(handle) => { - handle.read().iter_physical_chunks().cloned().collect() - } - ChunkStoreInternal::IndexedRrd(lazy) => { - lazy.collect_physical_chunks() - .map_err(|err| ChunkPipelineError::RrdRead { - path: lazy.rrd_path().to_path_buf(), - reason: err.to_string(), - })? - } - }; + let chunks = self.handle.read().iter_physical_chunks().cloned().collect(); Ok(Box::new(VecChunkStream { chunks, pos: 0 })) } } -/// Build a summary from a list of chunks. -fn summary_from_chunks(chunks: &[Arc]) -> String { - let mut chunks: Vec<&Chunk> = chunks.iter().map(|c| c.as_ref()).collect(); - chunks.sort_by(|a, b| { - a.entity_path() - .cmp(b.entity_path()) - .then_with(|| a.is_static().cmp(&b.is_static()).reverse()) - }); - - let mut lines = Vec::new(); - for chunk in &chunks { - let mut timelines: Vec<&str> = chunk.timelines().keys().map(|t| t.as_str()).collect(); - timelines.sort(); - - let mut cols: Vec<&str> = chunk - .timelines() - .keys() - .map(|t| t.as_str()) - .chain( - chunk - .components() - .component_descriptors() - .map(|d| d.display_name()), - ) - .collect(); - cols.sort(); - - let timelines_str = timelines - .iter() - .map(|t| format!("'{t}'")) - .collect::>() - .join(", "); - let cols_str = cols - .iter() - .map(|c| format!("'{c}'")) - .collect::>() - .join(", "); - let is_static = if chunk.is_static() { "True" } else { "False" }; - let bytes = re_format::format_bytes(chunk.total_size_bytes() as f64); - - lines.push(format!( - "{entity_path} rows={rows} bytes={bytes} static={is_static} timelines=[{timelines_str}] cols=[{cols_str}]", - entity_path = chunk.entity_path(), - rows = chunk.num_rows(), - )); - } - - lines.join("\n") -} - // --- Streaming --- /// Pull-based stream over a pre-collected `Vec` of chunks. @@ -207,7 +233,6 @@ struct VecChunkStream { pos: usize, } -// Vec> + usize are Send. impl ChunkStream for VecChunkStream { fn next(&mut self) -> Result>, ChunkPipelineError> { if self.pos < self.chunks.len() { diff --git a/rerun_py/src/chunk_stream/engine.rs b/rerun_py/src/chunk_stream/engine.rs index 372e97449cc3..8be521ba53ec 100644 --- a/rerun_py/src/chunk_stream/engine.rs +++ b/rerun_py/src/chunk_stream/engine.rs @@ -17,11 +17,12 @@ use pyo3::prelude::*; use re_chunk::Chunk; -use super::ChunkStream; use super::error::{ChunkPipelineError, PythonException, py_callable_err}; use super::stream::{ - LazyChunkStream, PipelineStep, SplitOrigin, SplitSide, StreamSource, StructuredFilter, + LazyChunkStream, MergeResult, PipelineStep, SplitOrigin, SplitSide, StreamSource, + StructuredFilter, }; +use super::{ChunkStream, ChunkStreamFactory}; /// Compile a [`LazyChunkStream`] into a runnable [`ChunkStream`] chain. pub fn compile(stream: &LazyChunkStream) -> Box { @@ -232,78 +233,98 @@ fn scan_inner(stream: &LazyChunkStream, usage: &mut HashMap Box { - let mut compiled: Box = match stream.source() { - StreamSource::StreamFactory(factory) => match factory.create() { - Ok(source) => source, - Err(err) => Box::new(FailedSource(Some(err))), - }, + let all_steps = stream.steps(); + + let (source, remaining_steps) = match stream.source() { + StreamSource::StreamFactory(factory) => build_factory_source(factory.as_ref(), all_steps), StreamSource::PyIterable(obj) => { let cloned = Python::attach(|py| obj.clone_ref(py)); - Box::new(PyIteratorSource::new(cloned)) + ( + Box::new(PyIteratorSource::new(cloned)) as Box, + all_steps, + ) } - StreamSource::Merged(streams) => { - let compiled: Vec> = - streams.iter().map(|s| compile_inner(s, ctx)).collect(); + StreamSource::Merged(streams) => (compile_merged(streams, ctx), all_steps), - let (tx, rx) = - crossbeam::channel::bounded::(super::CHUNK_CHANNEL_CAPACITY); - for upstream in compiled { - let tx = tx.clone(); - std::thread::Builder::new() - .name("chunk-merge-source".into()) - .spawn(move || { - let mut stream = upstream; - loop { - match stream.next() { - Ok(Some(chunk)) => { - if re_quota_channel::send_crossbeam(&tx, Ok(chunk)).is_err() { - break; // receiver dropped - } - } + StreamSource::SplitBranch { origin, side } => { + (compile_split_branch(origin, *side, ctx), all_steps) + } + }; - Ok(None) => break, + apply_steps(source, remaining_steps) +} - Err(err) => { - re_quota_channel::send_crossbeam(&tx, Err(err)).ok(); - break; - } - } - } - }) - .expect("Failed to spawn merge source thread"); +/// Collect and AND-merge leading [`PipelineStep::Filter`] steps into a single filter. +/// +/// Stops at the first non-`Filter` step (`Drop`, `Lenses`, `Map`, `FlatMap`) and at the first +/// merge `Conflict` or `Empty` (both leave the offending filter in place for normal post-source +/// execution). Returns `(merged, remaining)` where `remaining` is the slice of unconsumed steps, +/// or `None` if no meaningful filter can be pushed (no leading filters at all, or the merged +/// filter ended up a no-op). +fn collect_leading_filters(steps: &[PipelineStep]) -> Option<(StructuredFilter, &[PipelineStep])> { + let mut merged: Option = None; + let mut consumed = 0; + + for step in steps { + let PipelineStep::Filter(f) = step else { + break; + }; + + match merged.as_ref() { + None => { + merged = Some(f.clone()); + consumed += 1; } - drop(tx); // channel closes when all source threads finish - Box::new(ChannelStream::new(rx)) - } - StreamSource::SplitBranch { origin, side } => { - let key = SplitOriginId::new(origin); - if ctx.split_has_both_sides(&key) { - // Both branches are reachable: use the full router thread. - let rx = ctx.take_split_receiver(origin, *side); - Box::new(ChannelStream::new(rx)) - } else { - // Only one branch is reachable: degenerate to filter/drop. - re_log::warn!( - "Only one branch of a split is connected to the pipeline. \ - The split has been optimized into a filter/drop operation." - ); - let upstream = compile_inner(&origin.upstream, ctx); - match side { - SplitSide::Matched => { - Box::new(FilterStream::new(upstream, origin.filter.clone())) - } - SplitSide::Unmatched => { - Box::new(DropStream::new(upstream, origin.filter.clone())) - } + Some(cur) => match cur.try_merge(f) { + MergeResult::Merged(m) => { + merged = Some(m); + consumed += 1; } - } + + MergeResult::Conflict | MergeResult::Empty => break, + }, } + } + + let m = merged?; + if m.is_noop() { + None + } else { + Some((m, &steps[consumed..])) + } +} + +/// Build the source `ChunkStream` for a [`StreamSource::StreamFactory`], opportunistically +/// pushing leading filters into the factory. Returns the source stream and the slice of +/// pipeline steps that remain to be applied by `apply_steps`. +/// +/// Factories that can't push down anything inherit the trait's default `create_with_pushdown`, +/// which calls `create()` and wraps the result in a `FilterStream`. The net effect is that any +/// leading-filter prefix is consolidated into a single post-source `FilterStream`, whether or +/// not the source did real pushdown work. +fn build_factory_source<'a>( + factory: &dyn ChunkStreamFactory, + steps: &'a [PipelineStep], +) -> (Box, &'a [PipelineStep]) { + let Some((merged, remaining)) = collect_leading_filters(steps) else { + let source: Box = match factory.create() { + Ok(s) => s, + Err(err) => Box::new(FailedSource(Some(err))), + }; + return (source, steps); }; - for step in stream.steps() { + match factory.create_with_pushdown(&merged) { + Ok(stream) => (stream, remaining), + Err(err) => (Box::new(FailedSource(Some(err))), steps), + } +} + +fn apply_steps(mut compiled: Box, steps: &[PipelineStep]) -> Box { + for step in steps { compiled = match step { PipelineStep::Filter(f) => Box::new(FilterStream::new(compiled, f.clone())), PipelineStep::Drop(f) => Box::new(DropStream::new(compiled, f.clone())), @@ -327,10 +348,67 @@ fn compile_inner(stream: &LazyChunkStream, ctx: &mut CompileContext) -> Box Box { + let compiled: Vec> = + streams.iter().map(|s| compile_inner(s, ctx)).collect(); + + let (tx, rx) = crossbeam::channel::bounded::(super::CHUNK_CHANNEL_CAPACITY); + for upstream in compiled { + let tx = tx.clone(); + std::thread::Builder::new() + .name("chunk-merge-source".into()) + .spawn(move || { + let mut stream = upstream; + loop { + match stream.next() { + Ok(Some(chunk)) => { + if re_quota_channel::send_crossbeam(&tx, Ok(chunk)).is_err() { + break; // receiver dropped + } + } + + Ok(None) => break, + + Err(err) => { + re_quota_channel::send_crossbeam(&tx, Err(err)).ok(); + break; + } + } + } + }) + .expect("Failed to spawn merge source thread"); + } + drop(tx); // channel closes when all source threads finish + Box::new(ChannelStream::new(rx)) +} + +fn compile_split_branch( + origin: &Arc, + side: SplitSide, + ctx: &mut CompileContext, +) -> Box { + let key = SplitOriginId::new(origin); + if ctx.split_has_both_sides(&key) { + // Both branches are reachable: use the full router thread. + let rx = ctx.take_split_receiver(origin, side); + Box::new(ChannelStream::new(rx)) + } else { + // Only one branch is reachable: degenerate to filter/drop. + re_log::warn!( + "Only one branch of a split is connected to the pipeline. \ + The split has been optimized into a filter/drop operation." + ); + let upstream = compile_inner(&origin.upstream, ctx); + match side { + SplitSide::Matched => Box::new(FilterStream::new(upstream, origin.filter.clone())), + SplitSide::Unmatched => Box::new(DropStream::new(upstream, origin.filter.clone())), + } + } +} + // --------------------------------------------------------------------------- // PyIteratorSource // --------------------------------------------------------------------------- @@ -351,9 +429,10 @@ impl ChunkStream for PyIteratorSource { let iter = self.iter_obj.bind(py); match iter.call_method0("__next__") { Ok(obj) => { - let internal: PyRef<'_, crate::chunk::PyChunkInternal> = - obj.extract().map_err(|err| { - ChunkPipelineError::PythonIterator(PythonException::new(err)) + let internal: PyRef<'_, crate::chunk::PyChunkInternal> = obj + .extract() + .map_err(|err: pyo3::pyclass::PyClassGuardError<'_, '_>| { + ChunkPipelineError::PythonIterator(PythonException::new(err.into())) })?; Ok(Some(Arc::clone(internal.inner()))) } @@ -372,7 +451,7 @@ impl ChunkStream for PyIteratorSource { // FilterStream // --------------------------------------------------------------------------- -struct FilterStream { +pub struct FilterStream { inner: Box, filter: StructuredFilter, } @@ -474,7 +553,7 @@ impl ChunkStream for LensesStream { } // Apply lenses — may produce 0..N output chunks. - for result in self.lenses.apply(&chunk) { + for result in self.lenses.apply(&chunk, &re_lenses::default_runtime()) { match result { Ok(out) => self.buffer.push_back(Arc::new(out)), Err(partial) => { @@ -517,8 +596,9 @@ impl ChunkStream for MapStream { .map_fn .call1(py, (py_chunk,)) .map_err(py_callable_err)?; - let result_ref: PyRef<'_, crate::chunk::PyChunkInternal> = - result.extract(py).map_err(py_callable_err)?; + let result_ref: PyRef<'_, crate::chunk::PyChunkInternal> = result + .extract(py) + .map_err(|e: pyo3::pyclass::PyClassGuardError<'_, '_>| py_callable_err(e.into()))?; Ok(Some(Arc::clone(result_ref.inner()))) }) } @@ -555,8 +635,11 @@ impl ChunkStream for FlatMapStream { let bound = result.bind(py); for item in bound.try_iter().map_err(py_callable_err)? { let item = item.map_err(py_callable_err)?; - let chunk_ref: PyRef<'_, crate::chunk::PyChunkInternal> = - item.extract().map_err(py_callable_err)?; + let chunk_ref: PyRef<'_, crate::chunk::PyChunkInternal> = item + .extract() + .map_err(|e: pyo3::pyclass::PyClassGuardError<'_, '_>| { + py_callable_err(e.into()) + })?; self.buffer.push_back(Arc::clone(chunk_ref.inner())); } Ok(()) diff --git a/rerun_py/src/chunk_stream/error.rs b/rerun_py/src/chunk_stream/error.rs index 8b96be590d61..2dd3274d9704 100644 --- a/rerun_py/src/chunk_stream/error.rs +++ b/rerun_py/src/chunk_stream/error.rs @@ -55,9 +55,20 @@ pub enum ChunkPipelineError { #[error("Failed to read RRD file at {path}: {reason}")] RrdRead { path: PathBuf, reason: String }, + #[error( + "Legacy RRD without footer: {path}. Use RrdReader.stream().collect() to read it eagerly into a ChunkStore." + )] + RrdNoManifest { path: PathBuf }, + #[error("MCAP error: {reason}")] Mcap { reason: String }, + #[error("MP4 error: {reason}")] + Mp4 { reason: String }, + + #[error("HDF5 error: {reason}")] + Hdf5 { reason: String }, + #[error("Parquet error: {reason}")] Parquet { reason: String }, @@ -70,6 +81,9 @@ pub enum ChunkPipelineError { #[error("Lenses error: {reason}")] Lenses { reason: String }, + #[error("Failed to load chunks from {from}: {reason}")] + IndexedLoad { from: String, reason: String }, + #[error("{0}")] PythonIterator(PythonException), @@ -91,11 +105,15 @@ impl From for pyo3::PyErr { ChunkPipelineError::RrdChunkDecode { .. } | ChunkPipelineError::RrdRead { .. } + | ChunkPipelineError::RrdNoManifest { .. } | ChunkPipelineError::Mcap { .. } + | ChunkPipelineError::Mp4 { .. } + | ChunkPipelineError::Hdf5 { .. } | ChunkPipelineError::Parquet { .. } | ChunkPipelineError::Urdf { .. } | ChunkPipelineError::ChunkStoreInsert { .. } - | ChunkPipelineError::Lenses { .. } => PyRuntimeError::new_err(err.to_string()), + | ChunkPipelineError::Lenses { .. } + | ChunkPipelineError::IndexedLoad { .. } => PyRuntimeError::new_err(err.to_string()), } } } diff --git a/rerun_py/src/chunk_stream/hdf5_reader.rs b/rerun_py/src/chunk_stream/hdf5_reader.rs new file mode 100644 index 000000000000..50c5ac0579c4 --- /dev/null +++ b/rerun_py/src/chunk_stream/hdf5_reader.rs @@ -0,0 +1,244 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use pyo3::exceptions::{PyFileNotFoundError, PyKeyError, PyRuntimeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use re_chunk::{Chunk, EntityPath}; +use re_hdf5::{AttrValue, Hdf5Config, Hdf5Error, IndexColumn, IndexType, TimeUnit}; + +use super::error::ChunkPipelineError; +use super::py_stream::PyLazyChunkStreamInternal; +use super::stream::LazyChunkStream; +use super::{ChunkStream, ChunkStreamFactory}; + +/// Internal HDF5 reader binding. +#[pyclass( + frozen, + name = "Hdf5ReaderInternal", + module = "rerun_bindings.rerun_bindings" +)] +pub struct PyHdf5ReaderInternal { + path: PathBuf, +} + +/// The re-creatable source behind a single `stream()` call: path + the fully +/// resolved config for that stream. +struct Hdf5StreamFactory { + path: PathBuf, + config: Hdf5Config, +} + +/// Map a `stream()` validation error to the Python exception the `Hdf5Reader` +/// contract promises: bad configuration (misalignment, bad `index_column`) +/// and — deliberately, since `validate_layout` opens the file eagerly — +/// open/parse failures of a present file all become `ValueError`. +fn validate_err_to_py(err: &Hdf5Error, path: &std::path::Path) -> PyErr { + PyValueError::new_err(format!("{err}\nFile path: {}", path.display())) +} + +/// Map a metadata-accessor error: a missing object is a `KeyError`, anything +/// else (a genuine read failure) a `RuntimeError`. +fn accessor_err_to_py(err: &Hdf5Error) -> PyErr { + if err.is_not_found() { + PyKeyError::new_err(err.to_string()) + } else { + PyRuntimeError::new_err(err.to_string()) + } +} + +#[pymethods] +impl PyHdf5ReaderInternal { + #[new] + #[pyo3(text_signature = "(self, path)")] + fn new(path: &str) -> PyResult { + let path = PathBuf::from(path); + if !path.exists() { + return Err(PyFileNotFoundError::new_err(format!( + "HDF5 file not found: {}", + path.display() + ))); + } + Ok(Self { path }) + } + + /// Return a new lazy stream over all chunks in the HDF5 file. + /// + /// The layout is validated against `config` up front (metadata only), so bad + /// configuration fails here rather than mid-stream. + #[pyo3(signature = ( + entity_path_prefix = None, + index_column = None, + ignore_datasets = None, + use_structs = true, + ))] + fn stream( + &self, + entity_path_prefix: Option, + index_column: Option<(String, String, Option)>, + ignore_datasets: Option>, + use_structs: bool, + ) -> PyResult { + let index_column = index_column + .map(|(dataset_path, type_str, unit_str)| { + let unit = match unit_str.as_deref().unwrap_or("ns") { + "ns" => TimeUnit::Nanoseconds, + "us" => TimeUnit::Microseconds, + "ms" => TimeUnit::Milliseconds, + "s" => TimeUnit::Seconds, + other => { + return Err(PyValueError::new_err(format!( + "Unknown time unit: '{other}'. Expected 'ns', 'us', 'ms', or 's'." + ))); + } + }; + let index_type = match type_str.as_str() { + "timestamp" => IndexType::Timestamp(unit), + "duration" => IndexType::Duration(unit), + "sequence" => IndexType::Sequence, + other => { + return Err(PyValueError::new_err(format!( + "Unknown index type: '{other}'. Expected 'timestamp', 'duration', or 'sequence'." + ))); + } + }; + Ok(IndexColumn { + path: dataset_path, + index_type, + }) + }) + .transpose()?; + + let config = Hdf5Config { + index_column, + ignore_datasets: ignore_datasets.unwrap_or_default(), + use_structs, + entity_path_prefix: entity_path_prefix + .map(EntityPath::from) + .unwrap_or_else(|| Hdf5Config::default().entity_path_prefix), + }; + + // Per-stream structural validation: fail fast before spawning the worker. + re_hdf5::validate_layout(&self.path, &config) + .map_err(|err| validate_err_to_py(&err, &self.path))?; + + Ok(PyLazyChunkStreamInternal::new( + LazyChunkStream::from_factory(Hdf5StreamFactory { + path: self.path.clone(), + config, + }), + )) + } + + /// List the group paths under `path`, recursively. + #[pyo3(signature = (path = "/"))] + fn groups(&self, path: &str) -> PyResult> { + re_hdf5::list_groups(&self.path, path).map_err(|err| accessor_err_to_py(&err)) + } + + /// List the datasets under `path`, recursively, as `(path, shape, dtype)` tuples. + #[pyo3(signature = (path = "/"))] + fn datasets(&self, path: &str) -> PyResult, String)>> { + Ok(re_hdf5::list_datasets(&self.path, path) + .map_err(|err| accessor_err_to_py(&err))? + .into_iter() + .map(|info| (info.path, info.shape, info.dtype.to_string())) + .collect()) + } + + /// Read the attributes of the object at `path` as a typed Python dict. + #[pyo3(signature = (path = "/"))] + fn attributes<'py>(&self, py: Python<'py>, path: &str) -> PyResult> { + let attrs = + re_hdf5::read_attributes(&self.path, path).map_err(|err| accessor_err_to_py(&err))?; + + let dict = PyDict::new(py); + for (name, value) in attrs { + match value { + AttrValue::F64(value) => dict.set_item(name, value)?, + AttrValue::I32(value) => dict.set_item(name, value)?, + AttrValue::I64(value) => dict.set_item(name, value)?, + AttrValue::U32(value) => dict.set_item(name, value)?, + AttrValue::U64(value) => dict.set_item(name, value)?, + AttrValue::String(value) | AttrValue::AsciiString(value) => { + dict.set_item(name, value)?; + } + AttrValue::F64Array(values) => dict.set_item(name, values)?, + AttrValue::I64Array(values) => dict.set_item(name, values)?, + AttrValue::StringArray(values) + | AttrValue::AsciiStringArray(values) + | AttrValue::VarLenAsciiArray(values) => dict.set_item(name, values)?, + } + } + Ok(dict) + } + + /// The file path this reader was constructed with. + #[getter] + fn path(&self) -> PathBuf { + self.path.clone() + } +} + +// TODO(RR-4850): this spawn-thread + bounded-channel block is hand-copied across +// mp4/mcap/parquet/hdf5. Factor it into a shared `spawn_threaded_stream` adapter. +// The iterator is created and consumed entirely on the worker thread, so nothing +// here requires `re_hdf5`'s iterator (or `hdf5_pure::File`) to be `Send`. +impl ChunkStreamFactory for Hdf5StreamFactory { + fn create(&self) -> Result, ChunkPipelineError> { + let (tx, rx) = crossbeam::channel::bounded::, ChunkPipelineError>>( + super::CHUNK_CHANNEL_CAPACITY, + ); + + let path = self.path.clone(); + let config = self.config.clone(); + + std::thread::Builder::new() + .name("hdf5-chunk-source".into()) + .spawn(move || { + match re_hdf5::load_hdf5(&path, &config) { + Ok(iter) => { + for chunk_result in iter { + let msg = match chunk_result { + Ok(chunk) => Ok(Arc::new(chunk)), + Err(err) => Err(ChunkPipelineError::Hdf5 { + reason: err.to_string(), + }), + }; + if re_quota_channel::send_crossbeam(&tx, msg).is_err() { + break; // receiver dropped + } + } + } + Err(err) => { + re_quota_channel::send_crossbeam( + &tx, + Err(ChunkPipelineError::Hdf5 { + reason: err.to_string(), + }), + ) + .ok(); + } + } + // tx drops here → channel closes → Hdf5Stream::next() returns Ok(None) + }) + .expect("Failed to spawn hdf5 decode thread"); + + Ok(Box::new(Hdf5Stream { rx })) + } +} + +/// Chunk stream that receives decoded chunks from a background thread. +struct Hdf5Stream { + rx: crossbeam::channel::Receiver, ChunkPipelineError>>, +} + +impl ChunkStream for Hdf5Stream { + fn next(&mut self) -> Result>, ChunkPipelineError> { + match self.rx.recv() { + Ok(Ok(chunk)) => Ok(Some(chunk)), + Ok(Err(err)) => Err(err), + Err(crossbeam::channel::RecvError) => Ok(None), // channel closed — loading finished + } + } +} diff --git a/rerun_py/src/chunk_stream/lazy_store.rs b/rerun_py/src/chunk_stream/lazy_store.rs new file mode 100644 index 000000000000..91eb55a33739 --- /dev/null +++ b/rerun_py/src/chunk_stream/lazy_store.rs @@ -0,0 +1,705 @@ +use std::collections::{BTreeSet, HashMap, VecDeque}; +use std::sync::Arc; + +use pyo3::prelude::*; + +use re_chunk::{Chunk, ChunkId}; +use re_chunk_store::LazyStore; +use re_log_encoding::{RrdManifest, RrdManifestStaticMap, RrdManifestTemporalMap}; +use re_log_types::EntityPath; +use re_types_core::{ComponentIdentifier, TimelineName}; + +use super::engine::FilterStream; +use super::error::ChunkPipelineError; +use super::py_stream::PyLazyChunkStreamInternal; +use super::stream::{ChunkPredicateView, LazyChunkStream, StructuredFilter}; +use super::summary::{SummaryRow, format_summary}; +use super::{ChunkStream, ChunkStreamFactory}; +use crate::catalog::PySchemaInternal; +use crate::utils::wait_for_future; + +/// An index-based, lazily-loaded chunk store. +/// +/// Constructed from a [`LazyStore`]; the manifest is held in memory but chunks are loaded on +/// demand. Implements [`ChunkStreamFactory`] so `stream()` produces an [`IndexedChunkStream`] +/// that pulls chunks in byte-budgeted batches. +#[pyclass( + frozen, + from_py_object, + name = "LazyStoreInternal", + module = "rerun_bindings.rerun_bindings" +)] +#[derive(Clone)] +pub struct PyLazyStoreInternal { + inner: Arc, +} + +impl PyLazyStoreInternal { + pub fn new(lazy: LazyStore) -> Self { + Self { + inner: Arc::new(lazy), + } + } +} + +#[pymethods] +impl PyLazyStoreInternal { + /// The schema describing all columns in this store. + fn schema(&self) -> PySchemaInternal { + PySchemaInternal { + columns: self.inner.schema().chunk_column_descriptors().into(), + metadata: Default::default(), + } + } + + /// The total number of chunks described by the manifest (virtual and physical). + fn num_chunks(&self) -> usize { + self.inner.manifest().num_chunks() + } + + /// Monotonic count of chunks physically loaded from this store since it was opened. + /// + /// Exposed as `_chunks_loaded` (underscore-prefixed) — intended for test-side validation + /// that pushdown / lazy loading is engaged. Not a performance metric and not part of the + /// stable public API. + #[getter(_chunks_loaded)] + fn chunks_loaded(&self) -> u64 { + self.inner.chunks_loaded() + } + + /// Compact, deterministic summary of every chunk in the store for snapshot testing. + /// + /// Each line describes one chunk: + /// `{entity_path} rows={n} static={bool} timelines=[…] cols=[…]` + /// + /// Built from the manifest only — no chunk data is loaded. + fn summary(&self) -> String { + let manifest = self.inner.manifest(); + let chunk_ids = manifest.col_chunk_ids(); + let entity_paths = manifest.col_chunk_entity_path_raw(); + let is_static_iter: Vec = manifest.col_chunk_is_static().collect(); + let num_rows = manifest.col_chunk_num_rows(); + + // Per-chunk (timelines, cols), using BTreeSet for sorted-by-construction order. + let mut per_chunk: HashMap, BTreeSet<&'static str>)> = + HashMap::new(); + + for per_entity in manifest.temporal_map().values() { + for (timeline, per_component) in per_entity { + let timeline_name = timeline.name().as_str(); + for (component, per_chunk_map) in per_component { + let component_name = component.as_str(); + for chunk_id in per_chunk_map.keys() { + let entry = per_chunk.entry(*chunk_id).or_default(); + entry.0.insert(timeline_name); + entry.1.insert(timeline_name); + entry.1.insert(component_name); + } + } + } + } + for per_entity in manifest.static_map().values() { + for (component, chunk_id) in per_entity { + let entry = per_chunk.entry(*chunk_id).or_default(); + entry.1.insert(component.as_str()); + } + } + + let rows = chunk_ids.iter().enumerate().map(|(i, id)| { + let (timelines, cols) = per_chunk.remove(id).unwrap_or_default(); + SummaryRow { + entity_path: entity_paths.value(i).to_owned(), + num_rows: num_rows[i], + is_static: is_static_iter[i], + timelines: timelines.into_iter().map(str::to_owned).collect(), + cols: cols.into_iter().map(str::to_owned).collect(), + } + }); + format_summary(rows) + } + + /// Return a lazy stream over all chunks in this store. + fn stream(&self) -> PyLazyChunkStreamInternal { + PyLazyChunkStreamInternal::new(LazyChunkStream::from_factory(Arc::clone(&self.inner))) + } +} + +/// `Arc` is itself the factory: it owns the manifest and serves on-demand chunk +/// loads, which is exactly what a [`ChunkStreamFactory`] needs. +impl ChunkStreamFactory for Arc { + fn create(&self) -> Result, ChunkPipelineError> { + Ok(Box::new(IndexedChunkStream::new(Self::clone(self)))) + } + + fn create_with_pushdown( + &self, + filter: &StructuredFilter, + ) -> Result, ChunkPipelineError> { + let manifest = self.manifest(); + let (matching_ids, remainder) = evaluate_filter_on_manifest(filter, manifest); + + let stream: Box = Box::new(IndexedChunkStream::new_with_ids( + Self::clone(self), + matching_ids, + )); + Ok(match remainder { + Some(rem) => Box::new(FilterStream::new(stream, rem)), + None => stream, + }) + } +} + +/// Evaluate chunk-level predicates against the manifest. +/// +/// Returns `(matching_chunk_ids, remainder_filter)`. The remainder is non-`None` only when +/// component column slicing must still run post-load — chunks that pass the predicate may +/// still carry columns we want to drop. +fn evaluate_filter_on_manifest( + filter: &StructuredFilter, + manifest: &RrdManifest, +) -> (Vec, Option) { + let chunk_ids = manifest.col_chunk_ids(); + + //TODO(perf): `col_chunk_entity_path()` parses+interns one `EntityPath` per chunk. + // When `filter.content.is_none()`, we don't need the parsed form at all, and could + // iterate `col_chunk_entity_path_raw()` (a `&StringArray`) instead, parsing only when + // a temporal/static_map lookup actually requires it. Skipped for v1; revisit when + // profiling points here. + let entity_paths: Vec = manifest.col_chunk_entity_path().collect(); + let is_static_col: Vec = manifest.col_chunk_is_static().collect(); + + let temporal_map = manifest.temporal_map(); + let static_map = manifest.static_map(); + + let matching: Vec = itertools::izip!(chunk_ids, &entity_paths, &is_static_col) + .filter_map(|(&chunk_id, entity_path, &is_static)| { + let view = ManifestRow { + chunk_id, + entity_path, + is_static, + temporal_map, + static_map, + }; + filter.matches(&view).then_some(chunk_id) + }) + .collect(); + + // Remainder: `components` slices columns post-load. The predicate above already drops + // chunks that have *none* of the listed components, but each surviving chunk may still + // carry unwanted columns that the `FilterStream` will trim. + let remainder = filter.components.as_ref().map(|c| StructuredFilter { + content: None, + has_timeline: None, + is_static: None, + components: Some(c.clone()), + }); + + (matching, remainder) +} + +/// Per-row view into an [`RrdManifest`] for [`StructuredFilter::matches`]. +/// +/// Holds references into the manifest's chunk-id, entity-path, static/temporal maps so the +/// trait methods can answer `has_timeline` / `has_any_component` with the same lookups the +/// previous inline loop did — no allocations, scoped to one row of the manifest. +struct ManifestRow<'a> { + chunk_id: ChunkId, + entity_path: &'a EntityPath, + is_static: bool, + temporal_map: &'a RrdManifestTemporalMap, + static_map: &'a RrdManifestStaticMap, +} + +impl ChunkPredicateView for ManifestRow<'_> { + fn entity_path(&self) -> &EntityPath { + self.entity_path + } + + fn is_static(&self) -> bool { + self.is_static + } + + fn has_timeline(&self, name: &TimelineName) -> bool { + if self.is_static { + // Static chunks have no timelines. + return false; + } + self.temporal_map + .get(self.entity_path) + .and_then(|per_tl| per_tl.iter().find(|(tl, _)| tl.name() == name)) + .is_some_and(|(_, per_comp)| { + per_comp + .values() + .any(|per_chunk| per_chunk.contains_key(&self.chunk_id)) + }) + } + + fn has_any_component(&self, components: &[ComponentIdentifier]) -> bool { + if self.is_static { + self.static_map + .get(self.entity_path) + .is_some_and(|per_comp| { + components + .iter() + .any(|c| per_comp.get(c).is_some_and(|id| *id == self.chunk_id)) + }) + } else { + self.temporal_map + .get(self.entity_path) + .is_some_and(|per_tl| { + per_tl.values().any(|per_comp| { + components.iter().any(|c| { + per_comp + .get(c) + .is_some_and(|per_chunk| per_chunk.contains_key(&self.chunk_id)) + }) + }) + }) + } + } +} + +// --- Streaming --- + +/// Streaming loader for an indexed (lazy) [`ChunkStore`]. +/// +/// Pulls chunks from the underlying [`ChunkProvider`][re_log_encoding::ChunkProvider] in +/// byte-budgeted batches so resident memory stays bounded regardless of total recording size. +//TODO(RR-4545): this is hardly an optimal strategy. We need the ChunkProvider to expose a streaming +// API so that specific optimizations can be applied (e.g. adjacency for RRD, parallelism for +// segments, etc.) +struct IndexedChunkStream { + lazy: Arc, + chunk_ids: Vec, + next_id: usize, + buffer: VecDeque>, +} + +impl IndexedChunkStream { + /// Target bytes per batch — bounds memory while still letting `read_chunks` coalesce. + const BATCH_BYTE_BUDGET: u64 = 8 * 1024 * 1024; + + /// Stream all chunks in the manifest (current behavior). + fn new(lazy: Arc) -> Self { + let chunk_ids = lazy.manifest().col_chunk_ids().to_vec(); + Self::new_with_ids(lazy, chunk_ids) + } + + /// Stream only the given chunk IDs (used by pushdown). + /// + /// IDs that do not appear in the manifest are tolerated — `next_batch_end` assigns + /// them a size of `0` via [`LazyStore::chunk_row_index`]'s `None` branch, and + /// `load_chunks` is the layer that would ultimately reject them. Manifest membership + /// is the caller's invariant. + fn new_with_ids(lazy: Arc, chunk_ids: Vec) -> Self { + Self { + lazy, + chunk_ids, + next_id: 0, + buffer: VecDeque::new(), + } + } + + /// End index (exclusive) of the next batch starting at `self.next_id`, + /// chosen so the cumulative byte size stays under [`Self::BATCH_BYTE_BUDGET`]. + /// Always advances by at least one chunk to guarantee progress on huge chunks. + fn next_batch_end(&self) -> usize { + let sizes = self.lazy.manifest().col_chunk_byte_size(); + let mut end = self.next_id; + let mut accumulated: u64 = 0; + while end < self.chunk_ids.len() { + let size = self + .lazy + .chunk_row_index(&self.chunk_ids[end]) + .map(|row| sizes[row]) + .unwrap_or(0); + if end > self.next_id && accumulated.saturating_add(size) > Self::BATCH_BYTE_BUDGET { + break; + } + accumulated = accumulated.saturating_add(size); + end += 1; + } + end + } +} + +impl ChunkStream for IndexedChunkStream { + fn next(&mut self) -> Result>, ChunkPipelineError> { + loop { + if let Some(chunk) = self.buffer.pop_front() { + return Ok(Some(chunk)); + } + if self.next_id >= self.chunk_ids.len() { + return Ok(None); + } + + let end = self.next_batch_end(); + let ids = &self.chunk_ids[self.next_id..end]; + let chunks = Python::attach(|py| wait_for_future(py, self.lazy.load_chunks(ids))) + .map_err(|err| ChunkPipelineError::IndexedLoad { + from: self.lazy.source(), + reason: err.to_string(), + })?; + self.next_id = end; + self.buffer = chunks.into(); + } + } +} + +#[cfg(test)] +mod pushdown_tests { + //! Unit tests for [`evaluate_filter_on_manifest`]. + + use std::fs::File; + use std::path::Path; + + use re_chunk::{Chunk, RowId, TimePoint, Timeline}; + use re_log_encoding::{EncodingOptions, RrdChunkProvider}; + use re_log_types::example_components::{MyColor, MyPoint, MyPoints}; + use re_log_types::{ + EntityPath, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind, StoreSource, + }; + use re_types_core::{ComponentDescriptor, ComponentIdentifier, TimelineName}; + + use super::*; + + /// Which example component a test chunk should carry. + #[derive(Copy, Clone)] + pub(crate) enum TestComponent { + Points, + Colors, + } + + impl TestComponent { + fn descriptor(self) -> ComponentDescriptor { + match self { + Self::Points => MyPoints::descriptor_points(), + Self::Colors => MyPoints::descriptor_colors(), + } + } + + pub(crate) fn identifier(self) -> ComponentIdentifier { + self.descriptor().component + } + } + + /// Per-chunk recipe. + pub(crate) struct ChunkSpec<'a> { + pub entity: &'a str, + pub component: TestComponent, + pub is_static: bool, + pub num_frames: usize, + } + + pub(crate) fn build_test_store(specs: &[ChunkSpec<'_>]) -> (Arc, tempfile::TempDir) { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.rrd"); + let store = build_test_store_at(&path, specs); + (store, dir) + } + + fn build_test_store_at(path: &Path, specs: &[ChunkSpec<'_>]) -> Arc { + let store_id = StoreId::random(StoreKind::Recording, "pushdown-test"); + let timeline = Timeline::new_sequence("frame"); + + let mut chunks: Vec> = Vec::new(); + for spec in specs { + let entity = EntityPath::from(spec.entity); + let descriptor = spec.component.descriptor(); + + if spec.is_static { + let row_id = RowId::new(); + let chunk = match spec.component { + TestComponent::Points => { + let points = MyPoint::from_iter(0..1); + Chunk::builder(entity) + .with_sparse_component_batches( + row_id, + TimePoint::default(), + [(descriptor, Some(&points as _))], + ) + .build() + .unwrap() + } + TestComponent::Colors => { + let colors = MyColor::from_iter([0xFF_00_00_FFu32]); + Chunk::builder(entity) + .with_sparse_component_batches( + row_id, + TimePoint::default(), + [(descriptor, Some(&colors as _))], + ) + .build() + .unwrap() + } + }; + chunks.push(Arc::new(chunk)); + } else { + let mut builder = Chunk::builder(entity); + for frame in 0..spec.num_frames { + let row_id = RowId::new(); + #[expect(clippy::cast_possible_wrap)] + let timepoint = TimePoint::default().with(timeline, frame as i64); + builder = match spec.component { + TestComponent::Points => { + #[expect(clippy::cast_possible_truncation)] + let points = MyPoint::from_iter(frame as u32..frame as u32 + 1); + builder.with_sparse_component_batches( + row_id, + timepoint, + [(descriptor.clone(), Some(&points as _))], + ) + } + TestComponent::Colors => { + #[expect(clippy::cast_possible_truncation)] + let colors = MyColor::from_iter([frame as u32 + 1]); + builder.with_sparse_component_batches( + row_id, + timepoint, + [(descriptor.clone(), Some(&colors as _))], + ) + } + }; + } + chunks.push(Arc::new(builder.build().unwrap())); + } + } + + let mut file = std::fs::File::create(path).unwrap(); + let mut encoder = re_log_encoding::Encoder::new_eager( + re_log_encoding::CrateVersion::LOCAL, + EncodingOptions::PROTOBUF_COMPRESSED, + &mut file, + ) + .unwrap(); + encoder + .append(&LogMsg::SetStoreInfo(SetStoreInfo { + row_id: *RowId::ZERO, + info: StoreInfo::new(store_id.clone(), StoreSource::Unknown), + })) + .unwrap(); + for chunk in &chunks { + let arrow_msg = chunk.to_arrow_msg().unwrap(); + encoder + .append(&LogMsg::ArrowMsg(store_id.clone(), arrow_msg)) + .unwrap(); + } + encoder.finish().unwrap(); + + let mut reader = futures::io::AllowStdIo::new(File::open(path).unwrap()); + let footer = futures::executor::block_on(re_log_encoding::read_rrd_footer(&mut reader)) + .unwrap() + .unwrap(); + let raw = Arc::new(footer.manifests[&store_id].clone()); + let reader = futures::io::AllowStdIo::new(File::open(path).unwrap()); + let provider = Arc::new( + RrdChunkProvider::from_reader(reader, path.display().to_string(), raw).unwrap(), + ); + Arc::new(LazyStore::new(provider)) + } + + fn epf(rules: &str) -> re_log_types::ResolvedEntityPathFilter { + re_log_types::EntityPathFilter::parse_forgiving(rules).resolve_without_substitutions() + } + + fn ids_for_entity(store: &LazyStore, entity: &str) -> Vec { + let path = EntityPath::from(entity); + let manifest = store.manifest(); + let entity_paths: Vec = manifest.col_chunk_entity_path().collect(); + std::iter::zip(manifest.col_chunk_ids(), &entity_paths) + .filter_map(|(id, p)| if p == &path { Some(*id) } else { None }) + .collect() + } + + fn sort_ids(mut v: Vec) -> Vec { + v.sort(); + v + } + + #[test] + fn test_eval_entity_path() { + let (store, _dir) = build_test_store(&[ + ChunkSpec { + entity: "/robot", + component: TestComponent::Points, + is_static: false, + num_frames: 2, + }, + ChunkSpec { + entity: "/camera", + component: TestComponent::Points, + is_static: false, + num_frames: 2, + }, + ]); + + let filter = StructuredFilter { + content: Some(epf("+ /robot/**")), + ..Default::default() + }; + let (matching, remainder) = evaluate_filter_on_manifest(&filter, store.manifest()); + assert!(remainder.is_none()); + assert_eq!( + sort_ids(matching), + sort_ids(ids_for_entity(&store, "/robot")) + ); + } + + #[test] + fn test_eval_is_static() { + let (store, _dir) = build_test_store(&[ + ChunkSpec { + entity: "/static_one", + component: TestComponent::Points, + is_static: true, + num_frames: 0, + }, + ChunkSpec { + entity: "/temporal", + component: TestComponent::Points, + is_static: false, + num_frames: 2, + }, + ]); + + let filter = StructuredFilter { + is_static: Some(true), + ..Default::default() + }; + let (matching, remainder) = evaluate_filter_on_manifest(&filter, store.manifest()); + assert!(remainder.is_none()); + assert_eq!( + sort_ids(matching), + sort_ids(ids_for_entity(&store, "/static_one")) + ); + } + + #[test] + fn test_eval_has_timeline() { + let (store, _dir) = build_test_store(&[ + ChunkSpec { + entity: "/temporal", + component: TestComponent::Points, + is_static: false, + num_frames: 2, + }, + ChunkSpec { + entity: "/static_one", + component: TestComponent::Points, + is_static: true, + num_frames: 0, + }, + ]); + + let filter = StructuredFilter { + has_timeline: Some(TimelineName::from("frame")), + ..Default::default() + }; + let (matching, _) = evaluate_filter_on_manifest(&filter, store.manifest()); + assert_eq!( + sort_ids(matching), + sort_ids(ids_for_entity(&store, "/temporal")) + ); + + // A non-existent timeline should match nothing. + let filter = StructuredFilter { + has_timeline: Some(TimelineName::from("never_logged")), + ..Default::default() + }; + let (matching, _) = evaluate_filter_on_manifest(&filter, store.manifest()); + assert!(matching.is_empty()); + } + + #[test] + fn test_eval_components() { + let (store, _dir) = build_test_store(&[ + ChunkSpec { + entity: "/a", + component: TestComponent::Points, + is_static: false, + num_frames: 1, + }, + ChunkSpec { + entity: "/b", + component: TestComponent::Colors, + is_static: false, + num_frames: 1, + }, + ]); + + let filter = StructuredFilter { + components: Some(vec![TestComponent::Points.identifier()]), + ..Default::default() + }; + let (matching, remainder) = evaluate_filter_on_manifest(&filter, store.manifest()); + assert_eq!(sort_ids(matching), sort_ids(ids_for_entity(&store, "/a"))); + + let remainder = remainder.expect("components remainder must be present for slicing"); + assert!(remainder.content.is_none()); + assert!(remainder.has_timeline.is_none()); + assert!(remainder.is_static.is_none()); + assert_eq!( + remainder.components, + Some(vec![TestComponent::Points.identifier()]) + ); + } + + #[test] + fn test_eval_combined() { + let (store, _dir) = build_test_store(&[ + ChunkSpec { + entity: "/robot", + component: TestComponent::Points, + is_static: false, + num_frames: 1, + }, + ChunkSpec { + entity: "/robot", + component: TestComponent::Points, + is_static: true, + num_frames: 0, + }, + ChunkSpec { + entity: "/camera", + component: TestComponent::Points, + is_static: false, + num_frames: 1, + }, + ]); + + let filter = StructuredFilter { + content: Some(epf("+ /robot/**")), + is_static: Some(false), + ..Default::default() + }; + let (matching, _) = evaluate_filter_on_manifest(&filter, store.manifest()); + + let manifest = store.manifest(); + let is_static_col: Vec = manifest.col_chunk_is_static().collect(); + let entity_paths: Vec = manifest.col_chunk_entity_path().collect(); + let expected: Vec = + itertools::izip!(manifest.col_chunk_ids(), &entity_paths, &is_static_col) + .filter_map(|(id, ep, &is_static)| { + (ep == &EntityPath::from("/robot") && !is_static).then_some(*id) + }) + .collect(); + assert_eq!(sort_ids(matching), sort_ids(expected)); + } + + #[test] + fn test_eval_no_match() { + let (store, _dir) = build_test_store(&[ChunkSpec { + entity: "/robot", + component: TestComponent::Points, + is_static: false, + num_frames: 1, + }]); + + let filter = StructuredFilter { + content: Some(epf("+ /nope/**")), + ..Default::default() + }; + let (matching, _) = evaluate_filter_on_manifest(&filter, store.manifest()); + assert!(matching.is_empty()); + } +} diff --git a/rerun_py/src/chunk_stream/mcap_reader.rs b/rerun_py/src/chunk_stream/mcap_reader.rs index 5a8cc659dcbe..ecb450695615 100644 --- a/rerun_py/src/chunk_stream/mcap_reader.rs +++ b/rerun_py/src/chunk_stream/mcap_reader.rs @@ -24,13 +24,20 @@ pub struct PyMcapReaderInternal { loader: re_importer::importer_mcap::McapImporter, timeline_type: TimeType, timestamp_offset_ns: Option, + + /// Whether to reconstruct a missing/invalid summary in memory (truncated files). + recover: bool, + + /// The parsed MCAP summary, read once and shared across `time_bounds()` and every `stream()` + /// so repeated (e.g. windowed) scans don't each re-parse it. + summary: std::sync::OnceLock>, } #[pymethods] impl PyMcapReaderInternal { #[new] #[pyo3( - text_signature = "(self, path, timeline_type, timestamp_offset_ns, decoders, include_topic_regex, exclude_topic_regex)" + text_signature = "(self, path, timeline_type, timestamp_offset_ns, decoders, include_topic_regex, exclude_topic_regex, start_time_ns, end_time_ns, recover)" )] fn new( path: &str, @@ -39,6 +46,9 @@ impl PyMcapReaderInternal { decoders: Option>, include_topic_regex: Option>, exclude_topic_regex: Option>, + start_time_ns: Option, + end_time_ns: Option, + recover: bool, ) -> PyResult { let path = PathBuf::from(path); if !path.exists() { @@ -80,27 +90,112 @@ impl PyMcapReaderInternal { }; let topic_filter = compile_topic_filter(include_topic_regex, exclude_topic_regex)?; + let time_range = compile_time_range(start_time_ns, end_time_ns)?; let loader = re_importer::importer_mcap::McapImporter::new(&selected_decoders) .with_raw_fallback(true) - .with_topic_filter(topic_filter); + .with_topic_filter(topic_filter) + .with_time_range(time_range) + .with_recover(recover); Ok(Self { path, loader, timeline_type, timestamp_offset_ns, + recover, + summary: std::sync::OnceLock::new(), }) } - /// Return a new lazy stream over all chunks in the MCAP file. - fn stream(&self) -> PyLazyChunkStreamInternal { - PyLazyChunkStreamInternal::new(LazyChunkStream::from_factory(McapStreamFactory::new( - self.path.clone(), - self.loader.clone(), - self.timeline_type, - self.timestamp_offset_ns, - ))) + /// Return a new lazy stream over the MCAP file. + /// + /// `start_time_ns` and `end_time_ns` override the values baked in at construction, for this + /// scan only; `None` keeps the reader's default. If either time bound is given, the pair + /// replaces the reader's time range as a whole (a missing side opens that end). + #[pyo3(signature = (*, start_time_ns=None, end_time_ns=None))] + fn stream( + &self, + start_time_ns: Option, + end_time_ns: Option, + ) -> PyResult { + let mut loader = self.loader.clone(); + if start_time_ns.is_some() || end_time_ns.is_some() { + loader = loader.with_time_range(compile_time_range(start_time_ns, end_time_ns)?); + } + + Ok(PyLazyChunkStreamInternal::new( + LazyChunkStream::from_factory(McapStreamFactory::new( + self.path.clone(), + loader, + self.timeline_type, + self.timestamp_offset_ns, + self.summary()?, + )), + )) + } + + /// Return the `(min, max)` MCAP `log_time` bounds (nanoseconds, inclusive) of the file. + fn time_bounds(&self) -> PyResult<(u64, u64)> { + // If we already have a summary (cached from an earlier `stream()`), use it directly. + // Otherwise read the summary. We deliberately do *not* + // reconstruct the full summary here (which decompresses chunks to harvest channels): time + // bounds only need the chunk time ranges, preserving the cheap "reads no chunks" contract. + let bounds_from_summary = |summary: &re_mcap::Summary| { + let stats = summary + .stats + .as_ref() + .map(|s| (s.message_count, s.message_start_time, s.message_end_time)); + compute_time_bounds( + stats, + summary + .chunk_indexes + .iter() + .map(|c| (c.message_start_time, c.message_end_time)), + ) + }; + + if let Some(summary) = self.summary.get() { + return bounds_from_summary(summary); + } + + let mmap = mmap_file(&self.path)?; + let bounds_from_scan = || { + let scan = re_mcap::build_chunk_index(&mmap).map_err(|err| { + PyValueError::new_err(format!("Failed to scan MCAP chunk index: {err}")) + })?; + scan.reject_if_unrecoverable() + .map_err(|err| PyValueError::new_err(err.to_string()))?; + // `usable_chunks` is the same set `reconstruct_summary` (and thus `stream()`) keeps, so + // the bounds don't report a `max` past the last message any `stream()` can decode. + compute_time_bounds( + None, + scan.usable_chunks() + .map(|c| (c.message_start_time, c.message_end_time)), + ) + }; + + match re_mcap::read_summary(std::io::Cursor::new(&mmap[..])) { + Ok(Some(summary)) => bounds_from_summary(&summary), + Ok(None) if self.recover => { + re_log::warn!( + "MCAP file has no summary; scanning the chunk index for time bounds. The file may be truncated" + ); + bounds_from_scan() + } + Err(err) if self.recover => { + re_log::warn!( + "Failed to read the MCAP summary ({err}); scanning the chunk index for time bounds. The file may be truncated" + ); + bounds_from_scan() + } + Ok(None) => Err(PyValueError::new_err( + "MCAP file does not contain a summary", + )), + Err(err) => Err(PyValueError::new_err(format!( + "Failed to read MCAP summary: {err}" + ))), + } } /// The file path this reader was constructed with. @@ -119,6 +214,47 @@ impl PyMcapReaderInternal { } } +impl PyMcapReaderInternal { + /// Return the parsed MCAP summary, reading and caching it on first use. + fn summary(&self) -> PyResult> { + if let Some(summary) = self.summary.get() { + return Ok(summary.clone()); + } + + let mmap = mmap_file(&self.path)?; + let summary = re_mcap::read_or_reconstruct_summary(&mmap, self.recover) + .map_err(|err| PyValueError::new_err(format!("Failed to read MCAP summary: {err}")))?; + + // A concurrent caller may have won the race; `get_or_init` keeps whichever landed first. + Ok(self.summary.get_or_init(|| Arc::new(summary)).clone()) + } +} + +/// Computes the inclusive `(min, max)` `log_time` bounds, preferring the statistics record +/// (`(message_count, start, end)`) and falling back to the per-chunk `(start, end)` time ranges +/// (both are optional per the MCAP spec). +fn compute_time_bounds( + stats: Option<(u64, u64, u64)>, + chunk_ranges: impl Iterator, +) -> PyResult<(u64, u64)> { + if let Some((message_count, start, end)) = stats + && message_count > 0 + { + return Ok((start, end)); + } + + let mut lo = u64::MAX; + let mut hi = 0_u64; + for (start, end) in chunk_ranges { + lo = lo.min(start); + hi = hi.max(end); + } + if lo > hi { + return Err(PyValueError::new_err("MCAP file contains no messages")); + } + Ok((lo, hi)) +} + /// Factory for creating chunk streams from MCAP files. /// /// Wraps a [`re_importer::importer_mcap::McapImporter`] (which holds decoder config @@ -128,6 +264,7 @@ pub struct McapStreamFactory { loader: re_importer::importer_mcap::McapImporter, timeline_type: TimeType, timestamp_offset_ns: Option, + summary: Arc, } impl McapStreamFactory { @@ -136,16 +273,23 @@ impl McapStreamFactory { loader: re_importer::importer_mcap::McapImporter, timeline_type: TimeType, timestamp_offset_ns: Option, + summary: Arc, ) -> Self { Self { path, loader, timeline_type, timestamp_offset_ns, + summary, } } } +// TODO(RR-4850): this spawn-thread + bounded-channel block is hand-copied across +// mp4/mcap/parquet. Factor it into a shared `spawn_threaded_stream` adapter and +// benchmark whether mcap benefits from threaded pipelining. Note mcap pushes chunks +// via an `emit_chunks` callback rather than returning an iterator, so the shared +// adapter must accept a callback-style producer too (not just `Iterator`). impl ChunkStreamFactory for McapStreamFactory { fn create(&self) -> Result, ChunkPipelineError> { let (tx, rx) = crossbeam::channel::bounded::, ChunkPipelineError>>( @@ -156,15 +300,21 @@ impl ChunkStreamFactory for McapStreamFactory { let loader = self.loader.clone(); let timeline_type = self.timeline_type; let timestamp_offset_ns = self.timestamp_offset_ns; + let summary = self.summary.clone(); std::thread::Builder::new() .name("mcap-chunk-source".into()) .spawn(move || { - let result = - loader.emit_chunks(&mmap, timeline_type, timestamp_offset_ns, &mut |chunk| { + let result = loader.emit_chunks_with_summary( + &mmap, + &summary, + timeline_type, + timestamp_offset_ns, + &|chunk| { // Stop producing if the receiver has been dropped. re_quota_channel::send_crossbeam(&tx, Ok(Arc::new(chunk))).ok(); - }); + }, + ); if let Err(err) = result { re_quota_channel::send_crossbeam( &tx, @@ -225,6 +375,49 @@ fn compile_topic_filter( .map_err(|err| PyValueError::new_err(format!("Invalid topic regex: {err}"))) } +/// Normalize the optional `start`/`end` `log_time` bounds into an inclusive-start, +/// exclusive-end `[start, end)` range in nanoseconds. +/// +/// Returns `None` (no filtering) when both bounds are `None`. A missing `start` opens the +/// range at 0; a missing `end` opens it at `u64::MAX`. MCAP `log_time` is unsigned, so +/// negative inputs are rejected, as is `start >= end` (a half-open range with `start == end` +/// is empty). +fn compile_time_range( + start_time_ns: Option, + end_time_ns: Option, +) -> PyResult> { + if start_time_ns.is_none() && end_time_ns.is_none() { + return Ok(None); + } + + let start = match start_time_ns { + Some(s) if s < 0 => { + return Err(PyValueError::new_err(format!( + "start_time_ns must be non-negative (MCAP log_time is unsigned), got {s}" + ))); + } + Some(s) => s as u64, + None => 0, + }; + let end = match end_time_ns { + Some(e) if e < 0 => { + return Err(PyValueError::new_err(format!( + "end_time_ns must be non-negative (MCAP log_time is unsigned), got {e}" + ))); + } + Some(e) => e as u64, + None => u64::MAX, + }; + + if start >= end { + return Err(PyValueError::new_err(format!( + "start_time_ns ({start}) must be less than end_time_ns ({end}); the range is half-open [start, end)" + ))); + } + + Ok(Some((start, end))) +} + fn mmap_file(path: &Path) -> Result { let file = std::fs::File::open(path).map_err(|err| ChunkPipelineError::Mcap { reason: format!("{}: {err}", path.display()), diff --git a/rerun_py/src/chunk_stream/mod.rs b/rerun_py/src/chunk_stream/mod.rs index 80dbb15dbed9..bb5e2fb7cbe8 100644 --- a/rerun_py/src/chunk_stream/mod.rs +++ b/rerun_py/src/chunk_stream/mod.rs @@ -17,35 +17,50 @@ //! - The PyO3 bindings ([`rrd_reader`], [`py_stream`]) translate between //! Python objects and the Rust pipeline types. -mod chunk_store; +pub mod chunk_store; mod engine; pub mod error; +mod hdf5_reader; +pub mod lazy_store; mod mcap_reader; +mod mp4_reader; mod parquet_reader; mod py_stream; -pub(crate) mod rrd_reader; +pub mod rrd_reader; pub mod stream; -pub(crate) mod urdf_tree_stream; +mod summary; +pub mod urdf_tree_stream; use std::sync::Arc; use pyo3::types::{PyModule, PyModuleMethods as _}; -use pyo3::{Bound, PyResult}; +use pyo3::{Bound, PyResult, wrap_pyfunction}; -pub(crate) use py_stream::PyLazyChunkStreamInternal; +pub use py_stream::PyLazyChunkStreamInternal; /// Register chunk pipeline classes into the module. pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!( + py_stream::_optimization_profile_values, + m + )?)?; Ok(()) } -// TODO(ab): this is a blind guess. We should benchmark/profile to find a good value. +// TODO(RR-4850): revisit as part of the shared iterator→ChunkStream adapter — this +// capacity should likely be a parameter of the threaded adapter (and the benchmark +// should determine whether each reader wants threading at all). const CHUNK_CHANNEL_CAPACITY: usize = 16; /// Pull-based chunk stream. Terminals call `next()` in a loop. @@ -63,4 +78,22 @@ pub trait ChunkStream: Send { /// (e.g. paths, decoder settings, etc.). pub trait ChunkStreamFactory: Send + Sync { fn create(&self) -> Result, error::ChunkPipelineError>; + + /// Create a stream with `filter` pushed into the source as far as the source can manage. + /// + /// The returned stream is responsible for producing chunks that satisfy `filter` — + /// implementations that can't fully absorb the filter wrap their result in an + /// [`engine::FilterStream`] (the default impl does exactly that). + /// + /// The default implementation does no pushdown: it calls [`Self::create`] and wraps the + /// result with the input filter. Sources that can do better should override this. + fn create_with_pushdown( + &self, + filter: &stream::StructuredFilter, + ) -> Result, error::ChunkPipelineError> { + Ok(Box::new(engine::FilterStream::new( + self.create()?, + filter.clone(), + ))) + } } diff --git a/rerun_py/src/chunk_stream/mp4_reader.rs b/rerun_py/src/chunk_stream/mp4_reader.rs new file mode 100644 index 000000000000..26bde0446db9 --- /dev/null +++ b/rerun_py/src/chunk_stream/mp4_reader.rs @@ -0,0 +1,253 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use pyo3::exceptions::{PyFileNotFoundError, PyValueError}; +use pyo3::prelude::*; +use re_chunk::{Chunk, EntityPath}; +use re_log_types::{TimeType, TimelineName}; +use re_mp4_reader::{Mode, Mp4Config}; +use re_sdk_types::components::VideoCodec; +use re_video::{HwAccel, Mp4TranscodeOptions}; + +use super::error::ChunkPipelineError; +use super::py_stream::PyLazyChunkStreamInternal; +use super::stream::LazyChunkStream; +use super::{ChunkStream, ChunkStreamFactory}; + +/// Internal transcode-options binding, wrapping [`Mp4TranscodeOptions`]. +/// +/// Constructed by the Python `Mp4TranscodeOptions` wrapper, which owns the +/// user-facing enum and validation; this just carries already-validated values so +/// [`PyMp4ReaderInternal`] takes a single options object rather than growing a +/// keyword argument per transcode knob. +#[pyclass( + frozen, + name = "Mp4TranscodeOptionsInternal", + module = "rerun_bindings.rerun_bindings" +)] +pub struct PyMp4TranscodeOptions { + inner: Mp4TranscodeOptions, +} + +#[pymethods] +impl PyMp4TranscodeOptions { + #[new] + #[pyo3( + signature = (gop_size = None, output_codec = None, try_gpu = false, ffmpeg_override = None), + text_signature = "(self, gop_size=None, output_codec=None, try_gpu=False, ffmpeg_override=None)" + )] + fn new( + gop_size: Option, + output_codec: Option, + try_gpu: bool, + ffmpeg_override: Option, + ) -> PyResult { + let mut inner = Mp4TranscodeOptions::default().with_hardware_acceleration(if try_gpu { + HwAccel::Auto + } else { + HwAccel::Off + }); + if let Some(gop_size) = gop_size { + inner = inner.with_gop_size(gop_size); + } + if let Some(fourcc) = output_codec { + // `fourcc` is a `rerun.components.VideoCodec` enum value from Python; + // reuse the canonical fourcc→codec conversion rather than re-mapping here. + let codec = VideoCodec::try_from_u32(fourcc).ok_or_else(|| { + PyValueError::new_err(format!("Unknown video codec fourcc: {fourcc:#010x}")) + })?; + inner = inner.with_output_codec(codec.into()); + } + if let Some(ffmpeg_override) = ffmpeg_override { + inner = inner.with_ffmpeg_override(ffmpeg_override); + } + Ok(Self { inner }) + } + + fn __repr__(&self) -> String { + format!("Mp4TranscodeOptionsInternal({:?})", self.inner) + } +} + +/// Internal MP4 reader binding. +#[pyclass( + frozen, + name = "Mp4ReaderInternal", + module = "rerun_bindings.rerun_bindings" +)] +pub struct PyMp4ReaderInternal { + path: PathBuf, + config: Mp4Config, + entity_path: EntityPath, +} + +#[pymethods] +impl PyMp4ReaderInternal { + #[new] + #[pyo3( + signature = ( + path, + mode = "stream", + chunk_by_gop = true, + timeline_name = "video", + timeline_type = "duration", + transcode = None, + entity_path = None, + ), + text_signature = "(self, path, mode='stream', chunk_by_gop=True, timeline_name='video', timeline_type='duration', transcode=None, entity_path=None)" + )] + fn new( + path: PathBuf, + mode: &str, + chunk_by_gop: bool, + timeline_name: &str, + timeline_type: &str, + transcode: Option>, + entity_path: Option, + ) -> PyResult { + if !path.exists() { + return Err(PyFileNotFoundError::new_err(format!( + "MP4 file not found: {}", + path.display() + ))); + } + + let timeline_type = match timeline_type { + "duration" => TimeType::DurationNs, + "timestamp" => TimeType::TimestampNs, + other => { + return Err(PyValueError::new_err(format!( + "Invalid timeline_type: {other:?}. Expected \"duration\" or \"timestamp\"" + ))); + } + }; + + let mode = match mode { + "asset" => { + if !chunk_by_gop { + return Err(PyValueError::new_err( + "`chunk_by_gop=False` is only valid with `mode=\"stream\"`", + )); + } + // `transcode` is validated and rejected for asset mode on the Python + // side, so it's simply ignored here. + Mode::Asset { + timepoint: re_chunk::TimePoint::default(), + } + } + "stream" => Mode::Stream { + chunk_by_gop, + transcode: transcode.map(|t| t.inner.clone()).unwrap_or_default(), + }, + other => { + return Err(PyValueError::new_err(format!( + "Invalid mode: {other:?}. Expected \"asset\" or \"stream\"" + ))); + } + }; + + let config = Mp4Config { + mode, + timeline_name: TimelineName::try_new(timeline_name) + .map_err(|err| PyValueError::new_err(err.to_string()))?, + timeline_type, + }; + + let entity_path = match entity_path { + Some(s) => EntityPath::from(s), + None => EntityPath::from_file_path(&path), + }; + + Ok(Self { + path, + config, + entity_path, + }) + } + + /// Return a new lazy stream over all chunks in the MP4 file. + fn stream(&self) -> PyLazyChunkStreamInternal { + PyLazyChunkStreamInternal::new(LazyChunkStream::from_factory(Self { + path: self.path.clone(), + config: self.config.clone(), + entity_path: self.entity_path.clone(), + })) + } + + /// The file path this reader was constructed with. + #[getter] + fn path(&self) -> PathBuf { + self.path.clone() + } + + /// The entity path under which chunks are emitted. + #[getter] + fn entity_path(&self) -> String { + self.entity_path.to_string() + } +} + +// TODO(RR-4850): this spawn-thread + bounded-channel block is hand-copied across +// mp4/mcap/parquet. Factor it into a shared `spawn_threaded_stream` adapter (and a +// synchronous `IterStream` sibling), then benchmark whether mp4 wants threaded +// pipelining at all or should use the synchronous wrap. `load_mp4` yields a clean +// `'static + Send` iterator, so mp4 could use either variant. +impl ChunkStreamFactory for PyMp4ReaderInternal { + fn create(&self) -> Result, ChunkPipelineError> { + let (tx, rx) = crossbeam::channel::bounded::, ChunkPipelineError>>( + super::CHUNK_CHANNEL_CAPACITY, + ); + + let path = self.path.clone(); + let config = self.config.clone(); + let entity_path = self.entity_path.clone(); + + std::thread::Builder::new() + .name("mp4-chunk-source".into()) + .spawn(move || { + match re_mp4_reader::load_mp4(&path, &config, &entity_path) { + Ok(iter) => { + for chunk_result in iter { + let msg = match chunk_result { + Ok(chunk) => Ok(Arc::new(chunk)), + Err(err) => Err(ChunkPipelineError::Mp4 { + reason: err.to_string(), + }), + }; + if re_quota_channel::send_crossbeam(&tx, msg).is_err() { + break; // receiver dropped + } + } + } + Err(err) => { + re_quota_channel::send_crossbeam( + &tx, + Err(ChunkPipelineError::Mp4 { + reason: err.to_string(), + }), + ) + .ok(); + } + } + // tx drops here → channel closes → Mp4Stream::next() returns Ok(None) + }) + .expect("Failed to spawn mp4 decode thread"); + + Ok(Box::new(Mp4Stream { rx })) + } +} + +/// Chunk stream that receives decoded chunks from a background thread. +struct Mp4Stream { + rx: crossbeam::channel::Receiver, ChunkPipelineError>>, +} + +impl ChunkStream for Mp4Stream { + fn next(&mut self) -> Result>, ChunkPipelineError> { + match self.rx.recv() { + Ok(Ok(chunk)) => Ok(Some(chunk)), + Ok(Err(err)) => Err(err), + Err(crossbeam::channel::RecvError) => Ok(None), // channel closed — loading finished + } + } +} diff --git a/rerun_py/src/chunk_stream/parquet_reader.rs b/rerun_py/src/chunk_stream/parquet_reader.rs index ecf1a3a00752..521e264fbd57 100644 --- a/rerun_py/src/chunk_stream/parquet_reader.rs +++ b/rerun_py/src/chunk_stream/parquet_reader.rs @@ -4,9 +4,7 @@ use std::sync::Arc; use pyo3::exceptions::{PyFileNotFoundError, PyValueError}; use pyo3::prelude::*; use re_chunk::{Chunk, EntityPath}; -use re_parquet::{ - ColumnGrouping, ColumnMapping, ColumnRule, IndexColumn, IndexType, ParquetConfig, TimeUnit, -}; +use re_parquet::{ColumnGrouping, IndexColumn, IndexType, ParquetConfig, TimeUnit}; use super::error::ChunkPipelineError; use super::py_stream::PyLazyChunkStreamInternal; @@ -28,7 +26,6 @@ pub struct PyParquetReaderInternal { #[pymethods] impl PyParquetReaderInternal { #[new] - #[expect(clippy::too_many_arguments)] #[pyo3( signature = ( path, @@ -39,9 +36,8 @@ impl PyParquetReaderInternal { use_structs = true, static_columns = None, index_columns = None, - column_rules = None, ), - text_signature = "(self, path, entity_path_prefix=None, column_grouping='prefix', delimiter='_', prefixes=None, use_structs=True, static_columns=None, index_columns=None, column_rules=None)" + text_signature = "(self, path, entity_path_prefix=None, column_grouping='prefix', delimiter='_', prefixes=None, use_structs=True, static_columns=None, index_columns=None)" )] fn new( path: &str, @@ -52,7 +48,6 @@ impl PyParquetReaderInternal { use_structs: bool, static_columns: Option>, index_columns: Option)>>, - column_rules: Option>>, ) -> PyResult { let path = PathBuf::from(path); if !path.exists() { @@ -104,12 +99,6 @@ impl PyParquetReaderInternal { } }; - let rules: Vec = if let Some(rules) = column_rules { - parse_column_rules(rules)? - } else { - Vec::new() - }; - let index_cols: Vec = index_columns .unwrap_or_default() .into_iter() @@ -143,7 +132,6 @@ impl PyParquetReaderInternal { column_grouping: grouping, index_columns: index_cols, static_columns: static_columns.unwrap_or_default(), - column_rules: rules, }; let prefix = entity_path_prefix @@ -173,6 +161,12 @@ impl PyParquetReaderInternal { } } +// TODO(RR-4850): this spawn-thread + bounded-channel block is hand-copied across +// mp4/mcap/parquet. Factor it into a shared `spawn_threaded_stream` adapter and +// benchmark. Note parquet's iterator is `!Send` (see below), so the threaded +// adapter must bound only the `produce` closure as `Send` — not `I` itself, since +// the iterator is created and consumed entirely on the worker thread. This reader +// therefore cannot use the synchronous `IterStream` variant. impl ChunkStreamFactory for PyParquetReaderInternal { fn create(&self) -> Result, ChunkPipelineError> { let (tx, rx) = crossbeam::channel::bounded::, ChunkPipelineError>>( @@ -236,46 +230,3 @@ impl ChunkStream for ParquetStream { } } } - -/// Parse `column_rules` from Python `ColumnRule` dataclass instances. -fn parse_column_rules(rules: Vec>) -> PyResult> { - rules - .into_iter() - .map(|item| { - let suffixes: Vec = item.getattr("suffixes")?.extract()?; - let target: String = item.getattr("target")?.extract()?; - let names: Option> = item.getattr("names")?.extract()?; - let field_name_override: Option = - item.getattr("field_name_override")?.extract()?; - - let rotation_suffixes: Option> = - item.getattr("rotation_suffixes")?.extract()?; - - let mapping = match target.as_str() { - "Translation3D" => ColumnMapping::translation3d(), - "RotationQuat" => ColumnMapping::rotation_quat(), - "RotationAxisAngle" => ColumnMapping::rotation_axis_angle(), - "Scale3D" => ColumnMapping::scale3d(), - "Scalars" => ColumnMapping::Scalars { - names: names - .ok_or_else(|| PyValueError::new_err("Scalars target requires 'names'"))?, - }, - "Transform" => ColumnMapping::transform(rotation_suffixes.ok_or_else(|| { - PyValueError::new_err("Transform target requires 'rotation_suffixes'") - })?), - other => { - return Err(PyValueError::new_err(format!( - "Unknown target: '{other}'. Valid targets: Translation3D, RotationQuat, \ - RotationAxisAngle, Scale3D, Scalars, Transform." - ))); - } - }; - - Ok(ColumnRule { - suffixes, - mapping, - field_name_override, - }) - }) - .collect() -} diff --git a/rerun_py/src/chunk_stream/py_stream.rs b/rerun_py/src/chunk_stream/py_stream.rs index 079b2b4610dd..cbeea3d8e149 100644 --- a/rerun_py/src/chunk_stream/py_stream.rs +++ b/rerun_py/src/chunk_stream/py_stream.rs @@ -3,26 +3,34 @@ use std::sync::Arc; use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; +use pyo3::types::PyDict; +use re_log::ResultExt as _; use re_log_types::{ EntityPathFilter, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind, StoreSource, }; use re_types_core::ComponentIdentifier; -use re_chunk_store::{ChunkStore, ChunkStoreConfig, CompactionOptions, IsStartOfGop}; +use re_chunk_store::{ + ChunkStore, ChunkStoreConfig, CompactionOptions, IsStartOfGop, OptimizationProfile, +}; use super::ChunkStream; use super::chunk_store::PyChunkStoreInternal; use super::error::ChunkPipelineError; use super::stream::{LazyChunkStream, StructuredFilter}; use crate::chunk::PyChunkInternal; +use crate::python_bridge::{PyRecordingStream, flush_garbage_queue, get_data_recording}; /// Internal lazy chunk stream binding. /// -/// This class implements of form of Rust-like move semantics. Builder methods (filter, split, etc.) -/// **consume** the inner stream via `Option::take()`. This ensures that no lazy stream is used more -/// than once in a pipeline. Terminals (collect, write_rrd, __iter__) borrow without consuming, so -/// the same stream can be materialized multiple times. +/// This class implements a form of Rust-like move semantics. Builder methods +/// (`filter`, `drop_matching`, `split`, `map`, `flat_map`, `lenses`, `merge`) +/// **consume** the inner stream via `Option::take()`: a consumed stream raises +/// `ValueError` on further use, ensuring no lazy stream is used in more than +/// one pipeline. Terminals (`to_chunks`, `__iter__`, `collect`, `write_rrd`, +/// `send_to_recording`) **borrow** the inner stream and run the pipeline; the +/// stream remains usable and can be re-executed. #[pyclass( frozen, name = "LazyChunkStreamInternal", @@ -67,7 +75,7 @@ impl PyLazyChunkStreamInternal { #[pymethods] impl PyLazyChunkStreamInternal { - /// Keep the matching portion of each chunk. + /// Keep the matching portion of each chunk. Consumes this stream. #[pyo3(signature = (*, content=None, has_timeline=None, is_static=None, components=None))] fn filter( &self, @@ -77,11 +85,11 @@ impl PyLazyChunkStreamInternal { components: Option>, ) -> PyResult { let stream = self.take_inner()?; - let f = build_structured_filter(content, has_timeline, is_static, components); + let f = build_structured_filter(content, has_timeline, is_static, components)?; Ok(Self::new(stream.filter(f))) } - /// Drop the matching portion of each chunk. + /// Drop the matching portion of each chunk. Consumes this stream. #[pyo3(signature = (*, content=None, has_timeline=None, is_static=None, components=None))] fn drop_matching( &self, @@ -91,11 +99,11 @@ impl PyLazyChunkStreamInternal { components: Option>, ) -> PyResult { let stream = self.take_inner()?; - let f = build_structured_filter(content, has_timeline, is_static, components); + let f = build_structured_filter(content, has_timeline, is_static, components)?; Ok(Self::new(stream.drop_matching(f))) } - /// Split into (matching, non_matching). + /// Split into (matching, non_matching). Consumes this stream. #[pyo3(signature = (*, content=None, has_timeline=None, is_static=None, components=None))] fn split( &self, @@ -105,7 +113,7 @@ impl PyLazyChunkStreamInternal { components: Option>, ) -> PyResult<(Self, Self)> { let stream = self.take_inner()?; - let f = build_structured_filter(content, has_timeline, is_static, components); + let f = build_structured_filter(content, has_timeline, is_static, components)?; let (a, b) = stream.split(f); Ok((Self::new(a), Self::new(b))) } @@ -126,15 +134,16 @@ impl PyLazyChunkStreamInternal { #[expect(clippy::needless_pass_by_value)] // PyO3 requires owned Vec fn lenses( &self, - lenses: Vec>, + py: Python<'_>, + lenses: Vec, output_mode: &str, content: Option>, ) -> PyResult { let stream = self.take_inner()?; let mode = crate::lenses::parse_output_mode(output_mode)?; let mut collection = re_lenses_core::Lenses::new(mode); - for lens in &lenses { - collection = collection.add_lens(lens.inner().clone()); + for py_lens in &lenses { + collection = collection.add_lens(py_lens.build(py)?); } let content = content.map(|exprs| { let rules = exprs.join(" "); @@ -143,7 +152,7 @@ impl PyLazyChunkStreamInternal { Ok(Self::new(stream.lenses(collection, content))) } - /// Concatenate chunks from multiple streams into one. + /// Concatenate chunks from multiple streams into one. Consumes all input streams. #[staticmethod] #[expect(clippy::needless_pass_by_value)] // PyO3 requires owned Vec for #[staticmethod] fn merge(streams: Vec>) -> PyResult { @@ -154,7 +163,7 @@ impl PyLazyChunkStreamInternal { Ok(Self::new(LazyChunkStream::merge(inners))) } - /// Consume the stream and write all chunks to an RRD file. + /// Run the pipeline and write all chunks to an RRD file. fn write_rrd( &self, py: Python<'_>, @@ -171,7 +180,7 @@ impl PyLazyChunkStreamInternal { .map_err(|err| PyRuntimeError::new_err(err.to_string())) } - /// Consume the stream and materialize all chunks into a ChunkStore. + /// Run the pipeline and materialize all chunks into a ChunkStore. /// /// The defaults (`extra_passes=0`, `gop_batching=False`) produce a store that /// has only received the single-pass compaction that happens naturally during @@ -185,8 +194,9 @@ impl PyLazyChunkStreamInternal { extra_passes = 0, gop_batching = false, split_size_ratio = None, + fix_keyframe = false, ))] - #[expect(clippy::too_many_arguments)] + #[allow(clippy::fn_params_excessive_bools)] // PyO3 signature mirrors the Python `OptimizationProfile` dataclass; collapsed into `VideoRebatching` inside. fn collect( &self, py: Python<'_>, @@ -196,6 +206,7 @@ impl PyLazyChunkStreamInternal { extra_passes: usize, gop_batching: bool, split_size_ratio: Option, + fix_keyframe: bool, ) -> PyResult { let mut compiled = self.compile_inner()?; py.detach(move || -> Result<_, ChunkPipelineError> { @@ -220,6 +231,7 @@ impl PyLazyChunkStreamInternal { num_extra_passes: Some(extra_passes), is_start_of_gop, split_size_ratio, + fix_keyframe, }; let store_id = StoreId::random(StoreKind::Recording, "chunk-store"); @@ -243,12 +255,12 @@ impl PyLazyChunkStreamInternal { } })?; - Ok(PyChunkStoreInternal::in_memory(store)) + Ok(PyChunkStoreInternal::new(store)) }) .map_err(PyErr::from) } - /// Consume the stream and return all chunks as a list. + /// Run the pipeline and return all chunks as a list. fn to_chunks(&self, py: Python<'_>) -> PyResult> { let mut compiled = self.compile_inner()?; let chunks: Vec> = py @@ -278,6 +290,31 @@ impl PyLazyChunkStreamInternal { let iter_obj = iterable.call_method0(py, "__iter__")?; Ok(Self::new(LazyChunkStream::from_py_iter(iter_obj))) } + + /// Run the pipeline and send chunks to a recording stream. + /// + /// If `recording` is `None`, the active recording is used. Blocks until every + /// chunk has been pushed to the recording's batcher. A silent no-op when + /// there is no active recording. + #[pyo3(signature = (recording=None))] + fn send_to_recording( + &self, + py: Python<'_>, + recording: Option<&PyRecordingStream>, + ) -> PyResult<()> { + let Some(recording) = get_data_recording(recording) else { + return Ok(()); + }; + let mut compiled = self.compile_inner()?; + py.detach(|| -> Result<(), ChunkPipelineError> { + while let Some(chunk) = compiled.next()? { + recording.send_chunk((*chunk).clone()); + } + flush_garbage_queue(); + Ok(()) + }) + .map_err(PyErr::from) + } } // --------------------------------------------------------------------------- @@ -329,26 +366,31 @@ fn build_structured_filter( has_timeline: Option, is_static: Option, components: Option>, -) -> StructuredFilter { +) -> PyResult { let content = content.map(|exprs| { let rules = exprs.join(" "); EntityPathFilter::parse_forgiving(&rules).resolve_without_substitutions() }); - let has_timeline = has_timeline.map(|s| re_types_core::TimelineName::from(s.as_str())); + let has_timeline = has_timeline + .map(|s| { + re_types_core::TimelineName::try_new(s) + .map_err(|err| pyo3::exceptions::PyValueError::new_err(err.to_string())) + }) + .transpose()?; let components = components.map(|cs| { cs.iter() - .map(|s| ComponentIdentifier::from(s.as_str())) + .filter_map(|s| ComponentIdentifier::try_new(s.as_str()).ok_or_log_error_once()) .collect() }); - StructuredFilter { + Ok(StructuredFilter { content, has_timeline, is_static, components, - } + }) } /// Write all chunks from a pre-compiled [`ChunkStream`] to an RRD file. @@ -384,3 +426,35 @@ fn write_rrd_compiled( encoder.finish()?; Ok(()) } + +/// Test-only: return a dict of the Rust `OptimizationProfile::` field values. +/// +/// Used by the Python parity test to confirm that +/// `OptimizationProfile.{LIVE,OBJECT_STORE}` on the Python side stays in sync +/// with the Rust constants this module forwards into `ChunkStoreConfig` / +/// `CompactionOptions` above. +/// +/// Names: `"LIVE"`, `"OBJECT_STORE"`. +#[pyfunction] +pub fn _optimization_profile_values<'py>( + py: Python<'py>, + name: &str, +) -> PyResult> { + let p = match name { + "LIVE" => OptimizationProfile::LIVE, + "OBJECT_STORE" => OptimizationProfile::OBJECT_STORE, + other => { + return Err(PyValueError::new_err(format!( + "unknown profile name: {other}" + ))); + } + }; + let d = PyDict::new(py); + d.set_item("chunk_max_bytes", p.chunk_max_bytes)?; + d.set_item("chunk_max_rows", p.chunk_max_rows)?; + d.set_item("chunk_max_rows_if_unsorted", p.chunk_max_rows_if_unsorted)?; + d.set_item("num_extra_passes", p.num_extra_passes)?; + d.set_item("gop_batching", p.gop_batching)?; + d.set_item("split_size_ratio", p.split_size_ratio)?; + Ok(d) +} diff --git a/rerun_py/src/chunk_stream/rrd_reader.rs b/rerun_py/src/chunk_stream/rrd_reader.rs index 756a5472052d..9bb94630368e 100644 --- a/rerun_py/src/chunk_stream/rrd_reader.rs +++ b/rerun_py/src/chunk_stream/rrd_reader.rs @@ -5,20 +5,67 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use re_chunk::Chunk; -use re_chunk_store::{ChunkStore, ChunkStoreConfig, LazyRrdStore}; -use re_log_types::{LogMsg, StoreId, StoreInfo, StoreKind}; +use re_chunk_store::LazyStore; +use re_log_encoding::{RawRrdManifest, RrdChunkProvider}; +use re_log_types::{LogMsg, StoreId, StoreKind}; -use super::chunk_store::PyChunkStoreInternal; +use crate::utils::wait_for_future; use super::error::ChunkPipelineError; +use super::lazy_store::PyLazyStoreInternal; use super::py_stream::PyLazyChunkStreamInternal; use super::stream::LazyChunkStream; use super::{ChunkStream, ChunkStreamFactory}; +/// Describes a single store found in an RRD file. +#[pyclass( + frozen, + from_py_object, + name = "StoreEntryInternal", + module = "rerun_bindings.rerun_bindings" +)] +#[derive(Clone)] +pub struct PyStoreEntryInternal { + store_id: StoreId, +} + +#[pymethods] +impl PyStoreEntryInternal { + #[getter] + fn kind(&self) -> &str { + match self.store_id.kind() { + StoreKind::Recording => "recording", + StoreKind::Blueprint => "blueprint", + } + } + + #[getter] + fn application_id(&self) -> &str { + self.store_id.application_id().as_str() + } + + #[getter] + fn recording_id(&self) -> &str { + self.store_id.recording_id().as_str() + } + + fn __eq__(&self, other: &Self) -> bool { + self.store_id == other.store_id + } + + fn __hash__(&self) -> u64 { + use std::hash::{Hash as _, Hasher as _}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.store_id.hash(&mut hasher); + hasher.finish() + } +} + /// Internal RRD reader binding. /// -/// Opens an RRD file and extracts header metadata. -/// Each call to `stream()` produces an independent lazy chunk stream. +/// Opens an RRD file. Store discovery is lazy: `store_entries()` scans the file (footer or +/// header) on first call and caches the result. Each call to `stream()` produces an +/// independent lazy chunk stream; `store()` opens a specific store as a [`LazyStore`]. #[pyclass( frozen, name = "RrdReaderInternal", @@ -26,15 +73,16 @@ use super::{ChunkStream, ChunkStreamFactory}; )] pub struct PyRrdReaderInternal { path: PathBuf, - application_id: Option, - recording_id: Option, + + /// Lazily populated on first `stores()` call. + cached_stores: parking_lot::Mutex>>, } #[pymethods] impl PyRrdReaderInternal { #[new] #[pyo3(text_signature = "(self, path)")] - fn new(path: &str) -> PyResult { + fn new(py: Python<'_>, path: &str) -> PyResult { let path = PathBuf::from(path); if !path.exists() { @@ -44,63 +92,101 @@ impl PyRrdReaderInternal { ))); } - // Read the header to extract StoreInfo - let store_info = read_rrd_store_info(&path).map_err(PyErr::from)?; - - let (application_id, recording_id) = if let Some(info) = store_info { - ( - Some(info.application_id().as_str().to_owned()), - Some(info.recording_id().as_str().to_owned()), + // Reading the footer is cheap (3 seeks) and tells us whether this is a + // legacy RRD that has no manifest. Without one, store enumeration falls + // back to a whole-file frame scan and `store()` won't work at all, + // so it's worth surfacing this up-front rather than at first use. + if let Ok(file) = std::fs::File::open(&path) + && matches!( + wait_for_future( + py, + re_log_encoding::read_rrd_footer(&mut futures::io::AllowStdIo::new(file)), + ), + Ok(None) ) - } else { - (None, None) - }; + { + crate::utils::py_rerun_warn(&format!( + "RRD file has no footer/manifest: {}. \ + This is a legacy format; store enumeration will fall back to a \ + whole-file scan, and `store()` is not supported (use `stream()` instead).", + path.display() + ))?; + } Ok(Self { path, - application_id, - recording_id, + cached_stores: parking_lot::Mutex::new(None), }) } - /// Return a new lazy stream over all chunks in the RRD file. - fn stream(&self) -> PyLazyChunkStreamInternal { - PyLazyChunkStreamInternal::new(LazyChunkStream::from_factory(RrdStreamFactory::new( - self.path.clone(), - ))) + /// List all store entries in this RRD file. + /// + /// Lazily computed on first call, then cached. + fn store_entries(&self, py: Python<'_>) -> PyResult> { + Ok(self + .ensure_cached_stores(py)? + .into_iter() + .map(|store_id| PyStoreEntryInternal { store_id }) + .collect()) + } + + /// Return a new lazy stream over chunks from a specific store. + /// + /// If `store` is `None`, streams the first recording store found in this RRD. Errors + /// if the file contains no recording stores. + #[pyo3(signature = (store=None))] + fn stream( + &self, + store: Option<&PyStoreEntryInternal>, + py: Python<'_>, + ) -> PyResult { + let target = self.resolve_target(py, store)?; + Ok(PyLazyChunkStreamInternal::new( + LazyChunkStream::from_factory(RrdStreamFactory::new(self.path.clone(), target)), + )) } - /// Load a ChunkStore from the RRD file. + /// Open a specific store as a [`LazyStore`]: read the manifest now, load chunks on demand. /// - /// If the file has a footer/manifest, this uses lazy loading: only the index is read - /// immediately, and chunk data is loaded on demand (e.g., when streaming or compacting). - /// For legacy RRD files without a footer, falls back to eager full loading. - fn store(&self, py: Python<'_>) -> PyResult { + /// If `store` is `None`, opens the first recording store. Errors if the file contains + /// no recording stores. Errors with `RrdNoManifest` for legacy RRDs that lack a + /// footer/manifest — those must be materialized via `RrdReader.stream().collect()`. + #[pyo3(signature = (store=None))] + fn store( + &self, + store: Option<&PyStoreEntryInternal>, + py: Python<'_>, + ) -> PyResult { let path = self.path.clone(); - py.detach(move || -> Result<_, ChunkPipelineError> { - let mut file = - std::fs::File::open(&path).map_err(|err| ChunkPipelineError::RrdRead { - path: path.clone(), - reason: err.to_string(), - })?; + let target_store_id = self.resolve_target(py, store)?; + + wait_for_future(py, async move { + let path_buf = path.clone(); + let file = std::fs::File::open(&path).map_err(|err| ChunkPipelineError::RrdRead { + path: path_buf.clone(), + reason: err.to_string(), + })?; + let mut reader = futures::io::AllowStdIo::new(file); - match re_log_encoding::read_rrd_footer(&mut file) { + match re_log_encoding::read_rrd_footer(&mut reader).await { Ok(Some(rrd_footer)) => { - let raw = pick_first_recording_manifest(&rrd_footer, &path)?; - let lazy = LazyRrdStore::try_new(file, path.clone(), Arc::new(raw)).map_err( - |err| ChunkPipelineError::RrdRead { - path, + let raw = pick_manifest(&rrd_footer, &path, &target_store_id)?; + let provider = Arc::new( + RrdChunkProvider::from_reader( + reader, + path.display().to_string(), + Arc::new(raw), + ) + .map_err(|err| ChunkPipelineError::RrdRead { + path: path_buf.clone(), reason: format!("Invalid RRD manifest: {err}"), - }, - )?; - Ok(PyChunkStoreInternal::indexed_rrd(lazy)) - } - Ok(None) => { - // No footer (legacy RRD) — eager fallback. - load_rrd_to_chunk_store(&path) + })?, + ); + Ok(PyLazyStoreInternal::new(LazyStore::new(provider))) } + Ok(None) => Err(ChunkPipelineError::RrdNoManifest { path: path_buf }), Err(err) => Err(ChunkPipelineError::RrdRead { - path, + path: path_buf, reason: err.to_string(), }), } @@ -108,60 +194,102 @@ impl PyRrdReaderInternal { .map_err(PyErr::from) } - /// Application ID from the RRD's StoreInfo, if present. + /// The file path of the RRD file. #[getter] - fn application_id(&self) -> Option<&str> { - self.application_id.as_deref() + fn path(&self) -> PathBuf { + self.path.clone() } +} - /// Recording ID from the RRD's StoreInfo, if present. - #[getter] - fn recording_id(&self) -> Option<&str> { - self.recording_id.as_deref() +impl PyRrdReaderInternal { + /// Populate the store cache on first call, then return a clone of the cached list. + fn ensure_cached_stores(&self, py: Python<'_>) -> PyResult> { + let mut cache = self.cached_stores.lock(); + if cache.is_none() { + *cache = Some(enumerate_rrd_stores(py, &self.path).map_err(PyErr::from)?); + } + Ok(cache.as_ref().expect("just populated above").clone()) } - /// The file path of the RRD file. - #[getter] - fn path(&self) -> PathBuf { - self.path.clone() + /// Resolve the `store` argument to a concrete [`StoreId`]. + /// + /// If `store` is `Some`, returns its id after validating it belongs to this RRD. + /// If `None`, picks the first recording store, erroring if there isn't one and + /// warning if there are several (so the implicit pick doesn't go unnoticed). + fn resolve_target( + &self, + py: Python<'_>, + store: Option<&PyStoreEntryInternal>, + ) -> PyResult { + let cached = self.ensure_cached_stores(py)?; + if let Some(s) = store { + if !cached.contains(&s.store_id) { + return Err(PyValueError::new_err(format!( + "Store {:?} not found in RRD file", + s.store_id + ))); + } + return Ok(s.store_id.clone()); + } + let recordings: Vec = cached + .into_iter() + .filter(|id| id.kind() == StoreKind::Recording) + .collect(); + let first = recordings + .first() + .cloned() + .ok_or_else(|| PyValueError::new_err("No recording store found in RRD file"))?; + if recordings.len() > 1 { + crate::utils::py_rerun_warn(&format!( + "RRD contains {} recording stores; implicitly using {:?}. \ + Pass `store=…` to select explicitly (see `recordings()`).", + recordings.len(), + first + ))?; + } + Ok(first) } } -/// Factory for creating RRD chunk streams. +/// Factory for creating RRD chunk streams targeting a specific store. pub struct RrdStreamFactory { path: PathBuf, + target_store_id: StoreId, } impl RrdStreamFactory { - pub fn new(path: PathBuf) -> Self { - Self { path } + pub fn new(path: PathBuf, target_store_id: StoreId) -> Self { + Self { + path, + target_store_id, + } } } impl ChunkStreamFactory for RrdStreamFactory { fn create(&self) -> Result, ChunkPipelineError> { - Ok(Box::new(RrdStream::new(&self.path))) + Ok(Box::new(RrdStream::new( + &self.path, + self.target_store_id.clone(), + ))) } } -/// Chunk stream that lazily decodes an RRD file. -/// -/// Streams only the **first recording store** found in the file. Blueprint stores are silently -/// skipped (`info!`), and additional recording stores are skipped with a `warn!`. -/// -/// TODO(RR-4263): make this more flexible. +/// Chunk stream that lazily decodes an RRD file, yielding chunks from a single target store. /// /// Construction is fallible: I/O errors (missing file, permission denied) are /// captured and surfaced on the first `next()` call rather than panicking. +// TODO(RR-4850): this is the synchronous (no-thread) reference case for the shared +// adapter. Once `IterStream` lands, `RrdStream` could be replaced by it, keeping the +// per-item store-filtering as a `filter_map` on the decoder iterator. enum RrdStream { /// Normal operation: lazily decode messages from the file. Live { path: PathBuf, decoder: Box> + Send>, - /// The `StoreId` of the first recording store we encounter. Only chunks - /// belonging to this store are yielded; everything else is skipped. - target_store_id: Option, + /// The `StoreId` of the store whose chunks we yield. Everything else is skipped. + target_store_id: StoreId, }, /// The file could not be opened. The error is yielded once, then the stream terminates. @@ -169,7 +297,7 @@ enum RrdStream { } impl RrdStream { - fn new(path: &Path) -> Self { + fn new(path: &Path, target_store_id: StoreId) -> Self { match std::fs::File::open(path) { Ok(file) => { let reader = std::io::BufReader::new(file); @@ -177,7 +305,7 @@ impl RrdStream { Self::Live { path: path.to_path_buf(), decoder: Box::new(decoder), - target_store_id: None, + target_store_id, } } @@ -212,164 +340,53 @@ impl ChunkStream for RrdStream { })?; match msg { - LogMsg::SetStoreInfo(set_store_info) => { - let info = &set_store_info.info; - if info.store_id.kind() == StoreKind::Recording { - if target_store_id.is_none() { - *target_store_id = Some(info.store_id.clone()); - } else if target_store_id.as_ref() != Some(&info.store_id) { - re_log::warn!( - "RRD contains multiple recording stores; \ - ignoring store {:?}", - info.store_id, - ); - } - } else { - re_log::info!("Skipping blueprint store {:?} in RRD", info.store_id,); - } - } + LogMsg::SetStoreInfo(_) | LogMsg::BlueprintActivationCommand(_) => {} LogMsg::ArrowMsg(ref store_id, ref arrow_msg) => { - // Only yield chunks from the active recording. - let is_target_store = target_store_id - .as_ref() - .is_some_and(|active| active == store_id); - - if is_target_store { + if store_id == target_store_id { let chunk = Chunk::from_arrow_msg(arrow_msg).map_err(|err| { ChunkPipelineError::RrdChunkDecode { reason: err.to_string(), } })?; - return Ok(Some(Arc::new(chunk))); } - // Chunk belongs to a different store — skip it. } - - LogMsg::BlueprintActivationCommand(_) => {} } }, } } } -/// Load an RRD file into a fully materialized [`ChunkStore`]. -/// -/// Reads the first recording store from the file -- matching the same behavior as the -/// streaming [`RrdStream`]. -//TODO(RR-4263): we should deal better with multi-store RRDs. -fn load_rrd_to_chunk_store(path: &Path) -> Result { - let path_buf = path.to_path_buf(); +/// Open `path` and enumerate its stores, wrapping I/O and codec errors into [`ChunkPipelineError`]. +fn enumerate_rrd_stores(py: Python<'_>, path: &Path) -> Result, ChunkPipelineError> { let file = std::fs::File::open(path).map_err(|err| ChunkPipelineError::RrdRead { - path: path_buf.clone(), - reason: format!("Failed to open file: {err}"), + path: path.to_path_buf(), + reason: err.to_string(), })?; - let decoder = - re_log_encoding::Decoder::decode_eager(std::io::BufReader::new(file)).map_err(|err| { - ChunkPipelineError::RrdRead { - path: path_buf.clone(), - reason: format!("Failed to start decoding: {err}"), - } - })?; - let mut store: Option = None; - - for msg in decoder { - let msg = msg.map_err(|err| ChunkPipelineError::RrdRead { - path: path_buf.clone(), - reason: format!("Failed to read message: {err}"), - })?; - match &msg { - LogMsg::SetStoreInfo(set_store_info) => { - if set_store_info.info.store_id.kind() == StoreKind::Recording && store.is_none() { - store = Some(ChunkStore::new( - set_store_info.info.store_id.clone(), - ChunkStoreConfig::ALL_DISABLED, - )); - } - } - - LogMsg::ArrowMsg(msg_store_id, arrow_msg) => { - if let Some(s) = &mut store - && s.id() == *msg_store_id - { - let chunk = Chunk::from_arrow_msg(arrow_msg).map_err(|err| { - ChunkPipelineError::RrdChunkDecode { - reason: err.to_string(), - } - })?; - s.insert_chunk(&Arc::new(chunk)).map_err(|err| { - ChunkPipelineError::ChunkStoreInsert { - reason: err.to_string(), - } - })?; - } - } - - LogMsg::BlueprintActivationCommand(_) => {} + let mut reader = futures::io::AllowStdIo::new(file); + wait_for_future(py, re_log_encoding::enumerate_rrd_stores(&mut reader)).map_err(|err| { + ChunkPipelineError::RrdRead { + path: path.to_path_buf(), + reason: err.to_string(), } - } - - let store = store.ok_or_else(|| ChunkPipelineError::RrdRead { - path: path_buf, - reason: "No recording store found in file".to_owned(), - })?; - Ok(PyChunkStoreInternal::in_memory(store)) + }) } -/// Pick the first recording manifest from an RRD footer. -fn pick_first_recording_manifest( +/// Look up `target`'s manifest in an RRD footer. +fn pick_manifest( rrd_footer: &re_log_encoding::RrdFooter, path: &Path, -) -> Result { - let (_, raw_manifest) = rrd_footer - .manifests - .iter() - .find(|(store_id, _)| store_id.kind() == StoreKind::Recording) - .ok_or_else(|| ChunkPipelineError::RrdRead { - path: path.to_path_buf(), - reason: "No recording store found in RRD footer".to_owned(), - })?; + target: &StoreId, +) -> Result { + let raw_manifest = + rrd_footer + .manifests + .get(target) + .ok_or_else(|| ChunkPipelineError::RrdRead { + path: path.to_path_buf(), + reason: format!("Store {target:?} not found in RRD footer"), + })?; Ok(raw_manifest.clone()) } - -/// Open an RRD file and extract the [`StoreInfo`] from the first recording store. -/// -/// Blueprint stores are skipped. Returns `None` if no recording store is found -/// before the first `ArrowMsg` or end of file. -//TODO(RR-4263): we should deal better with multi-store RRDs. -fn read_rrd_store_info(path: &Path) -> Result, ChunkPipelineError> { - let path_buf = path.to_path_buf(); - let file = std::fs::File::open(path).map_err(|err| ChunkPipelineError::RrdRead { - path: path_buf.clone(), - reason: format!("Failed to open file: {err}"), - })?; - let reader = std::io::BufReader::new(file); - let decoder = re_log_encoding::Decoder::::decode_lazy(reader); - - for msg_result in decoder { - match msg_result { - Ok(LogMsg::SetStoreInfo(set_store_info)) - if set_store_info.info.store_id.kind() == StoreKind::Recording => - { - return Ok(Some(set_store_info.info)); - } - - Ok(LogMsg::ArrowMsg(..)) => { - return Ok(None); - } - - Err(err) => { - return Err(ChunkPipelineError::RrdRead { - path: path_buf, - reason: format!("Failed to read header: {err}"), - }); - } - - _ => {} - } - } - - Ok(None) -} diff --git a/rerun_py/src/chunk_stream/stream.rs b/rerun_py/src/chunk_stream/stream.rs index 5ffcef213e17..76b763402a7b 100644 --- a/rerun_py/src/chunk_stream/stream.rs +++ b/rerun_py/src/chunk_stream/stream.rs @@ -50,22 +50,200 @@ pub struct StructuredFilter { pub components: Option>, } +/// Outcome of [`StructuredFilter::try_merge`]. +//TODO(RR-4717): Why do we need this? Because `StructuredFilter`'s representation is not general +// enough to express all possible merges. We should explore making it more general, such that we +// can simplify the try_merge API and, possibly, make the pushdown filtering slightly more powerful. +#[derive(Debug)] +pub enum MergeResult { + /// Successfully merged into a single filter. + Merged(StructuredFilter), + + /// Some field couldn't be combined; keep `other` as a separate post-filter. + Conflict, + + /// AND is unsatisfiable (e.g. `is_static=true AND is_static=false`). + Empty, +} + +/// View over the chunk-level metadata that a [`StructuredFilter`] inspects. +/// +/// This is used such that the same [`StructuredFilter::matches`] code path can be applied +/// to both actual chunk filtering, and manifest filtering during pushdown. +pub(super) trait ChunkPredicateView { + fn entity_path(&self) -> &re_log_types::EntityPath; + fn is_static(&self) -> bool; + fn has_timeline(&self, name: &re_types_core::TimelineName) -> bool; + fn has_any_component(&self, components: &[ComponentIdentifier]) -> bool; +} + +impl ChunkPredicateView for Chunk { + fn entity_path(&self) -> &re_log_types::EntityPath { + Self::entity_path(self) + } + + fn is_static(&self) -> bool { + Self::is_static(self) + } + + fn has_timeline(&self, name: &re_types_core::TimelineName) -> bool { + self.timelines().contains_key(name) + } + + fn has_any_component(&self, components: &[ComponentIdentifier]) -> bool { + components + .iter() + .any(|c| self.components().contains_component(*c)) + } +} + impl StructuredFilter { - /// Check chunk-level predicates (content, has_timeline, is_static). - /// Returns `true` if all pass. - fn predicates_match(&self, chunk: &Chunk) -> bool { - if let Some(ref content) = self.content - && !content.matches(chunk.entity_path()) + /// AND-merge `other` into `self`. + /// + /// Per-field semantics when both sides are `Some` (when one side is `None`, the result is + /// always whichever side is `Some`): + /// + /// | Field | Both `Some` | + /// |----------------|---------------------------------------------------------------| + /// | `content` | equal → take it; different → [`MergeResult::Conflict`] | + /// | `has_timeline` | same name → take it; different → [`MergeResult::Conflict`] | + /// | `is_static` | same → take it; different → [`MergeResult::Empty`] | + /// | `components` | intersect; empty intersection → [`MergeResult::Empty`] | + /// + /// `content` falls back to `Conflict` (rather than `Empty`) when the two filters differ + /// because [`re_log_types::ResolvedEntityPathFilter`] uses a specificity-ordered rule set + /// ("most specific match wins"); intersecting two such rule sets is not concatenation and + /// cannot be computed structurally. Equal filters are trivially their own intersection. + pub fn try_merge(&self, other: &Self) -> MergeResult { + //TODO(ab): in theory we should be able to merge strictly overlapping contents + let content = match (&self.content, &other.content) { + (Some(a), Some(b)) => { + if a == b { + Some(a.clone()) + } else { + return MergeResult::Conflict; + } + } + (Some(c), None) | (None, Some(c)) => Some(c.clone()), + (None, None) => None, + }; + + let has_timeline = match (self.has_timeline, other.has_timeline) { + (Some(a), Some(b)) => { + if a == b { + Some(a) + } else { + return MergeResult::Conflict; + } + } + (Some(t), None) | (None, Some(t)) => Some(t), + (None, None) => None, + }; + + let is_static = match (self.is_static, other.is_static) { + (Some(a), Some(b)) => { + if a == b { + Some(a) + } else { + return MergeResult::Empty; + } + } + (Some(v), None) | (None, Some(v)) => Some(v), + (None, None) => None, + }; + + let components = match (&self.components, &other.components) { + (Some(a), Some(b)) => { + let intersection: Vec<_> = a.iter().copied().filter(|c| b.contains(c)).collect(); + if intersection.is_empty() { + return MergeResult::Empty; + } + Some(intersection) + } + (Some(c), None) | (None, Some(c)) => Some(c.clone()), + (None, None) => None, + }; + + MergeResult::Merged(Self { + content, + has_timeline, + is_static, + components, + }) + } + + /// `true` iff this filter has no predicates and no component selection, + /// i.e. `apply(c)` always returns `Some(c)` unchanged. + pub fn is_noop(&self) -> bool { + self.content.is_none() + && self.has_timeline.is_none() + && self.is_static.is_none() + && self.components.is_none() + } + + /// Check chunk-level predicates (content, has_timeline, is_static) against `view`. + /// Does NOT check the `components` clause — callers like [`Self::apply_complement`] and + /// [`Self::split`] distinguish "predicate failed" from "predicate passed but components + /// don't match", so they need the predicates-only answer. + fn predicates_match(&self, view: &impl ChunkPredicateView) -> bool { + // Destructure so adding a new predicate field forces a compile error here. + let Self { + content, + has_timeline, + is_static, + components: _, + } = self; + + if let Some(c) = content + && !c.matches(view.entity_path()) { return false; } - if let Some(ref timeline) = self.has_timeline - && !chunk.timelines().contains_key(timeline) + if let Some(want) = is_static + && view.is_static() != *want { return false; } - if let Some(is_static) = self.is_static - && chunk.is_static() != is_static + if let Some(tl) = has_timeline + && !view.has_timeline(tl) + { + return false; + } + true + } + + /// Full match: predicates AND components clause. + /// + /// Single source of truth for "does this chunk pass the filter as a whole?" — used by + /// both [`Self::apply`] (post-load) and `evaluate_filter_on_manifest` (pre-load + /// pushdown). Returning `true` here means the chunk survives filtering; the caller is + /// responsible for any column slicing implied by `components`. + pub(super) fn matches(&self, view: &impl ChunkPredicateView) -> bool { + // Destructure so adding a new field forces a compile error here. + let Self { + content, + has_timeline, + is_static, + components, + } = self; + + if let Some(c) = content + && !c.matches(view.entity_path()) + { + return false; + } + if let Some(want) = is_static + && view.is_static() != *want + { + return false; + } + if let Some(tl) = has_timeline + && !view.has_timeline(tl) + { + return false; + } + if let Some(comps) = components + && !view.has_any_component(comps) { return false; } @@ -77,18 +255,11 @@ impl StructuredFilter { /// When no component filter is set and the chunk passes all predicates, /// the original `Arc` is returned as-is (zero-cost move). pub fn apply(&self, chunk: Arc) -> Option> { - if !self.predicates_match(&chunk) { + if !self.matches(&*chunk) { return None; } - if let Some(ref components) = self.components { - // OR semantics: chunk must have at least one of the listed components. - let has_any = components - .iter() - .any(|c| chunk.components().contains_component(*c)); - if !has_any { - return None; - } + if let Some(components) = &self.components { Some(Arc::new(chunk.components_sliced(components))) } else { Some(chunk) @@ -100,7 +271,7 @@ impl StructuredFilter { /// When predicates don't match (chunk is kept entirely), the original `Arc` /// is returned as-is (zero-cost move). pub fn apply_complement(&self, chunk: Arc) -> Option> { - if !self.predicates_match(&chunk) { + if !self.predicates_match(&*chunk) { return Some(chunk); } @@ -121,7 +292,7 @@ impl StructuredFilter { /// When no component filter is set and the chunk passes all predicates, /// the original `Arc` is moved to the matching side (zero-cost). pub fn split(&self, chunk: Arc) -> (Option>, Option>) { - if !self.predicates_match(&chunk) { + if !self.predicates_match(&*chunk) { return (None, Some(chunk)); } @@ -341,3 +512,217 @@ impl LazyChunkStream { super::engine::compile(self) } } + +#[cfg(test)] +mod tests { + use super::*; + use re_log_types::{EntityPathFilter, ResolvedEntityPathFilter}; + use re_types_core::{ComponentIdentifier, TimelineName}; + + fn epf(s: &str) -> ResolvedEntityPathFilter { + EntityPathFilter::parse_forgiving(s).resolve_without_substitutions() + } + + fn comp(s: &str) -> ComponentIdentifier { + ComponentIdentifier::try_new(s).expect("valid component identifier") + } + + #[test] + fn test_merge_disjoint_fields() { + let a = StructuredFilter { + content: Some(epf("+ /robot/**")), + ..Default::default() + }; + let b = StructuredFilter { + is_static: Some(true), + ..Default::default() + }; + match a.try_merge(&b) { + MergeResult::Merged(m) => { + assert!(m.content.is_some()); + assert_eq!(m.is_static, Some(true)); + assert!(m.has_timeline.is_none()); + assert!(m.components.is_none()); + } + other => panic!("expected Merged, got {other:?}"), + } + } + + #[test] + fn test_merge_same_timeline() { + let a = StructuredFilter { + has_timeline: Some(TimelineName::from("frame")), + ..Default::default() + }; + let b = StructuredFilter { + has_timeline: Some(TimelineName::from("frame")), + ..Default::default() + }; + match a.try_merge(&b) { + MergeResult::Merged(m) => { + assert_eq!(m.has_timeline, Some(TimelineName::from("frame"))); + } + other => panic!("expected Merged, got {other:?}"), + } + } + + #[test] + fn test_merge_different_timeline() { + let a = StructuredFilter { + has_timeline: Some(TimelineName::from("frame")), + ..Default::default() + }; + let b = StructuredFilter { + has_timeline: Some(TimelineName::from("log_time")), + ..Default::default() + }; + match a.try_merge(&b) { + MergeResult::Conflict => {} + other => panic!("expected Conflict, got {other:?}"), + } + } + + #[test] + fn test_merge_is_static_conflict() { + let a = StructuredFilter { + is_static: Some(true), + ..Default::default() + }; + let b = StructuredFilter { + is_static: Some(false), + ..Default::default() + }; + match a.try_merge(&b) { + MergeResult::Empty => {} + other => panic!("expected Empty, got {other:?}"), + } + } + + #[test] + fn test_merge_content_conflict() { + let a = StructuredFilter { + content: Some(epf("+ /robot/**")), + ..Default::default() + }; + let b = StructuredFilter { + content: Some(epf("+ /camera/**")), + ..Default::default() + }; + match a.try_merge(&b) { + MergeResult::Conflict => {} + other => panic!("expected Conflict, got {other:?}"), + } + } + + #[test] + fn test_merge_content_equal() { + let a = StructuredFilter { + content: Some(epf("+ /robot/**")), + ..Default::default() + }; + let b = StructuredFilter { + content: Some(epf("+ /robot/**")), + ..Default::default() + }; + match a.try_merge(&b) { + MergeResult::Merged(m) => { + assert_eq!(m.content, Some(epf("+ /robot/**"))); + } + other => panic!("expected Merged, got {other:?}"), + } + } + + #[test] + fn test_merge_components_intersect() { + let a = StructuredFilter { + components: Some(vec![comp("A"), comp("B")]), + ..Default::default() + }; + let b = StructuredFilter { + components: Some(vec![comp("B"), comp("C")]), + ..Default::default() + }; + match a.try_merge(&b) { + MergeResult::Merged(m) => { + assert_eq!(m.components, Some(vec![comp("B")])); + } + other => panic!("expected Merged, got {other:?}"), + } + } + + #[test] + fn test_merge_components_disjoint() { + let a = StructuredFilter { + components: Some(vec![comp("A")]), + ..Default::default() + }; + let b = StructuredFilter { + components: Some(vec![comp("B")]), + ..Default::default() + }; + match a.try_merge(&b) { + MergeResult::Empty => {} + other => panic!("expected Empty, got {other:?}"), + } + } + + #[test] + fn test_merge_components_one_none() { + let a = StructuredFilter { + components: Some(vec![comp("A"), comp("B")]), + ..Default::default() + }; + let b = StructuredFilter::default(); + match a.try_merge(&b) { + MergeResult::Merged(m) => { + assert_eq!(m.components, Some(vec![comp("A"), comp("B")])); + } + other => panic!("expected Merged, got {other:?}"), + } + } + + #[test] + fn test_merge_all_none() { + let a = StructuredFilter::default(); + let b = StructuredFilter::default(); + match a.try_merge(&b) { + MergeResult::Merged(m) => { + assert!(m.is_noop()); + } + other => panic!("expected Merged, got {other:?}"), + } + } + + #[test] + fn test_is_noop() { + assert!(StructuredFilter::default().is_noop()); + assert!( + !StructuredFilter { + content: Some(epf("+ /a/**")), + ..Default::default() + } + .is_noop() + ); + assert!( + !StructuredFilter { + has_timeline: Some(TimelineName::from("frame")), + ..Default::default() + } + .is_noop() + ); + assert!( + !StructuredFilter { + is_static: Some(false), + ..Default::default() + } + .is_noop() + ); + assert!( + !StructuredFilter { + components: Some(vec![comp("A")]), + ..Default::default() + } + .is_noop() + ); + } +} diff --git a/rerun_py/src/chunk_stream/summary.rs b/rerun_py/src/chunk_stream/summary.rs new file mode 100644 index 000000000000..b04db95b5405 --- /dev/null +++ b/rerun_py/src/chunk_stream/summary.rs @@ -0,0 +1,53 @@ +//! Shared formatter for `ChunkStore`/`LazyStore` summary strings. +//! +//! Used by both `PyChunkStoreInternal::summary` (chunks-based) and +//! `PyLazyStoreInternal::summary` (manifest-based) so the two paths produce +//! identical lines on the same logical chunk. + +/// One line of a chunk-store summary. +/// +/// `timelines` and `cols` must be pre-sorted by the caller. +pub(super) struct SummaryRow { + pub entity_path: String, + pub num_rows: u64, + pub is_static: bool, + pub timelines: Vec, + pub cols: Vec, +} + +/// Format chunk-store rows into a deterministic snapshot string. +/// +/// One line per row, sorted by `(entity_path, !is_static)`. Format: +/// `{entity_path} rows={n} static={bool} timelines=[…] cols=[…]`. +pub(super) fn format_summary(rows: impl IntoIterator) -> String { + let mut rows: Vec = rows.into_iter().collect(); + rows.sort_by(|a, b| { + a.entity_path + .cmp(&b.entity_path) + .then_with(|| a.is_static.cmp(&b.is_static).reverse()) + }); + + let mut lines = Vec::with_capacity(rows.len()); + for row in &rows { + let timelines_str = row + .timelines + .iter() + .map(|t| format!("'{t}'")) + .collect::>() + .join(", "); + let cols_str = row + .cols + .iter() + .map(|c| format!("'{c}'")) + .collect::>() + .join(", "); + let is_static = if row.is_static { "True" } else { "False" }; + lines.push(format!( + "{entity_path} rows={rows} static={is_static} timelines=[{timelines_str}] cols=[{cols_str}]", + entity_path = row.entity_path, + rows = row.num_rows, + )); + } + + lines.join("\n") +} diff --git a/rerun_py/src/lenses.rs b/rerun_py/src/lenses.rs index a58bca7fb262..4a37964c2ce4 100644 --- a/rerun_py/src/lenses.rs +++ b/rerun_py/src/lenses.rs @@ -1,64 +1,81 @@ -use std::collections::BTreeMap; - +use arrow::datatypes::DataType; +use arrow::pyarrow::PyArrowType; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyModule; +use pyo3::{Borrowed, FromPyObject}; -use re_lenses_core::{DynExpr, Lens, LensBuilder, OutputMode, Selector}; -use re_types_core::{ComponentDescriptor, ComponentIdentifier}; +use re_lenses_core::{CastTo, DynExpr, Lens, OutputMode, Selector}; +use re_types_core::{ComponentDescriptor, ComponentIdentifier, TimelineName}; use crate::python_bridge::PyComponentDescriptor; use crate::selector::PySelectorInternal; /// Register lens classes. pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } -// --------------------------------------------------------------------------- -// LensOutputInternal -// --------------------------------------------------------------------------- - -/// Describes one output group of a lens. +/// A derive lens that creates new component/time columns from an input component. +/// +/// In Python, `scatter=True` maps to `Lens::Scatter` internally. #[pyclass( frozen, - name = "LensOutputInternal", + name = "DeriveLensInternal", module = "rerun_bindings.rerun_bindings" )] -pub struct PyLensOutputInternal { - components: Vec<(ComponentDescriptor, Selector)>, +pub struct PyDeriveLensInternal { + components: Vec<(ComponentDescriptor, Selector, Option)>, times: Vec<(String, re_log_types::TimeType, Selector)>, + input_component: ComponentIdentifier, + output_entity: Option, + scatter: bool, } #[pymethods] -impl PyLensOutputInternal { +impl PyDeriveLensInternal { #[new] - #[pyo3(text_signature = "(self)")] - fn new() -> Self { - Self { + #[pyo3( + signature = (input_component, *, output_entity = None, scatter = false), + text_signature = "(self, input_component, *, output_entity=None, scatter=False)" + )] + fn new(input_component: &str, output_entity: Option, scatter: bool) -> PyResult { + Ok(Self { components: Vec::new(), times: Vec::new(), - } + input_component: ComponentIdentifier::try_new(input_component) + .map_err(|err| PyValueError::new_err(err.to_string()))?, + output_entity, + scatter, + }) } - /// Add a component output column. Returns a new LensOutput with the component added. + /// Add a component output column. Returns a new instance with the component added. + /// + /// `cast_to` is `None` (no cast), the string `"auto"` (cast to the component's + /// canonical type), or a pyarrow `DataType` (cast to that explicit type). + #[pyo3(signature = (component, selector, cast_to = None))] fn to_component( &self, component: PyComponentDescriptor, selector: &PySelectorInternal, - ) -> Self { - let descr = component.0; + cast_to: Option>, + ) -> PyResult { + let cast = parse_cast_to(cast_to)?; let mut components = self.components.clone(); - components.push((descr, selector.selector().clone())); - Self { + components.push((component.0, selector.selector().clone(), cast)); + Ok(Self { components, times: self.times.clone(), - } + input_component: self.input_component, + output_entity: self.output_entity.clone(), + scatter: self.scatter, + }) } - /// Add a time extraction column. Returns a new LensOutput with the time added. + /// Add a time extraction column. Returns a new instance with the time added. fn to_timeline( &self, timeline_name: &str, @@ -75,91 +92,133 @@ impl PyLensOutputInternal { Ok(Self { components: self.components.clone(), times, + input_component: self.input_component, + output_entity: self.output_entity.clone(), + scatter: self.scatter, }) } } -// --------------------------------------------------------------------------- -// LensInternal -// --------------------------------------------------------------------------- +impl PyDeriveLensInternal { + /// Build the Rust `Lens` from this internal representation. + pub fn build(&self) -> PyResult { + let mut builder = if self.scatter { + Lens::scatter(self.input_component) + } else { + Lens::derive(self.input_component) + }; + if let Some(ref entity) = self.output_entity { + builder = builder.output_entity(entity.as_str()); + } + for (descr, selector, cast) in &self.components { + builder = match cast { + Some(cast) => { + builder.to_component_with_cast(descr.clone(), selector.clone(), cast.clone()) + } + None => builder.to_component(descr.clone(), selector.clone()), + }; + } + for (name, timeline_type, selector) in &self.times { + let timeline_name = TimelineName::try_new(name.as_str()) + .map_err(|err| PyValueError::new_err(err.to_string()))?; + builder = builder.to_timeline(timeline_name, *timeline_type, selector.clone()); + } + builder + .build() + .map_err(|err| PyValueError::new_err(err.to_string())) + } +} +/// A mutate lens that modifies the input component in-place. #[pyclass( frozen, - name = "LensInternal", + name = "MutateLensInternal", module = "rerun_bindings.rerun_bindings" )] -pub struct PyLensInternal { - inner: Lens, -} - -impl PyLensInternal { - pub fn inner(&self) -> &Lens { - &self.inner - } +pub struct PyMutateLensInternal { + input_component: ComponentIdentifier, + selector: Selector, + keep_row_ids: bool, } #[pymethods] -impl PyLensInternal { +impl PyMutateLensInternal { #[new] #[pyo3( - signature = (input_component, output = None, *, to_entity = None), - text_signature = "(self, input_component, output=None, *, to_entity=None)" + signature = (input_component, selector, *, keep_row_ids = false), + text_signature = "(self, input_component, selector, *, keep_row_ids=False)" )] - #[expect(clippy::needless_pass_by_value)] // PyO3 requires owned arguments fn new( - py: Python<'_>, input_component: &str, - output: Option>, - to_entity: Option>>, + selector: &PySelectorInternal, + keep_row_ids: bool, ) -> PyResult { - if output.is_none() && to_entity.as_ref().is_none_or(BTreeMap::is_empty) { - return Err(PyValueError::new_err( - "At least one of `output` or `to_entity` must be provided", - )); - } - - let component: ComponentIdentifier = input_component.into(); - let mut builder = Lens::for_input_column(component); + Ok(Self { + input_component: ComponentIdentifier::try_new(input_component) + .map_err(|err| PyValueError::new_err(err.to_string()))?, + selector: selector.selector().clone(), + keep_row_ids, + }) + } +} - if let Some(ref out) = output { - builder = build_output(builder, out, None)?; +impl PyMutateLensInternal { + /// Build the Rust `Lens` from this internal representation. + pub fn build(&self) -> Lens { + let mut builder = Lens::mutate(self.input_component, self.selector.clone()); + if self.keep_row_ids { + builder = builder.keep_row_ids(); } + builder.build() + } +} - if let Some(ref to_entity) = to_entity { - for (entity_path, out) in to_entity { - let out = out.borrow(py); - builder = build_output(builder, &out, Some(entity_path.as_str()))?; - } - } +/// Extracts a `Lens` from either derive or mutate Python lens types. +pub enum PyLens { + Derive(Py), + Mutate(Py), +} - Ok(Self { - inner: builder.build(), - }) +impl PyLens { + pub fn build(&self, py: Python<'_>) -> PyResult { + match self { + Self::Derive(d) => d.borrow(py).build(), + Self::Mutate(i) => Ok(i.borrow(py).build()), + } } } -/// Build one output group from its description, appending it to the lens builder. -fn build_output( - builder: LensBuilder, - desc: &PyLensOutputInternal, - target_entity: Option<&str>, -) -> PyResult { - let build_fn = |mut out: re_lenses_core::OutputBuilder| { - for (descr, selector) in &desc.components { - out = out.component(descr.clone(), selector.clone())?; +impl<'py> FromPyObject<'_, 'py> for PyLens { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { + if let Ok(d) = ob.cast::() { + Ok(Self::Derive(d.to_owned().unbind())) + } else if let Ok(i) = ob.cast::() { + Ok(Self::Mutate(i.to_owned().unbind())) + } else { + Err(PyValueError::new_err( + "Expected a DeriveLensInternal or MutateLensInternal instance", + )) } - for (name, timeline_type, selector) in &desc.times { - out = out.time(name.as_str(), *timeline_type, selector.clone())?; - } - Ok(out) - }; + } +} - let result = match target_entity { - None => builder.output_columns(build_fn), - Some(target) => builder.output_columns_at(target, build_fn), +/// Parse the Python `cast_to` argument: `None`, the string `"auto"`, or a pyarrow `DataType`. +fn parse_cast_to(cast_to: Option>) -> PyResult> { + let Some(obj) = cast_to else { + return Ok(None); }; - - result.map_err(|err| PyValueError::new_err(err.to_string())) + if let Ok(s) = obj.extract::() { + return match s.as_str() { + "auto" => Ok(Some(CastTo::Auto)), + other => Err(PyValueError::new_err(format!( + "Unknown cast_to '{other}', expected 'auto' or a pyarrow DataType" + ))), + }; + } + let PyArrowType(datatype) = obj.extract::>()?; + Ok(Some(CastTo::Type(datatype))) } fn parse_timeline_type(s: &str) -> PyResult { diff --git a/rerun_py/src/lib.rs b/rerun_py/src/lib.rs index 5332e032a14d..31f2faa92408 100644 --- a/rerun_py/src/lib.rs +++ b/rerun_py/src/lib.rs @@ -28,10 +28,11 @@ mod chunk; mod chunk_stream; mod lenses; mod python_bridge; -mod recording; +mod query_metrics; mod selector; mod server; mod trace_context; +mod tracing_session; mod urdf; mod utils; mod video; diff --git a/rerun_py/src/python_bridge.rs b/rerun_py/src/python_bridge.rs index 49de0c119505..851da563f760 100644 --- a/rerun_py/src/python_bridge.rs +++ b/rerun_py/src/python_bridge.rs @@ -1,6 +1,5 @@ #![expect(clippy::fn_params_excessive_bools)] // We used named arguments, so this is fine #![expect(clippy::needless_pass_by_value)] // A lot of arguments to #[pyfunction] need to be by value -#![expect(clippy::too_many_arguments)] // We used named arguments, so this is fine use std::borrow::Borrow as _; use std::io::IsTerminal as _; @@ -10,20 +9,25 @@ use std::time::Duration; use arrow::array::RecordBatch as ArrowRecordBatch; use itertools::Itertools as _; -use pyo3::exceptions::{PyKeyboardInterrupt, PyRuntimeError}; +use pyo3::exceptions::{ + PyKeyboardInterrupt, PyRuntimeError, PyStopIteration, PyTypeError, PyValueError, +}; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict}; use re_auth::oauth::Credentials; use re_auth::oauth::login_flow::{DeviceCodeFlow, DeviceCodeFlowState}; //use crate::reflection::ComponentDescriptorExt as _; -use re_chunk::ChunkBatcherConfig; +use re_chunk::{ChunkBatcherConfig, TimelineName}; use re_log::ResultExt as _; use re_log_types::external::re_types_core::reflection::ComponentDescriptorExt as _; use re_log_types::{BlueprintActivationCommand, EntityPathPart, LogMsg, RecordingId}; use re_sdk::external::re_log_encoding::Encoder; use re_sdk::sink::{BinaryStreamStorage, CallbackSink, MemorySinkStorage, SinkFlushError}; use re_sdk::time::TimePoint; -use re_sdk::{ComponentDescriptor, EntityPath, RecordingStream, RecordingStreamBuilder, TimeCell}; +use re_sdk::{ + ArchetypeName, ComponentDescriptor, ComponentIdentifier, ComponentType, EntityPath, + RecordingStream, RecordingStreamBuilder, TimeCell, +}; #[cfg(feature = "web_viewer")] use re_web_viewer_server::WebViewerServerPort; @@ -39,7 +43,7 @@ impl PyRuntimeErrorExt for PyRuntimeError { } } -use crate::recording::PyRecordingInternal; +use crate::chunk::PyChunkInternal; // The bridge needs to have complete control over the lifetimes of the individual recordings, // otherwise all the recording shutdown machinery (which includes deallocating C, Rust and Python @@ -95,7 +99,7 @@ static GARBAGE_QUEUE: LazyLock<(GarbageSender, GarbageReceiver)> = LazyLock::new /// /// Any time you release the GIL (e.g. `py.allow_threads()`), try to slip in a call to this /// function so we don't accumulate too much garbage. -fn flush_garbage_queue() { +pub(crate) fn flush_garbage_queue() { while GARBAGE_QUEUE.1.try_recv().is_ok() { // Implicitly dropping chunks, therefore triggering their `release` callbacks, therefore // triggering the native Python GC. @@ -112,6 +116,30 @@ fn global_web_viewer_server() WEB_HANDLE.get_or_init(Default::default).lock() } +/// Static holding the puffin profiler so it stays alive for the lifetime of the SDK. +/// +/// Wrapped in `Option` so [`shutdown_puffin_profiler`] can take and drop it explicitly, +/// flushing any pending frames to the connected `puffin_viewer`. +fn puffin_profiler_slot() -> &'static parking_lot::Mutex> { + static PROFILER: OnceLock>> = OnceLock::new(); + PROFILER.get_or_init(|| parking_lot::Mutex::new(None)) +} + +/// Start a `puffin` profiling server and spawn `puffin_viewer` to connect to it. +fn init_puffin_profiler() { + let mut slot = puffin_profiler_slot().lock(); + slot.get_or_insert_with(|| { + let mut profiler = re_tracing::Profiler::default(); + profiler.start(); + profiler + }); +} + +/// Flush the last profiling scopes to the puffin viewer. +fn shutdown_puffin_profiler() { + puffin_profiler_slot().lock().take(); +} + /// Initialize the performance telemetry stack in a static so it can keep running for the entire /// lifetime of the SDK. /// @@ -127,13 +155,24 @@ fn init_perf_telemetry() -> parking_lot::MutexGuard<'static, re_perf_telemetry:: let runtime = crate::utils::get_tokio_runtime(); // telemetry must be init in a Tokio context runtime.block_on(async { - let telemetry = re_perf_telemetry::Telemetry::init( + // Wire a Python `ContextVar` reader as the session-id source for + // `re_perf_telemetry::current_rerun_session_id`. The crate itself + // doesn't know Python exists; we hand it a closure it can call. + let telemetry = re_perf_telemetry::Telemetry::init_with_session_id_reader( args, // NOTE: It's a static in this case, so it's never dropped anyhow. re_perf_telemetry::TelemetryDropBehavior::Shutdown, + || { + pyo3::Python::attach( + crate::tracing_session::current_rerun_session_id_from_contextvar, + ) + }, ) // Perf telemetry is a developer tool, it's not compiled into final user builds. .expect("could not start perf telemetry"); + // `Telemetry::init` sets `re_perf_telemetry::is_telemetry_active()` on + // its own success path; the Python `_is_telemetry_active()` binding + // reads from there. Single source of truth. parking_lot::Mutex::new(telemetry) }) }) @@ -165,6 +204,10 @@ fn rerun_bindings(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { #[cfg(all(not(target_arch = "wasm32"), feature = "perf_telemetry"))] let _telemetry = init_perf_telemetry(); + if re_log::env_var_is_truthy("RERUN_PUFFIN") { + init_puffin_profiler(); + } + // These two components are necessary for imports to work m.add_class::()?; m.add_class::()?; @@ -227,6 +270,7 @@ fn rerun_bindings(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(serve_web_viewer, m)?)?; m.add_function(wrap_pyfunction!(serve_web, m)?)?; m.add_function(wrap_pyfunction!(disconnect, m)?)?; + m.add_function(wrap_pyfunction!(finalize_deferred_sinks, m)?)?; m.add_function(wrap_pyfunction!(flush, m)?)?; // time @@ -235,15 +279,16 @@ fn rerun_bindings(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(set_time_timestamp_nanos_since_epoch, m)?)?; m.add_function(wrap_pyfunction!(disable_timeline, m)?)?; m.add_function(wrap_pyfunction!(reset_time, m)?)?; + m.add_function(wrap_pyfunction!(set_log_tick_enabled, m)?)?; + m.add_function(wrap_pyfunction!(set_log_time_enabled, m)?)?; // log any m.add_function(wrap_pyfunction!(log_arrow_msg, m)?)?; m.add_function(wrap_pyfunction!(log_file_from_path, m)?)?; m.add_function(wrap_pyfunction!(log_file_from_contents, m)?)?; m.add_function(wrap_pyfunction!(send_arrow_chunk, m)?)?; - m.add_function(wrap_pyfunction!(send_chunk, m)?)?; + m.add_function(wrap_pyfunction!(send_chunks, m)?)?; m.add_function(wrap_pyfunction!(send_blueprint, m)?)?; - m.add_function(wrap_pyfunction!(send_recording, m)?)?; // misc m.add_function(wrap_pyfunction!(version, m)?)?; @@ -264,8 +309,12 @@ fn rerun_bindings(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m )?)?; - // recording - crate::recording::register(m)?; + // video stream utilities + m.add_function(wrap_pyfunction!(crate::video::video_detect_gop_start, m)?)?; + m.add_function(wrap_pyfunction!( + crate::video::video_length_prefixed_to_annex_b, + m + )?)?; // chunk crate::chunk::register(m)?; @@ -279,6 +328,41 @@ fn rerun_bindings(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m )?)?; + // tracing_session(): customer-facing context manager for support correlation. + m.add_function(wrap_pyfunction!( + crate::tracing_session::get_tracing_session_var, + m + )?)?; + m.add_function(wrap_pyfunction!( + crate::tracing_session::is_telemetry_active, + m + )?)?; + m.add_function(wrap_pyfunction!( + crate::tracing_session::inc_active_tracing_sessions, + m + )?)?; + m.add_function(wrap_pyfunction!( + crate::tracing_session::dec_active_tracing_sessions, + m + )?)?; + m.add_function(wrap_pyfunction!( + crate::tracing_session::log_tracing_session_started, + m + )?)?; + m.add_function(wrap_pyfunction!( + crate::tracing_session::log_tracing_session_finished, + m + )?)?; + + // query_metrics(): experimental, customer-facing context manager for + // capturing DataFusion query metrics from Python. + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!( + crate::query_metrics::new_metrics_collector, + m + )?)?; + // viewer crate::viewer::register(py, m)?; @@ -311,7 +395,7 @@ fn flush_and_cleanup_orphaned_recordings(py: Python<'_>) -> PyResult<()> { py.detach(|| -> Result<(), SinkFlushError> { // Now flush all recordings to handle weird cases where the data in the queue // is actually holding onto the ref to the recording. - for recording in all_recordings().iter().chain(orphaned_recordings().iter()) { + for recording in std::iter::chain(&*all_recordings(), &*orphaned_recordings()) { recording.flush_blocking()?; } @@ -387,6 +471,7 @@ impl DurationLike { /// Defines the different batching thresholds used within the RecordingStream. #[pyclass( eq, + from_py_object, name = "ChunkBatcherConfig", module = "rerun_bindings.rerun_bindings" )] @@ -528,8 +613,16 @@ impl PyChunkBatcherConfig { #[expect(non_snake_case)] #[staticmethod] /// Always flushes ASAP. - fn ALWAYS() -> Self { - Self(ChunkBatcherConfig::ALWAYS) + /// + /// !!! warning + /// Test-only configuration. Produces an unrealistically large number of chunks and is + /// not suitable for production workloads. With a file sink in particular, per-chunk + /// metadata is accumulated in memory until the SDK process ends and the file footer + /// can be written, which can drive memory usage through the roof. Use + /// [`LOW_LATENCY`][rerun_bindings.ChunkBatcherConfig.LOW_LATENCY] instead for fast + /// flushing in real applications. + fn ALWAYS_TEST_ONLY() -> Self { + Self(ChunkBatcherConfig::ALWAYS_TEST_ONLY) } #[expect(non_snake_case)] @@ -691,11 +784,13 @@ fn shutdown(py: Python<'_>) { #[cfg(all(not(target_arch = "wasm32"), feature = "perf_telemetry"))] init_perf_telemetry().shutdown(); + + shutdown_puffin_profiler(); } // --- Recordings --- -#[pyclass(frozen, module = "rerun_bindings.rerun_bindings")] // NOLINT: ignore[py-cls-eq] non-trivial implementation +#[pyclass(frozen, from_py_object, module = "rerun_bindings.rerun_bindings")] // NOLINT: ignore[py-cls-eq] non-trivial implementation #[derive(Clone)] pub(crate) struct PyRecordingStream(RecordingStream); @@ -944,6 +1039,7 @@ fn send_mem_sink_as_default_blueprint( executable_path = None, extra_args = vec![], extra_env = vec![], + headless = false, ))] fn spawn( port: u16, @@ -955,7 +1051,8 @@ fn spawn( executable_path: Option, extra_args: Vec, extra_env: Vec<(String, String)>, -) -> PyResult<()> { + headless: bool, +) -> PyResult> { let spawn_opts = re_sdk::SpawnOptions { port, wait_for_bind: true, @@ -968,10 +1065,11 @@ fn spawn( extra_args, extra_env, new: false, + headless, }; re_sdk::spawn(&spawn_opts) - .map(|_| ()) + .map(|info| info.child_pid) .map_err(|err| PyRuntimeError::new_err(err.to_string())) } @@ -1028,19 +1126,23 @@ impl PyGrpcSink { #[derive(PartialEq, Hash)] struct PyFileSink { path: PathBuf, + write_footer: bool, } #[pymethods] impl PyFileSink { #[new] - #[pyo3(signature = (path))] - #[pyo3(text_signature = "(self, path)")] - fn new(path: PathBuf) -> Self { - Self { path } + #[pyo3(signature = (path, *, write_footer = true))] + #[pyo3(text_signature = "(self, path, *, write_footer=True)")] + fn new(path: PathBuf, write_footer: bool) -> Self { + Self { path, write_footer } } pub fn __repr__(&self) -> String { - format!("FileSink({:#?})", self.path) + format!( + "FileSink({:#?}, write_footer={})", + self.path, self.write_footer + ) } } @@ -1064,24 +1166,29 @@ fn set_sinks<'py>( let mut resolved_sinks: Vec> = Vec::new(); for sink in sinks { - if let Ok(sink) = sink.downcast::() { + if let Ok(sink) = sink.cast::() { let sink = sink.get(); let sink = re_sdk::sink::GrpcSink::new(sink.uri.clone()); resolved_sinks.push(Box::new(sink)); - } else if let Ok(sink) = sink.downcast::() { + } else if let Ok(sink) = sink.cast::() { let sink = sink.get(); - let sink = re_sdk::sink::FileSink::new(sink.path.clone()) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; + let sink = re_sdk::sink::FileSink::with_options( + sink.path.clone(), + re_sdk::sink::FileSinkOptions { + write_footer: sink.write_footer, + }, + ) + .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; resolved_sinks.push(Box::new(sink)); - } else if let Ok(storage) = sink.downcast::() { + } else if let Ok(storage) = sink.cast::() { // Direct PyBinarySinkStorage let binary_sink = re_sdk::sink::BinaryStreamSink::with_shared_storage(&storage.get().inner); resolved_sinks.push(Box::new(binary_sink)); - } else if let Ok(storage) = sink.getattr("storage").and_then(|attr| { - attr.downcast_into::() - .map_err(Into::into) - }) { + } else if let Ok(storage) = sink + .getattr("storage") + .and_then(|attr| attr.cast_into::().map_err(Into::into)) + { // Python BinaryStream wrapper — extract .storage let binary_sink = re_sdk::sink::BinaryStreamSink::with_shared_storage(&storage.get().inner); @@ -1191,11 +1298,12 @@ fn connect_grpc_blueprint( /// Save the recording stream to a file. #[pyfunction] -#[pyo3(signature = (path, default_blueprint = None, recording = None))] +#[pyo3(signature = (path, default_blueprint = None, recording = None, *, write_footer = true))] fn save( path: &str, default_blueprint: Option<&PyMemorySinkStorage>, recording: Option<&PyRecordingStream>, + write_footer: bool, py: Python<'_>, ) -> PyResult<()> { let Some(recording) = get_data_recording(recording) else { @@ -1212,8 +1320,11 @@ fn save( py.detach(|| { // We create the sink manually so we can send the default blueprint // first before the rest of the current recording stream. - let sink = re_sdk::sink::FileSink::new(path) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; + let sink = re_sdk::sink::FileSink::with_options( + path, + re_sdk::sink::FileSinkOptions { write_footer }, + ) + .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; if let Some(default_blueprint) = default_blueprint { send_mem_sink_as_default_blueprint(&sink, default_blueprint); @@ -1263,10 +1374,11 @@ fn save_blueprint( /// Save to stdout. #[pyfunction] -#[pyo3(signature = (default_blueprint = None, recording = None))] +#[pyo3(signature = (default_blueprint = None, recording = None, *, write_footer = true))] fn stdout( default_blueprint: Option<&PyMemorySinkStorage>, recording: Option<&PyRecordingStream>, + write_footer: bool, py: Python<'_>, ) -> PyResult<()> { let Some(recording) = get_data_recording(recording) else { @@ -1286,8 +1398,10 @@ fn stdout( Box::new(re_sdk::sink::BufferedSink::new()) } else { Box::new( - re_sdk::sink::FileSink::stdout() - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?, + re_sdk::sink::FileSink::stdout_with_options(re_sdk::sink::FileSinkOptions { + write_footer, + }) + .map_err(|err| PyRuntimeError::new_err(err.to_string()))?, ) }; @@ -1751,6 +1865,27 @@ fn disconnect(py: Python<'_>, recording: Option<&PyRecordingStream>) { }); } +/// Finalize any deferred-finalization sinks (i.e. file-like sinks that write a footer at the end). +/// +/// For a bare `FileSink` this is equivalent to `disconnect()`. For a `MultiSink` containing both +/// streaming and file-like children, only the file-like children are dropped — the streaming +/// children stay live. For all other sinks this is a no-op. +/// +/// Used by `RecordingStream.__exit__` so that file-backed recordings are consumable as soon as +/// the `with`-block exits, without waiting for `__del__` / GC. +#[pyfunction] +#[pyo3(signature = (recording=None))] +fn finalize_deferred_sinks(py: Python<'_>, recording: Option<&PyRecordingStream>) { + let Some(recording) = get_data_recording(recording) else { + return; + }; + // Release the GIL in case any flushing behavior needs to cleanup a python object. + py.detach(|| { + recording.finalize_deferred_sinks(); + flush_garbage_queue(); + }); +} + /// Block until outstanding data has been flushed to the sink. #[pyfunction] #[pyo3(signature = (*, timeout_sec = 1e38, recording = None))] // Can't use infinity here because of python_check_signatures.py @@ -1784,6 +1919,7 @@ fn flush(py: Python<'_>, timeout_sec: f32, recording: Option<&PyRecordingStream> /// fields provide additional information about the semantics of the data. #[pyclass( eq, + from_py_object, name = "ComponentDescriptor", module = "rerun_bindings.rerun_bindings" )] @@ -1796,14 +1932,19 @@ impl PyComponentDescriptor { #[new] #[pyo3(signature = (component, archetype=None, component_type=None))] #[pyo3(text_signature = "(self, component, archetype=None, component_type=None)")] - fn new(component: &str, archetype: Option<&str>, component_type: Option<&str>) -> Self { + fn new( + component: &str, + archetype: Option<&str>, + component_type: Option<&str>, + ) -> PyResult { let descr = ComponentDescriptor { - archetype: archetype.map(Into::into), - component: component.into(), - component_type: component_type.map(Into::into), + archetype: archetype.and_then(|s| ArchetypeName::try_new(s).ok()), + component: ComponentIdentifier::try_new(component) + .map_err(|err| PyRuntimeError::new_err(err.to_string()))?, + component_type: component_type.and_then(|s| ComponentType::try_new(s).ok()), }; - Self(descr) + Ok(Self(descr)) } fn __hash__(&self) -> u64 { @@ -1851,11 +1992,11 @@ impl PyComponentDescriptor { #[pyo3(signature = (archetype=None, component_type=None))] fn with_overrides(&mut self, archetype: Option<&str>, component_type: Option<&str>) -> Self { let mut cloned = self.0.clone(); - if let Some(archetype) = archetype { - cloned = cloned.with_archetype(archetype.into()); + if let Some(archetype) = archetype.and_then(|s| ArchetypeName::try_new(s).ok()) { + cloned = cloned.with_archetype(archetype); } - if let Some(component_type) = component_type { - cloned = cloned.with_component_type(component_type.into()); + if let Some(component_type) = component_type.and_then(|s| ComponentType::try_new(s).ok()) { + cloned = cloned.with_component_type(component_type); } Self(cloned) } @@ -1864,18 +2005,22 @@ impl PyComponentDescriptor { #[pyo3(signature = (archetype=None, component_type=None))] fn or_with_overrides(&mut self, archetype: Option<&str>, component_type: Option<&str>) -> Self { let mut cloned = self.0.clone(); - if let Some(archetype) = archetype { - cloned = cloned.or_with_archetype(|| archetype.into()); + if let Some(archetype) = archetype.and_then(|s| ArchetypeName::try_new(s).ok()) { + cloned = cloned.or_with_archetype(|| archetype); } - if let Some(component_type) = component_type { - cloned = cloned.or_with_component_type(|| component_type.into()); + if let Some(component_type) = component_type.and_then(|s| ComponentType::try_new(s).ok()) { + cloned = cloned.or_with_component_type(|| component_type); } Self(cloned) } /// Sets `archetype` in a format similar to built-in archetypes. fn with_builtin_archetype(&mut self, archetype: &str) -> Self { - Self(self.0.clone().with_builtin_archetype(archetype)) + if let Ok(archetype) = ArchetypeName::try_new(archetype) { + Self(self.0.clone().with_builtin_archetype(archetype)) + } else { + Self(self.0.clone()) + } } } @@ -1884,21 +2029,35 @@ impl PyComponentDescriptor { /// Set the current time for this thread as an integer sequence. #[pyfunction] #[pyo3(signature = (timeline, sequence, recording=None))] -fn set_time_sequence(timeline: &str, sequence: i64, recording: Option<&PyRecordingStream>) { +fn set_time_sequence( + timeline: &str, + sequence: i64, + recording: Option<&PyRecordingStream>, +) -> PyResult<()> { + let timeline = + TimelineName::try_new(timeline).map_err(|err| PyValueError::new_err(err.to_string()))?; let Some(recording) = get_data_recording(recording) else { - return; + return Ok(()); }; recording.set_time(timeline, TimeCell::from_sequence(sequence)); + Ok(()) } /// Set the current duration for this thread in nanoseconds. #[pyfunction] #[pyo3(signature = (timeline, nanos, recording=None))] -fn set_time_duration_nanos(timeline: &str, nanos: i64, recording: Option<&PyRecordingStream>) { +fn set_time_duration_nanos( + timeline: &str, + nanos: i64, + recording: Option<&PyRecordingStream>, +) -> PyResult<()> { + let timeline = + TimelineName::try_new(timeline).map_err(|err| PyValueError::new_err(err.to_string()))?; let Some(recording) = get_data_recording(recording) else { - return; + return Ok(()); }; recording.set_time(timeline, TimeCell::from_duration_nanos(nanos)); + Ok(()) } /// Set the current time for this thread in nanoseconds. @@ -1908,21 +2067,27 @@ fn set_time_timestamp_nanos_since_epoch( timeline: &str, nanos: i64, recording: Option<&PyRecordingStream>, -) { +) -> PyResult<()> { + let timeline = + TimelineName::try_new(timeline).map_err(|err| PyValueError::new_err(err.to_string()))?; let Some(recording) = get_data_recording(recording) else { - return; + return Ok(()); }; recording.set_time(timeline, TimeCell::from_timestamp_nanos_since_epoch(nanos)); + Ok(()) } /// Clear time information for the specified timeline on this thread. #[pyfunction] #[pyo3(signature = (timeline, recording=None))] -fn disable_timeline(timeline: &str, recording: Option<&PyRecordingStream>) { +fn disable_timeline(timeline: &str, recording: Option<&PyRecordingStream>) -> PyResult<()> { + let timeline = + TimelineName::try_new(timeline).map_err(|err| PyValueError::new_err(err.to_string()))?; let Some(recording) = get_data_recording(recording) else { - return; + return Ok(()); }; recording.disable_timeline(timeline); + Ok(()) } /// Clear all timeline information on this thread. @@ -1935,6 +2100,26 @@ fn reset_time(recording: Option<&PyRecordingStream>) { recording.reset_time(); } +/// Enable or disable automatic injection of the `log_tick` timeline (disabled by default). +#[pyfunction] +#[pyo3(signature = (enabled, recording=None))] +fn set_log_tick_enabled(enabled: bool, recording: Option<&PyRecordingStream>) { + let Some(recording) = get_data_recording(recording) else { + return; + }; + recording.set_log_tick_enabled(enabled); +} + +/// Enable or disable automatic injection of the `log_time` timeline (enabled by default). +#[pyfunction] +#[pyo3(signature = (enabled, recording=None))] +fn set_log_time_enabled(enabled: bool, recording: Option<&PyRecordingStream>) { + let Some(recording) = get_data_recording(recording) else { + return; + }; + recording.set_log_time_enabled(enabled); +} + // --- Log special --- /// Log an arrow message. @@ -2023,25 +2208,56 @@ fn send_arrow_chunk( }) } -/// Send a pre-built chunk to the recording stream. +/// Send chunks to the recording stream. +/// +/// Accepts a single chunk or any iterable of chunks. Blocks until every chunk +/// has been pushed to the recording's batcher. #[pyfunction] -#[pyo3(signature = (chunk, recording=None))] -fn send_chunk( +#[pyo3(signature = (chunks, recording=None))] +fn send_chunks( py: Python<'_>, - chunk: &crate::chunk::PyChunkInternal, + chunks: Bound<'_, PyAny>, recording: Option<&PyRecordingStream>, ) -> PyResult<()> { let Some(recording) = get_data_recording(recording) else { return Ok(()); }; - let chunk = re_chunk::Chunk::clone(chunk.inner()); + // Single Chunk — fast path + if let Ok(chunk_obj) = chunks.cast::() { + let chunk = re_chunk::Chunk::clone(chunk_obj.borrow().inner()); + py.detach(|| { + recording.send_chunk(chunk); + flush_garbage_queue(); + }); + return Ok(()); + } - py.detach(|| { - recording.send_chunk(chunk); + // Iterable of Chunk. Streamed one at a time — we don't collect into a Vec + // because the iterable may be a generator yielding many chunks and buffering + // all of them would inflate peak memory. + let iter: Py = chunks + .try_iter() + .map_err(|_err| PyTypeError::new_err("send_chunks expected a Chunk or iterable of Chunk"))? + .into_any() + .unbind(); + py.detach(|| -> PyResult<()> { + loop { + let next_chunk = Python::attach(|py| -> PyResult> { + match iter.bind(py).call_method0("__next__") { + Ok(obj) => { + let internal: PyRef<'_, PyChunkInternal> = obj.extract()?; + Ok(Some(re_chunk::Chunk::clone(internal.inner()))) + } + Err(err) if err.is_instance_of::(py) => Ok(None), + Err(err) => Err(err), + } + })?; + let Some(chunk) = next_chunk else { break }; + recording.send_chunk(chunk); + } flush_garbage_queue(); - Ok(()) }) } @@ -2149,23 +2365,6 @@ fn send_blueprint( } } -/// Send all chunks from a [`PyRecording`] to the given recording stream. -/// -/// !!! Warning -/// ⚠️ This API is experimental and may change or be removed in future versions! ⚠️ -#[pyfunction] -#[pyo3(signature = (rrd, recording = None))] -fn send_recording(rrd: &PyRecordingInternal, recording: Option<&PyRecordingStream>) { - let Some(recording) = get_data_recording(recording) else { - return; - }; - - let store = rrd.store.read(); - for chunk in store.iter_physical_chunks() { - recording.send_chunk((**chunk).clone()); - } -} - // --- Misc --- /// Return a verbose version string. @@ -2370,7 +2569,7 @@ authkey = multiprocessing.current_process().authkey }) .and_then(|authkey| { authkey - .downcast() + .cast() .cloned() .map_err(|err| PyRuntimeError::new_err(err.to_string())) }) diff --git a/rerun_py/src/query_metrics.rs b/rerun_py/src/query_metrics.rs new file mode 100644 index 000000000000..41ef48300aec --- /dev/null +++ b/rerun_py/src/query_metrics.rs @@ -0,0 +1,435 @@ +//! PyO3 bridge for `rerun.experimental.query_metrics()`. +//! +//! See [`rerun_py/rerun_sdk/rerun/experimental/_query_metrics.py`] for the +//! user-facing context manager. This file exposes: +//! +//! - [`PyQueryMetrics`]: a frozen, getter-only Python class mirroring +//! [`re_datafusion::QuerySnapshot`]. +//! - [`PyMetricsCollectorHandle`]: opaque wrapper around +//! [`re_datafusion::MetricsCollector`] with `drain()` / `snapshot()` methods. +//! - [`new_metrics_collector`]: allocate a fresh handle. The Python wrapper +//! pushes it onto the `_active_collectors` `ContextVar` for the duration of +//! the `with` block. +//! - [`active_metrics_collectors`]: read that ContextVar from Rust so +//! `dataset_view.rs::reader()` can attach the collectors to a freshly-built +//! `DataframeQueryTableProvider`. +//! +//! The actual snapshot-on-stream-completion logic lives in +//! `re_datafusion::metrics_capture` and `re_datafusion::dataframe_query_provider`. + +use std::time::Duration; + +use pyo3::prelude::*; +use pyo3::types::PyTuple; +use re_datafusion::{MetricsCollector, QuerySnapshot}; + +/// Frozen, getter-only mirror of [`re_datafusion::QuerySnapshot`]. +/// +/// One per query that ran inside a `query_metrics()` scope. +#[pyclass( + frozen, + from_py_object, + eq, + name = "_QueryMetrics", + module = "rerun_bindings.rerun_bindings" +)] +#[derive(Clone, Debug, PartialEq)] +pub struct PyQueryMetrics { + snap: QuerySnapshot, +} + +impl PyQueryMetrics { + fn new(snap: QuerySnapshot) -> Self { + Self { snap } + } +} + +#[pymethods] +impl PyQueryMetrics { + // ---- Plan-time fields ---- + + /// The dataset being queried. + #[getter] + fn dataset_id(&self) -> &str { + &self.snap.query_info.dataset_id + } + + /// Number of unique chunks returned by `query_dataset` (subset of the dataset). + #[getter] + fn query_chunks(&self) -> usize { + self.snap.query_info.query_chunks + } + + /// Number of distinct segments involved in the query. + #[getter] + fn query_segments(&self) -> usize { + self.snap.query_info.query_segments + } + + /// Number of distinct layers touched by the query. + #[getter] + fn query_layers(&self) -> usize { + self.snap.query_info.query_layers + } + + /// Number of columns in the query output schema. + #[getter] + fn query_columns(&self) -> usize { + self.snap.query_info.query_columns + } + + /// Number of entity paths in the query request. + #[getter] + fn query_entities(&self) -> usize { + self.snap.query_info.query_entities + } + + /// Total size of all queried chunks in bytes (from chunk metadata). + #[getter] + fn query_bytes(&self) -> u64 { + self.snap.query_info.query_bytes + } + + /// Min number of chunks touched within any single segment in this query. + #[getter] + fn query_chunks_per_segment_min(&self) -> u32 { + self.snap.query_info.query_chunks_per_segment_min + } + + /// Max number of chunks touched within any single segment in this query. + #[getter] + fn query_chunks_per_segment_max(&self) -> u32 { + self.snap.query_info.query_chunks_per_segment_max + } + + /// Mean number of chunks touched per segment in this query. + #[getter] + fn query_chunks_per_segment_mean(&self) -> f32 { + self.snap.query_info.query_chunks_per_segment_mean + } + + /// Query shape: one of `"static"`, `"latest_at"`, `"range"`, `"dataframe"`, or `"full_scan"`. + #[getter] + fn query_type(&self) -> &'static str { + self.snap.query_info.query_type.as_str() + } + + /// Name of the sort/filter index (timeline) for this query, if any. + #[getter] + fn primary_index_name(&self) -> Option<&str> { + self.snap.query_info.primary_index_name.as_deref() + } + + /// Time from sending `query_dataset` until the first response message arrives + /// (the chunk metadata, not actual chunk data). + #[getter] + fn time_to_first_chunk_info(&self) -> Option { + self.snap.query_info.time_to_first_chunk_info + } + + /// Number of filter expressions the table provider was able to push down to + /// the server (`Exact` or `Inexact` from `supports_filters_pushdown`). + #[getter] + fn filters_pushed_down(&self) -> usize { + self.snap.query_info.filters_pushed_down + } + + /// Number of filter expressions that could not be pushed down — applied + /// client-side by DataFusion via a downstream `FilterExec`. + #[getter] + fn filters_applied_client_side(&self) -> usize { + self.snap.query_info.filters_applied_client_side + } + + /// True when projection-based entity-path narrowing actually trimmed the + /// set of entity paths sent to `query_dataset`. + #[getter] + fn entity_path_narrowing_applied(&self) -> bool { + self.snap.query_info.entity_path_narrowing_applied + } + + // ---- Execution-time fields ---- + + /// Wall-clock time from the start of `scan()` until the query finished + /// (cleanly or via error). Always populated. + #[getter] + fn total_duration(&self) -> Duration { + self.snap.total_duration + } + + /// Time from scan start until the first chunk reached the consumer. `None` + /// when no chunk was ever delivered (e.g. early error, empty result). + #[getter] + fn time_to_first_chunk(&self) -> Option { + self.snap.time_to_first_chunk + } + + /// `None` on success. On failure, one of the stable string labels + /// `"grpc_fetch"`, `"direct_fetch"`, `"decode"`, or `"other"`. + #[getter] + fn error_kind(&self) -> Option<&'static str> { + self.snap.error_kind + } + + /// Reason a direct (HTTP Range) fetch hit a terminal failure — i.e. a + /// non-retryable error or retries exhausted. `None` when no direct fetch + /// terminally failed (can be `None` even when `error_kind` is set, if the + /// failure was on the gRPC or decode path). + #[getter] + fn direct_terminal_reason(&self) -> Option<&'static str> { + self.snap.direct_terminal_reason.map(|r| r.as_str()) + } + + // ---- Fetch counters ---- + + /// Number of gRPC fetch calls the scanner issued. + #[getter] + fn fetch_grpc_requests(&self) -> u64 { + self.snap.fetch_grpc_requests + } + + /// Sum of `chunk_byte_length` (catalog metadata, compressed on-disk size) + /// over chunks fetched via gRPC. Excludes framing overhead and bytes + /// consumed by failed retries — a lower bound on wire traffic. + #[getter] + fn fetch_grpc_bytes(&self) -> u64 { + self.snap.fetch_grpc_bytes + } + + /// Number of direct (HTTP Range) fetches the scanner issued. Counts each + /// merged request once, regardless of byte ranges or retry attempts. + #[getter] + fn fetch_direct_requests(&self) -> u64 { + self.snap.fetch_direct_requests + } + + /// Sum of `chunk_byte_length` (catalog metadata, compressed on-disk size) + /// over chunks fetched via direct HTTP. Does **not** count filler bytes + /// that range-merging pulls between adjacent chunks, so actual wire + /// traffic can exceed this value. Includes successful merged-range fetches + /// even when a sibling range makes the overall batch fail. + #[getter] + fn fetch_direct_bytes(&self) -> u64 { + self.snap.fetch_direct_bytes + } + + /// Total number of direct-fetch retry *attempts* across all requests. + /// A request retried 3 times contributes 3 here. + #[getter] + fn fetch_direct_retries(&self) -> u64 { + self.snap.fetch_direct_retries + } + + /// Number of distinct direct-fetch requests that needed at least one + /// retry. Always `≤ fetch_direct_retries`; the ratio between them is the + /// average retries per retried request. + #[getter] + fn fetch_direct_requests_retried(&self) -> u64 { + self.snap.fetch_direct_requests_retried + } + + /// Total backoff time slept across all direct-fetch retries. + #[getter] + fn fetch_direct_retry_sleep(&self) -> Duration { + self.snap.fetch_direct_retry_sleep + } + + /// True maximum attempt number across all partitions. + #[getter] + fn fetch_direct_max_attempt(&self) -> u64 { + self.snap.fetch_direct_max_attempt + } + + /// Number of byte ranges the planner *wanted* to fetch directly, before + /// adjacent ranges were coalesced. With `fetch_direct_merged_ranges`, + /// gives the range-merging ratio. + #[getter] + fn fetch_direct_original_ranges(&self) -> u64 { + self.snap.fetch_direct_original_ranges + } + + /// Number of combined HTTP Range requests produced by merging adjacent + /// byte ranges. Normally equals `fetch_direct_requests` after a completed + /// scan, but can differ when cancellation stops only part of the planned + /// work from being issued. + #[getter] + fn fetch_direct_merged_ranges(&self) -> u64 { + self.snap.fetch_direct_merged_ranges + } + + /// Transport batches planned before splitting direct and gRPC work. + #[getter] + fn planned_fetch_batches(&self) -> u64 { + self.snap.planned_fetch_batches + } + + /// Segment waves produced by the current admission scheduler. + #[getter] + fn planned_segment_waves(&self) -> u64 { + self.snap.planned_segment_waves + } + + /// Maximum concurrently admitted segments configured for this query. + #[getter] + fn segment_admission_limit(&self) -> u64 { + self.snap.segment_admission_limit + } + + /// Largest distinct-segment count in a planned transport batch. + #[getter] + fn max_segments_per_fetch_batch(&self) -> u64 { + self.snap.max_segments_per_fetch_batch + } + + /// Largest distinct-segment count in a planned admission wave. + #[getter] + fn max_segments_per_wave(&self) -> u64 { + self.snap.max_segments_per_wave + } + + /// Highest observed number of active admitted segments. May exceed + /// `segment_admission_limit` when the stall breaker admits bypass segments. + #[getter] + fn peak_active_segments(&self) -> u64 { + self.snap.peak_active_segments + } + + /// Total decoded-byte capacity shared across all query partitions. + #[getter] + fn pipeline_budget_bytes(&self) -> u64 { + self.snap.pipeline_budget_bytes + } + + /// Highest observed number of decoded bytes charged to the pipeline budget. + #[getter] + fn pipeline_peak_decoded_bytes(&self) -> u64 { + self.snap.pipeline_peak_decoded_bytes + } + + /// Reservations that first parked because decoded-byte capacity was full. + #[getter] + fn pipeline_byte_waits(&self) -> u64 { + self.snap.pipeline_byte_waits + } + + /// Reservations that first parked because segment admission was full. + #[getter] + fn segment_admission_waits(&self) -> u64 { + self.snap.segment_admission_waits + } + + /// Number of saturated-pipeline stall-breaker activations. + #[getter] + fn pipeline_stall_breaker_activations(&self) -> u64 { + self.snap.pipeline_stall_breaker_activations + } + + fn __repr__(&self) -> String { + let qi = &self.snap.query_info; + format!( + "QueryMetrics(dataset_id={:?}, query_type={}, query_chunks={}, query_segments={}, \ + query_bytes={}, filters_pushed_down={}, filters_applied_client_side={}, \ + entity_path_narrowing_applied={}, fetch_grpc_bytes={}, fetch_direct_bytes={}, \ + total_duration={:?}, \ + error_kind={:?})", + qi.dataset_id, + qi.query_type.as_str(), + qi.query_chunks, + qi.query_segments, + re_format::format_bytes(qi.query_bytes as _), + qi.filters_pushed_down, + qi.filters_applied_client_side, + qi.entity_path_narrowing_applied, + re_format::format_bytes(self.snap.fetch_grpc_bytes as _), + re_format::format_bytes(self.snap.fetch_direct_bytes as _), + self.snap.total_duration, + self.snap.error_kind, + ) + } +} + +/// Opaque PyO3 handle to a [`MetricsCollector`]. +/// +/// Held by the Python `query_metrics()` context manager between +/// `__enter__` and `__exit__`, and pushed onto the `_active_collectors` +/// `ContextVar` so `dataset_view.rs::reader()` can pick it up at plan time. +/// Cheap to clone — the inner `MetricsCollector` is itself an `Arc` wrapper, +/// so each attached `DataframeQueryTableProvider` and the Python-side handle +/// share the same buffer. +#[pyclass( // NOLINT: ignore[py-cls-eq] opaque handle — eq would be the same as `is` + name = "_MetricsCollectorHandle", + module = "rerun_bindings.rerun_bindings" +)] +pub struct PyMetricsCollectorHandle { + pub(crate) collector: MetricsCollector, +} + +#[pymethods] // NOLINT: ignore[py-mthd-str] opaque handle +impl PyMetricsCollectorHandle { + /// Non-destructive copy of all snapshots received so far. + /// + /// Suitable for use mid-scope (`collector.queries` in the Python wrapper). + fn snapshot(&self) -> Vec { + self.collector + .snapshot() + .into_iter() + .map(PyQueryMetrics::new) + .collect() + } + + /// Take and clear all snapshots. + /// + /// Used by the context manager on `__exit__` to drain any remaining + /// snapshots into the user-visible Python `MetricsCollector` wrapper. + fn drain(&self) -> Vec { + self.collector + .drain() + .into_iter() + .map(PyQueryMetrics::new) + .collect() + } +} + +/// Allocate a fresh [`MetricsCollector`] and wrap it in a Python handle. +/// +/// The Python `query_metrics()` context manager pushes the returned handle +/// onto the `_active_collectors` `ContextVar` for the duration of the +/// `with` block; nothing is registered globally. +#[pyfunction] +#[pyo3(name = "_new_metrics_collector")] +pub fn new_metrics_collector() -> PyMetricsCollectorHandle { + PyMetricsCollectorHandle { + collector: MetricsCollector::new(), + } +} + +/// Read the `_active_collectors` `ContextVar` defined in +/// `rerun.experimental._query_metrics` and return the underlying Rust +/// collectors. +/// +/// Returns an empty `Vec` when no `query_metrics()` scope is active, when the +/// module isn't importable, or when the ContextVar holds an unexpected value. +/// Failures here are never propagated: a broken metrics hookup should never +/// take down a user query. +pub fn active_metrics_collectors(py: Python<'_>) -> Vec { + let read = || -> PyResult> { + let module = py.import("rerun.experimental._query_metrics")?; + let ctxvar = module.getattr("_active_collectors")?; + let current = ctxvar.call_method0("get")?; + let tuple = current.cast::()?; + let mut out = Vec::with_capacity(tuple.len()); + for item in tuple { + let handle: PyRef<'_, PyMetricsCollectorHandle> = item.extract()?; + out.push(handle.collector.clone()); + } + Ok(out) + }; + + match read() { + Ok(v) => v, + Err(err) => { + re_log::debug_once!("Failed to read query_metrics ContextVar: {err}"); + Vec::new() + } + } +} diff --git a/rerun_py/src/recording/mod.rs b/rerun_py/src/recording/mod.rs deleted file mode 100644 index 9cc5325e1230..000000000000 --- a/rerun_py/src/recording/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod rrd; - -use pyo3::types::{PyModule, PyModuleMethods as _}; -use pyo3::{Bound, PyResult, wrap_pyfunction}; - -pub use self::rrd::{PyRRDArchiveInternal, PyRecordingInternal, load_archive, load_recording}; - -/// Register the `rerun.recording` module. -pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - - m.add_function(wrap_pyfunction!(load_archive, m)?)?; - m.add_function(wrap_pyfunction!(load_recording, m)?)?; - - Ok(()) -} diff --git a/rerun_py/src/recording/rrd.rs b/rerun_py/src/recording/rrd.rs deleted file mode 100644 index 34a223a8af86..000000000000 --- a/rerun_py/src/recording/rrd.rs +++ /dev/null @@ -1,207 +0,0 @@ -use std::collections::BTreeMap; -use std::sync::Arc; - -use pyo3::exceptions::{PyRuntimeError, PyValueError}; -use pyo3::{PyResult, pyclass, pyfunction, pymethods}; - -use re_chunk::Chunk; -use re_chunk_store::{ChunkStore, ChunkStoreConfig, ChunkStoreHandle}; -use re_log_types::{LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreSource}; - -use crate::catalog::PySchemaInternal; -use crate::chunk::PyChunkIterator; - -/// An archive loaded from an RRD. -/// -/// RRD archives may include 1 or more recordings or blueprints. -#[pyclass( - frozen, - name = "RRDArchiveInternal", - module = "rerun_bindings.rerun_bindings" -)] -#[derive(Clone)] -pub struct PyRRDArchiveInternal { - pub datasets: BTreeMap)>, -} - -#[pymethods] -impl PyRRDArchiveInternal { - /// The number of recordings in the archive. - fn num_recordings(&self) -> usize { - self.datasets - .iter() - .filter(|(id, _)| id.is_recording()) - .count() - } - - /// All the recordings in the archive. - // TODO(jleibs): This should return an iterator - fn all_recordings(&self) -> Vec { - self.datasets - .iter() - .filter(|(id, _)| id.is_recording()) - .map(|(_, (store, store_info))| PyRecordingInternal { - store: store.clone(), - store_info: store_info.clone(), - }) - .collect() - } -} - -/// A single Rerun recording. -/// -/// This can be loaded from an RRD file using [`load_recording()`][rerun.recording.load_recording]. -/// -/// A recording is a collection of data that was logged to Rerun. This data is organized -/// as a column for each index (timeline) and each entity/component pair that was logged. -/// -/// You can examine the [`.schema()`][rerun.recording.Recording.schema] of the recording to see -/// what data is available. -#[pyclass(name = "RecordingInternal", module = "rerun_bindings.rerun_bindings")] -pub struct PyRecordingInternal { - pub(crate) store: ChunkStoreHandle, - pub(crate) store_info: Option, -} - -#[pymethods] -impl PyRecordingInternal { - /// The schema describing all the columns available in the recording. - fn schema(&self) -> PySchemaInternal { - PySchemaInternal { - columns: self.store.read().schema().chunk_column_descriptors().into(), - metadata: Default::default(), - } - } - - /// The recording ID of the recording. - fn recording_id(&self) -> String { - self.store.read().id().recording_id().to_string() - } - - /// The application ID of the recording. - fn application_id(&self) -> String { - self.store.read().id().application_id().to_string() - } - - /// Iterate over all physical chunks in this recording. - fn chunks(&self) -> PyChunkIterator { - // TODO(RR-4126): this should eventually become a streaming iterator which loads the chunk - // as it is iterated. - let chunks: Vec<_> = self.store.read().iter_physical_chunks().cloned().collect(); - PyChunkIterator::new(chunks) - } - - /// Save this recording to an RRD file. - #[expect(clippy::needless_pass_by_value)] - fn save(&self, path: std::path::PathBuf) -> PyResult<()> { - let store = self.store.read(); - let store_id = store.id().clone(); - - let info = self.store_info.clone().unwrap_or_else(|| { - StoreInfo::new( - store_id.clone(), - StoreSource::Other("rerun-sdk-python".into()), - ) - }); - - let file = - std::fs::File::create(&path).map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - - let mut encoder = re_log_encoding::Encoder::new_eager( - re_build_info::CrateVersion::LOCAL, - re_log_encoding::EncodingOptions::PROTOBUF_COMPRESSED, - file, - ) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - - encoder - .append(&LogMsg::SetStoreInfo(SetStoreInfo { - row_id: re_tuid::Tuid::new(), - info, - })) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - - for chunk in store.iter_physical_chunks() { - let arrow_msg = chunk - .to_arrow_msg() - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - encoder - .append(&LogMsg::ArrowMsg(store_id.clone(), arrow_msg)) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - } - - encoder - .finish() - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - - Ok(()) - } -} - -/// Load a single recording from an RRD file. -#[pyfunction] -pub fn load_recording(path_to_rrd: std::path::PathBuf) -> PyResult { - let archive = load_archive(path_to_rrd)?; - - let num_recordings = archive.num_recordings(); - - if num_recordings != 1 { - return Err(PyValueError::new_err(format!( - "Expected exactly one recording in the archive, but found {num_recordings}", - ))); - } - - if let Some(recording) = archive.all_recordings().into_iter().next() { - Ok(recording) - } else { - Err(PyValueError::new_err( - "Expected exactly one recording in the archive, but found none.", - )) - } -} - -/// Load a rerun archive from an RRD file. -#[pyfunction] -#[expect(clippy::needless_pass_by_value)] -pub fn load_archive(path_to_rrd: std::path::PathBuf) -> PyResult { - let rrd_file = std::fs::File::open(&path_to_rrd) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - let decoder = re_log_encoding::Decoder::decode_eager(std::io::BufReader::new(rrd_file)) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - - let mut stores: BTreeMap = BTreeMap::new(); - let mut store_infos: BTreeMap = BTreeMap::new(); - - for msg_result in decoder { - let msg = msg_result.map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - match msg { - LogMsg::SetStoreInfo(set_store_info) => { - let info = set_store_info.info; - stores.entry(info.store_id.clone()).or_insert_with(|| { - ChunkStore::new(info.store_id.clone(), ChunkStoreConfig::DEFAULT) - }); - store_infos.insert(info.store_id.clone(), info); - } - LogMsg::ArrowMsg(store_id, arrow_msg) => { - let chunk = Chunk::from_arrow_msg(&arrow_msg) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - if let Some(store) = stores.get_mut(&store_id) { - store - .insert_chunk(&Arc::new(chunk)) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))?; - } - } - LogMsg::BlueprintActivationCommand(_) => {} - } - } - - let datasets = stores - .into_iter() - .map(|(store_id, store)| { - let info = store_infos.remove(&store_id); - (store_id, (ChunkStoreHandle::new(store), info)) - }) - .collect(); - - Ok(PyRRDArchiveInternal { datasets }) -} diff --git a/rerun_py/src/selector.rs b/rerun_py/src/selector.rs index c309e58c60dc..fe37710d7812 100644 --- a/rerun_py/src/selector.rs +++ b/rerun_py/src/selector.rs @@ -70,9 +70,8 @@ impl PySelectorInternal { /// Execute this selector against a pyarrow array. fn execute(&self, py: Python<'_>, source: PyArrowType) -> PyResult> { let array: ArrayRef = make_array(source.0); - let result = self - .selector - .execute(array) + let result = re_lenses::default_runtime() + .execute(&self.selector, array) .map_err(|err| PyRuntimeError::new_err(format!("Selector execution failed: {err}")))?; match result { Some(arr) => arr.to_data().to_pyarrow(py).map(|obj| obj.unbind()), @@ -92,9 +91,8 @@ impl PySelectorInternal { let list_array = array.as_any().downcast_ref::().ok_or_else(|| { PyTypeError::new_err(format!("expected a ListArray, got {:?}", array.data_type())) })?; - let result = self - .selector - .execute_per_row(list_array) + let result = re_lenses::default_runtime() + .execute_per_row(&self.selector, list_array) .map_err(|err| PyRuntimeError::new_err(format!("Selector execution failed: {err}")))?; match result { Some(arr) => arr.to_data().to_pyarrow(py).map(|obj| obj.unbind()), @@ -133,4 +131,10 @@ impl PySelectorInternal { fn __str__(&self) -> String { self.selector.to_string_lossy() } + + /// Render this selector as a query string, or `None` if it contains + /// a Python callable from `.pipe()` (which cannot be serialized). + fn try_to_string(&self) -> Option { + self.selector.try_to_string() + } } diff --git a/rerun_py/src/server.rs b/rerun_py/src/server.rs index c2a997b0cc8d..36e4dba1d8bb 100644 --- a/rerun_py/src/server.rs +++ b/rerun_py/src/server.rs @@ -122,7 +122,7 @@ impl PyServerInternal { fn extract_named_paths(dict: &Bound<'_, PyDict>) -> Vec { dict.iter() .filter_map(|(k, v)| { - let name = k.downcast::().ok()?; + let name = k.cast::().ok()?; let path = v.extract::<&str>().ok()?; Some(re_server::NamedPath { @@ -136,7 +136,7 @@ fn extract_named_paths(dict: &Bound<'_, PyDict>) -> Vec { fn extract_named_collections(dict: &Bound<'_, PyDict>) -> Vec { dict.iter() .filter_map(|(k, v)| { - let name = k.downcast::().ok()?; + let name = k.cast::().ok()?; let paths: Vec = v.extract().ok()?; let entry_name = diff --git a/rerun_py/src/trace_context.rs b/rerun_py/src/trace_context.rs index e76af9f25ace..70eb515bf2bf 100644 --- a/rerun_py/src/trace_context.rs +++ b/rerun_py/src/trace_context.rs @@ -45,7 +45,7 @@ pub(crate) fn read_trace_context_from_python( ) -> tracing::Span { #[cfg(feature = "perf_telemetry")] { - let trace_headers = re_perf_telemetry::extract_trace_context_from_contextvar(py); + let trace_headers = extract_trace_context_from_contextvar(py); let _guard = trace_headers.attach(); tracing::span!(tracing::Level::INFO, "sdk", otel.name = name) } @@ -66,7 +66,7 @@ pub(crate) fn read_trace_context_from_python( pub fn get_trace_context_var(py: Python<'_>) -> PyResult> { #[cfg(feature = "perf_telemetry")] { - let context_var = re_perf_telemetry::get_trace_context_var(py)?; + let context_var = trace_context_var(py)?; Ok(context_var.unbind()) } #[cfg(not(feature = "perf_telemetry"))] @@ -74,3 +74,93 @@ pub fn get_trace_context_var(py: Python<'_>) -> PyResult> { Ok(py.None()) } } + +// --- +// Python `ContextVar` plumbing for trace-context propagation. +// +// All pyo3 use lives in this crate so `re_perf_telemetry` stays +// language-agnostic. The boundary between the two is a plain Rust +// `TraceHeaders` value: this side reads the `ContextVar` and hands the +// struct over; `re_perf_telemetry` consumes it without ever touching the +// Python runtime. + +/// Name of the Python `ContextVar` used for trace-context propagation. The +/// Python decorator side (`rerun._tracing.with_tracing`) uses the same name +/// to set headers; Rust reads them back through this ContextVar. +#[cfg(feature = "perf_telemetry")] +const TRACE_CONTEXT_VAR_NAME: &str = "TRACE_CONTEXT"; + +/// Get the trace context `ContextVar` object. +/// +/// This returns the same Python `ContextVar` instance every time, ensuring that +/// values set on it can be read back later. It is up to the caller to ensure trace context +/// is reset and cleared as needed. +#[cfg(feature = "perf_telemetry")] +fn trace_context_var(py: Python<'_>) -> PyResult> { + use pyo3::prelude::*; + + static CONTEXT_VAR: parking_lot::Mutex>> = parking_lot::Mutex::new(None); + + let mut guard = CONTEXT_VAR.lock(); + + if let Some(var) = guard.as_ref() { + return Ok(var.bind(py).clone()); + } + + // Create the trace context ContextVar + let module = py.import("contextvars")?; + let contextvar_class = module.getattr("ContextVar")?; + let trace_ctx_var = contextvar_class.call1((TRACE_CONTEXT_VAR_NAME,))?; + let trace_ctx_unbound = trace_ctx_var.clone().unbind(); + + *guard = Some(trace_ctx_unbound); + + Ok(trace_ctx_var) +} + +/// Extract trace context from the Python `ContextVar` for cross-boundary propagation. +/// +/// Returns empty [`re_perf_telemetry::TraceHeaders`] if the `ContextVar` is unset or extraction fails. +#[cfg(feature = "perf_telemetry")] +pub(crate) fn extract_trace_context_from_contextvar( + py: Python<'_>, +) -> re_perf_telemetry::TraceHeaders { + use pyo3::prelude::*; + use pyo3::types::PyDict; + use re_perf_telemetry::TraceHeaders; + + fn try_extract(py: Python<'_>) -> PyResult { + let context_var = trace_context_var(py)?; + + match context_var.call_method0("get") { + Ok(trace_data) => { + if let Ok(dict) = trace_data.cast::() { + let traceparent = dict + .get_item(TraceHeaders::TRACEPARENT_KEY)? + .and_then(|v| v.extract::().ok()) + .unwrap_or_default(); + + let tracestate = dict + .get_item(TraceHeaders::TRACESTATE_KEY)? + .and_then(|v| v.extract::().ok()); + + let headers = TraceHeaders { + traceparent, + tracestate, + }; + + tracing::debug!("Trace headers: {:?}", headers); + Ok(headers) + } else { + Ok(TraceHeaders::empty()) + } + } + Err(_) => Ok(TraceHeaders::empty()), + } + } + + try_extract(py).unwrap_or_else(|err| { + tracing::debug!("Failed to extract trace context: {err}"); + TraceHeaders::empty() + }) +} diff --git a/rerun_py/src/tracing_session.rs b/rerun_py/src/tracing_session.rs new file mode 100644 index 000000000000..a676ba7af86c --- /dev/null +++ b/rerun_py/src/tracing_session.rs @@ -0,0 +1,172 @@ +//! Python-side bridge for the `tracing_session()` context manager. +//! +//! See `rerun_py/rerun_sdk/rerun/_tracing_session.py` for the user-facing API. +//! +//! The bridge has two pyo3 entry points: +//! +//! - [`get_tracing_session_var`] — exposes the Python `ContextVar` whose current value is +//! read by the Rust-side [`re_perf_telemetry::current_rerun_session_id`] on every +//! outbound gRPC request. The `TraceStateEnricher` uses it to merge +//! `rerun_session_id=` into the `tracestate` header. +//! +//! - [`is_telemetry_active`] — lets the Python context manager fail fast with an +//! actionable error when `TELEMETRY_ENABLED` is not truthy. Without an active +//! telemetry stack, the `TracingInjectorInterceptor` has no valid OTel context to +//! inject from, so a session id would never reach the wire. +//! +//! [`re_perf_telemetry::current_rerun_session_id`]: https://docs.rs/re_perf_telemetry + +use pyo3::{Py, PyAny, PyResult, Python, pyfunction}; + +/// Return `True` if the rerun telemetry stack initialized successfully. +/// +/// `tracing_session()` requires this to be true; otherwise the W3C propagator +/// is not registered and the session id has no transport. +#[pyfunction] +#[pyo3(name = "_is_telemetry_active")] +pub fn is_telemetry_active() -> bool { + #[cfg(feature = "perf_telemetry")] + { + re_perf_telemetry::is_telemetry_active() + } + #[cfg(not(feature = "perf_telemetry"))] + { + false + } +} + +/// Return the `ContextVar` carrying the active rerun session id. +/// +/// Set by the `tracing_session()` context manager and read on every outbound +/// gRPC call to merge `rerun_session_id=` into the W3C `tracestate` header. +/// +/// Returns `None` when `perf_telemetry` is disabled. +#[pyfunction] +#[pyo3(name = "_get_tracing_session_var")] +pub fn get_tracing_session_var(py: Python<'_>) -> PyResult> { + #[cfg(feature = "perf_telemetry")] + { + let context_var = get_rerun_session_var(py)?; + Ok(context_var.unbind()) + } + #[cfg(not(feature = "perf_telemetry"))] + { + Ok(py.None()) + } +} + +// --- +// Python `ContextVar` plumbing for the active `tracing_session()` id. +// +// All pyo3 use lives in this crate so `re_perf_telemetry` stays +// language-agnostic. The boundary is the `SessionIdReader` closure +// registered at telemetry init (see `python_bridge.rs::init_perf_telemetry`): +// `re_perf_telemetry` invokes that closure to get an +// `Option` without knowing how it was sourced. + +/// Name of the Python `ContextVar` carrying the active `tracing_session()` id. +#[cfg(feature = "perf_telemetry")] +const RERUN_SESSION_VAR_NAME: &str = "RERUN_SESSION_ID"; + +/// Get the rerun session id `ContextVar` object. +/// +/// Set by the Python `tracing_session()` context manager. The Rust-side +/// [`re_perf_telemetry::current_rerun_session_id`] helper reads it on every +/// outbound RPC to enrich the W3C `tracestate` with `rerun_session_id=`. +#[cfg(feature = "perf_telemetry")] +fn get_rerun_session_var(py: Python<'_>) -> PyResult> { + use pyo3::prelude::*; + + static CONTEXT_VAR: parking_lot::Mutex>> = parking_lot::Mutex::new(None); + + let mut guard = CONTEXT_VAR.lock(); + + if let Some(var) = guard.as_ref() { + return Ok(var.bind(py).clone()); + } + + let module = py.import("contextvars")?; + let contextvar_class = module.getattr("ContextVar")?; + // Default to an explicit `None` so `.get()` never raises `LookupError`. + let kwargs = pyo3::types::PyDict::new(py); + kwargs.set_item("default", py.None())?; + let var = contextvar_class.call((RERUN_SESSION_VAR_NAME,), Some(&kwargs))?; + *guard = Some(var.clone().unbind()); + + Ok(var) +} + +/// Read the current rerun session id from the Python `ContextVar`. +/// +/// Returns `None` when no `tracing_session()` is active, the value is unset, or +/// the value fails [`re_perf_telemetry::RerunTracingSessionId::parse`]. +#[cfg(feature = "perf_telemetry")] +pub(crate) fn current_rerun_session_id_from_contextvar( + py: Python<'_>, +) -> Option { + use pyo3::prelude::*; + + let var = get_rerun_session_var(py).ok()?; + let value = var.call_method0("get").ok()?; + if value.is_none() { + return None; + } + let raw = value.extract::().ok()?; + re_perf_telemetry::RerunTracingSessionId::parse(&raw) +} + +/// Increment the process-wide active-tracing-session gate. Called by `tracing_session().__enter__`. +#[pyfunction] +#[pyo3(name = "_inc_active_tracing_sessions")] +pub fn inc_active_tracing_sessions() { + #[cfg(feature = "perf_telemetry")] + { + re_perf_telemetry::inc_active_tracing_session_count(); + } +} + +/// Decrement the process-wide active-tracing-session gate. Called by `tracing_session().__exit__`. +#[pyfunction] +#[pyo3(name = "_dec_active_tracing_sessions")] +pub fn dec_active_tracing_sessions() { + #[cfg(feature = "perf_telemetry")] + { + re_perf_telemetry::dec_active_tracing_session_count(); + } +} + +/// Emit `rerun tracing session started: ` through the Rust `tracing` stack at INFO level. +#[pyfunction] +#[pyo3(name = "_log_tracing_session_started")] +pub fn log_tracing_session_started(rerun_session_id: &str) { + tracing::info!("rerun tracing session started: {rerun_session_id}"); +} + +/// Emit a single structured INFO event summarizing the tracing session at scope exit. +/// +/// `Option` fields are `None` when the host platform or runtime can't supply +/// the metric (psutil missing, or `iowait` unavailable on macOS/Windows). Routed +/// through the Rust `tracing` stack so it follows `RUST_LOG` and the fmt-layer +/// pipeline like `_log_tracing_session_started`. +#[pyfunction] +#[pyo3(name = "_log_tracing_session_finished")] +#[pyo3(signature = (rerun_session_id, elapsed_s, cpu_user_s, cpu_system_s, cpu_iowait_s, net_rx_mb))] +pub fn log_tracing_session_finished( + rerun_session_id: &str, + elapsed_s: f64, + cpu_user_s: Option, + cpu_system_s: Option, + cpu_iowait_s: Option, + net_rx_mb: Option, +) { + let fmt = |v: Option| v.map_or_else(|| "na".to_owned(), |x| format!("{x:.3}")); + tracing::info!( + rerun_session_id, + elapsed_s = format!("{elapsed_s:.3}"), + cpu_user_s = fmt(cpu_user_s), + cpu_system_s = fmt(cpu_system_s), + cpu_iowait_s = fmt(cpu_iowait_s), + net_rx_mb = fmt(net_rx_mb), + "rerun tracing session finished", + ); +} diff --git a/rerun_py/src/urdf.rs b/rerun_py/src/urdf.rs index c273fa8904e6..7d3d9a4d8935 100644 --- a/rerun_py/src/urdf.rs +++ b/rerun_py/src/urdf.rs @@ -1,7 +1,9 @@ use std::path::PathBuf; use std::sync::Arc; -use pyo3::exceptions::{PyNotImplementedError, PyRuntimeError}; +use arrow::array::{Array as _, ArrayData, ListArray, make_array}; +use arrow::pyarrow::{PyArrowType, ToPyArrow as _}; +use pyo3::exceptions::{PyNotImplementedError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use re_sdk::external::re_importer::{UrdfTree, urdf_joint_transform}; use re_sdk::external::urdf_rs::{Joint, JointType, Link, Mimic}; @@ -143,13 +145,59 @@ impl PyUrdfTree { ))) } + /// Compute transform batches from per-row joint name and value arrays. + #[pyo3(signature = (names, values, *, clamp = false))] + pub fn compute_joint_transform_batches( + &self, + py: Python<'_>, + names: PyArrowType, + values: PyArrowType, + clamp: bool, + ) -> PyResult> { + let names = make_array(names.0); + let values = make_array(values.0); + + let names = names.as_any().downcast_ref::().ok_or_else(|| { + PyValueError::new_err(format!( + "joint names must be a list array, got {:?}", + names.data_type() + )) + })?; + let values = values.as_any().downcast_ref::().ok_or_else(|| { + PyValueError::new_err(format!( + "joint values must be a list array, got {:?}", + values.data_type() + )) + })?; + + let result = self + .0 + .compute_joint_transform_batches(names, values, clamp) + .map_err(|err| { + if matches!( + err.downcast_ref::(), + Some(urdf_joint_transform::Error::UnsupportedJointType(_)) + ) { + PyNotImplementedError::new_err(err.to_string()) + } else { + PyValueError::new_err(err.to_string()) + } + })?; + + result.to_data().to_pyarrow(py).map(|obj| obj.unbind()) + } + fn __repr__(&self) -> String { format!("UrdfTree(name={:?})", self.0.name()) } } /// Wrapper around a URDF joint. -#[pyclass(name = "_UrdfJointInternal", module = "rerun_bindings.rerun_bindings")] +#[pyclass( + name = "_UrdfJointInternal", + from_py_object, + module = "rerun_bindings.rerun_bindings" +)] #[derive(Clone)] pub struct PyUrdfJoint { pub joint: Joint, @@ -356,7 +404,7 @@ impl PyUrdfJoint { format!( "UrdfJoint(name={:?}, type={}, parent={:?}, child={:?})", self.joint.name, - &self.joint_type(), + self.joint_type(), self.joint.parent.link, self.joint.child.link ) @@ -373,7 +421,11 @@ impl PyUrdfJoint { } /// URDF `` tag: this joint's value is derived from a driver joint. -#[pyclass(name = "_UrdfMimicInternal", module = "rerun_bindings.rerun_bindings")] +#[pyclass( + name = "_UrdfMimicInternal", + from_py_object, + module = "rerun_bindings.rerun_bindings" +)] #[derive(Clone)] pub struct PyUrdfMimic(pub Mimic); @@ -412,7 +464,11 @@ impl PyUrdfMimic { } /// URDF link -#[pyclass(name = "_UrdfLinkInternal", module = "rerun_bindings.rerun_bindings")] +#[pyclass( + name = "_UrdfLinkInternal", + from_py_object, + module = "rerun_bindings.rerun_bindings" +)] #[derive(Clone)] pub struct PyUrdfLink(pub Link); diff --git a/rerun_py/src/utils.rs b/rerun_py/src/utils.rs index 8468ed041ef3..62b34730677f 100644 --- a/rerun_py/src/utils.rs +++ b/rerun_py/src/utils.rs @@ -35,6 +35,11 @@ where use tracing::Instrument as _; let runtime: &Runtime = get_tokio_runtime(); let f = f.in_current_span(); + // Read the active `tracing_session()` id once here (GIL still held) and stash + // it in a tokio task_local for the duration of `f`. Every gRPC injection + // inside `f` then reads the cached value without touching Python. + #[cfg(feature = "perf_telemetry")] + let f = re_perf_telemetry::with_current_tracing_session(f); py.detach(|| runtime.block_on(f)) } @@ -50,7 +55,6 @@ pub fn py_rerun_warn_cstr(msg: &std::ffi::CStr) -> PyResult<()> { } /// Logs a warning using rerun logging system and issues the warning to python runtime. -#[expect(dead_code)] pub fn py_rerun_warn(msg: &str) -> PyResult<()> { let cmsg = CString::new(msg)?; py_rerun_warn_cstr(&cmsg) diff --git a/rerun_py/src/video.rs b/rerun_py/src/video.rs index e15274c47a24..aae705c01d43 100644 --- a/rerun_py/src/video.rs +++ b/rerun_py/src/video.rs @@ -1,10 +1,54 @@ -use pyo3::exceptions::PyRuntimeError; -use pyo3::{Bound, PyAny, PyResult, pyfunction}; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::types::PyBytes; +use pyo3::{Bound, PyAny, PyResult, Python, pyfunction}; use re_arrow_util::ArrowArrayDowncastRef as _; +use re_sdk_types::components::VideoCodec; use re_video::VideoLoadError; use crate::arrow::array_to_rust; +/// `fourcc` is a `rerun.components.VideoCodec` enum value from Python; +/// reuse the canonical fourcc→codec conversion rather than re-mapping here. +fn codec_from_fourcc(fourcc: u32) -> PyResult { + Ok(VideoCodec::try_from_u32(fourcc) + .ok_or_else(|| { + PyValueError::new_err(format!("Unknown video codec fourcc: {fourcc:#010x}")) + })? + .into()) +} + +/// Detect whether a video sample starts a group of pictures, i.e. is a keyframe. +/// +/// H.264/H.265 samples must be in Annex B format. +/// `codec_fourcc` is a `rerun.components.VideoCodec` enum value. +#[pyfunction] +#[pyo3(signature = (sample, codec_fourcc))] +pub fn video_detect_gop_start(sample: &[u8], codec_fourcc: u32) -> PyResult { + match re_video::detect_gop_start(sample, codec_from_fourcc(codec_fourcc)?) { + Ok(re_video::GopStartDetection::StartOfGop(_)) => Ok(true), + Ok(re_video::GopStartDetection::NotStartOfGop) => Ok(false), + Err(err) => Err(PyValueError::new_err(err.to_string())), + } +} + +/// Convert a length-prefixed (AVCC-style) NAL unit sample to Annex B (start-code-prefixed). +#[pyfunction] +#[pyo3(signature = (sample, length_prefix_size = 4))] +pub fn video_length_prefixed_to_annex_b<'py>( + py: Python<'py>, + sample: &[u8], + length_prefix_size: usize, +) -> PyResult> { + let mut annex_b = Vec::with_capacity(sample.len() + 16); + re_video::write_length_prefixed_nalus_to_annexb_stream( + &mut annex_b, + sample, + length_prefix_size, + ) + .map_err(|err| PyValueError::new_err(err.to_string()))?; + Ok(PyBytes::new(py, &annex_b)) +} + /// Reads the timestamps of all frames in a video asset. /// /// Implementation note: @@ -39,14 +83,11 @@ pub fn asset_video_read_frame_timestamps_nanos( )); }; - Ok(re_video::VideoDataDescription::load_from_bytes( - video_bytes, - media_type, - "AssetVideo", - re_tuid::Tuid::new(), + Ok( + re_video::VideoDataDescription::load_from_bytes(video_bytes, media_type, "AssetVideo") + .map_err(|err| PyRuntimeError::new_err(err.to_string()))? + .frame_timestamps_nanos() + .ok_or_else(|| PyRuntimeError::new_err(VideoLoadError::NoTimescale.to_string()))? + .collect(), ) - .map_err(|err| PyRuntimeError::new_err(err.to_string()))? - .frame_timestamps_nanos() - .ok_or_else(|| PyRuntimeError::new_err(VideoLoadError::NoTimescale.to_string()))? - .collect()) } diff --git a/rerun_py/src/viewer.rs b/rerun_py/src/viewer.rs index e7fb0e0b8502..1d2b59a4b017 100644 --- a/rerun_py/src/viewer.rs +++ b/rerun_py/src/viewer.rs @@ -3,8 +3,9 @@ use arrow::array::RecordBatch; use pyo3::prelude::*; use pyo3::{Bound, PyResult}; -use re_grpc_client::write_table::viewer_client; +use re_grpc_client::write_table::channel; use re_protos::sdk_comms::v1alpha1::message_proxy_service_client::MessageProxyServiceClient; +use re_protos::sdk_comms::v1alpha1::viewer_control_service_client::ViewerControlServiceClient; use crate::catalog::to_py_err; use crate::utils::wait_for_future; @@ -74,13 +75,22 @@ impl PyViewerClientInternal { #[derive(Clone)] pub struct ViewerConnectionHandle { client: MessageProxyServiceClient, + control_client: ViewerControlServiceClient, } impl ViewerConnectionHandle { pub fn new(py: Python<'_>, origin: re_uri::Origin) -> PyResult { - let client = wait_for_future(py, viewer_client(origin.clone())).map_err(to_py_err)?; + let channel = wait_for_future(py, channel(origin.clone())).map_err(to_py_err)?; - Ok(Self { client }) + let client = MessageProxyServiceClient::new(channel.clone()) + .max_decoding_message_size(re_grpc_client::MAX_DECODING_MESSAGE_SIZE); + let control_client = ViewerControlServiceClient::new(channel) + .max_decoding_message_size(re_grpc_client::MAX_DECODING_MESSAGE_SIZE); + + Ok(Self { + client, + control_client, + }) } } @@ -112,11 +122,9 @@ impl ViewerConnectionHandle { ) -> PyResult<()> { wait_for_future( py, - self.client - .save_screenshot(re_protos::sdk_comms::v1alpha1::SaveScreenshotRequest { - view_id, - file_path, - }), + self.control_client.save_screenshot( + re_protos::sdk_comms::v1alpha1::SaveScreenshotRequest { view_id, file_path }, + ), ) .map_err(to_py_err)?; diff --git a/rerun_py/tests/api_sandbox/rerun_draft/catalog.py b/rerun_py/tests/api_sandbox/rerun_draft/catalog.py index 76f9fcf2e4e0..9bedd3d091c5 100644 --- a/rerun_py/tests/api_sandbox/rerun_draft/catalog.py +++ b/rerun_py/tests/api_sandbox/rerun_draft/catalog.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Any from rerun import catalog as _catalog -from typing_extensions import deprecated if TYPE_CHECKING: from collections.abc import Sequence @@ -192,8 +191,8 @@ def segment_table( return self._inner.segment_table(join_meta, join_key) - def manifest(self, include_diagnostic_data: bool = False) -> datafusion.DataFrame: - return self._inner.manifest(include_diagnostic_data=include_diagnostic_data) + def _manifest(self, include_diagnostic_data: bool = False) -> datafusion.DataFrame: + return self._inner._manifest(include_diagnostic_data=include_diagnostic_data) def segment_url( self, @@ -204,9 +203,7 @@ def segment_url( ) -> str: return self._inner.segment_url(segment_id, timeline, start, end) - def register( - self, recording_uri: str | Sequence[str], *, layer_name: str | Sequence[str] = "base" - ) -> RegistrationHandle: + def register(self, recording_uri: list[str], *, layer_name: str | Sequence[str] = "base") -> RegistrationHandle: return self._inner.register(recording_uri, layer_name=layer_name) def register_prefix(self, recordings_prefix: str, layer_name: str | None = None) -> RegistrationHandle: @@ -257,82 +254,6 @@ def get_index_ranges(self) -> datafusion.DataFrame: view = self.filter_contents(["/**"]) return view.get_index_ranges() - @deprecated( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def create_fts_search_index( - self, - *, - column: Any, - time_index: Any, - store_position: bool = False, - base_tokenizer: str = "simple", - ) -> None: - try: - return self._inner.create_fts_search_index( # ty: ignore[deprecated] - column=column, - time_index=time_index, - store_position=store_position, - base_tokenizer=base_tokenizer, - ) - except Exception as err: - raise NotImplementedError( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) from err - - @deprecated( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def create_vector_search_index( - self, - *, - column: Any, - time_index: Any, - target_partition_num_rows: int | None = None, - num_sub_vectors: int = 16, - distance_metric: Any = ..., - ) -> Any: - try: - return self._inner.create_vector_search_index( # ty: ignore[deprecated] - column=column, - time_index=time_index, - target_partition_num_rows=target_partition_num_rows, - num_sub_vectors=num_sub_vectors, - distance_metric=distance_metric, - ) - except Exception as err: - raise NotImplementedError( - "Index creation is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) from err - - def list_search_indexes(self) -> list: - return self._inner.list_search_indexes() - - def delete_search_indexes(self, column: Any) -> list[Any]: - return self._inner.delete_search_indexes(column) - - @deprecated( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def search_fts(self, query: str, column: Any) -> datafusion.DataFrame: - try: - return self._inner.search_fts(query, column) # ty: ignore[deprecated] - except Exception as err: - raise NotImplementedError( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) from err - - @deprecated( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) - def search_vector(self, query: Any, column: Any, top_k: int) -> datafusion.DataFrame: - try: - return self._inner.search_vector(query, column, top_k) # ty: ignore[deprecated] - except Exception as err: - raise NotImplementedError( - "Index search is currently not supported. Contact Rerun if this is a feature you would like us to support." - ) from err - def do_maintenance( self, optimize_indexes: bool = False, @@ -512,4 +433,3 @@ def arrow_schema(self) -> pa.Schema: EntryId = _catalog.EntryId EntryKind = _catalog.EntryKind NotFoundError = _catalog.NotFoundError -VectorDistanceMetric = _catalog.VectorDistanceMetric diff --git a/rerun_py/tests/api_sandbox/test_current/test_catalog_basics.py b/rerun_py/tests/api_sandbox/test_current/test_catalog_basics.py index 9b7d4e64b7d4..333b59890e3b 100644 --- a/rerun_py/tests/api_sandbox/test_current/test_catalog_basics.py +++ b/rerun_py/tests/api_sandbox/test_current/test_catalog_basics.py @@ -46,7 +46,10 @@ def test_catalog_basics(tmp_path: Path) -> None: """) assert str( - df.drop("id", "created_at", "updated_at").filter(col("entry_kind") != 5).sort("name") + df + .drop("id", "created_at", "updated_at") + .filter((col("entry_kind") != 5) & (col("entry_kind") != 6)) + .sort("name") ) == inline_snapshot( """\ ┌────────────────────────────────────────────────┐ diff --git a/rerun_py/tests/api_sandbox/test_current/test_dataframe_api.py b/rerun_py/tests/api_sandbox/test_current/test_dataframe_api.py index eaf484261c89..a77c5d1e21f1 100644 --- a/rerun_py/tests/api_sandbox/test_current/test_dataframe_api.py +++ b/rerun_py/tests/api_sandbox/test_current/test_dataframe_api.py @@ -1,8 +1,12 @@ from __future__ import annotations +import datetime from typing import TYPE_CHECKING +import numpy as np import rerun as rr +from datafusion import col, lit +from datafusion.functions import in_list from inline_snapshot import snapshot as inline_snapshot if TYPE_CHECKING: @@ -59,3 +63,103 @@ def test_dataframe_api_filter_segment_id(simple_dataset_prefix: Path) -> None: /points:Points2D:colors: [[[4278190335,16711935],[4278190847,16712447]]] /points:Points2D:positions: [[[[0,1],[3,4]],[[2,3],[5,6]]]]\ """) + + +# An unknown segment ID must succeed with zero rows along every entry point — +# `filter_segments`, `using_index_values`, and DataFusion `WHERE rerun_segment_id` +# filter pushdown. All three share the same `QueryDatasetRequest.segment_ids` +# field on the wire, so the server cannot tell them apart; behavior is uniform +# by construction. Validating IDs would cost a server roundtrip and turn SQL +# filters into hand-grenades — callers needing typo detection should validate +# client-side. + + +def test_dataframe_api_filter_segments_unknown(simple_dataset_prefix: Path) -> None: + with rr.server.Server(datasets={"ds": simple_dataset_prefix}) as server: + client = server.client() + ds = client.get_dataset(name="ds") + + view = ds.filter_segments(["does_not_exist"]) + table = view.reader(index="timeline").to_arrow_table() + + assert table.num_rows == 0 + + +def test_dataframe_api_using_index_values_unknown(simple_dataset_prefix: Path) -> None: + with rr.server.Server(datasets={"ds": simple_dataset_prefix}) as server: + client = server.client() + ds = client.get_dataset(name="ds") + + table = ds.reader( + index="timeline", + using_index_values={ + "does_not_exist": np.array( + [datetime.datetime(2000, 1, 1, 0, 0, 0)], + dtype="datetime64[ns]", + ), + }, + ).to_arrow_table() + + assert table.num_rows == 0 + + +def test_dataframe_api_filter_unknown_segment_id_pushdown(simple_dataset_prefix: Path) -> None: + with rr.server.Server(datasets={"ds": simple_dataset_prefix}) as server: + client = server.client() + ds = client.get_dataset(name="ds") + + table = ds.reader(index="timeline").filter(col("rerun_segment_id") == "does_not_exist").to_arrow_table() + + assert table.num_rows == 0 + + +# Heterogeneous variants: one known + one unknown segment id. The unknown one +# must contribute zero rows; the result is exactly what the known segment +# would yield on its own. Same three entry points. + + +def test_dataframe_api_filter_segments_mixed_known_unknown(simple_dataset_prefix: Path) -> None: + with rr.server.Server(datasets={"ds": simple_dataset_prefix}) as server: + client = server.client() + ds = client.get_dataset(name="ds") + + table = ds.filter_segments(["simple_recording_0", "does_not_exist"]).reader(index="timeline").to_arrow_table() + + assert table.column("rerun_segment_id").to_pylist() == ["simple_recording_0"] + + +def test_dataframe_api_using_index_values_mixed_known_unknown(simple_dataset_prefix: Path) -> None: + with rr.server.Server(datasets={"ds": simple_dataset_prefix}) as server: + client = server.client() + ds = client.get_dataset(name="ds") + + table = ds.reader( + index="timeline", + using_index_values={ + "simple_recording_0": np.array( + [datetime.datetime(2000, 1, 1, 0, 0, 0)], + dtype="datetime64[ns]", + ), + "does_not_exist": np.array( + [datetime.datetime(2000, 1, 1, 0, 0, 0)], + dtype="datetime64[ns]", + ), + }, + ).to_arrow_table() + + assert table.column("rerun_segment_id").to_pylist() == ["simple_recording_0"] + + +def test_dataframe_api_filter_mixed_segment_id_pushdown(simple_dataset_prefix: Path) -> None: + with rr.server.Server(datasets={"ds": simple_dataset_prefix}) as server: + client = server.client() + ds = client.get_dataset(name="ds") + + table = ( + ds + .reader(index="timeline") + .filter(in_list(col("rerun_segment_id"), [lit("simple_recording_0"), lit("does_not_exist")])) + .to_arrow_table() + ) + + assert table.column("rerun_segment_id").to_pylist() == ["simple_recording_0"] diff --git a/rerun_py/tests/api_sandbox/test_current/test_dataset_basics.py b/rerun_py/tests/api_sandbox/test_current/test_dataset_basics.py index 56577e424766..c643ea737287 100644 --- a/rerun_py/tests/api_sandbox/test_current/test_dataset_basics.py +++ b/rerun_py/tests/api_sandbox/test_current/test_dataset_basics.py @@ -23,10 +23,10 @@ def test_dataset_basics(complex_dataset_prefix: Path) -> None: assert partition_df.schema().to_string(show_field_metadata=False) == inline_snapshot("""\ rerun_segment_id: string not null -rerun_layer_names: list not null - child 0, rerun_layer_names: string not null -rerun_storage_urls: list not null - child 0, rerun_storage_urls: string not null +rerun_layer_names: list not null + child 0, item: string not null +rerun_storage_urls: list not null + child 0, item: string not null rerun_last_updated_at: timestamp[ns] not null rerun_num_chunks: uint64 not null rerun_size_bytes: uint64 not null @@ -46,30 +46,30 @@ def test_dataset_basics(complex_dataset_prefix: Path) -> None: "rerun_size_bytes", ).sort("rerun_segment_id") ) == inline_snapshot("""\ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ METADATA: │ -│ * version: 0.1.3 │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ┌─────────────────────┬────────────────────────────────────────────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┐ │ -│ │ rerun_segment_id ┆ rerun_layer_names ┆ rerun_num_chunks ┆ timeline:end ┆ timeline:start │ │ -│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ -│ │ type: non-null Utf8 ┆ type: non-null List(non-null Utf8, field: 'rerun_layer_names') ┆ type: non-null UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) │ │ -│ │ ┆ ┆ ┆ index: timeline ┆ index: timeline │ │ -│ │ ┆ ┆ ┆ index_kind: timestamp ┆ index_kind: timestamp │ │ -│ │ ┆ ┆ ┆ index_marker: end ┆ index_marker: start │ │ -│ │ ┆ ┆ ┆ kind: index ┆ kind: index │ │ -│ ╞═════════════════════╪════════════════════════════════════════════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╡ │ -│ │ complex_recording_0 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:02 ┆ 2000-01-01T00:00:00 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_1 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:03 ┆ 2000-01-01T00:00:01 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_2 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:04 ┆ 2000-01-01T00:00:02 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_3 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:05 ┆ 2000-01-01T00:00:03 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_4 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:06 ┆ 2000-01-01T00:00:04 │ │ -│ └─────────────────────┴────────────────────────────────────────────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┘ │ -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * version: 0.1.3 │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌─────────────────────┬────────────────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┐ │ +│ │ rerun_segment_id ┆ rerun_layer_names ┆ rerun_num_chunks ┆ timeline:end ┆ timeline:start │ │ +│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ +│ │ type: non-null Utf8 ┆ type: non-null List(non-null Utf8) ┆ type: non-null UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) │ │ +│ │ ┆ ┆ ┆ index: timeline ┆ index: timeline │ │ +│ │ ┆ ┆ ┆ index_kind: timestamp ┆ index_kind: timestamp │ │ +│ │ ┆ ┆ ┆ index_marker: end ┆ index_marker: start │ │ +│ │ ┆ ┆ ┆ kind: index ┆ kind: index │ │ +│ ╞═════════════════════╪════════════════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╡ │ +│ │ complex_recording_0 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:02 ┆ 2000-01-01T00:00:00 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_1 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:03 ┆ 2000-01-01T00:00:01 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_2 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:04 ┆ 2000-01-01T00:00:02 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_3 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:05 ┆ 2000-01-01T00:00:03 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_4 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:06 ┆ 2000-01-01T00:00:04 │ │ +│ └─────────────────────┴────────────────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ """) diff --git a/rerun_py/tests/api_sandbox/test_current/test_polars_interop.py b/rerun_py/tests/api_sandbox/test_current/test_polars_interop.py index 363efdb585df..cec657f8dadc 100644 --- a/rerun_py/tests/api_sandbox/test_current/test_polars_interop.py +++ b/rerun_py/tests/api_sandbox/test_current/test_polars_interop.py @@ -36,7 +36,12 @@ def test_entries_to_polars(tmp_path: Path) -> None: """ ) - df = df.drop(["id", "created_at", "updated_at"]).filter(pl.col("entry_kind") != 5).sort("name") + df = ( + df + .drop(["id", "created_at", "updated_at"]) + .filter((pl.col("entry_kind") != 5) & (pl.col("entry_kind") != 6)) + .sort("name") + ) assert str(df) == inline_snapshot("""\ shape: (3, 2) ┌────────────┬────────────┐ diff --git a/rerun_py/tests/api_sandbox/test_draft/test_dataset_basics.py b/rerun_py/tests/api_sandbox/test_draft/test_dataset_basics.py index 42f3fa99c06b..2a6e1f76f51b 100644 --- a/rerun_py/tests/api_sandbox/test_draft/test_dataset_basics.py +++ b/rerun_py/tests/api_sandbox/test_draft/test_dataset_basics.py @@ -33,10 +33,10 @@ def test_dataset_basics(complex_dataset_prefix: Path) -> None: assert segment_df.schema().to_string(show_field_metadata=False) == inline_snapshot("""\ rerun_segment_id: string not null -rerun_layer_names: list not null - child 0, rerun_layer_names: string not null -rerun_storage_urls: list not null - child 0, rerun_storage_urls: string not null +rerun_layer_names: list not null + child 0, item: string not null +rerun_storage_urls: list not null + child 0, item: string not null rerun_last_updated_at: timestamp[ns] not null rerun_num_chunks: uint64 not null rerun_size_bytes: uint64 not null @@ -59,30 +59,30 @@ def test_dataset_basics(complex_dataset_prefix: Path) -> None: "rerun_size_bytes", ).sort("rerun_segment_id") ) == inline_snapshot("""\ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ METADATA: │ -│ * version: 0.1.3 │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ┌─────────────────────┬────────────────────────────────────────────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┐ │ -│ │ rerun_segment_id ┆ rerun_layer_names ┆ rerun_num_chunks ┆ timeline:end ┆ timeline:start │ │ -│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ -│ │ type: non-null Utf8 ┆ type: non-null List(non-null Utf8, field: 'rerun_layer_names') ┆ type: non-null UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) │ │ -│ │ ┆ ┆ ┆ index: timeline ┆ index: timeline │ │ -│ │ ┆ ┆ ┆ index_kind: timestamp ┆ index_kind: timestamp │ │ -│ │ ┆ ┆ ┆ index_marker: end ┆ index_marker: start │ │ -│ │ ┆ ┆ ┆ kind: index ┆ kind: index │ │ -│ ╞═════════════════════╪════════════════════════════════════════════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╡ │ -│ │ complex_recording_0 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:02 ┆ 2000-01-01T00:00:00 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_1 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:03 ┆ 2000-01-01T00:00:01 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_2 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:04 ┆ 2000-01-01T00:00:02 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_3 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:05 ┆ 2000-01-01T00:00:03 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_4 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:06 ┆ 2000-01-01T00:00:04 │ │ -│ └─────────────────────┴────────────────────────────────────────────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┘ │ -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * version: 0.1.3 │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌─────────────────────┬────────────────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┐ │ +│ │ rerun_segment_id ┆ rerun_layer_names ┆ rerun_num_chunks ┆ timeline:end ┆ timeline:start │ │ +│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ +│ │ type: non-null Utf8 ┆ type: non-null List(non-null Utf8) ┆ type: non-null UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) │ │ +│ │ ┆ ┆ ┆ index: timeline ┆ index: timeline │ │ +│ │ ┆ ┆ ┆ index_kind: timestamp ┆ index_kind: timestamp │ │ +│ │ ┆ ┆ ┆ index_marker: end ┆ index_marker: start │ │ +│ │ ┆ ┆ ┆ kind: index ┆ kind: index │ │ +│ ╞═════════════════════╪════════════════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╡ │ +│ │ complex_recording_0 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:02 ┆ 2000-01-01T00:00:00 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_1 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:03 ┆ 2000-01-01T00:00:01 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_2 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:04 ┆ 2000-01-01T00:00:02 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_3 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:05 ┆ 2000-01-01T00:00:03 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_4 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:06 ┆ 2000-01-01T00:00:04 │ │ +│ └─────────────────────┴────────────────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ """) @@ -93,10 +93,10 @@ def test_dataset_register(rrd_paths: list[Path]) -> None: ds = client.create_dataset("dataset") # Single RRD, default layer name - ds.register(rrd_paths[0].as_uri()).wait() + ds.register([rrd_paths[0].as_uri()]).wait() # Single RRD, override layer name - ds.register(rrd_paths[1].as_uri(), layer_name="extra").wait() + ds.register([rrd_paths[1].as_uri()], layer_name="extra").wait() # Multiple RRDs, multiple layer names ds.register([p.as_uri() for p in rrd_paths[2:4]], layer_name=["fiz", "fuz"]).wait() @@ -107,7 +107,7 @@ def test_dataset_register(rrd_paths: list[Path]) -> None: with pytest.raises(ValueError): ds.register([p.as_uri() for p in rrd_paths], layer_name=["not", "enough"]).wait() - df = ds.manifest().select("rerun_layer_name", "rerun_segment_id").sort("rerun_layer_name", "rerun_segment_id") + df = ds._manifest().select("rerun_layer_name", "rerun_segment_id").sort("rerun_layer_name", "rerun_segment_id") df_schema = df.schema() for batch in df.collect(): assert batch.schema.equals(df_schema, check_metadata=True) @@ -269,19 +269,19 @@ def test_dataset_metadata(complex_dataset_prefix: Path) -> None: def test_manifest_diagnostic_data(complex_dataset_prefix: Path) -> None: - """Test the include_diagnostic_data parameter on manifest().""" + """Test the include_diagnostic_data parameter on _manifest().""" with rr.server.Server() as server: client = server.client() ds = client.create_dataset("dataset") ds.register_prefix(complex_dataset_prefix.as_uri()).wait() # Default: rerun_registration_status column should not be present - manifest = ds.manifest() + manifest = ds._manifest() column_names = [f.name for f in manifest.schema()] assert "rerun_registration_status" not in column_names # With include_diagnostic_data=True: column should be present - manifest_diag = ds.manifest(include_diagnostic_data=True) + manifest_diag = ds._manifest(include_diagnostic_data=True) column_names_diag = [f.name for f in manifest_diag.schema()] assert "rerun_registration_status" in column_names_diag diff --git a/rerun_py/tests/api_sandbox/test_draft/test_dataset_views.py b/rerun_py/tests/api_sandbox/test_draft/test_dataset_views.py index 606cd3736409..0516a0af747d 100644 --- a/rerun_py/tests/api_sandbox/test_draft/test_dataset_views.py +++ b/rerun_py/tests/api_sandbox/test_draft/test_dataset_views.py @@ -22,22 +22,22 @@ def test_dataset_view_filter_segments(complex_dataset: DatasetEntry, complex_met assert batch.schema.equals(df_schema, check_metadata=True) assert segment_stable_snapshot(df) == inline_snapshot("""\ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ METADATA: │ -│ * version: 0.1.3 │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ┌─────────────────────┬────────────────────────────────────────────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┬───────────────┐ │ -│ │ rerun_segment_id ┆ rerun_layer_names ┆ rerun_num_chunks ┆ timeline:end ┆ timeline:start ┆ success │ │ -│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ -│ │ type: non-null Utf8 ┆ type: non-null List(non-null Utf8, field: 'rerun_layer_names') ┆ type: non-null UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) ┆ type: Boolean │ │ -│ │ ┆ ┆ ┆ index: timeline ┆ index: timeline ┆ │ │ -│ │ ┆ ┆ ┆ index_kind: timestamp ┆ index_kind: timestamp ┆ │ │ -│ │ ┆ ┆ ┆ index_marker: end ┆ index_marker: start ┆ │ │ -│ │ ┆ ┆ ┆ kind: index ┆ kind: index ┆ │ │ -│ ╞═════════════════════╪════════════════════════════════════════════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════╡ │ -│ │ complex_recording_2 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:04 ┆ 2000-01-01T00:00:02 ┆ false │ │ -│ └─────────────────────┴────────────────────────────────────────────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┴───────────────┘ │ -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * version: 0.1.3 │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌─────────────────────┬────────────────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┬───────────────┐ │ +│ │ rerun_segment_id ┆ rerun_layer_names ┆ rerun_num_chunks ┆ timeline:end ┆ timeline:start ┆ success │ │ +│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ +│ │ type: non-null Utf8 ┆ type: non-null List(non-null Utf8) ┆ type: non-null UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) ┆ type: Boolean │ │ +│ │ ┆ ┆ ┆ index: timeline ┆ index: timeline ┆ │ │ +│ │ ┆ ┆ ┆ index_kind: timestamp ┆ index_kind: timestamp ┆ │ │ +│ │ ┆ ┆ ┆ index_marker: end ┆ index_marker: start ┆ │ │ +│ │ ┆ ┆ ┆ kind: index ┆ kind: index ┆ │ │ +│ ╞═════════════════════╪════════════════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════╡ │ +│ │ complex_recording_2 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:04 ┆ 2000-01-01T00:00:02 ┆ false │ │ +│ └─────────────────────┴────────────────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┴───────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ """) good_segments = complex_dataset.segment_table(join_meta=complex_metadata).filter(col("success")) @@ -52,24 +52,24 @@ def test_dataset_view_filter_segments(complex_dataset: DatasetEntry, complex_met assert batch.schema.equals(df_schema, check_metadata=True) assert segment_stable_snapshot(df) == inline_snapshot("""\ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ METADATA: │ -│ * version: 0.1.3 │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -│ ┌─────────────────────┬────────────────────────────────────────────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┐ │ -│ │ rerun_segment_id ┆ rerun_layer_names ┆ rerun_num_chunks ┆ timeline:end ┆ timeline:start │ │ -│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ -│ │ type: non-null Utf8 ┆ type: non-null List(non-null Utf8, field: 'rerun_layer_names') ┆ type: non-null UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) │ │ -│ │ ┆ ┆ ┆ index: timeline ┆ index: timeline │ │ -│ │ ┆ ┆ ┆ index_kind: timestamp ┆ index_kind: timestamp │ │ -│ │ ┆ ┆ ┆ index_marker: end ┆ index_marker: start │ │ -│ │ ┆ ┆ ┆ kind: index ┆ kind: index │ │ -│ ╞═════════════════════╪════════════════════════════════════════════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╡ │ -│ │ complex_recording_1 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:03 ┆ 2000-01-01T00:00:01 │ │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ -│ │ complex_recording_3 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:05 ┆ 2000-01-01T00:00:03 │ │ -│ └─────────────────────┴────────────────────────────────────────────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┘ │ -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * version: 0.1.3 │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌─────────────────────┬────────────────────────────────────┬───────────────────────┬───────────────────────┬───────────────────────┐ │ +│ │ rerun_segment_id ┆ rerun_layer_names ┆ rerun_num_chunks ┆ timeline:end ┆ timeline:start │ │ +│ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ +│ │ type: non-null Utf8 ┆ type: non-null List(non-null Utf8) ┆ type: non-null UInt64 ┆ type: Timestamp(ns) ┆ type: Timestamp(ns) │ │ +│ │ ┆ ┆ ┆ index: timeline ┆ index: timeline │ │ +│ │ ┆ ┆ ┆ index_kind: timestamp ┆ index_kind: timestamp │ │ +│ │ ┆ ┆ ┆ index_marker: end ┆ index_marker: start │ │ +│ │ ┆ ┆ ┆ kind: index ┆ kind: index │ │ +│ ╞═════════════════════╪════════════════════════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╡ │ +│ │ complex_recording_1 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:03 ┆ 2000-01-01T00:00:01 │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ complex_recording_3 ┆ [base] ┆ 3 ┆ 2000-01-01T00:00:05 ┆ 2000-01-01T00:00:03 │ │ +│ └─────────────────────┴────────────────────────────────────┴───────────────────────┴───────────────────────┴───────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ """) diff --git a/rerun_py/tests/api_sandbox/test_draft/test_removed_apis.py b/rerun_py/tests/api_sandbox/test_draft/test_removed_apis.py index 3fd02703efd2..f031176418ef 100644 --- a/rerun_py/tests/api_sandbox/test_draft/test_removed_apis.py +++ b/rerun_py/tests/api_sandbox/test_draft/test_removed_apis.py @@ -27,11 +27,17 @@ def test_removed_apis() -> None: assert "register_batch" not in dir(ds) # me - # These were renamed with `_index` -> `_search_index` + # The custom-index / vector-search subsystem was removed entirely. assert "create_fts_index" not in dir(ds) assert "create_vector_index" not in dir(ds) assert "list_indexes" not in dir(ds) assert "delete_indexes" not in dir(ds) + assert "create_fts_search_index" not in dir(ds) + assert "create_vector_search_index" not in dir(ds) + assert "list_search_indexes" not in dir(ds) + assert "delete_search_indexes" not in dir(ds) + assert "search_fts" not in dir(ds) + assert "search_vector" not in dir(ds) # Replaced by a better, simpler API outline in # https://linear.app/rerun/issue/RR-3018/improve-the-dataset-blueprint-apis-in-the-python-sdk diff --git a/rerun_py/tests/assets/hdf5/test_data.h5 b/rerun_py/tests/assets/hdf5/test_data.h5 new file mode 100644 index 000000000000..32480c41c926 --- /dev/null +++ b/rerun_py/tests/assets/hdf5/test_data.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4ac21edbff09592c4d490f951603a4eeaceebe5ec7e3aaf706c8cb157a5c0bc +size 5662 diff --git a/rerun_py/tests/assets/hdf5/test_data_misaligned.h5 b/rerun_py/tests/assets/hdf5/test_data_misaligned.h5 new file mode 100644 index 000000000000..63dee4014ecb --- /dev/null +++ b/rerun_py/tests/assets/hdf5/test_data_misaligned.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77ed9f4d6b3ba6c6fc94d47ad7e15aa9a4d5d2496cafdb7628011fb69208aa9c +size 357 diff --git a/rerun_py/tests/e2e_redap_tests/README.md b/rerun_py/tests/e2e_redap_tests/README.md index 700a4e31a5da..7abb87228e66 100644 --- a/rerun_py/tests/e2e_redap_tests/README.md +++ b/rerun_py/tests/e2e_redap_tests/README.md @@ -38,3 +38,18 @@ pytest -c rerun_py/pyproject.toml rerun_py/tests/e2e_redap_tests -m "not local_o Note: When using `--resource-prefix` with remote storage (s3://, gs://, etc.), local-only tests are automatically skipped. +## CI + +In CI, this suite runs against two server targets: + +| Target | Profile | Triggered on | +| ------------------------ | ------------ | --------------------------- | +| OSS `re_server` (Docker) | `dpf-docker` | Every PR and push to `main` | +| Rerun Hub (cloud stack) | `dpf-stack` | Pushes to `main` only | + +Tests marked `@pytest.mark.local_only` are skipped in both CI profiles (they require writing local `.rrd` files). +Tests marked `@pytest.mark.cloud_only` only run against cloud stacks (`dpf-stack`). + +## Related test suites + +There are more e2e tests in [`re_redap_tests`](../../../crates/store/re_redap_tests/README.md), written in Rust. diff --git a/rerun_py/tests/e2e_redap_tests/__snapshots__/test_entries.ambr b/rerun_py/tests/e2e_redap_tests/__snapshots__/test_entries.ambr index ace34e31c24f..57351b81e7f5 100644 --- a/rerun_py/tests/e2e_redap_tests/__snapshots__/test_entries.ambr +++ b/rerun_py/tests/e2e_redap_tests/__snapshots__/test_entries.ambr @@ -1,20 +1,16 @@ # serializer version: 1 -# name: test_entry_names_with_hidden +# name: test_entries_without_hidden list([ - '__bp_***', 'test_dataset', ]) # --- -# name: test_entry_names_with_hidden.1 +# name: test_entries_without_hidden.1 list([ - '__entries', 'test_table', ]) # --- -# name: test_entry_names_with_hidden.2 +# name: test_entries_without_hidden.2 list([ - '__bp_***', - '__entries', 'test_dataset', 'test_table', ]) @@ -35,39 +31,3 @@ 'test_table', ]) # --- -# name: test_entries_with_hidden - list([ - '__bp_***', - 'test_dataset', - ]) -# --- -# name: test_entries_with_hidden.1 - list([ - '__entries', - 'test_table', - ]) -# --- -# name: test_entries_with_hidden.2 - list([ - '__bp_***', - '__entries', - 'test_dataset', - 'test_table', - ]) -# --- -# name: test_entries_without_hidden - list([ - 'test_dataset', - ]) -# --- -# name: test_entries_without_hidden.1 - list([ - 'test_table', - ]) -# --- -# name: test_entries_without_hidden.2 - list([ - 'test_dataset', - 'test_table', - ]) -# --- diff --git a/rerun_py/tests/e2e_redap_tests/__snapshots__/test_segment_id.ambr b/rerun_py/tests/e2e_redap_tests/__snapshots__/test_segment_id.ambr index c167dfdc983e..ff370d0de2e1 100644 --- a/rerun_py/tests/e2e_redap_tests/__snapshots__/test_segment_id.ambr +++ b/rerun_py/tests/e2e_redap_tests/__snapshots__/test_segment_id.ambr @@ -2,8 +2,8 @@ # name: test_segment_ids pyarrow.Table rerun_segment_id: string not null - rerun_layer_names: list not null - child 0, rerun_layer_names: string not null + rerun_layer_names: list not null + child 0, item: string not null rerun_num_chunks: uint64 not null log_tick:end: int64 log_tick:start: int64 diff --git a/rerun_py/tests/e2e_redap_tests/_helpers.py b/rerun_py/tests/e2e_redap_tests/_helpers.py new file mode 100644 index 000000000000..925993fd77d0 --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/_helpers.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from rerun.catalog import DatasetEntry + + +def redact_segment_url(url: str, dataset: DatasetEntry) -> str: + """Replace the dynamic origin and dataset_id in a segment URL with placeholders.""" + origin = dataset.catalog.url + dataset_id = str(dataset.id) + return url.replace(origin, "").replace(dataset_id, "") diff --git a/rerun_py/tests/e2e_redap_tests/conftest.py b/rerun_py/tests/e2e_redap_tests/conftest.py index cf8d021ecd77..bdf48eceb428 100644 --- a/rerun_py/tests/e2e_redap_tests/conftest.py +++ b/rerun_py/tests/e2e_redap_tests/conftest.py @@ -15,12 +15,14 @@ import pyarrow as pa import pytest +import rerun as rr from rerun.catalog import CatalogClient, TableEntry from rerun.server import Server from syrupy.extensions.amber import AmberSnapshotExtension if TYPE_CHECKING: - from collections.abc import Generator + from collections.abc import Callable, Generator, Sequence + from pathlib import Path from rerun.catalog import DatasetEntry from syrupy import SnapshotAssertion @@ -285,6 +287,46 @@ def entry_factory(catalog_client: CatalogClient, request: pytest.FixtureRequest) factory.cleanup() +@pytest.fixture(scope="function") +def recording_factory(tmp_path: Path) -> Callable[[Sequence[str]], list[str]]: + def create_recordings(recording_ids: Sequence[str]) -> list[str]: + uris = [] + for i, recording_id in enumerate(recording_ids): + rrd_path = tmp_path / f"recording_{i}.rrd" + with rr.RecordingStream(f"test_recording_{i}", recording_id=recording_id) as rec: + # log_tick is opt-in; enable it for a deterministic index column. + rec.set_log_tick_enabled(True) + rec.save(rrd_path) + rec.log("points", rr.Points2D([[i, i]])) + rec.flush() + uris.append(rrd_path.absolute().as_uri()) + return uris + + return create_recordings + + +@pytest.fixture(scope="function") +def static_recording_factory(tmp_path: Path) -> Callable[[Sequence[str]], list[str]]: + """ + Like `recording_factory`, but logs only static data. + + Asset datasets reject temporal chunks, so assets must be registered from static-only recordings. + """ + + def create_recordings(recording_ids: Sequence[str]) -> list[str]: + uris = [] + for i, recording_id in enumerate(recording_ids): + rrd_path = tmp_path / f"static_recording_{i}.rrd" + with rr.RecordingStream(f"test_recording_{i}", recording_id=recording_id) as rec: + rec.save(rrd_path) + rec.log("points", rr.Points2D([[i, i]]), static=True) + rec.flush() + uris.append(rrd_path.absolute().as_uri()) + return uris + + return create_recordings + + @pytest.fixture(scope="session") def readonly_test_dataset(catalog_client: CatalogClient, resource_prefix: str) -> Generator[DatasetEntry, None, None]: """ diff --git a/rerun_py/tests/e2e_redap_tests/resources/.gitattributes b/rerun_py/tests/e2e_redap_tests/resources/.gitattributes index a713caad8497..652a181b7e92 100644 --- a/rerun_py/tests/e2e_redap_tests/resources/.gitattributes +++ b/rerun_py/tests/e2e_redap_tests/resources/.gitattributes @@ -1,4 +1,5 @@ *.rrd filter=lfs diff=lfs merge=lfs -text +*.rbl filter=lfs diff=lfs merge=lfs -text *.lance filter=lfs diff=lfs merge=lfs -text *.txn filter=lfs diff=lfs merge=lfs -text *.manifest filter=lfs diff=lfs merge=lfs -text diff --git a/rerun_py/tests/e2e_redap_tests/resources/README.md b/rerun_py/tests/e2e_redap_tests/resources/README.md index 496f3dcc5996..6188f219e70b 100644 --- a/rerun_py/tests/e2e_redap_tests/resources/README.md +++ b/rerun_py/tests/e2e_redap_tests/resources/README.md @@ -18,6 +18,19 @@ Lance table containing sample data with basic datatypes (int, bool, float). - Used by: `readonly_table_uri` fixture, table read/write tests - Used for testing DataFusion operations and table registration +### `blueprints/` +Static `.rbl` blueprint files for table blueprint tests. + +These files let the table blueprint tests run under non-local profiles such as `dpf-docker`, where resources are accessed through `--resource-prefix` instead of generated locally during the test. +Regenerate them from the repository root with: + +```bash +cd rerun +pixi run uvpy rerun_py/tests/e2e_redap_tests/resources/blueprints/generate_blueprints.py +``` + +Keep the filenames stable and make sure remote test resource mirrors are updated as well. + ## Remote resources @@ -27,4 +40,4 @@ When running tests against remote deployments, use `--resource-prefix` to point pytest … --resource-prefix=s3://bucket/path/to/resources/ ``` -The prefix should point to a directory containing `dataset/` and `simple_datatypes/` subdirectories. +The prefix should point to a directory containing the resource subdirectories used by the selected tests, such as `dataset/`, `simple_datatypes/`, and `blueprints/`. diff --git a/rerun_py/tests/e2e_redap_tests/resources/blueprints/generate_blueprints.py b/rerun_py/tests/e2e_redap_tests/resources/blueprints/generate_blueprints.py new file mode 100644 index 000000000000..f7fe3233af28 --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/resources/blueprints/generate_blueprints.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Regenerate static blueprint resources for E2E redap tests.""" + +from __future__ import annotations + +from pathlib import Path + +import rerun.blueprint as rrb + +BLUEPRINTS = { + "table_blueprint.rbl": [-1, 2], + "table_blueprint2.rbl": [-2, 3], +} + + +def main() -> None: + base = Path(__file__).parent + + for filename, x_range in BLUEPRINTS.items(): + blueprint = rrb.Blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=x_range, y_range=[-1, 2]))) + blueprint.save(f"e2e_{filename}", base / filename) + + +if __name__ == "__main__": + main() diff --git a/rerun_py/tests/e2e_redap_tests/resources/blueprints/table_blueprint.rbl b/rerun_py/tests/e2e_redap_tests/resources/blueprints/table_blueprint.rbl new file mode 100644 index 000000000000..e0c442148524 --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/resources/blueprints/table_blueprint.rbl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9c3b843ec185bfa2765d773d40f0b6b73671fe95708e316af39877e9c3965bb +size 23620 diff --git a/rerun_py/tests/e2e_redap_tests/resources/blueprints/table_blueprint2.rbl b/rerun_py/tests/e2e_redap_tests/resources/blueprints/table_blueprint2.rbl new file mode 100644 index 000000000000..db2521cf5448 --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/resources/blueprints/table_blueprint2.rbl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d7351146c1be8f3bf4ce92a1544c8be80a335565e924dfa87a544d080f3e183 +size 23638 diff --git a/rerun_py/tests/e2e_redap_tests/test_asset_dataset.py b/rerun_py/tests/e2e_redap_tests/test_asset_dataset.py new file mode 100644 index 000000000000..15612610379c --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/test_asset_dataset.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import uuid +from typing import TYPE_CHECKING + +import pytest +from rerun.catalog import EntryKind, NotFoundError + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from rerun.catalog import CatalogClient + + from e2e_redap_tests.conftest import EntryFactory + + +@pytest.mark.local_only +def test_register_asset_appears_in_asset_dataset( + entry_factory: EntryFactory, + static_recording_factory: Callable[[Sequence[str]], list[str]], +) -> None: + """Registering an asset puts it in the dataset's asset dataset and returns its segment id.""" + recording_id = "registered_asset" + [uri] = static_recording_factory([recording_id]) + + ds = entry_factory.create_dataset("dataset_with_asset") + + asset_dataset = ds.asset_dataset() + assert asset_dataset is not None + assert asset_dataset.segment_ids() == [] + + segment_id = ds.register_asset(uri) + + assert segment_id == recording_id + assert asset_dataset.segment_ids() == [recording_id] + + +@pytest.mark.local_only +def test_register_asset_replaces_duplicate( + entry_factory: EntryFactory, + static_recording_factory: Callable[[Sequence[str]], list[str]], +) -> None: + """Registering the same asset twice replaces it instead of raising.""" + recording_id = "duplicate_asset" + uris = static_recording_factory([recording_id, recording_id]) + + ds = entry_factory.create_dataset("dataset_with_replaced_asset") + + assert ds.register_asset(uris[0]) == recording_id + assert ds.register_asset(uris[1]) == recording_id + + asset_dataset = ds.asset_dataset() + assert asset_dataset is not None + assert asset_dataset.segment_ids() == [recording_id] + + +@pytest.mark.local_only +def test_unregister_asset_removes_it_from_asset_dataset( + entry_factory: EntryFactory, + static_recording_factory: Callable[[Sequence[str]], list[str]], +) -> None: + """Unregistering an asset removes it from the dataset's asset dataset.""" + recording_id = "asset_to_unregister" + [uri] = static_recording_factory([recording_id]) + + ds = entry_factory.create_dataset("dataset_with_unregistered_asset") + + segment_id = ds.register_asset(uri) + asset_dataset = ds.asset_dataset() + assert asset_dataset is not None + assert asset_dataset.segment_ids() == [recording_id] + + ds.unregister_asset(segment_id) + + assert asset_dataset.segment_ids() == [] + + +@pytest.mark.local_only +def test_unregister_unknown_asset_is_noop(entry_factory: EntryFactory) -> None: + """Unregistering an asset that was never registered does nothing.""" + ds = entry_factory.create_dataset("dataset_with_noop_unregister") + + ds.unregister_asset("never-registered-segment") + + asset_dataset = ds.asset_dataset() + assert asset_dataset is not None + assert asset_dataset.segment_ids() == [] + + +def test_deleting_dataset_deletes_asset_dataset(catalog_client: CatalogClient) -> None: + """Creating a dataset creates an asset dataset of the right kind, and deleting the dataset deletes it too.""" + dataset_name = f"dataset_with_asset_{uuid.uuid4().hex}" + dataset = catalog_client.create_dataset(dataset_name) + deleted = False + + try: + asset_dataset = dataset.asset_dataset() + assert asset_dataset is not None + assert asset_dataset.kind == EntryKind.ASSET_DATASET + asset_dataset_id = asset_dataset.id + + dataset.delete() + deleted = True + + with pytest.raises(NotFoundError): + catalog_client.get_dataset(id=asset_dataset_id) + assert all(entry.id != asset_dataset_id for entry in catalog_client.entries(include_hidden=True)) + finally: + if not deleted: + dataset.delete() diff --git a/rerun_py/tests/e2e_redap_tests/test_blueprint_dataset.py b/rerun_py/tests/e2e_redap_tests/test_blueprint_dataset.py index 968390a48272..888d3b6d0153 100644 --- a/rerun_py/tests/e2e_redap_tests/test_blueprint_dataset.py +++ b/rerun_py/tests/e2e_redap_tests/test_blueprint_dataset.py @@ -2,59 +2,30 @@ from typing import TYPE_CHECKING +import pyarrow as pa import pytest -import rerun as rr -import rerun.blueprint as rrb if TYPE_CHECKING: - from pathlib import Path - from e2e_redap_tests.conftest import EntryFactory -@pytest.mark.local_only -def test_configure_blueprint_dataset(entry_factory: EntryFactory, tmp_path: Path) -> None: - """ - Test configuring a blueprint dataset. - - This test is marked as local_only because it uses RecordingStream to generate - .rrd and .rbl files on-the-fly, which cannot be used with remote deployments. - """ - # Create a recording and save it to a temporary file - rrd_path = tmp_path / "recording.rrd" - rec = rr.RecordingStream("rerun_example_dataset_blueprint") - rec.save(rrd_path) - rec.log("points", rr.Points2D([[0, 0], [1, 1]])) - rec.flush() - - # Create a blueprint and save it to a temporary file - rbl_path = tmp_path / "blueprint.rbl" - blueprint = rrb.Blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]))) - blueprint.save("rerun_example_dataset_blueprint", rbl_path) - - # Create another blueprint - rbl_path2 = tmp_path / "blueprint2.rbl" - blueprint = rrb.Blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]))) - blueprint.save("rerun_example_dataset_blueprint", rbl_path2) - - # Create a new dataset - ds = entry_factory.create_dataset("my_new_dataset") +def test_configure_blueprint_dataset(entry_factory: EntryFactory, resource_prefix: str) -> None: + """Test configuring a blueprint dataset.""" + rbl_uri = resource_prefix + "blueprints/table_blueprint.rbl" + rbl_uri2 = resource_prefix + "blueprints/table_blueprint2.rbl" - # Register our recording to the dataset - ds.register(rrd_path.absolute().as_uri()).wait() + ds = entry_factory.create_dataset("my_new_dataset") + ds.register_prefix(resource_prefix + "dataset").wait() - # Register our blueprint to the corresponding blueprint dataset bds = ds.blueprint_dataset() assert bds is not None - # Register first blueprint - ds.register_blueprint(rbl_path.absolute().as_uri()) + ds.register_blueprint(rbl_uri) assert len(bds.segment_ids()) == 1 first_blueprint_name = ds.default_blueprint() - # Register the second blueprint - ds.register_blueprint(rbl_path2.absolute().as_uri(), set_default=False) + ds.register_blueprint(rbl_uri2, set_default=False) assert len(bds.segment_ids()) == 2 assert first_blueprint_name == ds.default_blueprint() @@ -66,34 +37,109 @@ def test_configure_blueprint_dataset(entry_factory: EntryFactory, tmp_path: Path assert second_blueprint_name == ds.default_blueprint() -@pytest.mark.local_only -def test_reregister_same_blueprint(entry_factory: EntryFactory, tmp_path: Path) -> None: +def test_reregister_same_blueprint(entry_factory: EntryFactory, resource_prefix: str) -> None: """Re-registering the same blueprint should succeed, not raise AlreadyExistsError (regression test for RR-3904).""" + rbl_uri = resource_prefix + "blueprints/table_blueprint.rbl" - # Create a recording and save it to a temporary file - rrd_path = tmp_path / "recording.rrd" - rec = rr.RecordingStream("rerun_example_dataset_blueprint") - rec.save(rrd_path) - rec.log("points", rr.Points2D([[0, 0], [1, 1]])) - rec.flush() - - # Create a blueprint and save it to a temporary file - rbl_path = tmp_path / "blueprint.rbl" - blueprint = rrb.Blueprint(rrb.Spatial2DView(visual_bounds=rrb.VisualBounds2D(x_range=[-1, 2], y_range=[-1, 2]))) - blueprint.save("rerun_example_dataset_blueprint", rbl_path) - - # Create a new dataset ds = entry_factory.create_dataset("reregister_blueprint_dataset") + ds.register_prefix(resource_prefix + "dataset").wait() - # Register our recording to the dataset - ds.register(rrd_path.absolute().as_uri()).wait() - - # Register the blueprint - ds.register_blueprint(rbl_path.absolute().as_uri()) + ds.register_blueprint(rbl_uri) bds = ds.blueprint_dataset() assert bds is not None assert len(bds.segment_ids()) == 1 # Re-register the exact same blueprint — this should not raise - ds.register_blueprint(rbl_path.absolute().as_uri()) + ds.register_blueprint(rbl_uri) + + +def test_configure_table_blueprint_dataset(entry_factory: EntryFactory, resource_prefix: str) -> None: + """Test configuring a table blueprint dataset.""" + rbl_uri = resource_prefix + "blueprints/table_blueprint.rbl" + rbl_uri2 = resource_prefix + "blueprints/table_blueprint2.rbl" + + table = entry_factory.create_table("table_with_blueprints", pa.schema([pa.field("col", pa.int32())])) + + assert table.blueprint_dataset() is not None + assert table.blueprints() == [] + assert table.default_blueprint() is None + + table.register_blueprint(rbl_uri) + + blueprint_dataset = table.blueprint_dataset() + assert blueprint_dataset is not None + assert len(blueprint_dataset.segment_ids()) == 1 + assert table.blueprints() == blueprint_dataset.segment_ids() + + first_blueprint_name = table.default_blueprint() + assert first_blueprint_name is not None + assert first_blueprint_name in table.blueprints() + + table.register_blueprint(rbl_uri2, set_default=False) + + assert len(table.blueprints()) == 2 + assert table.default_blueprint() == first_blueprint_name + + [second_blueprint_name] = list(set(table.blueprints()) - {first_blueprint_name}) + table.set_default_blueprint(second_blueprint_name) + assert table.default_blueprint() == second_blueprint_name + + table.set_default_blueprint(None) + assert table.default_blueprint() is None + + +def test_table_blueprint_set_default_false_creates_dataset_without_default( + entry_factory: EntryFactory, resource_prefix: str +) -> None: + """Registering the first table blueprint with set_default=False leaves default unset.""" + rbl_uri = resource_prefix + "blueprints/table_blueprint.rbl" + + table = entry_factory.create_table("table_blueprint_set_default_false", pa.schema([pa.field("col", pa.int32())])) + + assert table.blueprint_dataset() is not None + assert table.default_blueprint() is None + + table.register_blueprint(rbl_uri, set_default=False) + + assert table.default_blueprint() is None + blueprint_dataset = table.blueprint_dataset() + assert blueprint_dataset is not None + assert len(blueprint_dataset.segment_ids()) == 1 + assert table.blueprints() == blueprint_dataset.segment_ids() + + +def test_table_default_blueprint_uses_creation_blueprint_dataset(entry_factory: EntryFactory) -> None: + """Setting a table default blueprint uses the dataset created with the table.""" + table = entry_factory.create_table("table_default_with_blueprint_dataset", pa.schema([pa.field("col", pa.int32())])) + + table.set_default_blueprint("missing_blueprint_segment") + + assert table.blueprint_dataset() is not None + assert table.default_blueprint() == "missing_blueprint_segment" + + +def test_table_default_blueprint_rejects_deleted_blueprint_dataset( + entry_factory: EntryFactory, resource_prefix: str +) -> None: + """Setting a table default blueprint should fail if the referenced blueprint dataset is gone.""" + table = entry_factory.create_table("table_deleted_blueprint_dataset", pa.schema([pa.field("col", pa.int32())])) + table.register_blueprint(resource_prefix + "blueprints/table_blueprint.rbl", set_default=False) + + blueprint_dataset = table.blueprint_dataset() + assert blueprint_dataset is not None + blueprint_dataset.delete() + + with pytest.raises(Exception, match="table blueprint dataset does not exist"): + table.set_default_blueprint("missing_blueprint_segment") + + +def test_dataset_default_blueprint_rejects_deleted_blueprint_dataset(entry_factory: EntryFactory) -> None: + """Setting a dataset default blueprint should fail if the referenced blueprint dataset is gone.""" + dataset = entry_factory.create_dataset("dataset_deleted_blueprint_dataset") + blueprint_dataset = dataset.blueprint_dataset() + assert blueprint_dataset is not None + blueprint_dataset.delete() + + with pytest.raises(Exception, match="dataset blueprint dataset does not exist"): + dataset.set_default_blueprint("missing_blueprint_segment") diff --git a/rerun_py/tests/e2e_redap_tests/test_catalog_navigation.py b/rerun_py/tests/e2e_redap_tests/test_catalog_navigation.py new file mode 100644 index 000000000000..7fdc4d9e85c2 --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/test_catalog_navigation.py @@ -0,0 +1,124 @@ +""" +End-to-end behavior of the SDK catalog and its bundled `datafusion.SessionContext`. + +These tests document how table entries on the Rerun server are exposed through SQL and the +DataFusion catalog API. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pyarrow as pa +import pytest + +if TYPE_CHECKING: + from datafusion import SessionContext + + from .conftest import EntryFactory + + +SCHEMA = pa.schema([("id", pa.int64()), ("name", pa.utf8())]) + + +def test_unqualified_name_resolves_in_default_catalog(entry_factory: EntryFactory) -> None: + """ + A bare-named entry is queryable as an unqualified SQL identifier. + + The entry lives in the default catalog (`datafusion`) and the default schema (`public`); + DataFusion fills those in automatically when the user writes `SELECT * FROM my_table`. + """ + table_name = entry_factory.apply_prefix("flat_select") + entry_factory.create_table("flat_select", SCHEMA) + + ctx: SessionContext = entry_factory.client.ctx + result = ctx.sql(f'SELECT COUNT(*) AS n FROM "{table_name}"').to_arrow_table() + assert result.column("n")[0].as_py() == 0 + + +def test_dotted_name_resolves_through_virtual_hierarchy(entry_factory: EntryFactory) -> None: + """ + An entry whose name contains dots is exposed as a multi-part SQL reference. + + The server stores `my_catalog.my_schema.qualified_table` as a single flat name; the SDK + parses it client-side into (catalog, schema, table) so SQL like + `SELECT * FROM my_catalog.my_schema.qualified_table` resolves to the same entry. + """ + full_name = entry_factory.apply_prefix("my_catalog.my_schema.qualified_select") + entry_factory.create_table("my_catalog.my_schema.qualified_select", SCHEMA) + + ctx: SessionContext = entry_factory.client.ctx + quoted = ".".join(f'"{p}"' for p in full_name.split(".")) + result = ctx.sql(f"SELECT COUNT(*) AS n FROM {quoted}").to_arrow_table() + assert result.column("n")[0].as_py() == 0 + + +def test_catalog_schema_table_navigation_returns_provider(entry_factory: EntryFactory) -> None: + """ + `ctx.catalog(c).schema(s).table(t)` returns a DataFusion `TableProvider` for the entry. + + This is the imperative counterpart to the SQL form above: any entry reachable as `c.s.t` + in SQL is also reachable by walking the catalog API. The returned provider exposes the + entry's schema (and would be used to scan it). + """ + full_name = entry_factory.apply_prefix("nav_catalog.nav_schema.nav_table") + catalog, schema, leaf = full_name.split(".") + + entry_factory.create_table("nav_catalog.nav_schema.nav_table", SCHEMA) + + ctx: SessionContext = entry_factory.client.ctx + table_provider = ctx.catalog(catalog).schema(schema).table(leaf) + assert table_provider.schema.remove_metadata() == SCHEMA + + +def test_runtime_created_catalog_is_reachable_without_reconnect(entry_factory: EntryFactory) -> None: + """ + Catalogs introduced after the `CatalogClient` was constructed are reachable immediately. + + The SDK does not bake a fixed catalog list at construction time; creating an entry whose + multi-part name introduces a brand-new catalog (`late_catalog.late_schema.…`) makes that + catalog queryable in the same session without rebuilding the client. + """ + full_name = entry_factory.apply_prefix("late_catalog.late_schema.late_table") + entry_factory.create_table("late_catalog.late_schema.late_table", SCHEMA) + + ctx: SessionContext = entry_factory.client.ctx + quoted = ".".join(f'"{p}"' for p in full_name.split(".")) + result = ctx.sql(f"SELECT COUNT(*) AS n FROM {quoted}").to_arrow_table() + assert result.column("n")[0].as_py() == 0 + + +def test_missing_table_surfaces_error(entry_factory: EntryFactory) -> None: + """ + A SQL reference to a non-existent table errors out rather than hanging or returning empty. + + The exact error type and message are intentionally not asserted: they vary across DataFusion + versions and may surface as either "catalog not found" or "table not found". The contract + documented here is only that *some* error is raised. + """ + ctx: SessionContext = entry_factory.client.ctx + with pytest.raises(Exception): # noqa: B017 — error type may vary across DataFusion versions + ctx.sql(f'SELECT * FROM "{entry_factory.apply_prefix("definitely_not_a_real_table")}"').collect() + + +def test_lazy_catalog_lookups_do_not_appear_in_catalog_names(entry_factory: EntryFactory) -> None: + """ + Looking up a bad catalog name about must not subsequently appear in `ctx.catalog_names()`. + + The SDK's catalog list lazily mints a placeholder for any name the planner asks about (so + that DataFusion can keep walking down to `schema(...).table(...)`, where the real + name-filtered server check happens). Listing operations like `SHOW CATALOGS` and + `INFORMATION_SCHEMA.schemata` should reflect only catalogs the server actually knows about + plus any catalogs the user has explicitly registered, never the lazy probe-cache. + """ + ctx: SessionContext = entry_factory.client.ctx + + phantom = entry_factory.apply_prefix("phantom_catalog") + + # Probe the typo'd name so any lazy cache populates. + _ = ctx.catalog(phantom) + + assert phantom not in ctx.catalog_names(), ( + f"{phantom!r} leaked into catalog_names() after a lazy probe; lazy lookups must not " + f"surface as listable catalogs" + ) diff --git a/rerun_py/tests/e2e_redap_tests/test_datafusion_utils.py b/rerun_py/tests/e2e_redap_tests/test_datafusion_utils.py index 84bdd9e1392a..3f9e467e7f51 100644 --- a/rerun_py/tests/e2e_redap_tests/test_datafusion_utils.py +++ b/rerun_py/tests/e2e_redap_tests/test_datafusion_utils.py @@ -7,17 +7,12 @@ from inline_snapshot import snapshot as inline_snapshot from rerun.utilities.datafusion.functions.url_generation import segment_url +from ._helpers import redact_segment_url + if TYPE_CHECKING: from rerun.catalog import DatasetEntry -def redact_segment_url(url: str, dataset: DatasetEntry) -> str: - """Replace the dynamic origin and dataset_id in a segment URL with placeholders.""" - origin = dataset.catalog.url - dataset_id = str(dataset.id) - return url.replace(origin, "").replace(dataset_id, "") - - def collect_urls(result: list[pa.RecordBatch], dataset: DatasetEntry) -> list[str]: """Extract and redact all URL values from query results.""" urls = [] diff --git a/rerun_py/tests/e2e_redap_tests/test_dataloader_sample_index.py b/rerun_py/tests/e2e_redap_tests/test_dataloader_sample_index.py new file mode 100644 index 000000000000..e86bc0d2c0d6 --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/test_dataloader_sample_index.py @@ -0,0 +1,72 @@ +""" +End-to-end coverage for `SampleIndex.build` across the three timeline kinds. + +Uses the shared `readonly_test_dataset` fixture, which exposes timelines of +each kind on the same recording: + +- `log_tick` (sequence / Int64) +- `log_time` (timestamp / Timestamp(ns)) +- `time_2` (duration / Duration(ns)) +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pytest +from rerun.experimental.dataloader import DataSource, FixedRateSampling, SampleIndex + +if TYPE_CHECKING: + from rerun.catalog import DatasetEntry + + +def test_build_integer_timeline(readonly_test_dataset: DatasetEntry) -> None: + sample_index = SampleIndex.build(DataSource(dataset=readonly_test_dataset), index="log_tick", fields={}) + + assert sample_index.ns_dtype is None + assert sample_index.ns_per_sample is None + assert sample_index.total_samples > 0 + + _segment, value = sample_index.global_to_local(0) + assert isinstance(value, int) + + +def test_build_timestamp_timeline(readonly_test_dataset: DatasetEntry) -> None: + sample_index = SampleIndex.build( + DataSource(dataset=readonly_test_dataset), + index="log_time", + fields={}, + timeline_sampling=FixedRateSampling(rate_hz=1000.0), + ) + + assert sample_index.ns_dtype == "datetime64[ns]" + assert sample_index.is_timestamp + assert sample_index.ns_per_sample == 1_000_000 # 1 ms + assert sample_index.total_samples > 0 + + _segment, value = sample_index.global_to_local(0) + assert isinstance(value, np.datetime64) + + +def test_build_duration_timeline(readonly_test_dataset: DatasetEntry) -> None: + """Regression: duration timelines were routed through the integer path and crashed on `int(Timedelta)`.""" + sample_index = SampleIndex.build( + DataSource(dataset=readonly_test_dataset), + index="time_2", + fields={}, + timeline_sampling=FixedRateSampling(rate_hz=1.0), + ) + + assert sample_index.ns_dtype == "timedelta64[ns]" + assert sample_index.is_duration + assert sample_index.ns_per_sample == 1_000_000_000 # 1 s + assert sample_index.total_samples > 0 + + _segment, value = sample_index.global_to_local(0) + assert isinstance(value, np.timedelta64) + + +def test_build_duration_without_rate_raises(readonly_test_dataset: DatasetEntry) -> None: + with pytest.raises(TypeError, match="duration timeline"): + SampleIndex.build(DataSource(dataset=readonly_test_dataset), index="time_2", fields={}) diff --git a/rerun_py/tests/e2e_redap_tests/test_dataset_delete.py b/rerun_py/tests/e2e_redap_tests/test_dataset_delete.py new file mode 100644 index 000000000000..e465afb942ff --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/test_dataset_delete.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import uuid +from typing import TYPE_CHECKING + +import pytest +from rerun.catalog import NotFoundError + +if TYPE_CHECKING: + from rerun.catalog import CatalogClient + + +def test_delete_dataset_removes_catalog_entry(catalog_client: CatalogClient, resource_prefix: str) -> None: + """Deleting a dataset removes it from catalog lookup and listing.""" + dataset_name = f"test_delete_dataset_{uuid.uuid4().hex}" + dataset = catalog_client.create_dataset(dataset_name) + deleted = False + + try: + handle = dataset.register_prefix(resource_prefix + "dataset") + handle.wait(timeout_secs=50) + assert dataset.segment_ids() + + dataset_id = dataset.id + blueprint_dataset = dataset.blueprint_dataset() + assert blueprint_dataset is not None + blueprint_dataset_id = blueprint_dataset.id + + dataset.delete() + deleted = True + + with pytest.raises(LookupError): + catalog_client.get_dataset(dataset_name) + with pytest.raises(NotFoundError): + catalog_client.get_dataset(id=dataset_id) + with pytest.raises(NotFoundError): + catalog_client.get_dataset(id=blueprint_dataset_id) + + assert dataset_name not in catalog_client.dataset_names() + assert dataset_name not in catalog_client.entry_names() + assert all(entry.id != dataset_id for entry in catalog_client.entries(include_hidden=True)) + assert all(entry.id != blueprint_dataset_id for entry in catalog_client.entries(include_hidden=True)) + finally: + if not deleted: + dataset.delete() diff --git a/rerun_py/tests/e2e_redap_tests/test_dataset_entry.py b/rerun_py/tests/e2e_redap_tests/test_dataset_entry.py new file mode 100644 index 000000000000..cbe2f9056641 --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/test_dataset_entry.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING + +from ._helpers import redact_segment_url + +if TYPE_CHECKING: + from rerun.catalog import DatasetEntry + + +def test_segment_url_with_datetime(readonly_test_dataset: DatasetEntry) -> None: + """Test segment URLs with Python datetime values.""" + + segment_id = sorted(readonly_test_dataset.segment_ids())[0] + url = readonly_test_dataset.segment_url( + segment_id, + "real_time", + datetime.datetime(1970, 1, 1, 0, 0, 1, 234567, tzinfo=datetime.timezone.utc), + datetime.datetime(1970, 1, 1, 0, 0, 2, 345678, tzinfo=datetime.timezone.utc), + ) + + assert redact_segment_url(url, readonly_test_dataset) == ( + "/dataset/?segment_id=141a866deb2d49f69eb3215e8a404ffc" + "#when=real_time@1970-01-01T00:00:01.234567Z&time_selection=" + "real_time@1970-01-01T00:00:01.234567Z..1970-01-01T00:00:02.345678Z" + ) + + +def test_segment_url_with_timedelta(readonly_test_dataset: DatasetEntry) -> None: + """Test segment URLs with Python timedelta values.""" + + segment_id = sorted(readonly_test_dataset.segment_ids())[0] + url = readonly_test_dataset.segment_url( + segment_id, + "sim_time", + datetime.timedelta(seconds=1, milliseconds=96), + datetime.timedelta(seconds=2, milliseconds=97), + ) + + assert redact_segment_url(url, readonly_test_dataset) == ( + "/dataset/?segment_id=141a866deb2d49f69eb3215e8a404ffc" + "#when=sim_time@+1.096s&time_selection=sim_time@+1.096s..+2.097s" + ) + + +def test_segment_url_with_sequence_start_only(readonly_test_dataset: DatasetEntry) -> None: + """Test segment URLs with only a sequence start value.""" + + segment_id = sorted(readonly_test_dataset.segment_ids())[0] + url = readonly_test_dataset.segment_url(segment_id, "step", 42) + + assert redact_segment_url(url, readonly_test_dataset) == ( + "/dataset/?segment_id=141a866deb2d49f69eb3215e8a404ffc#when=step@42" + ) + + +def test_segment_url_with_datetime_start_only(readonly_test_dataset: DatasetEntry) -> None: + """Test segment URLs with only a datetime start value.""" + + segment_id = sorted(readonly_test_dataset.segment_ids())[0] + url = readonly_test_dataset.segment_url( + segment_id, + "real_time", + datetime.datetime(1970, 1, 1, 0, 0, 1, 234567, tzinfo=datetime.timezone.utc), + ) + + assert redact_segment_url(url, readonly_test_dataset) == ( + "/dataset/?segment_id=141a866deb2d49f69eb3215e8a404ffc" + "#when=real_time@1970-01-01T00:00:01.234567Z" + ) + + +def test_segment_url_with_timedelta_start_only(readonly_test_dataset: DatasetEntry) -> None: + """Test segment URLs with only a timedelta start value.""" + + segment_id = sorted(readonly_test_dataset.segment_ids())[0] + url = readonly_test_dataset.segment_url( + segment_id, + "sim_time", + datetime.timedelta(seconds=1, milliseconds=96), + ) + + assert redact_segment_url(url, readonly_test_dataset) == ( + "/dataset/?segment_id=141a866deb2d49f69eb3215e8a404ffc#when=sim_time@+1.096s" + ) diff --git a/rerun_py/tests/e2e_redap_tests/test_dataset_query_filter.py b/rerun_py/tests/e2e_redap_tests/test_dataset_query_filter.py index e5d3e511320d..71d77faa69a7 100644 --- a/rerun_py/tests/e2e_redap_tests/test_dataset_query_filter.py +++ b/rerun_py/tests/e2e_redap_tests/test_dataset_query_filter.py @@ -3,23 +3,41 @@ from typing import TYPE_CHECKING from datafusion import Expr, col, functions as f, lit +from rerun.experimental import query_metrics if TYPE_CHECKING: import pyarrow as pa from rerun.catalog import CatalogClient, DatasetEntry +# Filter classification — encodes which expression kinds we expect the +# server-side pushdown to handle today. Used by the assertions below; update +# alongside any change to `re_datafusion::pushdown_expressions`. +# +# - "pushable": expected to land entirely on the server. `filters_pushed_down >= 1` +# and `filters_applied_client_side == 0`. +# - "non_pushable": expected to land entirely client-side as a `FilterExec`. +# `filters_pushed_down == 0` and `filters_applied_client_side >= 1`. +# - "uncertain": shape that may go either way (e.g. OR combinations, negated +# in_list, negated between). Only assert the universal invariant — at least +# one side fires. +_PUSHABLE = "pushable" +_NON_PUSHABLE = "non_pushable" +_UNCERTAIN = "uncertain" + + def test_df_filters(catalog_client: CatalogClient, readonly_test_dataset: DatasetEntry) -> None: """ - Tests filter pushdown correctness. + Tests filter pushdown correctness *and* that pushdown actually fires. - These tests will verify that our push-down filtering returns the exact same results + These tests verify that our push-down filtering returns the exact same results as without push-down. It does this by first collecting record batches without any filters and turning them into an in-memory table. Then we run the same filters on both the in-memory table and the dataset to demonstrate we get exactly the same results. - This test does *not* guarantee that the push-down filters are being applied in the gRPC - requests. + In addition, each filter is wrapped in a `query_metrics()` scope so we can assert that + the server-side pushdown actually fired (or did not, for known-non-pushable shapes). + Previously this was unverifiable from Python — the docstring used to call out that gap. """ all_segments = ( @@ -41,8 +59,8 @@ def find_time_boundaries(time_index: str, segment: pa.Scalar) -> list[pa.Scalar] num_vals = len(values) return [values[0], values[num_vals // 3], values[2 * num_vals // 3], values[num_vals - 1]] - def generate_tests(time_index: str, segments: list[pa.Scalar]) -> list[Expr]: - """Create a set of filters for testing.""" + def generate_tests(time_index: str, segments: list[pa.Scalar]) -> list[tuple[Expr, str]]: + """Create a set of filters for testing, each labeled with its expected pushdown class.""" seg1_times = find_time_boundaries(time_index, segments[0]) seg2_times = find_time_boundaries(time_index, segments[1]) s1_min = lit(seg1_times[0]) @@ -53,41 +71,62 @@ def generate_tests(time_index: str, segments: list[pa.Scalar]) -> list[Expr]: return [ # Basic comparisons on time only - col(time_index) == s1_lower, - col(time_index) > s1_lower, - col(time_index) >= s1_lower, - col(time_index) < s1_lower, - col(time_index) <= s1_lower, - # Range inclusive - (col(time_index) >= s1_lower) & (col(time_index) <= s1_upper), - col(time_index).between(s1_lower, s1_upper, negated=False), - col(time_index).between(s1_lower, s1_upper, negated=True), + (col(time_index) == s1_lower, _PUSHABLE), + (col(time_index) > s1_lower, _PUSHABLE), + (col(time_index) >= s1_lower, _PUSHABLE), + (col(time_index) < s1_lower, _PUSHABLE), + (col(time_index) <= s1_lower, _PUSHABLE), + # Range inclusive — AND of two pushable filters + ((col(time_index) >= s1_lower) & (col(time_index) <= s1_upper), _PUSHABLE), + (col(time_index).between(s1_lower, s1_upper, negated=False), _PUSHABLE), + # `between(..., negated=True)` lowers to a disjunction; treat as uncertain. + (col(time_index).between(s1_lower, s1_upper, negated=True), _UNCERTAIN), # Range exclusive - (col(time_index) > s1_lower) & (col(time_index) < s1_upper), + ((col(time_index) > s1_lower) & (col(time_index) < s1_upper), _PUSHABLE), # Segment filtering only - col("rerun_segment_id") == segments[0], - col("rerun_segment_id") == segments[1], - f.in_list(col("rerun_segment_id"), [lit(segments[0]), lit(segments[1])], negated=True), - f.in_list(col("rerun_segment_id"), [lit(segments[0]), lit(segments[1])], negated=False), - # Segment + time combinations - (col("rerun_segment_id") == segments[0]) & (col(time_index) == s1_lower), - (col("rerun_segment_id") == segments[0]) & (col(time_index) > s1_lower), - (col("rerun_segment_id") == segments[0]) & (col(time_index) >= s1_lower), - (col("rerun_segment_id") == segments[0]) & (col(time_index) < s1_lower), - (col("rerun_segment_id") == segments[0]) & (col(time_index) <= s1_lower), - (col("rerun_segment_id") == segments[0]) & (col(time_index) >= s1_lower) & (col(time_index) <= s1_upper), - (col("rerun_segment_id") == segments[0]) & (col(time_index) > s1_lower) & (col(time_index) < s1_upper), - # Segment + time combinations with no results - (col("rerun_segment_id") == segments[0]) & (col(time_index) < s1_min), - (col("rerun_segment_id") == segments[0]) & (col(time_index) > s1_max), - # OR combinations - (col("rerun_segment_id") == segments[0]) | (col("rerun_segment_id") == segments[1]), - ((col("rerun_segment_id") == segments[0]) & (col(time_index) > s1_lower)) - | ((col("rerun_segment_id") == segments[1]) & (col(time_index) < s2_lower)), + (col("rerun_segment_id") == segments[0], _PUSHABLE), + (col("rerun_segment_id") == segments[1], _PUSHABLE), + # Negated in_list: not a simple set membership, may not push. + (f.in_list(col("rerun_segment_id"), [lit(segments[0]), lit(segments[1])], negated=True), _UNCERTAIN), + (f.in_list(col("rerun_segment_id"), [lit(segments[0]), lit(segments[1])], negated=False), _PUSHABLE), + # Segment + time AND combinations — each conjunct is pushable. + ((col("rerun_segment_id") == segments[0]) & (col(time_index) == s1_lower), _PUSHABLE), + ((col("rerun_segment_id") == segments[0]) & (col(time_index) > s1_lower), _PUSHABLE), + ((col("rerun_segment_id") == segments[0]) & (col(time_index) >= s1_lower), _PUSHABLE), + ((col("rerun_segment_id") == segments[0]) & (col(time_index) < s1_lower), _PUSHABLE), + ((col("rerun_segment_id") == segments[0]) & (col(time_index) <= s1_lower), _PUSHABLE), + ( + (col("rerun_segment_id") == segments[0]) + & (col(time_index) >= s1_lower) + & (col(time_index) <= s1_upper), + _PUSHABLE, + ), + ( + (col("rerun_segment_id") == segments[0]) & (col(time_index) > s1_lower) & (col(time_index) < s1_upper), + _PUSHABLE, + ), + # Segment + time combinations with no results — still pushable shape. + ((col("rerun_segment_id") == segments[0]) & (col(time_index) < s1_min), _PUSHABLE), + ((col("rerun_segment_id") == segments[0]) & (col(time_index) > s1_max), _PUSHABLE), + # OR combinations — pushdown behavior depends on the optimizer's + # disjunction handling. Don't pin a side. + ((col("rerun_segment_id") == segments[0]) | (col("rerun_segment_id") == segments[1]), _UNCERTAIN), + ( + ((col("rerun_segment_id") == segments[0]) & (col(time_index) > s1_lower)) + | ((col("rerun_segment_id") == segments[1]) & (col(time_index) < s2_lower)), + _UNCERTAIN, + ), # Edge cases - col(time_index) == s1_lower, # Exact match, multiple segments - # Non-parsable cases should have no impact - f.substring(col("rerun_segment_id"), lit(2), lit(3)) == "some_value", + (col(time_index) == s1_lower, _PUSHABLE), # Exact match, multiple segments + # Non-parsable: a `substring()` expression has no analytical + # form the pushdown layer can rewrite into a server request. In + # practice the optimizer applies the filter at a different layer + # (a FilterExec sibling of `SegmentStreamExec`, not on the table + # provider itself), so it shows up as neither pushed nor + # client-side from our counters' perspective. Classify as + # uncertain — the row-correctness check still validates the + # filter actually applies. + (f.substring(col("rerun_segment_id"), lit(2), lit(3)) == "some_value", _UNCERTAIN), ] # Cannot run "time_1" due to https://github.com/apache/datafusion-python/pull/1319 @@ -101,9 +140,44 @@ def generate_tests(time_index: str, segments: list[pa.Scalar]) -> list[Expr]: catalog_client.ctx.register_record_batches(time_idx, [full_data_batches]) full_data = catalog_client.ctx.table(time_idx) - for test_filter in all_tests: - # We must sort to guarantee the output ordering - results = readonly_test_dataset.reader(index=time_idx).filter(test_filter).sort(col("log_time")).collect() + for test_filter, pushdown_class in all_tests: + # We must sort to guarantee the output ordering. Wrap just the + # Rerun-side read in `query_metrics()` so `m.last_query()` + # unambiguously refers to that scan; `full_data` is an in-memory + # DataFusion table and doesn't go through `SegmentStreamExec`. + with query_metrics() as m: + results = ( + readonly_test_dataset.reader(index=time_idx).filter(test_filter).sort(col("log_time")).collect() + ) expected = full_data.filter(test_filter).sort(col("log_time")).collect() assert results == expected + + qm = m.last_query() + assert qm is not None, f"no QueryMetrics captured for filter: {test_filter}" + + # Note: some shapes (notably negated `IN` lists) get rewritten by + # DataFusion's optimizer into a form that bypasses both pushdown + # paths — the filter still applies, but neither counter + # increments. So we *don't* assert a universal `total >= 1` + # invariant; instead we only assert the known-strong cases below. + + if pushdown_class == _PUSHABLE: + assert qm.filters_pushed_down >= 1, ( + f"pushable filter {test_filter} did not push down " + f"(pushed_down={qm.filters_pushed_down}, " + f"client_side={qm.filters_applied_client_side})" + ) + assert qm.filters_applied_client_side == 0, ( + f"pushable filter {test_filter} unexpectedly fell back to client side " + f"(client_side={qm.filters_applied_client_side})" + ) + elif pushdown_class == _NON_PUSHABLE: + assert qm.filters_pushed_down == 0, ( + f"non-pushable filter {test_filter} unexpectedly pushed down (pushed_down={qm.filters_pushed_down})" + ) + assert qm.filters_applied_client_side >= 1, ( + f"non-pushable filter {test_filter} did not register client side " + f"(client_side={qm.filters_applied_client_side})" + ) + # _UNCERTAIN: only the universal invariant above applies. diff --git a/rerun_py/tests/e2e_redap_tests/test_dataset_query_select_pushdown.py b/rerun_py/tests/e2e_redap_tests/test_dataset_query_select_pushdown.py index 79f34de463e7..2af914ad9b8b 100644 --- a/rerun_py/tests/e2e_redap_tests/test_dataset_query_select_pushdown.py +++ b/rerun_py/tests/e2e_redap_tests/test_dataset_query_select_pushdown.py @@ -22,6 +22,7 @@ import pyarrow as pa import pytest from datafusion import col +from rerun.experimental import query_metrics if TYPE_CHECKING: from datafusion import DataFrame @@ -56,42 +57,83 @@ def _full_query(dataset: DatasetEntry, time_idx: str) -> DataFrame: @pytest.mark.parametrize("time_idx", ["time_1", "time_2", "time_3"]) def test_narrowing_drops_all_null_rows_single_entity(readonly_test_dataset: DatasetEntry, time_idx: str) -> None: - """SELECT one entity column — narrowing drops rows where that column would be null.""" - narrowed = ( - readonly_test_dataset - .reader(index=time_idx) - .select("rerun_segment_id", time_idx, OBJ1) - .sort("rerun_segment_id", time_idx) - ) + """ + SELECT one entity column — narrowing drops rows where that column would be null. - expected = ( - _full_query(readonly_test_dataset, time_idx) - .filter(col(OBJ1).is_not_null()) - .select("rerun_segment_id", time_idx, OBJ1) - .sort("rerun_segment_id", time_idx) + Asserts both row-correctness *and* that the narrowing optimization actually fired — + previously the latter was only observable in `EXPLAIN ANALYZE` output. + """ + # Build both queries inside the `query_metrics()` scope: collectors are + # bound at `reader()` time, so readers built outside the scope are not + # captured. Order: narrowed first (via `_materialize`), then the + # `_full_query`-derived expected. + with query_metrics() as m: + narrowed = ( + readonly_test_dataset + .reader(index=time_idx) + .select("rerun_segment_id", time_idx, OBJ1) + .sort("rerun_segment_id", time_idx) + ) + + expected = ( + _full_query(readonly_test_dataset, time_idx) + .filter(col(OBJ1).is_not_null()) + .select("rerun_segment_id", time_idx, OBJ1) + .sort("rerun_segment_id", time_idx) + ) + + narrowed_tbl = _materialize(narrowed) + expected_tbl = _materialize(expected) + + assert narrowed_tbl == expected_tbl + + qs = m.queries + assert len(qs) == 2, f"expected 2 captured queries (narrowed + baseline), got {len(qs)}" + nar, _base = qs + # Core claim: a single-entity SELECT triggers narrowing. This was + # previously unverifiable from Python — only inspectable in the + # `EXPLAIN ANALYZE` output. + # + # We deliberately *don't* compare `nar.query_chunks` to the baseline's: + # the `_full_query` baseline also triggers narrowing (the dataset has + # more entities than its SELECT references), and the OR'd + # `IS NOT NULL` filter it applies pushes server-side, so the two + # ultimately fetch comparable chunk counts. The flag itself is the + # cleanest assertion. + assert nar.entity_path_narrowing_applied is True, ( + f"single-entity SELECT must trigger entity-path narrowing, got {nar}" ) - assert _materialize(narrowed) == _materialize(expected) - @pytest.mark.parametrize("time_idx", ["time_1", "time_2", "time_3"]) def test_narrowing_drops_all_null_rows_two_entities(readonly_test_dataset: DatasetEntry, time_idx: str) -> None: """SELECT two entity columns — narrowing drops rows where both would be null.""" - narrowed = ( - readonly_test_dataset - .reader(index=time_idx) - .select("rerun_segment_id", time_idx, OBJ1, OBJ2) - .sort("rerun_segment_id", time_idx) - ) - - expected = ( - _full_query(readonly_test_dataset, time_idx) - .filter(col(OBJ1).is_not_null() | col(OBJ2).is_not_null()) - .select("rerun_segment_id", time_idx, OBJ1, OBJ2) - .sort("rerun_segment_id", time_idx) - ) - - assert _materialize(narrowed) == _materialize(expected) + with query_metrics() as m: + narrowed = ( + readonly_test_dataset + .reader(index=time_idx) + .select("rerun_segment_id", time_idx, OBJ1, OBJ2) + .sort("rerun_segment_id", time_idx) + ) + + expected = ( + _full_query(readonly_test_dataset, time_idx) + .filter(col(OBJ1).is_not_null() | col(OBJ2).is_not_null()) + .select("rerun_segment_id", time_idx, OBJ1, OBJ2) + .sort("rerun_segment_id", time_idx) + ) + + narrowed_tbl = _materialize(narrowed) + expected_tbl = _materialize(expected) + + assert narrowed_tbl == expected_tbl + + qs = m.queries + assert len(qs) == 2, f"expected 2 captured queries (narrowed + baseline), got {len(qs)}" + nar, _base = qs + # See `test_narrowing_drops_all_null_rows_single_entity` for why we only + # assert the flag here, not chunk-count ordinality. + assert nar.entity_path_narrowing_applied is True @pytest.mark.parametrize("time_idx", ["time_1", "time_2", "time_3"]) @@ -102,29 +144,48 @@ def test_fill_latest_at_disables_narrowing(readonly_test_dataset: DatasetEntry, Under LatestAtGlobal, excluded entities' timestamps would generate rows filled with the latest values, so the optimization must not drop them. We assert the narrowed query's row count equals the baseline's, and the projected /obj1 column matches. - """ - narrowed_fill = ( - readonly_test_dataset - .reader(index=time_idx, fill_latest_at=True) - .select("rerun_segment_id", time_idx, OBJ1) - .sort("rerun_segment_id", time_idx) - ) - - full_fill = ( - readonly_test_dataset - .reader(index=time_idx, fill_latest_at=True) - .select("rerun_segment_id", time_idx, OBJ1, OBJ2, OBJ3) - .sort("rerun_segment_id", time_idx) - ) - expected = full_fill.select("rerun_segment_id", time_idx, OBJ1).sort("rerun_segment_id", time_idx) - - narrowed_table = _materialize(narrowed_fill) - full_table = _materialize(full_fill) + With `query_metrics()` we now also directly verify the gating logic fired — previously + we could only infer it from the row-count parity. + """ + with query_metrics() as m: + narrowed_fill = ( + readonly_test_dataset + .reader(index=time_idx, fill_latest_at=True) + .select("rerun_segment_id", time_idx, OBJ1) + .sort("rerun_segment_id", time_idx) + ) + + full_fill = ( + readonly_test_dataset + .reader(index=time_idx, fill_latest_at=True) + .select("rerun_segment_id", time_idx, OBJ1, OBJ2, OBJ3) + .sort("rerun_segment_id", time_idx) + ) + + narrowed_table = _materialize(narrowed_fill) + full_table = _materialize(full_fill) # Narrowing is skipped → row count matches the unprojected baseline. assert narrowed_table.num_rows == full_table.num_rows - assert narrowed_table == _materialize(expected) + # Project full_table down to the same columns post-hoc rather than running + # another DataFrame query — re-materializing a DataFrame whose provider + # was bound inside the scope would emit a 3rd snapshot to the collector. + assert narrowed_table == full_table.select(["rerun_segment_id", time_idx, OBJ1]) + + qs = m.queries + assert len(qs) == 2, f"expected 2 captured queries, got {len(qs)}" + nar, full = qs + # The whole point of this test: narrowing is gated off by fill_latest_at=True. + assert nar.entity_path_narrowing_applied is False, ( + f"fill_latest_at=True must disable narrowing, but it fired on a single-entity SELECT: {nar}" + ) + assert full.entity_path_narrowing_applied is False + # Both queries fetch the same data when narrowing is gated off. + assert nar.query_chunks == full.query_chunks, ( + f"with narrowing gated off, both queries should fetch the same chunks: " + f"nar={nar.query_chunks} vs full={full.query_chunks}" + ) @pytest.mark.parametrize("time_idx", ["time_1", "time_2", "time_3"]) @@ -195,6 +256,9 @@ def test_filter_on_index_column_does_not_expand_fetch_set(readonly_test_dataset: Time index columns have no entity-path metadata, so referencing one in a filter doesn't add any entity to the projected set. Narrowing still drops all-null-/obj1 rows that pass the time filter. + + With `query_metrics()` we additionally assert the fetch set didn't grow — comparing the + time-filtered narrowed query to a baseline narrowed query without the time filter. """ # Pick a threshold from the dataset itself so the filter is meaningful regardless of which # time index we're parametrized over. @@ -202,24 +266,52 @@ def test_filter_on_index_column_does_not_expand_fetch_set(readonly_test_dataset: values = [v for rb in times for v in rb[0] if v.is_valid] threshold = values[len(values) // 3] - narrowed = ( - readonly_test_dataset - .reader(index=time_idx) - .filter(col(time_idx) > threshold) - .select("rerun_segment_id", time_idx, OBJ1) - .sort("rerun_segment_id", time_idx) + # Build the three readers inside the scope so each gets the active + # collector bound at `reader()` time. + with query_metrics() as m: + narrowed = ( + readonly_test_dataset + .reader(index=time_idx) + .filter(col(time_idx) > threshold) + .select("rerun_segment_id", time_idx, OBJ1) + .sort("rerun_segment_id", time_idx) + ) + + expected = ( + _full_query(readonly_test_dataset, time_idx) + .filter(col(time_idx) > threshold) + .filter(col(OBJ1).is_not_null()) # explicit — narrowing drops these implicitly + .select("rerun_segment_id", time_idx, OBJ1) + .sort("rerun_segment_id", time_idx) + ) + + # Baseline: same narrowed SELECT without the time-index filter. Used to + # check that adding the filter doesn't expand the entity fetch set. + narrowed_no_filter = ( + readonly_test_dataset + .reader(index=time_idx) + .select("rerun_segment_id", time_idx, OBJ1) + .sort("rerun_segment_id", time_idx) + ) + + narrowed_tbl = _materialize(narrowed) + expected_tbl = _materialize(expected) + baseline_tbl = _materialize(narrowed_no_filter) + + assert narrowed_tbl == expected_tbl + # Baseline materialization just keeps things consistent with the + # `query_metrics()` scope; we use its metrics, not its rows. + _ = baseline_tbl + + qs = m.queries + assert len(qs) == 3, f"expected 3 captured queries, got {len(qs)}" + nar, _exp, baseline_qm = qs + assert nar.entity_path_narrowing_applied is True, f"narrowing must fire on single-entity SELECT: {nar}" + assert nar.query_chunks <= baseline_qm.query_chunks, ( + f"time-index filter must NOT expand the fetch set: " + f"with-filter={nar.query_chunks} vs without-filter={baseline_qm.query_chunks}" ) - expected = ( - _full_query(readonly_test_dataset, time_idx) - .filter(col(time_idx) > threshold) - .filter(col(OBJ1).is_not_null()) # explicit — narrowing drops these implicitly - .select("rerun_segment_id", time_idx, OBJ1) - .sort("rerun_segment_id", time_idx) - ) - - assert _materialize(narrowed) == _materialize(expected) - # ----------------------------------------------------------------------------- # Snapshot tests (regression guard for exact output against the committed .rrd fixture). diff --git a/rerun_py/tests/e2e_redap_tests/test_dataset_views.py b/rerun_py/tests/e2e_redap_tests/test_dataset_views.py index f51b0600328e..896a6ad8d5e8 100644 --- a/rerun_py/tests/e2e_redap_tests/test_dataset_views.py +++ b/rerun_py/tests/e2e_redap_tests/test_dataset_views.py @@ -16,7 +16,6 @@ from pathlib import Path import datafusion - from pytest import LogCaptureFixture from rerun.catalog import DatasetEntry, IndexValuesLike from syrupy import SnapshotAssertion @@ -369,8 +368,11 @@ def test_dataframe_api_using_index_values_partial_overlap( def test_dataframe_api_using_index_values_empty( - readonly_test_dataset: DatasetEntry, caplog: LogCaptureFixture, snapshot: SnapshotAssertion + readonly_test_dataset: DatasetEntry, snapshot: SnapshotAssertion ) -> None: + # Unknown segment IDs (e.g. "doesnt_exist") are silently ignored — no warning, + # no error, they just contribute no rows. Same goes for segments whose value + # array is empty. df = readonly_test_dataset.reader( index="time_1", using_index_values={ @@ -396,11 +398,6 @@ def test_dataframe_api_using_index_values_empty( "/text2:TextDocument:text", ) - assert len(caplog.records) == 1 - assert caplog.records[0].msg == inline_snapshot( - "Index values for the following inexistent or filtered segments were ignored: doesnt_exist" - ) - assert str(df) == inline_snapshot("No data to display") assert str(pa.table(df)) == snapshot diff --git a/rerun_py/tests/e2e_redap_tests/test_entries.py b/rerun_py/tests/e2e_redap_tests/test_entries.py index 4b04d5ec87ce..91f69f8515a7 100644 --- a/rerun_py/tests/e2e_redap_tests/test_entries.py +++ b/rerun_py/tests/e2e_redap_tests/test_entries.py @@ -39,33 +39,32 @@ def test_entries_without_hidden(entry_factory: EntryFactory, snapshot: SnapshotA assert new_entries == snapshot -def test_entries_with_hidden(entry_factory: EntryFactory, snapshot_redact_id: SnapshotAssertion) -> None: - """Test that entries(), datasets(), and tables() include hidden entries when include_hidden=True.""" +def test_entries_with_hidden(entry_factory: EntryFactory) -> None: + """ + Test that entries(), datasets(), and tables() reveal more entries when include_hidden=True. + + The exact set of hidden entries (blueprint datasets, system tables, …) is an implementation detail, + so we only assert that the hidden listing is a superset of the visible one — and strictly larger for + datasets/entries, since creating a dataset also creates hidden blueprint datasets. + """ client = entry_factory.client - # Capture entries before creating test entries (with hidden) - datasets_before = {d.name for d in client.datasets(include_hidden=True)} - tables_before = {t.name for t in client.tables(include_hidden=True) if not t.name.startswith("__entries")} - entries_before = {e.name for e in client.entries(include_hidden=True) if not e.name.startswith("__entries")} - # Create test entries entry_factory.create_dataset("test_dataset") entry_factory.create_table("test_table", pa.schema([pa.field("col", pa.int32())])) - # Get entries after with hidden - should include blueprint datasets and system tables - datasets_after = {d.name for d in client.datasets(include_hidden=True)} - tables_after = {t.name for t in client.tables(include_hidden=True)} - entries_after = {e.name for e in client.entries(include_hidden=True)} + # Capture entries creating test entries, both visible-only and with hidden. + datasets = {d.name for d in client.datasets()} + datasets_hidden = {d.name for d in client.datasets(include_hidden=True)} + tables = {t.name for t in client.tables()} + tables_hidden = {t.name for t in client.tables(include_hidden=True)} + entries = {e.name for e in client.entries()} + entries_hidden = {e.name for e in client.entries(include_hidden=True)} - # Diff to find newly created entries (including hidden ones like blueprint datasets) - prefix = entry_factory.prefix - new_datasets = sorted([d.removeprefix(prefix) for d in datasets_after - datasets_before]) - new_tables = sorted([t.removeprefix(prefix) for t in tables_after - tables_before]) - new_entries = sorted([e.removeprefix(prefix) for e in entries_after - entries_before]) - - assert new_datasets == snapshot_redact_id - assert new_tables == snapshot_redact_id - assert new_entries == snapshot_redact_id + # include_hidden reveals everything the visible listing does, plus hidden implementation-detail entries. + assert datasets_hidden >= datasets + assert tables_hidden >= tables + assert entries_hidden >= entries def test_entry_names_without_hidden(entry_factory: EntryFactory, snapshot: SnapshotAssertion) -> None: @@ -97,33 +96,32 @@ def test_entry_names_without_hidden(entry_factory: EntryFactory, snapshot: Snaps assert new_entry_names == snapshot -def test_entry_names_with_hidden(entry_factory: EntryFactory, snapshot_redact_id: SnapshotAssertion) -> None: - """Test that entry_names(), dataset_names(), and table_names() include hidden entries when include_hidden=True.""" - client = entry_factory.client +def test_entry_names_with_hidden(entry_factory: EntryFactory) -> None: + """ + Test that entry_names(), dataset_names(), and table_names() reveal more entries when include_hidden=True. - # Capture names before creating test entries (with hidden) - dataset_names_before = set(client.dataset_names(include_hidden=True)) - table_names_before = {t for t in client.table_names(include_hidden=True) if not t.startswith("__entries")} - entry_names_before = {e for e in client.entry_names(include_hidden=True) if not e.startswith("__entries")} + The exact set of hidden entries (blueprint datasets, system tables, …) is an implementation detail, + so we only assert that the hidden listing is a superset of the visible one — and strictly larger for + datasets/entries, since creating a dataset also creates hidden blueprint datasets. + """ + client = entry_factory.client # Create test entries entry_factory.create_dataset("test_dataset") entry_factory.create_table("test_table", pa.schema([pa.field("col", pa.int32())])) - # Get names after with hidden - should include blueprint datasets and system tables - dataset_names_after = set(client.dataset_names(include_hidden=True)) - table_names_after = set(client.table_names(include_hidden=True)) - entry_names_after = set(client.entry_names(include_hidden=True)) - - # Diff to find newly created entries (including hidden ones like blueprint datasets) - prefix = entry_factory.prefix - new_dataset_names = sorted([d.removeprefix(prefix) for d in dataset_names_after - dataset_names_before]) - new_table_names = sorted([t.removeprefix(prefix) for t in table_names_after - table_names_before]) - new_entry_names = sorted([e.removeprefix(prefix) for e in entry_names_after - entry_names_before]) - - assert new_dataset_names == snapshot_redact_id - assert new_table_names == snapshot_redact_id - assert new_entry_names == snapshot_redact_id + # Capture names creating test entries, both visible-only and with hidden. + dataset_names = set(client.dataset_names()) + dataset_names_hidden = set(client.dataset_names(include_hidden=True)) + table_names = set(client.table_names()) + table_names_hidden = set(client.table_names(include_hidden=True)) + entry_names = set(client.entry_names()) + entry_names_hidden = set(client.entry_names(include_hidden=True)) + + # include_hidden reveals everything the visible listing does, plus hidden implementation-detail entries. + assert dataset_names_hidden >= dataset_names + assert table_names_hidden >= table_names + assert entry_names_hidden >= entry_names def test_entry_eq(entry_factory: EntryFactory) -> None: diff --git a/rerun_py/tests/e2e_redap_tests/test_query_metrics_behaviors.py b/rerun_py/tests/e2e_redap_tests/test_query_metrics_behaviors.py new file mode 100644 index 000000000000..896641b4f77f --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/test_query_metrics_behaviors.py @@ -0,0 +1,196 @@ +""" +End-to-end tests for behaviors that were previously unverifiable from Python. + +Each test exercises a query-planning or execution invariant that was +inaccessible before `rerun.experimental.query_metrics()` — either because the +metric in question wasn't surfaced anywhere Python could read it, or because +the DataFusion FFI bug stripped `df.explain(analyze=True)`'s `metrics=[…]` +block. + +Tests in this file use the same `readonly_test_dataset` fixture as the other +e2e suites; they run against the local OSS catalog by default. +""" + +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING + +import pytest +from datafusion import col, lit +from rerun.experimental import query_metrics + +if TYPE_CHECKING: + from rerun.catalog import DatasetEntry + + +# `time_1` hits the datafusion-python bug noted in other tests in this suite. +_INDEX = "time_2" + + +def test_limit_does_not_propagate_into_server_request(readonly_test_dataset: DatasetEntry) -> None: + """ + Documents that `df.limit(N)` does not shrink the server-side fetch set. + + DataFusion's `LimitExec` operates *downstream* of the IO pipeline — by + the time it has enough rows and drops the upstream stream, many gRPC + requests are already in flight or completed. This test pins that + behavior: the plan-time `query_chunks` and execution-time fetch counters + match a no-limit scan within a factor of one another. If a future + optimization pushes `LIMIT` into the `query_dataset` request, the + assertion direction here would need to flip — at which point this test + becomes the regression guard for that change. + + The intent is documentation. The check is intentionally loose: just + that limit doesn't somehow *zero out* fetches. + """ + with query_metrics() as m: + readonly_test_dataset.reader(index=_INDEX).limit(1).collect() + readonly_test_dataset.reader(index=_INDEX).collect() + + qs = m.queries + assert len(qs) == 2, f"expected 2 captured queries (limited + full), got {len(qs)}" + limited, full = qs + + # Plan-time `query_chunks` is identical: LIMIT doesn't change which + # chunks the planner sees. + assert limited.query_chunks == full.query_chunks, ( + f"LIMIT changed plan-time chunk count (would imply server-side pushdown): " + f"limited={limited.query_chunks} vs full={full.query_chunks}" + ) + # Limit query still fetched non-trivial data; not a no-op. + assert limited.fetch_requests >= 1 + assert limited.fetch_bytes > 0 + assert limited.error_kind is None + assert full.error_kind is None + + +def test_empty_result_filter_still_pushes_down(readonly_test_dataset: DatasetEntry) -> None: + """ + A pushable filter that selects no rows must still register as pushed-down. + + Uses a `time_index > ` filter — pushable in shape + and guaranteed to match no data. Guards against a regression where the + pushdown counter is silently dropped on empty results. + + We pick the threshold above the dataset's actual max so the filter is + well-formed but selects nothing. This also exercises a corner of the + fetch path that bails out early. + """ + # Find the dataset's max time so we can build a filter just past it. + times = readonly_test_dataset.reader(index=_INDEX).select(_INDEX).sort(col(_INDEX)).collect() + values = [v for rb in times for v in rb[0] if v.is_valid] + assert values, f"expected readonly_test_dataset to contain at least one valid {_INDEX} value" + max_time = values[-1] + + with query_metrics() as m: + rbs = readonly_test_dataset.reader(index=_INDEX).filter(col(_INDEX) > lit(max_time)).collect() + + # Sanity: the filter does in fact match nothing. + total_rows = sum(rb.num_rows for rb in rbs) + assert total_rows == 0, f"expected zero rows, got {total_rows}" + + qm = m.last_query() + assert qm is not None + assert qm.filters_pushed_down >= 1, f"empty-result time-index filter must still push down, got: {qm}" + assert qm.filters_applied_client_side == 0, ( + f"a fully-pushed time-index comparison should leave nothing for the client side, got: {qm}" + ) + assert qm.error_kind is None + + +def test_cancellation_mid_stream_still_produces_snapshot(readonly_test_dataset: DatasetEntry) -> None: + """ + If a query's stream is dropped before being fully consumed, the snapshot path still fires. + + The Rust-side `DataframeSegmentStreamInner::Drop` impl is the fallback that + catches this case. Test it from Python by issuing a `limit(1)` query (which + causes the IO loop to short-circuit before fetching all chunks) and + verifying we still receive a `QueryMetrics` record. + + We don't pin specific counter values — only that a snapshot is produced and + looks structurally valid. + """ + with query_metrics() as m: + readonly_test_dataset.reader(index=_INDEX).limit(1).collect() + + qm = m.last_query() + assert qm is not None, "limit(1) query must still produce a QueryMetrics snapshot" + assert qm.query_type, f"snapshot must have a non-empty query_type label, got: {qm.query_type!r}" + assert qm.error_kind is None, f"limit(1) on a healthy query must succeed, got: {qm.error_kind}" + # Some chunks must have been fetched even for limit(1) (we only stop after + # the first batch is ready). + assert qm.fetch_requests >= 1 + + +def test_no_filter_no_pushdown(readonly_test_dataset: DatasetEntry) -> None: + """ + An unfiltered scan must register zero filters on both sides. + + Trivial but useful: catches regressions where the pushdown counter is + incremented spuriously for filterless queries. + """ + with query_metrics() as m: + readonly_test_dataset.reader(index=_INDEX).collect() + + qm = m.last_query() + assert qm is not None + assert qm.filters_pushed_down == 0 + assert qm.filters_applied_client_side == 0 + + +def test_queries_outside_scope_do_not_appear(readonly_test_dataset: DatasetEntry) -> None: + """ + Queries issued before / after the `with` block must not appear in the collector. + + Verifies the registry-based capture is scope-bounded — entering the context + manager doesn't retroactively grab earlier queries, and exiting it stops + capturing. + """ + # A query before the scope — must NOT be captured. + readonly_test_dataset.reader(index=_INDEX).limit(1).collect() + + with query_metrics() as m: + readonly_test_dataset.reader(index=_INDEX).limit(1).collect() + + # A query after the scope — must NOT be captured either. + readonly_test_dataset.reader(index=_INDEX).limit(1).collect() + + qs = m.queries + assert len(qs) == 1, f"only the in-scope query should be captured, got {len(qs)}: {qs}" + + +@pytest.mark.parametrize("time_idx", ["time_2", "time_3"]) +def test_query_metrics_smoke_e2e(readonly_test_dataset: DatasetEntry, time_idx: str) -> None: + """ + End-to-end smoke: every captured field should be structurally valid. + + Validates the round-trip through the Rust→PyO3→Python wrapper for a + realistic-looking query. If any field comes back missing or with a + nonsensical default, this catches it early. + """ + with query_metrics() as m: + readonly_test_dataset.reader(index=time_idx).collect() + + qm = m.last_query() + assert qm is not None + + # Plan-time fields populated. + assert qm.dataset_id # non-empty + assert qm.query_chunks > 0 + assert qm.query_segments > 0 + assert qm.query_layers >= 1 + assert qm.query_columns >= 1 + assert qm.query_entities >= 1 + assert qm.query_bytes > 0 + assert qm.query_type + assert qm.primary_index_name == time_idx + + # Execution-time: positive duration; error fields unset. + assert qm.total_duration >= datetime.timedelta(0) + assert qm.error_kind is None + assert qm.direct_terminal_reason is None + + # Wire counters: at least one transport (gRPC or direct) must have fired. + assert qm.fetch_requests >= 1 + assert qm.fetch_bytes > 0 diff --git a/rerun_py/tests/e2e_redap_tests/test_registration.py b/rerun_py/tests/e2e_redap_tests/test_registration.py index 4f134e77dc78..c92cf571b65f 100644 --- a/rerun_py/tests/e2e_redap_tests/test_registration.py +++ b/rerun_py/tests/e2e_redap_tests/test_registration.py @@ -33,29 +33,6 @@ def temp_empty_directory() -> Iterator[str]: os.rmdir(tmp_dir) -@pytest.fixture(scope="function") -def recording_factory(tmp_path: Path) -> Callable[[Sequence[str]], list[str]]: - """ - Factory fixture for creating test recordings with known recording IDs. - - Returns a callable that takes a sequence of recording IDs and returns the - corresponding file URIs. - """ - - def create_recordings(recording_ids: Sequence[str]) -> list[str]: - uris = [] - for i, recording_id in enumerate(recording_ids): - rrd_path = tmp_path / f"recording_{i}.rrd" - with rr.RecordingStream(f"test_recording_{i}", recording_id=recording_id) as rec: - rec.save(rrd_path) - rec.log("points", rr.Points2D([[i, i]])) - rec.flush() - uris.append(rrd_path.absolute().as_uri()) - return uris - - return create_recordings - - @pytest.mark.local_only def test_registration_invalidargs( catalog_client: CatalogClient, temp_empty_file: str, temp_empty_directory: str @@ -88,7 +65,7 @@ def test_register_single_with_wait( ds = entry_factory.create_dataset("test_register_single") - handle = ds.register(uris[0]) + handle = ds.register([uris[0]]) result = handle.wait() assert len(result.segment_ids) == 1 @@ -106,7 +83,7 @@ def test_register_single_with_iter_results( ds = entry_factory.create_dataset("test_register_iter") - handle = ds.register(uris[0]) + handle = ds.register([uris[0]]) results = list(handle.iter_results()) assert len(results) == 1 @@ -267,7 +244,7 @@ def test_register_with_layer_name( ds = entry_factory.create_dataset("test_layer_name") - handle = ds.register(uris[0], layer_name="custom_layer") + handle = ds.register([uris[0]], layer_name="custom_layer") result = handle.wait() assert len(result.segment_ids) == 1 @@ -408,10 +385,10 @@ def test_failed_registration_not_in_segment_table(entry_factory: EntryFactory, t dataset = entry_factory.create_dataset("test_conflicting_property_schema") - dataset.register(seg_1_path.as_uri()).wait() + dataset.register([seg_1_path.as_uri()]).wait() with pytest.raises(ValueError, match="schema"): - dataset.register(seg_2_path.as_uri()).wait() + dataset.register([seg_2_path.as_uri()]).wait() # Verify it's segment1 (the successful one), not segment2 (the failed one) segment_ids = dataset.segment_ids() @@ -439,11 +416,11 @@ def test_failed_layer_registration_not_in_segment_table(entry_factory: EntryFact dataset = entry_factory.create_dataset("test_failed_layer_not_in_segment_table") # Register base layer - should succeed - dataset.register(base_path.as_uri(), layer_name="base").wait() + dataset.register([base_path.as_uri()], layer_name="base").wait() # Register extra layer with conflicting schema - should fail with pytest.raises(ValueError, match="schema"): - dataset.register(extra_path.as_uri(), layer_name="extra").wait() + dataset.register([extra_path.as_uri()], layer_name="extra").wait() # The segment table should still show the segment (because the base layer succeeded) df = dataset.segment_table() @@ -471,14 +448,14 @@ def test_register_duplicate_error_behavior( ds = entry_factory.create_dataset("test_dup_error") # First registration should succeed - handle = ds.register(uris[0], on_duplicate=OnDuplicateSegmentLayer.ERROR) + handle = ds.register([uris[0]], on_duplicate=OnDuplicateSegmentLayer.ERROR) result = handle.wait() assert len(result.segment_ids) == 1 assert result.segment_ids[0] == recording_id # Second registration of the same segment should fail with pytest.raises(AlreadyExistsError, match="already exists"): - ds.register(uris[0], on_duplicate=OnDuplicateSegmentLayer.ERROR).wait() + ds.register([uris[0]], on_duplicate=OnDuplicateSegmentLayer.ERROR).wait() @pytest.mark.local_only @@ -495,7 +472,7 @@ def test_register_duplicate_ignore_behavior( ds = entry_factory.create_dataset("test_dup_ignore") # First registration - handle = ds.register(uris[0], on_duplicate=OnDuplicateSegmentLayer.SKIP) + handle = ds.register([uris[0]], on_duplicate=OnDuplicateSegmentLayer.SKIP) result = handle.wait() assert len(result.segment_ids) == 1 assert result.segment_ids[0] == recording_id @@ -505,7 +482,7 @@ def test_register_duplicate_ignore_behavior( assert points == [[0.0, 0.0]], f"Expected [[0.0, 0.0]] but got {points}" # Second registration should succeed but not replace the data - handle = ds.register(uris[1], on_duplicate=OnDuplicateSegmentLayer.SKIP) + handle = ds.register([uris[1]], on_duplicate=OnDuplicateSegmentLayer.SKIP) result = handle.wait() # The result still contains the segment_id even though it was skipped assert len(result.segment_ids) == 1 @@ -534,7 +511,7 @@ def test_register_duplicate_replace_behavior( ds = entry_factory.create_dataset("test_dup_replace") # First registration - handle = ds.register(uris[0], on_duplicate=OnDuplicateSegmentLayer.REPLACE) + handle = ds.register([uris[0]], on_duplicate=OnDuplicateSegmentLayer.REPLACE) result = handle.wait() assert len(result.segment_ids) == 1 assert result.segment_ids[0] == recording_id @@ -544,7 +521,7 @@ def test_register_duplicate_replace_behavior( assert points == [[0.0, 0.0]], f"Expected [[0.0, 0.0]] but got {points}" # Second registration should succeed and replace the data - handle = ds.register(uris[1], on_duplicate=OnDuplicateSegmentLayer.REPLACE) + handle = ds.register([uris[1]], on_duplicate=OnDuplicateSegmentLayer.REPLACE) result = handle.wait() assert len(result.segment_ids) == 1 @@ -611,7 +588,7 @@ def test_registration_crossregion(catalog_client: CatalogClient) -> None: @pytest.mark.aws_only def test_registration_footerless(catalog_client: CatalogClient) -> None: - """Tests whether registration of footerless datasets fails as expected on Rerun Cloud.""" + """Tests whether registration of footerless datasets fails as expected on Rerun Hub.""" dataset_url = "s3://rerun-redap-datasets-pdx/test-resources/dataset-footerless/" expected_msg = "try running `rerun rrd migrate`" diff --git a/rerun_py/tests/e2e_redap_tests/test_scan_path_errors.py b/rerun_py/tests/e2e_redap_tests/test_scan_path_errors.py index 45e4d2638009..616e0c3303ba 100644 --- a/rerun_py/tests/e2e_redap_tests/test_scan_path_errors.py +++ b/rerun_py/tests/e2e_redap_tests/test_scan_path_errors.py @@ -61,7 +61,7 @@ def test_datafusion_error_surfaces_to_python_with_trace_id(grpc_method: str, exp client = server.client() ds = client.create_dataset(f"scan_error_test_{grpc_method}") - handle = ds.register(rrd_path.absolute().as_uri()) + handle = ds.register([rrd_path.absolute().as_uri()]) handle.wait(timeout_secs=10) # Verify the normal query path works before injecting the error. diff --git a/rerun_py/tests/e2e_redap_tests/test_segment_store.py b/rerun_py/tests/e2e_redap_tests/test_segment_store.py new file mode 100644 index 000000000000..755bcde79188 --- /dev/null +++ b/rerun_py/tests/e2e_redap_tests/test_segment_store.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from rerun.catalog import NotFoundError +from rerun.experimental import LazyStore, RrdReader + +if TYPE_CHECKING: + from pathlib import Path + + from rerun.catalog import DatasetEntry + + from e2e_redap_tests.conftest import EntryFactory + + +@pytest.fixture(scope="module") +def first_segment_store(readonly_test_dataset: DatasetEntry) -> LazyStore: + """The `LazyStore` for the first segment in [`readonly_test_dataset`][].""" + segment_ids = readonly_test_dataset.segment_ids() + assert len(segment_ids) > 0 + return readonly_test_dataset.segment_store(segment_ids[0]) + + +@pytest.fixture +def single_segment_store(entry_factory: EntryFactory, resource_prefix: str) -> LazyStore: + """A `LazyStore` over a freshly-registered dataset containing exactly one segment.""" + ds = entry_factory.create_dataset("single_segment") + handle = ds.register([resource_prefix + "dataset/file1.rrd"]) + handle.wait(timeout_secs=50) + segment_ids = ds.segment_ids() + assert len(segment_ids) == 1 + return ds.segment_store(segment_ids[0]) + + +def test_segment_store_basic(first_segment_store: LazyStore) -> None: + assert isinstance(first_segment_store, LazyStore) + assert len(first_segment_store) > 0 + paths = first_segment_store.schema().entity_paths() + assert any(p.startswith("/obj") for p in paths), f"got {paths!r}" + + +def test_segment_store_summary_uses_manifest(first_segment_store: LazyStore) -> None: + """`summary()` walks the manifest only — no chunk fetch.""" + summary = first_segment_store.summary() + assert summary + assert "rows=" in summary + + +def test_segment_store_stream_to_chunks(first_segment_store: LazyStore) -> None: + chunks = first_segment_store.stream().to_chunks() + assert len(chunks) > 0 + for chunk in chunks: + assert chunk.num_rows > 0 + + +def test_segment_store_write_rrd_roundtrip(single_segment_store: LazyStore, tmp_path: Path) -> None: + """Round-trip a single segment through `write_rrd`: schema and chunk count are preserved.""" + out = tmp_path / "out.rrd" + + single_segment_store.stream().write_rrd(out, application_id="rerun_example_test", recording_id="rec") + + roundtripped = RrdReader(out).store() + assert roundtripped.schema() == single_segment_store.schema() + assert len(roundtripped) == len(single_segment_store) + + +def test_segment_store_unknown_segment_raises(readonly_test_dataset: DatasetEntry) -> None: + """Unknown segment id surfaces synchronously at construction (eager manifest).""" + with pytest.raises(NotFoundError, match=r"does-not-exist"): + readonly_test_dataset.segment_store("does-not-exist") + + +def test_segment_store_compile_twice_works(first_segment_store: LazyStore) -> None: + """Each `compile()` opens its own FetchChunks; same chunks both times.""" + stream = first_segment_store.stream() + + first = stream.to_chunks() + second = stream.to_chunks() + assert len(first) == len(second) + assert {c.id for c in first} == {c.id for c in second} diff --git a/rerun_py/tests/integration/__snapshots__/test_lazy_chunk_store.ambr b/rerun_py/tests/integration/__snapshots__/test_lazy_chunk_store.ambr new file mode 100644 index 000000000000..173739bf46e3 --- /dev/null +++ b/rerun_py/tests/integration/__snapshots__/test_lazy_chunk_store.ambr @@ -0,0 +1,11 @@ +# serializer version: 1 +# name: test_summary_format + ''' + /__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] + /entity_0 rows=10 static=False timelines=['frame'] cols=['Scalars:scalars', 'frame'] + /entity_1 rows=10 static=False timelines=['frame'] cols=['Scalars:scalars', 'frame'] + /entity_2 rows=10 static=False timelines=['frame'] cols=['Scalars:scalars', 'frame'] + /entity_3 rows=10 static=False timelines=['frame'] cols=['Scalars:scalars', 'frame'] + /entity_4 rows=10 static=False timelines=['frame'] cols=['Scalars:scalars', 'frame'] + ''' +# --- diff --git a/rerun_py/tests/integration/__snapshots__/test_mcap_reader.ambr b/rerun_py/tests/integration/__snapshots__/test_mcap_reader.ambr index 2d5693ec6785..a5a8ae812573 100644 --- a/rerun_py/tests/integration/__snapshots__/test_mcap_reader.ambr +++ b/rerun_py/tests/integration/__snapshots__/test_mcap_reader.ambr @@ -7,16 +7,16 @@ # --- # name: test_load_log ''' - /__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] - /__properties rows=1 static=True timelines=[] cols=['McapStatistics:attachment_count', 'McapStatistics:channel_count', 'McapStatistics:channel_message_counts', 'McapStatistics:chunk_count', 'McapStatistics:message_count', 'McapStatistics:message_end_time', 'McapStatistics:message_start_time', 'McapStatistics:metadata_count', 'McapStatistics:schema_count'] + /__mcap_properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] + /__mcap_properties rows=1 static=True timelines=[] cols=['McapStatistics:attachment_count', 'McapStatistics:channel_count', 'McapStatistics:channel_message_counts', 'McapStatistics:chunk_count', 'McapStatistics:message_count', 'McapStatistics:message_end_time', 'McapStatistics:message_start_time', 'McapStatistics:metadata_count', 'McapStatistics:schema_count'] /text_log rows=1 static=True timelines=[] cols=['McapChannel:id', 'McapChannel:message_encoding', 'McapChannel:metadata', 'McapChannel:topic', 'McapSchema:data', 'McapSchema:encoding', 'McapSchema:id', 'McapSchema:name'] /text_log rows=6 static=False timelines=['message_log_time', 'message_publish_time', 'timestamp'] cols=['TextLog:level', 'TextLog:text', 'message_log_time', 'message_publish_time', 'timestamp'] ''' # --- # name: test_load_point_cloud ''' - /__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] - /__properties rows=1 static=True timelines=[] cols=['McapStatistics:attachment_count', 'McapStatistics:channel_count', 'McapStatistics:channel_message_counts', 'McapStatistics:chunk_count', 'McapStatistics:message_count', 'McapStatistics:message_end_time', 'McapStatistics:message_start_time', 'McapStatistics:metadata_count', 'McapStatistics:schema_count'] + /__mcap_properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] + /__mcap_properties rows=1 static=True timelines=[] cols=['McapStatistics:attachment_count', 'McapStatistics:channel_count', 'McapStatistics:channel_message_counts', 'McapStatistics:chunk_count', 'McapStatistics:message_count', 'McapStatistics:message_end_time', 'McapStatistics:message_start_time', 'McapStatistics:metadata_count', 'McapStatistics:schema_count'] /point_cloud rows=1 static=True timelines=[] cols=['McapChannel:id', 'McapChannel:message_encoding', 'McapChannel:metadata', 'McapChannel:topic', 'McapSchema:data', 'McapSchema:encoding', 'McapSchema:id', 'McapSchema:name'] /point_cloud rows=1 static=False timelines=['message_log_time', 'message_publish_time', 'timestamp'] cols=['CoordinateFrame:frame', 'InstancePoses3D:quaternions', 'InstancePoses3D:translations', 'Points3D:colors', 'Points3D:positions', 'message_log_time', 'message_publish_time', 'timestamp'] /point_cloud_with_pose rows=1 static=True timelines=[] cols=['McapChannel:id', 'McapChannel:message_encoding', 'McapChannel:metadata', 'McapChannel:topic', 'McapSchema:data', 'McapSchema:encoding', 'McapSchema:id', 'McapSchema:name'] diff --git a/rerun_py/tests/integration/test_chunk.py b/rerun_py/tests/integration/test_chunk.py index b1d0e6c69094..9dc3f2f2ed0d 100644 --- a/rerun_py/tests/integration/test_chunk.py +++ b/rerun_py/tests/integration/test_chunk.py @@ -8,40 +8,12 @@ import pytest import rerun as rr from inline_snapshot import snapshot as inline_snapshot -from rerun.experimental import Chunk, Lens, LensOutput, RrdReader, Selector +from rerun.experimental import Chunk, DeriveLens, LazyChunkStream, Lens, MutateLens, RrdReader, Selector if TYPE_CHECKING: from pathlib import Path -# --------------------------------------------------------------------------- -# from_record_batch -# --------------------------------------------------------------------------- - - -def test_chunk_from_record_batch_round_trip(test_rrd_path: Path) -> None: - """to_record_batch() -> from_record_batch() round-trips correctly.""" - chunks = RrdReader(test_rrd_path).stream().to_chunks() - assert len(chunks) > 0 - - for original in chunks: - rb = original.to_record_batch() - restored = Chunk.from_record_batch(rb) - assert restored.entity_path == original.entity_path - assert restored.num_rows == original.num_rows - assert restored.num_columns == original.num_columns - assert restored.is_static == original.is_static - assert sorted(restored.timeline_names) == sorted(original.timeline_names) - - -def test_chunk_from_record_batch_rejects_plain_batch() -> None: - """from_record_batch() raises on a RecordBatch without Rerun metadata.""" - - plain_batch = pa.record_batch({"x": [1, 2, 3]}) - with pytest.raises(ValueError): - Chunk.from_record_batch(plain_batch) - - # --------------------------------------------------------------------------- # from_columns # --------------------------------------------------------------------------- @@ -109,6 +81,37 @@ def test_chunk_from_columns_static() -> None: """) +def test_chunk_format_keeps_rerun_metadata_prefixes() -> None: + """`trim_metadata_keys=False` preserves the `rerun:` / `sorbet:` prefixes on metadata keys.""" + chunk = Chunk.from_columns( + "/test/static", + indexes=[], + columns=rr.Points3D.columns(positions=[[1, 2, 3], [4, 5, 6]]), + ) + assert chunk.format(redact=True, trim_metadata_keys=False) == inline_snapshot("""\ +┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * rerun:entity_path: /test/static │ +│ * rerun:id: [**REDACTED**] │ +│ * sorbet:version: [**REDACTED**] │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌───────────────────────────────────────────────┬─────────────────────────────────────────────────┐ │ +│ │ RowId ┆ Points3D:positions │ │ +│ │ --- ┆ --- │ │ +│ │ type: non-null FixedSizeBinary(16) ┆ type: List(FixedSizeList(3 x non-null Float32)) │ │ +│ │ ARROW:extension:metadata: {"namespace":"row"} ┆ rerun:archetype: Points3D │ │ +│ │ ARROW:extension:name: TUID ┆ rerun:component: Points3D:positions │ │ +│ │ rerun:is_sorted: true ┆ rerun:component_type: Position3D │ │ +│ │ rerun:kind: control ┆ rerun:kind: data │ │ +│ ╞═══════════════════════════════════════════════╪═════════════════════════════════════════════════╡ │ +│ │ row_[**REDACTED**] ┆ [[1.0, 2.0, 3.0]] │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ row_[**REDACTED**] ┆ [[4.0, 5.0, 6.0]] │ │ +│ └───────────────────────────────────────────────┴─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────┘\ +""") + + def test_chunk_from_columns_into_store() -> None: """Chunks built via from_columns can be inserted into a ChunkStore.""" from rerun.experimental import ChunkStore @@ -207,10 +210,7 @@ def test_apply_lenses_field_extraction() -> None: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ """) - lens = Lens( - "Imu:accel", - LensOutput().to_component(rr.Scalars.descriptor_scalars(), ".x"), - ) + lens = DeriveLens("Imu:accel").to_component(rr.Scalars.descriptor_scalars(), ".x") results = chunk.apply_lenses(lens) assert len(results) == 1 @@ -239,6 +239,25 @@ def test_apply_lenses_field_extraction() -> None: """) +def test_apply_lenses_string_prefix_builtin() -> None: + """apply_lenses can use the built-in string_prefix selector function.""" + image_data = pa.StructArray.from_arrays( + [pa.array(["png", "jpeg"], type=pa.string())], + names=["format"], + ) + chunk = Chunk.from_columns( + "/camera", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.DynamicArchetype.columns(archetype="Image", components={"format": image_data}), + ) + + lens = DeriveLens("Image:format").to_component("Image:mime", '.format | string_prefix("image/")') + results = chunk.apply_lenses(lens) + + assert len(results) == 1 + assert results[0].to_record_batch().column("Image:mime").to_pylist() == [["image/png"], ["image/jpeg"]] + + def test_apply_lenses_no_match() -> None: """apply_lenses forwards the original chunk when no lens input component matches.""" chunk = Chunk.from_columns( @@ -247,10 +266,7 @@ def test_apply_lenses_no_match() -> None: columns=rr.Points3D.columns(positions=[[1, 2, 3]]), ) - lens = Lens( - "Nonexistent:foo", - LensOutput().to_component("out:bar", "."), - ) + lens = DeriveLens("Nonexistent:foo").to_component("out:bar", ".") results = chunk.apply_lenses(lens) assert len(results) == 1 assert str(results[0]) == str(chunk) # TODO(ab): we should have Chunk.__eq__ @@ -301,14 +317,11 @@ def test_apply_lenses_multiple_outputs() -> None: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ """) - lens = Lens( - "Imu:accel", - to_entity={ - "/out/x": LensOutput().to_component(rr.Scalars.descriptor_scalars(), ".x"), - "/out/y": LensOutput().to_component(rr.Scalars.descriptor_scalars(), ".y"), - }, - ) - results = chunk.apply_lenses(lens) + lenses = [ + DeriveLens("Imu:accel", output_entity="/out/x").to_component(rr.Scalars.descriptor_scalars(), ".x"), + DeriveLens("Imu:accel", output_entity="/out/y").to_component(rr.Scalars.descriptor_scalars(), ".y"), + ] + results = chunk.apply_lenses(lenses) assert len(results) == 2 @@ -395,14 +408,11 @@ def test_apply_lenses_multiple_outputs_preserves_other_columns() -> None: └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ """) - lens = Lens( - "Imu:accel", - to_entity={ - "/out/x": LensOutput().to_component(rr.Scalars.descriptor_scalars(), ".x"), - "/out/y": LensOutput().to_component(rr.Scalars.descriptor_scalars(), ".y"), - }, - ) - results = chunk.apply_lenses(lens) + lenses = [ + DeriveLens("Imu:accel", output_entity="/out/x").to_component(rr.Scalars.descriptor_scalars(), ".x"), + DeriveLens("Imu:accel", output_entity="/out/y").to_component(rr.Scalars.descriptor_scalars(), ".y"), + ] + results = chunk.apply_lenses(lenses) # The original chunk should not be forwarded as is, so it's id must not be visible here assert chunk.id not in {r.id for r in results} @@ -470,6 +480,183 @@ def test_apply_lenses_multiple_outputs_preserves_other_columns() -> None: ]) +def test_apply_lenses_combined_mutate_derive_and_derive_to_entity() -> None: + """Combining MutateLens, DeriveLens, and DeriveLens(output_entity=\u2026) in one call.""" + data = pa.StructArray.from_arrays( + [pa.array([1.0, 2.0], type=pa.float64()), pa.array([3.0, 4.0], type=pa.float64())], + names=["x", "y"], + ) + chunk = Chunk.from_columns( + "/sensor", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.DynamicArchetype.columns(archetype="Imu", components={"accel": data}), + ) + + lenses: list[Lens] = [ + # Mutate the original component in-place (extracts .x, replacing the struct) + MutateLens("Imu:accel", ".x"), + # Derive .y as a Scalar at the same entity + DeriveLens("Imu:accel").to_component(rr.Scalars.descriptor_scalars(), ".y"), + # Derive .x as a Scalar at a different entity + DeriveLens("Imu:accel", output_entity="/derived").to_component(rr.Scalars.descriptor_scalars(), ".x"), + ] + results = chunk.apply_lenses(lenses) + + assert chunk.id not in {r.id for r in results} + assert [r.format(redact=True) for r in results] == inline_snapshot([ + """\ +┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * entity_path: /sensor │ +│ * id: [**REDACTED**] │ +│ * version: [**REDACTED**] │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌───────────────────────────────────────────────┬───────────────────┬──────────────────────┬────────────────────────────┐ │ +│ │ RowId ┆ frame ┆ Imu:accel ┆ Scalars:scalars │ │ +│ │ --- ┆ --- ┆ --- ┆ --- │ │ +│ │ type: non-null FixedSizeBinary(16) ┆ type: Int64 ┆ type: List(Float64) ┆ type: List(Float64) │ │ +│ │ ARROW:extension:metadata: {"namespace":"row"} ┆ index_name: frame ┆ archetype: Imu ┆ archetype: Scalars │ │ +│ │ ARROW:extension:name: TUID ┆ is_sorted: true ┆ component: Imu:accel ┆ component: Scalars:scalars │ │ +│ │ is_sorted: true ┆ kind: index ┆ kind: data ┆ component_type: Scalar │ │ +│ │ kind: control ┆ ┆ ┆ kind: data │ │ +│ ╞═══════════════════════════════════════════════╪═══════════════════╪══════════════════════╪════════════════════════════╡ │ +│ │ row_[**REDACTED**] ┆ 0 ┆ [1.0] ┆ [3.0] │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ row_[**REDACTED**] ┆ 1 ┆ [2.0] ┆ [4.0] │ │ +│ └───────────────────────────────────────────────┴───────────────────┴──────────────────────┴────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ +""", + """\ +┌────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * entity_path: /derived │ +│ * id: [**REDACTED**] │ +│ * version: [**REDACTED**] │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌───────────────────────────────────────────────┬───────────────────┬────────────────────────────┐ │ +│ │ RowId ┆ frame ┆ Scalars:scalars │ │ +│ │ --- ┆ --- ┆ --- │ │ +│ │ type: non-null FixedSizeBinary(16) ┆ type: Int64 ┆ type: List(Float64) │ │ +│ │ ARROW:extension:metadata: {"namespace":"row"} ┆ index_name: frame ┆ archetype: Scalars │ │ +│ │ ARROW:extension:name: TUID ┆ is_sorted: true ┆ component: Scalars:scalars │ │ +│ │ is_sorted: true ┆ kind: index ┆ component_type: Scalar │ │ +│ │ kind: control ┆ ┆ kind: data │ │ +│ ╞═══════════════════════════════════════════════╪═══════════════════╪════════════════════════════╡ │ +│ │ row_[**REDACTED**] ┆ 0 ┆ [1.0] │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ row_[**REDACTED**] ┆ 1 ┆ [2.0] │ │ +│ └───────────────────────────────────────────────┴───────────────────┴────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────────────────────────────────────────┘\ +""", + ]) + + +def test_apply_lenses_mutate_same_column_collision() -> None: + """Two MutateLens on the same column raises an error.""" + data = pa.StructArray.from_arrays( + [pa.array([1.0, 2.0], type=pa.float64()), pa.array([3.0, 4.0], type=pa.float64())], + names=["x", "y"], + ) + chunk = Chunk.from_columns( + "/sensor", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.DynamicArchetype.columns(archetype="Imu", components={"accel": data}), + ) + + lenses = [ + MutateLens("Imu:accel", ".x"), # first wins + MutateLens("Imu:accel", ".y"), # collision + ] + with pytest.raises(ValueError, match="collision"): + chunk.apply_lenses(lenses) + + +def _int64_accel_chunk() -> Chunk: + """A chunk with a single Int64-typed `Imu:accel` component column.""" + return Chunk.from_columns( + "/sensor", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.DynamicArchetype.columns(archetype="Imu", components={"accel": pa.array([1, 2], type=pa.int64())}), + ) + + +def _component_value_type(chunks: list[Chunk], column_name: str) -> pa.DataType: + """The element type of the named component column across the produced chunks.""" + for chunk in chunks: + for field in chunk.to_record_batch().schema: + if field.name == column_name: + return field.type.value_type + raise AssertionError(f"column {column_name} not found in produced chunks") + + +def test_apply_lenses_cast_to_auto() -> None: + """`cast_to="auto"` casts the output to the component's canonical type (Scalar -> float64).""" + chunk = _int64_accel_chunk() + lens = DeriveLens("Imu:accel", output_entity="/derived").to_component( + rr.Scalars.descriptor_scalars(), ".", cast_to="auto" + ) + results = chunk.apply_lenses([lens]) + assert _component_value_type(results, "Scalars:scalars") == pa.float64() + + +def test_apply_lenses_cast_to_explicit_type() -> None: + """`cast_to=` casts the output to that explicit type.""" + chunk = _int64_accel_chunk() + lens = DeriveLens("Imu:accel", output_entity="/derived").to_component( + rr.Scalars.descriptor_scalars(), ".", cast_to=pa.float32() + ) + results = chunk.apply_lenses([lens]) + assert _component_value_type(results, "Scalars:scalars") == pa.float32() + + +def test_apply_lenses_no_cast_preserves_type() -> None: + """Without `cast_to`, the produced column is emitted as-is (Int64 here).""" + chunk = _int64_accel_chunk() + lens = DeriveLens("Imu:accel", output_entity="/derived").to_component(rr.Scalars.descriptor_scalars(), ".") + results = chunk.apply_lenses([lens]) + assert _component_value_type(results, "Scalars:scalars") == pa.int64() + + +def test_apply_lenses_derive_same_entity_collision() -> None: + """Two DeriveLens targeting the same output component on the same entity raises an error.""" + data = pa.StructArray.from_arrays( + [pa.array([1.0, 2.0], type=pa.float64()), pa.array([3.0, 4.0], type=pa.float64())], + names=["x", "y"], + ) + chunk = Chunk.from_columns( + "/sensor", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.DynamicArchetype.columns(archetype="Imu", components={"accel": data}), + ) + + lenses = [ + DeriveLens("Imu:accel").to_component("shared", ".x"), # first wins + DeriveLens("Imu:accel").to_component("shared", ".y"), # collision + ] + with pytest.raises(ValueError, match="collision"): + chunk.apply_lenses(lenses) + + +def test_apply_lenses_derive_new_entity_collision() -> None: + """Two DeriveLens targeting the same output component on a new entity raises an error.""" + data = pa.StructArray.from_arrays( + [pa.array([1.0, 2.0], type=pa.float64()), pa.array([3.0, 4.0], type=pa.float64())], + names=["x", "y"], + ) + chunk = Chunk.from_columns( + "/sensor", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.DynamicArchetype.columns(archetype="Imu", components={"accel": data}), + ) + + lenses = [ + DeriveLens("Imu:accel", output_entity="/new").to_component("shared", ".x"), # first wins + DeriveLens("Imu:accel", output_entity="/new").to_component("shared", ".y"), # collision + ] + with pytest.raises(ValueError, match="collision"): + chunk.apply_lenses(lenses) + + def test_apply_lenses_time_extraction() -> None: """apply_lenses can extract a time column from struct data.""" data = pa.StructArray.from_arrays( @@ -508,11 +695,10 @@ def test_apply_lenses_time_extraction() -> None: └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\ """) - lens = Lens( - "Sensor:data", - LensOutput() + lens = ( + DeriveLens("Sensor:data") .to_component(rr.Scalars.descriptor_scalars(), ".value") - .to_timeline("sensor_time", "timestamp_ns", ".ts"), + .to_timeline("sensor_time", "timestamp_ns", ".ts") ) results = chunk.apply_lenses(lens) @@ -580,10 +766,7 @@ def test_apply_lenses_with_pipe() -> None: """) selector = Selector(".x").pipe(lambda arr: pc.multiply(arr, 2.0)) - lens = Lens( - "S:d", - LensOutput().to_component(rr.Scalars.descriptor_scalars(), selector), - ) + lens = DeriveLens("S:d").to_component(rr.Scalars.descriptor_scalars(), selector) results = chunk.apply_lenses(lens) assert len(results) == 1 @@ -695,3 +878,71 @@ def test_apply_selector_component_not_found() -> None: with pytest.raises(ValueError, match="not found"): chunk.apply_selector("nonexistent:component", Selector(".")) + + +# --------------------------------------------------------------------------- +# with_entity_path +# --------------------------------------------------------------------------- + + +def test_with_entity_path_preserves_data() -> None: + """with_entity_path swaps the entity path and assigns a fresh chunk ID while preserving rows and components.""" + chunk = Chunk.from_columns( + "/sensor", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.Points3D.columns(positions=[[1, 2, 3], [4, 5, 6]]), + ) + moved = chunk.with_entity_path("/left/sensor") + + assert moved.entity_path == "/left/sensor" + assert moved.id != chunk.id + assert moved.num_rows == chunk.num_rows + assert moved.num_columns == chunk.num_columns + assert sorted(moved.timeline_names) == sorted(chunk.timeline_names) + + +def _write_simple_rrd(path: Path, app_id: str, recording_id: str, *, send_properties: bool) -> None: + """Write an RRD with a fixed two-entity, two-archetype schema.""" + with rr.RecordingStream(app_id, recording_id=recording_id, send_properties=send_properties) as rec: + rec.save(path) + rec.send_columns( + "/points", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.Points3D.columns(positions=[[1, 2, 3], [4, 5, 6]]), + ) + rec.send_columns( + "/log", + indexes=[rr.TimeColumn("frame", sequence=[0])], + columns=rr.TextLog.columns(text=["hello"]), + ) + + +@pytest.mark.parametrize("send_properties", [False, True]) +def test_merge_two_rrds_with_distinct_entity_path_prefixes(tmp_path: Path, send_properties: bool) -> None: + """ + Merge two RRDs with the same schema, prefixing each side's entity paths uniquely. + + Parametrized over `send_properties` to cover both the clean case (no auto properties chunk) + and the realistic case (recordings with `/__properties` need to be filtered out before + prefixing, so each merged recording keeps a single canonical properties chunk). + """ + a_path = tmp_path / "a.rrd" + b_path = tmp_path / "b.rrd" + _write_simple_rrd(a_path, "merge_test_a", "rec_a", send_properties=send_properties) + _write_simple_rrd(b_path, "merge_test_b", "rec_b", send_properties=send_properties) + + def prefixed(reader: RrdReader, prefix: str) -> LazyChunkStream: + stream = reader.stream() + if send_properties: + # Properties are recording-scope and shouldn't be relocated under a prefix. + stream = stream.drop(content=f"{rr.RECORDING_PROPERTIES_PATH}/**") + return stream.map(lambda c: c.with_entity_path(f"{prefix}{c.entity_path}")) + + left = prefixed(RrdReader(a_path), "/left") + right = prefixed(RrdReader(b_path), "/right") + + merged_path = tmp_path / "merged.rrd" + LazyChunkStream.merge(left, right).write_rrd(merged_path, application_id="merged", recording_id="merged") + + paths = set(RrdReader(merged_path).store().schema().entity_paths()) + assert paths == {"/left/points", "/left/log", "/right/points", "/right/log"} diff --git a/rerun_py/tests/integration/test_chunk_from_record_batch.py b/rerun_py/tests/integration/test_chunk_from_record_batch.py new file mode 100644 index 000000000000..c567ebc743b5 --- /dev/null +++ b/rerun_py/tests/integration/test_chunk_from_record_batch.py @@ -0,0 +1,328 @@ +"""Tests for `Chunk.from_record_batch` and `Chunk.from_dataframe`.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pyarrow as pa +import pytest +import rerun as rr +from rerun import AUTO_INDEX +from rerun.experimental import Chunk, RrdReader + +if TYPE_CHECKING: + from pathlib import Path + +# A simple list-of-floats component column, two rows. +_VALUES = pa.array([[1.0], [2.0]], type=pa.list_(pa.float32())) + +SORBET_INDEX_NAME = b"rerun:index_name" +SORBET_ENTITY_PATH = b"rerun:entity_path" +RERUN_KIND = b"rerun:kind" + + +# --------------------------------------------------------------------------- +# Round-trip / identity +# --------------------------------------------------------------------------- + + +def test_round_trip_preserves_id() -> None: + """A fully-annotated chunk batch round-trips to a single chunk, preserving the chunk id.""" + original = Chunk.from_columns( + "/robots/arm", + indexes=[rr.TimeColumn("frame", sequence=[0, 1, 2])], + columns=rr.Points3D.columns(positions=[[1, 2, 3], [4, 5, 6], [7, 8, 9]]), + ) + rb = original.to_record_batch() + chunks = Chunk.from_record_batch(rb) + assert len(chunks) == 1 + [restored] = chunks + assert restored.id == original.id + assert restored.entity_path == original.entity_path + assert restored.num_rows == original.num_rows + assert restored.num_columns == original.num_columns + assert restored.is_static == original.is_static + assert sorted(restored.timeline_names) == sorted(original.timeline_names) + # The contents (data and schema metadata) round-trip identically. + assert restored.to_record_batch().equals(rb, check_metadata=True) + + +def test_round_trip_from_rrd(test_rrd_path: Path) -> None: + """to_record_batch() -> from_record_batch() round-trips real chunks read from an RRD.""" + chunks = RrdReader(test_rrd_path).stream().to_chunks() + assert len(chunks) > 0 + + for original in chunks: + rb = original.to_record_batch() + # A fully-annotated chunk batch round-trips to a single chunk, preserving identity. + [restored] = Chunk.from_record_batch(rb) + assert restored.id == original.id + assert restored.entity_path == original.entity_path + assert restored.num_rows == original.num_rows + assert restored.num_columns == original.num_columns + assert restored.is_static == original.is_static + assert sorted(restored.timeline_names) == sorted(original.timeline_names) + + +# --------------------------------------------------------------------------- +# `index=` promotion +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "index_type", + [ + pa.int64(), + pa.timestamp("ns"), + pa.duration("ns"), + ], +) +def test_index_promotion_time_types(index_type: pa.DataType) -> None: + """`index=` promotes the named column for each supported time dtype.""" + index = pa.array([0, 1], type=index_type) + schema = pa.schema([ + pa.field("t", index.type), + pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"}), + ]) + rb = pa.RecordBatch.from_arrays([index, _VALUES], schema=schema) + [chunk] = Chunk.from_record_batch(rb, index="t") + assert chunk.entity_path == "/e" + assert chunk.timeline_names == ["t"] + assert not chunk.is_static + + +def test_metadata_driven_temporal() -> None: + """A batch tagged with `kind=index` (no explicit `index=`) is interpreted as temporal.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={RERUN_KIND: b"index"}), + pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"}), + ]) + [chunk] = Chunk.from_record_batch(pa.RecordBatch.from_arrays([index, _VALUES], schema=schema)) + assert chunk.timeline_names == ["frame"] + + +def test_index_name_only_temporal() -> None: + """An `index_name`-only column (no `rerun:kind`) is still promoted under AUTO.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame"}), + pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"}), + ]) + [chunk] = Chunk.from_record_batch(pa.RecordBatch.from_arrays([index, _VALUES], schema=schema)) + assert chunk.timeline_names == ["frame"] + + +# --------------------------------------------------------------------------- +# Entity grouping / name convention +# --------------------------------------------------------------------------- + + +def test_multi_entity_split_preserves_order() -> None: + """Component columns on different entities split into one chunk each, in first-seen order.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={RERUN_KIND: b"index"}), + pa.field("/b:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/b"}), + pa.field("/a:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/a"}), + ]) + rb = pa.RecordBatch.from_arrays([index, _VALUES, _VALUES], schema=schema) + chunks = Chunk.from_record_batch(rb) + assert [c.entity_path for c in chunks] == ["/b", "/a"] + + +@pytest.mark.parametrize( + ("name", "expected_entity"), + [ + ("/e:c", "/e"), + ("/e:Arch:c", "/e"), + ("foo:bar", "/"), # no leading slash → root + ("property:foo", "/"), # not recognized → root + ], +) +def test_name_convention(name: str, expected_entity: str) -> None: + """The column-name convention requires a leading `/`; otherwise the column lands on root.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={RERUN_KIND: b"index"}), + pa.field(name, _VALUES.type), + ]) + [chunk] = Chunk.from_record_batch(pa.RecordBatch.from_arrays([index, _VALUES], schema=schema)) + assert chunk.entity_path == expected_entity + + +def test_entity_path_argument() -> None: + """`entity_path=` is the default for un-located component columns.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={RERUN_KIND: b"index"}), + pa.field("bare", _VALUES.type), + ]) + [chunk] = Chunk.from_record_batch(pa.RecordBatch.from_arrays([index, _VALUES], schema=schema), entity_path="/world") + assert chunk.entity_path == "/world" + + +def test_plain_array_is_list_wrapped() -> None: + """A plain (non-list) component array is wrapped as single-element lists.""" + index = pa.array([0, 1], type=pa.int64()) + plain = pa.array([1.0, 2.0], type=pa.float32()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={RERUN_KIND: b"index"}), + pa.field("/e:c", plain.type, metadata={SORBET_ENTITY_PATH: b"/e"}), + ]) + [chunk] = Chunk.from_record_batch(pa.RecordBatch.from_arrays([index, plain], schema=schema)) + batch = chunk.to_record_batch() + [component_field] = [f for f in batch.schema if f.metadata and f.metadata.get(RERUN_KIND) == b"data"] + assert pa.types.is_list(component_field.type) + + +# --------------------------------------------------------------------------- +# Static +# --------------------------------------------------------------------------- + + +def test_static_single_row() -> None: + """`index=None` with a single row produces a static chunk.""" + one = pa.array([[1.0]], type=pa.list_(pa.float32())) + schema = pa.schema([pa.field("/e:c", one.type, metadata={SORBET_ENTITY_PATH: b"/e"})]) + [chunk] = Chunk.from_record_batch(pa.RecordBatch.from_arrays([one], schema=schema), index=None) + assert chunk.is_static + assert chunk.timeline_names == [] + + +def test_static_with_index_metadata_is_contradiction() -> None: + """`index=None` plus index metadata is a contradiction.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={RERUN_KIND: b"index"}), + pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"}), + ]) + with pytest.raises(ValueError): + Chunk.from_record_batch(pa.RecordBatch.from_arrays([index, _VALUES], schema=schema), index=None) + + +# --------------------------------------------------------------------------- +# Error cases (all `ValueError`) +# --------------------------------------------------------------------------- + + +def test_auto_no_index_raises() -> None: + schema = pa.schema([pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"})]) + with pytest.raises(ValueError): + Chunk.from_record_batch(pa.RecordBatch.from_arrays([_VALUES], schema=schema)) + + +def test_rejects_plain_batch() -> None: + """A batch with no Rerun metadata at all is ambiguous under AUTO → ValueError.""" + plain_batch = pa.record_batch({"x": [1, 2, 3]}) + with pytest.raises(ValueError): + Chunk.from_record_batch(plain_batch) + + +def test_null_in_index_raises() -> None: + index = pa.array([0, None], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={RERUN_KIND: b"index"}), + pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"}), + ]) + with pytest.raises(ValueError): + Chunk.from_record_batch(pa.RecordBatch.from_arrays([index, _VALUES], schema=schema)) + + +@pytest.mark.parametrize( + "bad_type", + [ + pa.timestamp("us"), + pa.duration("ms"), + pa.time64("ns"), + ], +) +def test_bad_time_dtype_raises(bad_type: pa.DataType) -> None: + index = pa.array([0, 1], type=bad_type) + schema = pa.schema([ + pa.field("t", index.type), + pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"}), + ]) + with pytest.raises(ValueError): + Chunk.from_record_batch(pa.RecordBatch.from_arrays([index, _VALUES], schema=schema), index="t") + + +def test_missing_named_index_raises() -> None: + schema = pa.schema([pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"})]) + with pytest.raises(ValueError): + Chunk.from_record_batch(pa.RecordBatch.from_arrays([_VALUES], schema=schema), index="nope") + + +def test_no_component_columns_raises() -> None: + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([pa.field("frame", index.type, metadata={RERUN_KIND: b"index"})]) + with pytest.raises(ValueError): + Chunk.from_record_batch(pa.RecordBatch.from_arrays([index], schema=schema)) + + +# --------------------------------------------------------------------------- +# `from_dataframe` +# --------------------------------------------------------------------------- + + +def _temporal_batch() -> pa.RecordBatch: + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={RERUN_KIND: b"index"}), + pa.field("/e:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e"}), + ]) + return pa.RecordBatch.from_arrays([index, _VALUES], schema=schema) + + +def test_from_dataframe_table() -> None: + table = pa.Table.from_batches([_temporal_batch(), _temporal_batch()]) + chunks = list(Chunk.from_dataframe(table)) + assert len(chunks) == 2 + assert all(c.entity_path == "/e" for c in chunks) + + +def test_from_dataframe_reader() -> None: + table = pa.Table.from_batches([_temporal_batch()]) + chunks = list(Chunk.from_dataframe(table.to_reader())) + assert len(chunks) == 1 + + +def test_from_dataframe_datafusion() -> None: + """A `datafusion.DataFrame` is accepted via the soft dependency.""" + datafusion = pytest.importorskip("datafusion") + + table = pa.Table.from_batches([_temporal_batch()]) + ctx = datafusion.SessionContext() + df = ctx.from_arrow(table) + chunks = list(Chunk.from_dataframe(df)) + assert len(chunks) == 1 + assert chunks[0].entity_path == "/e" + assert chunks[0].timeline_names == ["frame"] + + +def test_from_dataframe_validates_input_eagerly() -> None: + """The input type is validated eagerly (not deferred to first iteration).""" + with pytest.raises(TypeError): + Chunk.from_dataframe("not a dataframe") # type: ignore[arg-type] + + +def test_from_dataframe_record_batch() -> None: + """A single `RecordBatch` is accepted (it implements the Arrow C stream interface).""" + chunks = list(Chunk.from_dataframe(_temporal_batch())) + assert len(chunks) == 1 + assert chunks[0].entity_path == "/e" + + +@pytest.mark.parametrize("bad", [123, b"bytes", object(), "not a dataframe"]) +def test_from_dataframe_bad_input(bad: object) -> None: + # Objects that are neither pyarrow Table/RecordBatchReader nor Arrow-C-stream sources are rejected. + with pytest.raises(TypeError): + Chunk.from_dataframe(bad) # type: ignore[arg-type] + + +def test_auto_index_sentinel_is_default() -> None: + """The default `index` is the `AUTO_INDEX` sentinel.""" + import inspect + + sig = inspect.signature(Chunk.from_record_batch) + assert sig.parameters["index"].default is AUTO_INDEX diff --git a/rerun_py/tests/integration/test_chunk_store.py b/rerun_py/tests/integration/test_chunk_store.py index 2d38ccd27f38..aacde62bcbe2 100644 --- a/rerun_py/tests/integration/test_chunk_store.py +++ b/rerun_py/tests/integration/test_chunk_store.py @@ -11,7 +11,7 @@ from rerun.experimental import ( ChunkStore, LazyChunkStream, - OptimizationSettings, + OptimizationProfile, RrdReader, ) @@ -22,27 +22,50 @@ from syrupy import SnapshotAssertion +FRAGMENTED_NUM_ROWS = 4_200 + @pytest.fixture(scope="session") def fragmented_rrd_path(tmp_path_factory: pytest.TempPathFactory) -> Path: - """RRD with many tiny single-row chunks, ideal for compaction testing.""" + """ + RRD with `FRAGMENTED_NUM_ROWS` sorted scalar rows on /sensor, one chunk per row. - rrd_path = tmp_path_factory.mktemp("compact") / "fragmented.rrd" + Row count is sized to be larger than LIVE's `max_rows=4096` ceiling and + smaller than OBJECT_STORE's `max_rows=65_536`, so the splitter behaves + visibly differently under the two profiles. - with rr.RecordingStream("rerun_example_compact_test", recording_id="compact-test-id") as rec: + Uses `ChunkBatcherConfig.ALWAYS_TEST_ONLY()` so the microbatcher cannot coalesce + sends behind our back: each `send_columns` becomes its own chunk. + """ + rrd_path = tmp_path_factory.mktemp("compact") / "fragmented.rrd" + with rr.RecordingStream( + "rerun_example_compact_test", + recording_id="compact-test-id", + batcher_config=rr.ChunkBatcherConfig.ALWAYS_TEST_ONLY(), + ) as rec: rec.save(rrd_path) - - # 20 individual send_columns calls -> 20 separate chunks for the same entity - for i in range(20): + for i in range(FRAGMENTED_NUM_ROWS): rec.send_columns( "/sensor", indexes=[rr.TimeColumn("frame", sequence=[i])], columns=rr.Scalars.columns(scalars=[float(i)]), ) - return rrd_path +# Session-scoped collected stores: each `collect()` over the fragmented RRD takes +# ~0.5s, and several tests below need the same outputs. Compute once, share across +# tests — they only read from the resulting `ChunkStore`. +@pytest.fixture(scope="session") +def fragmented_default_store(fragmented_rrd_path: Path) -> ChunkStore: + return RrdReader(fragmented_rrd_path).stream().collect() + + +@pytest.fixture(scope="session") +def fragmented_optimized_store(fragmented_rrd_path: Path) -> ChunkStore: + return RrdReader(fragmented_rrd_path).stream().collect(optimize=OptimizationProfile()) + + VIDEO_ASSETS_DIR = pathlib.Path(__file__).parents[3] / "tests" / "assets" / "video" # (filename, rerun codec) pairs exercised by the VideoStream compaction test. @@ -116,14 +139,14 @@ def log_packet(packet: av.Packet) -> None: # --------------------------------------------------------------------------- -def test_store_from_rrd_reader(test_rrd_path: Path) -> None: - """RrdReader.store() returns a ChunkStore.""" - store = RrdReader(test_rrd_path).store() +def test_collect_from_rrd_reader(test_rrd_path: Path) -> None: + """`reader.stream().collect()` returns a fully-materialized ChunkStore.""" + store = RrdReader(test_rrd_path).stream().collect() assert isinstance(store, ChunkStore) def test_repr(test_rrd_path: Path) -> None: - store = RrdReader(test_rrd_path).store() + store = RrdReader(test_rrd_path).stream().collect() assert "ChunkStore" in repr(store) @@ -134,12 +157,12 @@ def test_repr(test_rrd_path: Path) -> None: def test_schema(test_rrd_path: Path, snapshot: SnapshotAssertion) -> None: """schema() returns a Schema matching the stored data.""" - store = RrdReader(test_rrd_path).store() + store = RrdReader(test_rrd_path).stream().collect() assert repr(store.schema()) == snapshot def test_schema_entity_paths(test_rrd_path: Path) -> None: - store = RrdReader(test_rrd_path).store() + store = RrdReader(test_rrd_path).stream().collect() paths = store.schema().entity_paths() assert "/robots/arm" in paths assert "/cameras/front" in paths @@ -152,21 +175,21 @@ def test_schema_entity_paths(test_rrd_path: Path) -> None: def test_stream_returns_lazy_chunk_stream(test_rrd_path: Path) -> None: - store = RrdReader(test_rrd_path).store() + store = RrdReader(test_rrd_path).stream().collect() assert isinstance(store.stream(), LazyChunkStream) def test_stream_is_repeatable(test_rrd_path: Path) -> None: """stream() can be called multiple times; each produces the same schema.""" - store = RrdReader(test_rrd_path).store() + store = RrdReader(test_rrd_path).stream().collect() first = store.stream().collect() second = store.stream().collect() assert first.schema() == second.schema() def test_stream_supports_pipeline_ops(test_rrd_path: Path) -> None: - """Chunks from store().stream() work with filter/collect.""" - store = RrdReader(test_rrd_path).store() + """Chunks from load().stream() work with filter/collect.""" + store = RrdReader(test_rrd_path).stream().collect() filtered = store.stream().filter(is_static=True).collect() assert filtered.schema().entity_paths() == ["/config"] @@ -190,24 +213,26 @@ def test_same_schema(test_rrd_path: Path) -> None: def test_write_rrd_roundtrip(test_rrd_path: Path, tmp_path: Path) -> None: - """write_rrd() -> RrdReader().store() preserves schema.""" - store1 = RrdReader(test_rrd_path).store() + """write_rrd() -> RrdReader().stream().collect() preserves schema.""" + store1 = RrdReader(test_rrd_path).stream().collect() out = tmp_path / "roundtrip.rrd" store1.write_rrd(out, application_id=APP_ID, recording_id=RECORDING_ID) - store2 = RrdReader(out).store() + store2 = RrdReader(out).stream().collect() assert store1.schema() == store2.schema() def test_write_rrd_metadata(test_rrd_path: Path, tmp_path: Path) -> None: """write_rrd() writes the provided application_id and recording_id.""" - store = RrdReader(test_rrd_path).store() + store = RrdReader(test_rrd_path).stream().collect() out = tmp_path / "meta.rrd" - store.write_rrd(out, application_id="my-app", recording_id="my-rec") + store.write_rrd(out, application_id="rerun_example_my_app", recording_id="my-rec") reader = RrdReader(out) - assert reader.application_id == "my-app" - assert reader.recording_id == "my-rec" + recs = reader.recordings() + assert len(recs) == 1 + assert recs[0].application_id == "rerun_example_my_app" + assert recs[0].recording_id == "my-rec" # --------------------------------------------------------------------------- @@ -215,42 +240,82 @@ def test_write_rrd_metadata(test_rrd_path: Path, tmp_path: Path) -> None: # --------------------------------------------------------------------------- -def test_collect_default_single_pass_compacts(fragmented_rrd_path: Path) -> None: +def test_collect_default_single_pass_compacts(fragmented_default_store: ChunkStore) -> None: """Default collect() applies single-pass compaction (what happens on insert).""" - reader = RrdReader(fragmented_rrd_path) - default = reader.stream().collect() # Without any optimization, many tiny single-row chunks still get merged by # the natural insert-time compaction path. - assert len(default.stream().to_chunks()) < 20 - + assert len(fragmented_default_store.stream().to_chunks()) < FRAGMENTED_NUM_ROWS -def test_collect_optimize_further_reduces(fragmented_rrd_path: Path) -> None: - """Explicit optimize=OptimizationSettings() reduces chunk count further.""" - reader = RrdReader(fragmented_rrd_path) - default = reader.stream().collect() # single-pass only - optimized = reader.stream().collect(optimize=OptimizationSettings()) - assert len(optimized.stream().to_chunks()) <= len(default.stream().to_chunks()) +def test_collect_optimize_further_reduces( + fragmented_default_store: ChunkStore, + fragmented_optimized_store: ChunkStore, +) -> None: + """Explicit optimize=OptimizationProfile() reduces chunk count further.""" + assert len(fragmented_optimized_store.stream().to_chunks()) <= len(fragmented_default_store.stream().to_chunks()) -def test_collect_preserves_schema(fragmented_rrd_path: Path) -> None: +def test_collect_preserves_schema( + fragmented_default_store: ChunkStore, + fragmented_optimized_store: ChunkStore, +) -> None: """Optimization preserves the schema.""" - reader = RrdReader(fragmented_rrd_path) - default = reader.stream().collect() - optimized = reader.stream().collect(optimize=OptimizationSettings()) - assert default.schema() == optimized.schema() + assert fragmented_default_store.schema() == fragmented_optimized_store.schema() -def test_collect_preserves_row_count(fragmented_rrd_path: Path) -> None: +def test_collect_preserves_row_count( + fragmented_default_store: ChunkStore, + fragmented_optimized_store: ChunkStore, +) -> None: """Optimization preserves the total number of rows.""" - reader = RrdReader(fragmented_rrd_path) - default_rows = sum(c.num_rows for c in reader.stream().collect().stream().to_chunks()) - optimized_rows = sum( - c.num_rows for c in reader.stream().collect(optimize=OptimizationSettings()).stream().to_chunks() - ) + default_rows = sum(c.num_rows for c in fragmented_default_store.stream().to_chunks()) + optimized_rows = sum(c.num_rows for c in fragmented_optimized_store.stream().to_chunks()) assert optimized_rows == default_rows +def test_collect_with_object_store_profile_uses_object_store_thresholds( + fragmented_rrd_path: Path, +) -> None: + """ + End-to-end plumbing: OBJECT_STORE's larger thresholds reach the resulting ChunkStore. + + Proves the precedence chain `OptimizationProfile.OBJECT_STORE → PyO3 → ChunkStoreConfig` + forwards concrete values (no silent fallback to DEFAULT/LIVE) by checking + that the `chunk_max_rows` threshold is *enforced* on the /sensor chunks: + + - LIVE caps every chunk at 4096 rows. + - OBJECT_STORE lets at least one chunk hold more than 4096 rows. If + OBJECT_STORE's value did not reach the store, splitting would have + capped it at 4096 too. + + This avoids relying on compaction heuristics converging to a specific + chunk count: it only relies on the splitter respecting the configured + ceiling, which is a hard invariant. + """ + live_store = RrdReader(fragmented_rrd_path).stream().collect(optimize=OptimizationProfile.LIVE) + object_store_store = RrdReader(fragmented_rrd_path).stream().collect(optimize=OptimizationProfile.OBJECT_STORE) + + def sensor_rows(s: ChunkStore) -> list[int]: + return [c.num_rows for c in s.stream().to_chunks() if str(c.entity_path) == "/sensor"] + + live_sensor = sensor_rows(live_store) + object_store_sensor = sensor_rows(object_store_store) + + # Schema and total /sensor row count preserved across profiles. + assert live_store.schema() == object_store_store.schema() + assert sum(live_sensor) == sum(object_store_sensor) == FRAGMENTED_NUM_ROWS + + # LIVE enforces its 4096 row ceiling on every chunk. + assert all(n <= 4096 for n in live_sensor), f"LIVE must respect max_rows=4096: {live_sensor}" + + # OBJECT_STORE's higher 65_536 ceiling lets at least one chunk exceed 4096 + # rows, proving the OBJECT_STORE value reached the store. (If OBJECT_STORE's + # value were lost, the splitter would have capped chunks at 4096 just like LIVE.) + assert any(n > 4096 for n in object_store_sensor), ( + f"expected at least one chunk >4096 rows under OBJECT_STORE profile, got {object_store_sensor}" + ) + + def test_collect_optimize_video_stream_summary(tmp_path_factory: pytest.TempPathFactory) -> None: """Snapshot the summary of a VideoStream recording: optimize without vs with GoP batching.""" @@ -265,9 +330,9 @@ def report(label: str, num_gops: int, s: ChunkStore) -> str: reader = RrdReader(rrd_path) # Optimize without GoP alignment. - without_gop = reader.stream().collect(optimize=OptimizationSettings(gop_batching=False)) + without_gop = reader.stream().collect(optimize=OptimizationProfile(gop_batching=False)) # Re-optimize with GoP batching on top of the already-optimized store. - with_gop = without_gop.stream().collect(optimize=OptimizationSettings(gop_batching=True)) + with_gop = without_gop.stream().collect(optimize=OptimizationProfile(gop_batching=True)) sections.append(f"=== {filename} ===") sections.append(report("before_gop", num_gops, without_gop)) @@ -277,61 +342,64 @@ def report(label: str, num_gops: int, s: ChunkStore) -> str: assert "\n".join(sections) == inline_snapshot("""\ === Big_Buck_Bunny_1080_10s_av1.mp4 === before_gop: num_gops=1 num_chunks=17 -/video rows=1 bytes=1.1 KiB static=True timelines=[] cols=['VideoStream:codec'] -/video rows=1 bytes=534 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=19 bytes=378 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=19 bytes=382 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=24 bytes=378 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=22 bytes=330 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=17 bytes=277 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=17 bytes=279 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=17 bytes=280 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=18 bytes=381 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=32 bytes=377 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=18 bytes=278 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=19 bytes=377 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=16 bytes=297 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=31 bytes=371 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=30 bytes=237 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/__properties rows=1 bytes=1.1 KiB static=True timelines=[] cols=['RecordingInfo:start_time'] -after_gop: num_gops=1 num_chunks=3 -/video rows=1 bytes=1.1 KiB static=True timelines=[] cols=['VideoStream:codec'] -/video rows=315 bytes=6.4 MiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/__properties rows=1 bytes=1.1 KiB static=True timelines=[] cols=['RecordingInfo:start_time'] +/__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] +/video rows=1 static=True timelines=[] cols=['VideoStream:codec'] +/video rows=1 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=19 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=19 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=24 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=22 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=17 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=17 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=17 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=18 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=32 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=18 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=19 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=16 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=31 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=30 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +after_gop: num_gops=1 num_chunks=4 +/__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] +/video rows=1 static=True timelines=[] cols=['VideoStream:codec'] +/video rows=300 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=1 static=False timelines=['video_time'] cols=['VideoStream:is_keyframe', 'video_time'] === Big_Buck_Bunny_1080_1s_h264_nobframes.mp4 === before_gop: num_gops=1 num_chunks=11 -/video rows=1 bytes=1.1 KiB static=True timelines=[] cols=['VideoStream:codec'] -/video rows=1 bytes=348 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=4 bytes=353 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=4 bytes=371 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=3 bytes=293 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=3 bytes=297 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=4 bytes=379 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=3 bytes=302 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=4 bytes=379 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=4 bytes=260 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/__properties rows=1 bytes=1.1 KiB static=True timelines=[] cols=['RecordingInfo:start_time'] -after_gop: num_gops=1 num_chunks=3 -/video rows=1 bytes=1.1 KiB static=True timelines=[] cols=['VideoStream:codec'] -/video rows=39 bytes=4.0 MiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/__properties rows=1 bytes=1.1 KiB static=True timelines=[] cols=['RecordingInfo:start_time'] +/__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] +/video rows=1 static=True timelines=[] cols=['VideoStream:codec'] +/video rows=1 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=4 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=4 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=3 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=3 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=4 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=3 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=4 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=4 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +after_gop: num_gops=1 num_chunks=4 +/__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] +/video rows=1 static=True timelines=[] cols=['VideoStream:codec'] +/video rows=30 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=1 static=False timelines=['video_time'] cols=['VideoStream:is_keyframe', 'video_time'] === Sintel_1080_10s_av1.mp4 === before_gop: num_gops=12 num_chunks=5 -/video rows=1 bytes=1.1 KiB static=True timelines=[] cols=['VideoStream:codec'] -/video rows=114 bytes=382 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=111 bytes=382 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=75 bytes=279 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/__properties rows=1 bytes=1.1 KiB static=True timelines=[] cols=['RecordingInfo:start_time'] -after_gop: num_gops=12 num_chunks=6 -/video rows=1 bytes=1.1 KiB static=True timelines=[] cols=['VideoStream:codec'] -/video rows=100 bytes=381 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=84 bytes=324 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=52 bytes=216 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/video rows=77 bytes=318 KiB static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] -/__properties rows=1 bytes=1.1 KiB static=True timelines=[] cols=['RecordingInfo:start_time'] +/__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] +/video rows=1 static=True timelines=[] cols=['VideoStream:codec'] +/video rows=114 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=111 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=75 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +after_gop: num_gops=12 num_chunks=7 +/__properties rows=1 static=True timelines=[] cols=['RecordingInfo:start_time'] +/video rows=1 static=True timelines=[] cols=['VideoStream:codec'] +/video rows=39 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=107 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=68 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=86 static=False timelines=['video_time'] cols=['VideoStream:sample', 'video_time'] +/video rows=12 static=False timelines=['video_time'] cols=['VideoStream:is_keyframe', 'video_time'] """) diff --git a/rerun_py/tests/integration/test_chunk_store_reader.py b/rerun_py/tests/integration/test_chunk_store_reader.py new file mode 100644 index 000000000000..29d6a5c1847c --- /dev/null +++ b/rerun_py/tests/integration/test_chunk_store_reader.py @@ -0,0 +1,243 @@ +"""Roundtrip parity tests for `ChunkStore.reader()` vs. `dataset.reader()`.""" + +from __future__ import annotations + +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest +import rerun as rr +from rerun.experimental import ChunkStore, RrdReader + +if TYPE_CHECKING: + from collections.abc import Iterator + + import datafusion + import pyarrow as pa + from rerun.catalog import ContentFilter, DatasetEntry, IndexValuesLike + + +@dataclass(frozen=True) +class Case: + """One roundtrip parity case.""" + + name: str + index: str | None + contents: ContentFilter | str | list[str] | None = None + include_semantically_empty_columns: bool = False + include_tombstone_columns: bool = False + fill_latest_at: bool = False + using_index_values: IndexValuesLike | None = None + + def _common_kwargs(self) -> dict[str, object]: + return { + "index": self.index, + "include_semantically_empty_columns": self.include_semantically_empty_columns, + "include_tombstone_columns": self.include_tombstone_columns, + "fill_latest_at": self.fill_latest_at, + "using_index_values": self.using_index_values, + } + + def chunk_df(self, store: ChunkStore) -> datafusion.DataFrame: + return store.reader(contents=self.contents, **self._common_kwargs()) # type: ignore[arg-type] + + def dataset_df(self, ds: DatasetEntry) -> datafusion.DataFrame: + view = ds.filter_contents(self.contents) if self.contents is not None else ds + return view.reader(**self._common_kwargs()) # type: ignore[arg-type] + + +# Total non-static rows logged on timeline `t`. Sized above +# `DEFAULT_BATCH_ROWS=2048` so the batch-shape test sees multi-batch output. +FIXTURE_NUM_ROWS = 5000 +FIXTURE_INDEX_RANGE = range(FIXTURE_NUM_ROWS) + + +def _build_fixture_store() -> ChunkStore: + """ + `FIXTURE_NUM_ROWS` rows on timeline `t` across `/a` and `/b`, plus a static row on `/c`. + + Built via `RecordingStream` + `RrdReader.collect()` so the chunkification is + whatever the standard SDK pipeline produces — no hand-crafted chunks. + + The static row is on `/c` (not `/a` or `/b`) so it doesn't collide with the + temporal `Scalars:scalars` column on the same entity, which would mark the + column as static and zero out the temporal rows. + + `/a` also gets a `rr.Clear` to produce a tombstone column, and `/q` logs + `Points3D(positions=…, colors=[])` to produce a semantically-empty `colors` + column. Both are hidden under the default reader and surface only when + `include_tombstone_columns` / `include_semantically_empty_columns` is set. + """ + with tempfile.TemporaryDirectory() as td: + path = Path(td) / "build.rrd" + with rr.RecordingStream("rerun_example_fixture", recording_id="fix") as rec: + rec.save(path) + rec.log("/c", rr.Scalars(scalars=[42.0]), static=True) + for i in FIXTURE_INDEX_RANGE: + rec.set_time("t", sequence=i) + rec.log("/a", rr.Scalars(scalars=[float(i)])) + if i % 2 == 0: + rec.log("/b", rr.Scalars(scalars=[float(-i)])) + # Tombstone column: `Clear:is_recursive` on /a. + rec.set_time("t", sequence=FIXTURE_NUM_ROWS // 2) + rec.log("/a", rr.Clear(recursive=False)) + # Semantically-empty column: `/q:Points3D:colors` (positions logged, + # colors logged as an explicit empty list — registers the column + # with only null values). + rec.set_time("t", sequence=0) + rec.log("/q", rr.Points3D(positions=[[0.0, 0.0, 0.0]], colors=[])) + rec.disconnect() + + return RrdReader(path).stream().collect() + + +@pytest.fixture(scope="module") +def store_and_dataset( + tmp_path_factory: pytest.TempPathFactory, +) -> Iterator[tuple[ChunkStore, DatasetEntry]]: + """ + Module-scoped server hosting a dataset registered from a single RRD. + + The same `ChunkStore` is yielded so both reader paths see the same data. + """ + store = _build_fixture_store() + rrd_dir = tmp_path_factory.mktemp("rt_dir") + rrd = rrd_dir / "rt.rrd" + store.write_rrd(rrd, application_id="rerun_example_test", recording_id="rt-rec") + with rr.server.Server(datasets={"rt": rrd_dir}) as server: + client = server.client() + yield store, client.get_dataset("rt") + + +@pytest.fixture(scope="module") +def store_only() -> ChunkStore: + """Standalone fixture for tests that don't need a server.""" + return _build_fixture_store() + + +# --- Helpers --------------------------------------------------------------- + + +def _drop_segment_id(df: datafusion.DataFrame) -> datafusion.DataFrame: + return df.drop("rerun_segment_id") + + +def _normalized_fields(schema: pa.Schema) -> list[tuple[str, pa.DataType, dict[bytes, bytes]]]: + return sorted([(f.name, f.type, dict(f.metadata or {})) for f in schema]) + + +def _assert_field_parity(chunk_df: datafusion.DataFrame, dataset_df: datafusion.DataFrame) -> None: + """Compare `(name, type, per-field metadata)` triplets sorted by name; ignore table-level metadata.""" + chunk_fields = _normalized_fields(chunk_df.schema()) + dataset_fields = _normalized_fields(_drop_segment_id(dataset_df).schema()) + assert chunk_fields == dataset_fields + + +def _row_multiset(df: datafusion.DataFrame) -> list[str]: + """ + Convert every row to a deterministic Python `repr` and return sorted. + + Columns are read in alphabetical order so the two sides compare regardless + of physical column ordering — the schema-parity contract only guarantees + same fields (sorted by name), not same field order. + + `pyarrow.Table.sort_by` does not support List/Struct sort keys (which is + every component column), so we cannot use a column-sort comparison. + `to_pylist()` returns nested Python objects that `repr()` formats + deterministically, so a sorted multiset of repr-strings is a robust + row-set equality check. The fixture avoids NaN. + """ + tbl = df.to_arrow_table().combine_chunks() + names = sorted(tbl.column_names) + cols = [tbl.column(n).to_pylist() for n in names] + return sorted(repr(row) for row in zip(*cols, strict=True)) + + +def _assert_data_parity(chunk_df: datafusion.DataFrame, dataset_df: datafusion.DataFrame) -> None: + assert _row_multiset(chunk_df) == _row_multiset(_drop_segment_id(dataset_df)) + + +# --- Parameterized roundtrip cases ---------------------------------------- + + +# `using_index_values` entries must lie within FIXTURE_INDEX_RANGE so the +# dataset side's `_map_index_values_to_ranges` does not drop any value. +CASES: list[Case] = [ + Case("static_only", index=None), + Case("timeline", index="t"), + Case("narrow_contents", index="t", contents="/a/**"), + Case("exclude_contents", index="t", contents=["/**", "-/b/**"]), + Case("fill_latest_at", index="t", fill_latest_at=True), + Case("using_index_values", index="t", using_index_values=[1, 2, 3]), + Case("using_index_values_fill_latest_at", index="t", fill_latest_at=True, using_index_values=[5, 4999]), + Case("include_tombstones", index="t", include_tombstone_columns=True), + Case("include_semantically_empty", index="t", include_semantically_empty_columns=True), +] + + +@pytest.mark.parametrize("case", CASES, ids=[c.name for c in CASES]) +def test_roundtrip_parity( + store_and_dataset: tuple[ChunkStore, DatasetEntry], + case: Case, +) -> None: + store, ds = store_and_dataset + + ck_df = case.chunk_df(store) + ds_df = case.dataset_df(ds) + + _assert_field_parity(ck_df, ds_df) + assert ck_df.count() == ds_df.count() + _assert_data_parity(ck_df, ds_df) + + +# --- Batch-shape ---------------------------------------------------------- + + +def test_batch_shape(store_and_dataset: tuple[ChunkStore, DatasetEntry]) -> None: + store, ds = store_and_dataset + ck_batches = store.reader(index="t").collect() + ds_batches = ds.reader(index="t").collect() + + ck_total = sum(b.num_rows for b in ck_batches) + ds_total = sum(b.num_rows for b in ds_batches) + assert ck_total == ds_total + + half = 2048 // 2 + if len(ck_batches) > 1: + assert all(b.num_rows >= half for b in ck_batches[:-1]) + if len(ds_batches) > 1: + assert all(b.num_rows >= half for b in ds_batches[:-1]) + + # Sanity: fixture is large enough that we actually exercised the multi-batch path. + assert len(ck_batches) >= 2, f"fixture too small: got {len(ck_batches)} batch(es)" + + +# --- Standalone (non-roundtrip) ------------------------------------------- + + +def test_empty_contents_empty_result(store_only: ChunkStore) -> None: + df = store_only.reader(index="t", contents=[]) + assert df.count() == 0 + + +def test_unknown_index_errors(store_only: ChunkStore) -> None: + with pytest.raises(Exception, match="does not exist"): + store_only.reader(index="does_not_exist") + + +def test_include_tombstones_surfaces_clear_column(store_only: ChunkStore) -> None: + """`include_tombstone_columns=True` must add the Clear:is_recursive column hidden by default.""" + default_cols = set(store_only.reader(index="t").schema().names) + with_tombstones = set(store_only.reader(index="t", include_tombstone_columns=True).schema().names) + added = with_tombstones - default_cols + assert any("Clear" in c for c in added), f"expected a Clear:* column, got added={added}" + + +def test_include_semantically_empty_surfaces_null_column(store_only: ChunkStore) -> None: + """`include_semantically_empty_columns=True` must add the all-null `/q:Points3D:colors` column.""" + default_cols = set(store_only.reader(index="t").schema().names) + with_empty = set(store_only.reader(index="t", include_semantically_empty_columns=True).schema().names) + added = with_empty - default_cols + assert "/q:Points3D:colors" in added, f"expected /q:Points3D:colors, got added={added}" diff --git a/rerun_py/tests/integration/test_dataloader_video.py b/rerun_py/tests/integration/test_dataloader_video.py new file mode 100644 index 000000000000..118a4a5bfc84 --- /dev/null +++ b/rerun_py/tests/integration/test_dataloader_video.py @@ -0,0 +1,180 @@ +""" +Integration tests for the keyframe-aware video dataloader. + +Exercises `RerunMapDataset` + `VideoFrameDecoder` end-to-end against a small +H.264 stream served via `rr.server.Server`, covering both the anchor path +(sibling `is_keyframe` column present) and the heuristic fallback (column +absent from the schema). +""" + +from __future__ import annotations + +import pathlib +from typing import TYPE_CHECKING + +import pytest +import rerun as rr +from rerun.experimental.dataloader import ( + DataSource, + Field, + NumericDecoder, + RerunMapDataset, + VideoFrameDecoder, +) + +if TYPE_CHECKING: + from pathlib import Path + + +VIDEO_ASSET = ( + pathlib.Path(__file__).parents[3] / "tests" / "assets" / "video" / "Big_Buck_Bunny_1080_1s_h264_nobframes.mp4" +) + + +def _build_h264_rrd(rrd_path: Path, *, log_is_keyframe: bool) -> list[int]: + """ + Build an RRD with one VideoStream sample per demuxed packet on a sequence timeline. + + Returns the list of frame indices that are codec keyframes. When + `log_is_keyframe` is true, also writes a sparse `is_keyframe=True` row on + those indices. + """ + import av + from av.bitstream import BitStreamFilterContext + + container = av.open(str(VIDEO_ASSET)) + keyframe_indices: list[int] = [] + samples: list[bytes] = [] + try: + video_stream = container.streams.video[0] + bsf = BitStreamFilterContext("h264_mp4toannexb", video_stream) + + def absorb(packet: av.Packet) -> None: + if packet.pts is None or packet.size == 0: + return + if packet.is_keyframe: + keyframe_indices.append(len(samples)) + samples.append(bytes(packet)) + + for packet in container.demux(video_stream): + for out in bsf.filter(packet): + absorb(out) + finally: + container.close() + + assert keyframe_indices, "test asset must contain at least one keyframe" + assert keyframe_indices[0] == 0, "test asset's first packet must be a keyframe" + + with rr.RecordingStream("rerun_example_test_dataloader_video", recording_id="dataloader-video") as rec: + rec.save(rrd_path) + rec.log("/video", rr.VideoStream(codec=rr.VideoCodec.H264), static=True) + rec.send_columns( + "/video", + indexes=[rr.TimeColumn("frame", sequence=list(range(len(samples))))], + columns=rr.VideoStream.columns(sample=samples), + ) + # Companion scalar so tests cover the mixed-decoder query path + # (`prior_keyframe_path` on a non-video decoder must return None, not raise). + rec.send_columns( + "/state", + indexes=[rr.TimeColumn("frame", sequence=list(range(len(samples))))], + columns=rr.Scalars.columns(scalars=[float(i) for i in range(len(samples))]), + ) + if log_is_keyframe: + rec.send_columns( + "/video", + indexes=[rr.TimeColumn("frame", sequence=keyframe_indices)], + columns=rr.VideoStream.columns(is_keyframe=[True] * len(keyframe_indices)), + ) + + return keyframe_indices + + +@pytest.fixture +def rrd_with_keyframes(tmp_path: Path) -> tuple[Path, list[int]]: + rrd_dir = tmp_path / "with_keyframes" + rrd_dir.mkdir() + keyframes = _build_h264_rrd(rrd_dir / "recording.rrd", log_is_keyframe=True) + return rrd_dir, keyframes + + +@pytest.fixture +def rrd_without_keyframes(tmp_path: Path) -> tuple[Path, list[int]]: + rrd_dir = tmp_path / "without_keyframes" + rrd_dir.mkdir() + keyframes = _build_h264_rrd(rrd_dir / "recording.rrd", log_is_keyframe=False) + return rrd_dir, keyframes + + +@pytest.mark.filterwarnings("ignore:The default multiprocessing start method is 'fork':RuntimeWarning") +def test_anchor_path_decodes_mid_gop_target(rrd_with_keyframes: tuple[Path, list[int]]) -> None: + """ + Decode a mid-GOP target with `keyframe_interval=1` — heuristic alone can't satisfy it. + + For any non-keyframe target, the heuristic window collapses to a single + sample and decode fails. With the `is_keyframe` anchor, the prefetcher + expands the window back to the prior keyframe and the decode succeeds. + """ + rrd_dir, keyframes = rrd_with_keyframes + target = keyframes[0] + 5 + assert target not in keyframes, "target must sit strictly between keyframes" + + with rr.server.Server(datasets={"video": rrd_dir}) as server: + ds = server.client().get_dataset("video") + source = DataSource(ds) + dataset = RerunMapDataset( + source, + "frame", + { + "image": Field( + "/video:VideoStream:sample", + decode=VideoFrameDecoder(codec="h264", keyframe_interval=1), + ), + "state": Field("/state:Scalars:scalars", decode=NumericDecoder()), + }, + ) + sample = dataset[target] + + assert sample["image"] is not None + assert sample["image"].ndim == 3 + assert sample["image"].shape[0] == 3 # (C, H, W) + assert sample["state"] is not None + assert float(sample["state"][0]) == float(target) + + +@pytest.mark.filterwarnings("ignore:The default multiprocessing start method is 'fork':RuntimeWarning") +def test_heuristic_fallback_when_is_keyframe_column_absent( + rrd_without_keyframes: tuple[Path, list[int]], +) -> None: + """ + Decode succeeds via the heuristic fallback when the anchor column is absent. + + The fixture omits `is_keyframe` entirely. `_fetch_prior_keyframes` must + detect that the anchor column is missing from the schema and fall through + to the decoder's heuristic without raising a planner error. + """ + rrd_dir, keyframes = rrd_without_keyframes + target = keyframes[0] + 5 + + with rr.server.Server(datasets={"video": rrd_dir}) as server: + ds = server.client().get_dataset("video") + source = DataSource(ds) + dataset = RerunMapDataset( + source, + "frame", + { + "image": Field( + "/video:VideoStream:sample", + # Big enough to cover the whole single-GOP stream. + decode=VideoFrameDecoder(codec="h264", keyframe_interval=64), + ), + "state": Field("/state:Scalars:scalars", decode=NumericDecoder()), + }, + ) + sample = dataset[target] + + assert sample["image"] is not None + assert sample["image"].ndim == 3 + assert sample["image"].shape[0] == 3 + assert sample["state"] is not None + assert float(sample["state"][0]) == float(target) diff --git a/rerun_py/tests/integration/test_dataloader_video_codecs.py b/rerun_py/tests/integration/test_dataloader_video_codecs.py new file mode 100644 index 000000000000..283286fbb992 --- /dev/null +++ b/rerun_py/tests/integration/test_dataloader_video_codecs.py @@ -0,0 +1,544 @@ +""" +Integration tests for various video decoding scenarios seen in the video decoder. + +It tests each codec with a built-in keyframe detector (`h264`, `h265`, `av1`) at several GOP lengths, against both decode paths. +""" + +from __future__ import annotations + +import multiprocessing +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +import av +import numpy as np +import pyarrow as pa +import pytest +import rerun as rr +import torch +from av.bitstream import BitStreamFilterContext +from rerun.experimental.dataloader import ( + DataSource, + Field, + FixedRateSampling, + NumericDecoder, + RerunMapDataset, + VideoFrameDecoder, +) + +if TYPE_CHECKING: + from pathlib import Path + +# `RerunMapDataset.__init__` warns when the default start method is `fork`, +# because forked DataLoader workers would deadlock on their first catalog call. +# These tests never spawn workers (`num_workers=0`), but the warning fires at +# construction time. Switching the start method to `spawn` is what the warning +# asks for and removes the noise at its source. +if multiprocessing.get_start_method(allow_none=True) is None: + multiprocessing.set_start_method("spawn") + + +@dataclass(frozen=True) +class CodecConfig: + """Everything the generator, decoder, and SDK need for one codec.""" + + encoder: str + """PyAV/ffmpeg encoder name.""" + + annex_b_filter: str | None + """Bitstream filter that converts demuxed packets to Annex B, or `None` to pass raw bytes.""" + + sdk_codec: rr.VideoCodec + """Codec enum logged on the `VideoStream` archetype.""" + + decoder_codec: str + """`codec=` string passed to `VideoFrameDecoder`.""" + + force_no_b_frames: bool = False + """If True, force `max_b_frames = 0` so DTS == PTS (required for `VideoStream`). + + Needed for H.264 and H.265 (libx264 and libx265 both emit reordered B-frames + by default). AV1 never has DTS != PTS, so we leave the encoder default in + place to exercise more realistic bitstreams. + """ + + +CODEC_CONFIGS = { + "h264": CodecConfig("libx264", "h264_mp4toannexb", rr.VideoCodec.H264, "h264", force_no_b_frames=True), + "h265": CodecConfig("libx265", "hevc_mp4toannexb", rr.VideoCodec.H265, "h265", force_no_b_frames=True), + "av1": CodecConfig("libaom-av1", None, rr.VideoCodec.AV1, "av1"), +} + +# 1 = every frame is a keyframe; 8 and 24 give multiple GOPs over NUM_FRAMES. +GOP_SIZES = [1, 8, 24] + +NUM_FRAMES = 96 +WIDTH = 64 +HEIGHT = 128 + + +def _encoder_available(name: str) -> bool: + """True if this PyAV build can encode with *name*.""" + try: + av.codec.Codec(name, "w") + except Exception: + return False + return True + + +def _synthetic_frame(index: int) -> av.VideoFrame: + """A small RGB frame with content that changes each index (so motion compensation has work to do).""" + pixels = np.empty((HEIGHT, WIDTH, 3), dtype=np.uint8) + pixels[:, :, 0] = ((np.arange(WIDTH) + index) % 256)[np.newaxis, :] + pixels[:, :, 1] = ((np.arange(HEIGHT) + index) % 256)[:, np.newaxis] + pixels[:, :, 2] = (index * 7) % 256 + return av.VideoFrame.from_ndarray(pixels, format="rgb24") + + +def _generate_stream( + tmp_path: Path, config: CodecConfig, gop_size: int, num_frames: int = NUM_FRAMES +) -> tuple[list[bytes], list[int]]: + """ + Encode a synthetic clip with a fixed keyframe cadence, then demux it back to per-frame samples. + + Returns `(samples, keyframe_indices)`: one encoded sample per frame, and + the indices into `samples` that are codec keyframes. + """ + tmp_path.mkdir(parents=True, exist_ok=True) + container_path = tmp_path / "source.mp4" + + output = av.open(str(container_path), "w") + try: + stream = output.add_stream(config.encoder, rate=30) + assert isinstance(stream, av.VideoStream) + stream.width = WIDTH + stream.height = HEIGHT + stream.pix_fmt = "yuv420p" + stream.gop_size = gop_size + if config.force_no_b_frames: + stream.max_b_frames = 0 # Keep DTS == PTS, required for VideoStream. + + # Pin both max and min keyframe interval to `gop_size` so keyframes land on a fixed cadence + # (no early scene-cut keyframes shortening a GOP). + stream.codec_context.options = {"g": str(gop_size), "keyint_min": str(gop_size)} + + for index in range(num_frames): + for packet in stream.encode(_synthetic_frame(index)): + output.mux(packet) + for packet in stream.encode(None): + output.mux(packet) + finally: + output.close() + + keyframe_indices: list[int] = [] + samples: list[bytes] = [] + + container = av.open(str(container_path)) + try: + video_stream = container.streams.video[0] + bsf = None + if config.annex_b_filter is not None: + bsf = BitStreamFilterContext(config.annex_b_filter, video_stream) + + def collect_sample(packet: av.Packet) -> None: + if packet.pts is None or packet.size == 0: + return + if packet.is_keyframe: + keyframe_indices.append(len(samples)) + samples.append(bytes(packet)) + + for packet in container.demux(video_stream): + if bsf is None: + collect_sample(packet) + else: + for filtered in bsf.filter(packet): + collect_sample(filtered) + finally: + container.close() + + assert keyframe_indices, "generated clip must contain at least one keyframe" + assert keyframe_indices[0] == 0, "first packet of the generated clip must be a keyframe" + + return samples, keyframe_indices + + +KeyframeLogging = Literal["sparse", "dense", "none"] + + +def _build_rrd( + rrd_path: Path, + config: CodecConfig, + samples: list[bytes], + keyframe_indices: list[int], + *, + keyframe_logging: KeyframeLogging, +) -> None: + """ + Log one `VideoStream` sample per frame, a companion scalar, and optionally the `is_keyframe` column. + + `keyframe_logging` controls how `is_keyframe` is populated: + - `"sparse"`: only `True` at keyframe indices (relies on latest-at fill for non-keyframes). + - `"dense"`: `True` at keyframes and `False` at every other frame (no latest-at fill needed, + but exposes any decoder code that mistakenly treats `False` as "unknown"). + - `"none"`: don't log `is_keyframe`; decoder must fall back to the heuristic. + """ + with rr.RecordingStream( + "rerun_example_test_dataloader_video_codecs", recording_id="dataloader-video-codecs" + ) as rec: + rec.save(rrd_path) + rec.log("/video", rr.VideoStream(codec=config.sdk_codec), static=True) + rec.send_columns( + "/video", + indexes=[rr.TimeColumn("frame", sequence=list(range(len(samples))))], + columns=rr.VideoStream.columns(sample=samples), + ) + # Scalar column so decoder queries must rely on the decode window, not just the target row. + rec.send_columns( + "/state", + indexes=[rr.TimeColumn("frame", sequence=list(range(len(samples))))], + columns=rr.Scalars.columns(scalars=[float(i) for i in range(len(samples))]), + ) + if keyframe_logging == "sparse": + rec.send_columns( + "/video", + indexes=[rr.TimeColumn("frame", sequence=keyframe_indices)], + columns=rr.VideoStream.columns(is_keyframe=[True] * len(keyframe_indices)), + ) + elif keyframe_logging == "dense": + keyframe_set = set(keyframe_indices) + flags = [index in keyframe_set for index in range(len(samples))] + rec.send_columns( + "/video", + indexes=[rr.TimeColumn("frame", sequence=list(range(len(samples))))], + columns=rr.VideoStream.columns(is_keyframe=flags), + ) + + +def _decode_targets( + rrd_dir: Path, config: CodecConfig, keyframe_interval: int, targets: list[int] +) -> dict[int, dict[str, torch.Tensor | None]]: + """Serve *rrd_dir* in-memory and decode each target index, returning `{target: sample}`.""" + results: dict[int, dict[str, torch.Tensor | None]] = {} + with rr.server.Server(datasets={"video": rrd_dir}) as server: + ds = server.client().get_dataset("video") + source = DataSource(ds) + dataset = RerunMapDataset( + source, + "frame", + { + "image": Field( + "/video:VideoStream:sample", + decode=VideoFrameDecoder(codec=config.decoder_codec, keyframe_interval=keyframe_interval), + ), + "state": Field("/state:Scalars:scalars", decode=NumericDecoder()), + }, + ) + for target in targets: + results[target] = dataset[target] + return results + + +@pytest.mark.parametrize("codec", list(CODEC_CONFIGS)) +@pytest.mark.parametrize("gop_size", GOP_SIZES) +@pytest.mark.parametrize( + "keyframe_logging", + ["sparse", "dense", "none"], + ids=["anchor_sparse", "anchor_dense", "heuristic"], +) +def test_decode_matrix(tmp_path: Path, codec: str, gop_size: int, keyframe_logging: KeyframeLogging) -> None: + """ + Decode the first frame, the last frame, and (when the GOP spans multiple frames) a mid-GOP frame for one (codec, gop, path) cell. + + The anchor paths (`sparse`/`dense`) use `keyframe_interval=1` so any mid-GOP + target must consult the `is_keyframe` column. The `dense` variant logs an + explicit `False` at every non-keyframe, which exercises the path where + latest-at fill would otherwise propagate `False` into later rows. The + `heuristic` path drops `is_keyframe` entirely and uses `keyframe_interval=gop_size`. + """ + config = CODEC_CONFIGS[codec] + if not _encoder_available(config.encoder): + pytest.skip(f"PyAV build lacks the {config.encoder} encoder") + + samples, keyframe_indices = _generate_stream(tmp_path / "gen", config, gop_size) + rrd_dir = tmp_path / "recording" + rrd_dir.mkdir() + _build_rrd(rrd_dir / "recording.rrd", config, samples, keyframe_indices, keyframe_logging=keyframe_logging) + + targets = [0, len(samples) - 1] + # Pick a mid-GOP target strictly between the first two real keyframes, at least two frames + # past keyframe[0] so the anchor case's window `[target - 1, target]` contains no keyframe + # and the decode must go through the `is_keyframe` anchor instead of the heuristic. + if gop_size > 1: + assert len(keyframe_indices) >= 2, "need at least two keyframes to pick a mid-GOP target" + mid_gop_target = (keyframe_indices[0] + keyframe_indices[1]) // 2 + assert mid_gop_target - keyframe_indices[0] >= 2 + assert mid_gop_target < keyframe_indices[1] + targets.append(mid_gop_target) + + keyframe_interval = gop_size if keyframe_logging == "none" else 1 + results = _decode_targets(rrd_dir, config, keyframe_interval, targets) + + for target in targets: + sample = results[target] + image = sample["image"] + assert image is not None, f"decode returned None for target {target}" + assert image.ndim == 3 + assert image.shape[0] == 3 # (C, H, W) + assert image.shape[1] == HEIGHT + assert image.shape[2] == WIDTH + state = sample["state"] + assert state is not None + assert float(state[0]) == float(target) + + +# --------------------------------------------------------------------------- +# Duplicate-sample handling. +# +# When frames are dropped, `fill_latest_at` backfills the empty grid slots with +# the previous frame's encoded bytes, so the decode window contains consecutive +# duplicate samples. Re-feeding a duplicate packet corrupts the decoder's +# reference state, so `VideoFrameDecoder` skips consecutive duplicates. The +# tests below pin that behavior; they fail if the dedup is dropped. +# --------------------------------------------------------------------------- + +DEDUP_GOP_SIZE = 5 # Multiple GOPs over NUM_FRAMES, so a mid-GOP target has P-frames. + + +def _blob_column(samples: list[bytes]) -> pa.ChunkedArray: + """Wrap encoded samples as the `list` column shape the decoder expects.""" + return pa.chunked_array([pa.array([[sample] for sample in samples], type=pa.list_(pa.binary()))]) + + +def _decode_window(decoder: VideoFrameDecoder, samples: list[bytes], target: int) -> torch.Tensor | None: + """Decode a window of encoded samples through the public `VideoFrameDecoder.decode`.""" + return decoder.decode(_blob_column(samples), target, "segment") + + +def test_duplicate_window_matches_clean_decode(tmp_path: Path) -> None: + """ + A duplicated window decodes to the same frame as the clean window. + + Repeats one sample (as `fill_latest_at` would on an empty grid slot) and + asserts the decode is unchanged, because the decoder drops the duplicate. + """ + config = CODEC_CONFIGS["h264"] + samples, keyframe_indices = _generate_stream(tmp_path / "gen", config, DEDUP_GOP_SIZE) + + keyframe = keyframe_indices[1] # second GOP, so a P-frame references this keyframe + target = keyframe + 2 + assert target not in keyframe_indices + + decoder = VideoFrameDecoder(codec=config.decoder_codec, keyframe_interval=len(samples)) + + clean_window = samples[keyframe : target + 1] + # Repeat the frame just before the target, exactly as `fill_latest_at` backfills an empty slot. + duplicated_window = [*samples[keyframe:target], samples[target - 1], samples[target]] + assert duplicated_window != clean_window, "the duplicated window must actually contain a repeat" + + clean = _decode_window(decoder, clean_window, target) + duplicated = _decode_window(decoder, duplicated_window, target) + + assert clean is not None and duplicated is not None + assert torch.equal(duplicated, clean), "duplicate samples in the window must not change the decoded frame" + + +TimelineKind = Literal["timestamp", "duration"] + + +def _build_temporal_video_rrd( + rrd_path: Path, + config: CodecConfig, + samples: list[bytes], + keyframe_indices: list[int], + index_ns: list[int], + *, + timeline: str, + kind: TimelineKind, +) -> None: + """Log the VideoStream on a timestamp or duration timeline at explicit per-frame index values, with sparse `is_keyframe`.""" + dtype = "datetime64[ns]" if kind == "timestamp" else "timedelta64[ns]" + index_values = np.array(index_ns, dtype=dtype) + keyframe_values = index_values[keyframe_indices] + + def _time_column(values: np.ndarray) -> rr.TimeColumn: + if kind == "timestamp": + return rr.TimeColumn(timeline, timestamp=values) + return rr.TimeColumn(timeline, duration=values) + + with rr.RecordingStream("rerun_example_test_dataloader_video_dropped", recording_id="dropped-frames") as rec: + rec.save(rrd_path) + rec.log("/video", rr.VideoStream(codec=config.sdk_codec), static=True) + rec.send_columns( + "/video", + indexes=[_time_column(index_values)], + columns=rr.VideoStream.columns(sample=samples), + ) + rec.send_columns( + "/video", + indexes=[_time_column(keyframe_values)], + columns=rr.VideoStream.columns(is_keyframe=[True] * len(keyframe_indices)), + ) + + +@pytest.mark.parametrize( + ("timeline", "kind"), + [("real_time", "timestamp"), ("elapsed", "duration")], + ids=["timestamp", "duration"], +) +def test_fixed_rate_sampling_duplicates_decode_correctly(tmp_path: Path, timeline: str, kind: TimelineKind) -> None: + """ + Exercise the deployment path: dropped frames + `FixedRateSampling` + `fill_latest_at`. + + Real frames sit on a sparse subset of a 30 Hz grid, so the fixed-rate decode + window for a mid-GOP target is backfilled with duplicate samples. The served + decode matches a clean decode of the de-duplicated real frames. Run on both a + timestamp and a duration timeline, since `FixedRateSampling` and + `VideoFrameDecoder.context_range` handle `datetime64`/`timedelta64` indices. + """ + config = CODEC_CONFIGS["h264"] + samples, keyframe_indices = _generate_stream(tmp_path / "gen", config, DEDUP_GOP_SIZE) + + rate_hz = 30.0 + ns_per_slot = round(1e9 / rate_hz) + + # Target the second P-frame of the second GOP (`keyframe + 2`). + # The grid slot just before it has no captured frame, so `fill_latest_at` backfills it with + # the previous P-frame's bytes, the duplicate that desyncs libav. + keyframe_real = keyframe_indices[1] + target_real = keyframe_real + 2 + assert target_real not in keyframe_indices and target_real < keyframe_indices[2] + + slot_of_frame = list(range(len(samples))) + for frame_index in range(target_real, len(samples)): + slot_of_frame[frame_index] += 1 # leave the grid slot just before the target empty + target_slot = slot_of_frame[target_real] + + rrd_dir = tmp_path / "recording" + rrd_dir.mkdir() + timestamps_ns = [slot * ns_per_slot for slot in slot_of_frame] + _build_temporal_video_rrd( + rrd_dir / "recording.rrd", config, samples, keyframe_indices, timestamps_ns, timeline=timeline, kind=kind + ) + + # The real frames the grid maps to across the window, with the duplicate at the empty slot. + keyframe_slot = slot_of_frame[keyframe_real] + window_real_indices = [ + max(k for k, s in enumerate(slot_of_frame) if s <= grid_slot) + for grid_slot in range(keyframe_slot, target_slot + 1) + ] + assert window_real_indices == [keyframe_real, keyframe_real + 1, keyframe_real + 1, target_real], ( + f"unexpected window layout {window_real_indices}" + ) + + # Ground truth: a clean decode of the de-duplicated real frames in the window. + decoder = VideoFrameDecoder(codec=config.decoder_codec, keyframe_interval=len(samples), fps_estimate=rate_hz) + clean_samples = samples[keyframe_real : target_real + 1] + ground_truth = _decode_window(decoder, clean_samples, target_slot) + assert ground_truth is not None + + with rr.server.Server(datasets={"video": rrd_dir}) as server: + ds = server.client().get_dataset("video") + dataset = RerunMapDataset( + DataSource(ds), + timeline, + { + "image": Field( + "/video:VideoStream:sample", + decode=VideoFrameDecoder( + codec=config.decoder_codec, keyframe_interval=len(samples), fps_estimate=rate_hz + ), + ), + }, + timeline_sampling=FixedRateSampling(rate_hz=rate_hz), + ) + served = dataset[target_slot]["image"] + + assert served is not None, "served decode unexpectedly returned None" + assert torch.equal(served, ground_truth), "fixed-rate duplicate samples must not change the decoded frame" + + +OFF_GRID_NUM_FRAMES = 96 +OFF_GRID_GOP_SIZE = 5 +OFF_GRID_REAL_RATE_HZ = 27.0 +OFF_GRID_GRID_RATE_HZ = 30.0 + + +def test_off_grid_capture_rate_decodes_correctly(tmp_path: Path) -> None: + """ + Every grid slot of a ~27 fps capture decodes to the de-duplicated real frames up to that slot. + + A 30 fps camera dropping frames captures below nominal; ~27 fps served on the 30 Hz grid means every + slot is misaligned and the grid periodically laps the capture, backfilling duplicate samples. + """ + config = CODEC_CONFIGS["h264"] + if not _encoder_available(config.encoder): + pytest.skip(f"PyAV build lacks the {config.encoder} encoder") + + samples, keyframe_indices = _generate_stream( + tmp_path / "gen", config, OFF_GRID_GOP_SIZE, num_frames=OFF_GRID_NUM_FRAMES + ) + + ns_per_slot = round(1e9 / OFF_GRID_GRID_RATE_HZ) + timestamps_ns = [round(i / OFF_GRID_REAL_RATE_HZ * 1e9) for i in range(len(samples))] + + rrd_dir = tmp_path / "recording" + rrd_dir.mkdir() + _build_temporal_video_rrd( + rrd_dir / "recording.rrd", + config, + samples, + keyframe_indices, + timestamps_ns, + timeline="real_time", + kind="timestamp", + ) + + # Resolve each grid slot to the real frame `fill_latest_at` backfills it with + # (latest real frame at or before the slot) and that frame's prior keyframe. + timestamps_array = np.array(timestamps_ns) + keyframe_array = np.array(keyframe_indices) + num_slots = (timestamps_ns[-1] - timestamps_ns[0]) // ns_per_slot + 1 + real_for_slot = [ + int(np.searchsorted(timestamps_array, timestamps_ns[0] + slot * ns_per_slot, side="right") - 1) + for slot in range(num_slots) + ] + prior_keyframe_real = [ + int(keyframe_array[np.searchsorted(keyframe_array, real, side="right") - 1]) for real in real_for_slot + ] + + duplicate_slots = [slot for slot in range(1, num_slots) if real_for_slot[slot] == real_for_slot[slot - 1]] + assert duplicate_slots, "off-grid capture must lap the grid and produce at least one duplicate slot" + + # Ground truth: a clean decode of the de-duplicated real frames for each slot. + decoder = VideoFrameDecoder( + codec=config.decoder_codec, keyframe_interval=len(samples), fps_estimate=OFF_GRID_GRID_RATE_HZ + ) + ground_truth = [] + for slot in range(num_slots): + clean_samples = samples[prior_keyframe_real[slot] : real_for_slot[slot] + 1] + decoded = _decode_window(decoder, clean_samples, slot) + assert decoded is not None, f"clean decode returned None for slot {slot}" + ground_truth.append(decoded) + + with rr.server.Server(datasets={"video": rrd_dir}) as server: + ds = server.client().get_dataset("video") + dataset = RerunMapDataset( + DataSource(ds), + "real_time", + { + "image": Field( + "/video:VideoStream:sample", + decode=VideoFrameDecoder( + codec=config.decoder_codec, keyframe_interval=len(samples), fps_estimate=OFF_GRID_GRID_RATE_HZ + ), + ), + }, + timeline_sampling=FixedRateSampling(rate_hz=OFF_GRID_GRID_RATE_HZ), + ) + assert len(dataset) == num_slots, f"expected {num_slots} grid slots, got {len(dataset)}" + served = dataset.__getitems__(list(range(num_slots))) # one batched query for the whole grid + + for slot in range(num_slots): + image = served[slot]["image"] + assert image is not None, f"served decode returned None for slot {slot}" + assert torch.equal(image, ground_truth[slot]), f"off-grid decode mismatch at slot {slot}" diff --git a/rerun_py/tests/integration/test_hdf5_reader.py b/rerun_py/tests/integration/test_hdf5_reader.py new file mode 100644 index 000000000000..812a38fa3b67 --- /dev/null +++ b/rerun_py/tests/integration/test_hdf5_reader.py @@ -0,0 +1,254 @@ +""" +Tests for rerun.experimental.Hdf5Reader. + +Wrapper-plumbing tests against the committed canonical fixture +(`tests/assets/hdf5/`, generated by `cargo run -p re_hdf5 --example +gen_test_fixture`). The mapping logic itself is covered by `re_hdf5`'s Rust +integration tests. + +Fixture layout (5 rows): `/time` (float64 seconds), `/labels` (strings), +`/observations/{qpos [5,3], qvel [5]}`, `/observations/images/cam0 [5,2,2,3]`, +a 0-D `/meta/count`, and attributes on the root, `/observations`, and +`/observations/qpos`. +""" + +from __future__ import annotations + +from pathlib import Path + +import pyarrow as pa +import pytest +from rerun.experimental import Chunk, DatasetInfo, Hdf5Reader, IndexColumn + +HDF5_ASSETS_DIR = Path(__file__).resolve().parents[1] / "assets" / "hdf5" +TEST_DATA = HDF5_ASSETS_DIR / "test_data.h5" +TEST_DATA_MISALIGNED = HDF5_ASSETS_DIR / "test_data_misaligned.h5" + + +def _by_entity(chunks: list[Chunk], entity_path: str) -> Chunk: + matches = [c for c in chunks if c.entity_path == entity_path] + assert len(matches) == 1, f"expected exactly one chunk at {entity_path}, found {len(matches)}" + return matches[0] + + +# --------------------------------------------------------------------------- +# streaming +# --------------------------------------------------------------------------- + + +def test_stream_default_layout() -> None: + chunks = Hdf5Reader(TEST_DATA).stream().to_chunks() + + assert {c.entity_path for c in chunks} == { + "/", + "/observations", + "/observations/images", + "/meta", + "/__hdf5_properties", + "/__hdf5_properties/observations", + "/__hdf5_properties/observations/qpos", + } + + # Multi-dataset groups pack into a single `data` struct component. + root = _by_entity(chunks, "/") + assert root.num_rows == 5 + root_struct = root.to_record_batch().schema.field("data").type.value_type + assert root_struct.names == ["labels", "time"] + + observations = _by_entity(chunks, "/observations") + assert observations.num_rows == 5 + observations_struct = observations.to_record_batch().schema.field("data").type.value_type + assert observations_struct.names == ["qpos", "qvel"] + # 2-D [5, 3] → FixedSizeList<3> struct field; 1-D → scalar field. + assert observations_struct.field("qpos").type == pa.list_(pa.float64(), 3) + assert observations_struct.field("qvel").type == pa.float32() + + # A single-dataset group emits a bare component, not a one-field struct. + images = _by_entity(chunks, "/observations/images") + rb = images.to_record_batch() + assert "data" not in rb.schema.names + # 4-D [5, 2, 2, 3] → one 12-element row-major blob per row. + cam0 = rb.column("cam0").to_pylist() + assert len(cam0) == 5 + assert cam0[0] == [list(range(12))] + + # 0-D datasets are static. + meta = _by_entity(chunks, "/meta") + assert meta.is_static + assert meta.num_rows == 1 + assert meta.to_record_batch().column("count").to_pylist() == [[42]] + + +def test_stream_attribute_chunks() -> None: + chunks = Hdf5Reader(TEST_DATA).stream().to_chunks() + + root_props = _by_entity(chunks, "/__hdf5_properties") + assert root_props.is_static + rb = root_props.to_record_batch() + assert rb.column("description").to_pylist() == [["canonical re_hdf5 test fixture"]] + assert rb.column("version").to_pylist() == [[1]] + + observation_props = _by_entity(chunks, "/__hdf5_properties/observations") + rb = observation_props.to_record_batch() + assert rb.column("frequency").to_pylist() == [[30.0]] + # A float64[3] attribute maps to one fixed-size list row. + assert rb.schema.field("joints").type.value_type == pa.list_(pa.float64(), 3) + assert rb.column("joints").to_pylist() == [[[1.0, 2.0, 3.0]]] + + qpos_props = _by_entity(chunks, "/__hdf5_properties/observations/qpos") + assert qpos_props.to_record_batch().column("unit").to_pylist() == [["rad"]] + + +def test_use_structs_false() -> None: + chunks = Hdf5Reader(TEST_DATA).stream(use_structs=False).to_chunks() + + observations = _by_entity(chunks, "/observations") + rb = observations.to_record_batch() + assert "data" not in rb.schema.names + assert rb.column("qvel").to_pylist() == [[0.0], [1.0], [2.0], [3.0], [4.0]] + assert rb.column("qpos").to_pylist()[0] == [[0.0, 0.1, 0.2]] + + +def test_entity_path_prefix() -> None: + chunks = Hdf5Reader(TEST_DATA).stream(entity_path_prefix="/world").to_chunks() + entities = {c.entity_path for c in chunks} + assert "/world/observations" in entities + assert "/world/__hdf5_properties" in entities + assert all(e.startswith("/world") for e in entities) + + +def test_index_column() -> None: + chunks = Hdf5Reader(TEST_DATA).stream(index_column=IndexColumn.timestamp("/time", input_unit="s")).to_chunks() + + # The index is consumed: the root group is left with `labels` only, which + # emits as a bare component. + root = _by_entity(chunks, "/") + rb = root.to_record_batch() + assert "data" not in rb.schema.names + assert rb.column("labels").to_pylist() == [["idle"], ["reach"], ["grasp"], ["lift"], ["place"]] + + # The timeline is named after the index dataset's leaf, and float seconds + # scale to nanoseconds without losing sub-second precision. + assert rb.schema.field("time").type == pa.timestamp("ns") + assert rb.column("time").cast(pa.int64()).to_pylist() == [ + 0, + 500_000_000, + 1_000_000_000, + 1_500_000_000, + 2_000_000_000, + ] + + # The whole file shares the timeline. + observations = _by_entity(chunks, "/observations") + assert observations.to_record_batch().schema.field("time").type == pa.timestamp("ns") + + +def test_row_index_timeline_by_default() -> None: + chunks = Hdf5Reader(TEST_DATA).stream().to_chunks() + root = _by_entity(chunks, "/") + rb = root.to_record_batch() + assert rb.schema.field("row_index").type == pa.int64() + assert rb.column("row_index").to_pylist() == [0, 1, 2, 3, 4] + + +def test_ignore_datasets() -> None: + # Ignoring a group path excludes the whole subtree. + chunks = Hdf5Reader(TEST_DATA).stream(ignore_datasets=["/observations/images"]).to_chunks() + assert not any(c.entity_path == "/observations/images" for c in chunks) + + # Ignoring a single dataset flattens its group to the remaining dataset. + chunks = Hdf5Reader(TEST_DATA).stream(ignore_datasets=["/observations/qvel"]).to_chunks() + observations = _by_entity(chunks, "/observations") + rb = observations.to_record_batch() + assert "data" not in rb.schema.names + assert "qpos" in rb.schema.names + + +# --------------------------------------------------------------------------- +# metadata accessors +# --------------------------------------------------------------------------- + + +def test_groups() -> None: + reader = Hdf5Reader(TEST_DATA) + assert reader.groups() == ["/meta", "/observations", "/observations/images"] + assert reader.groups("/observations") == ["/observations/images"] + assert reader.groups("/observations/images") == [] + + +def test_datasets() -> None: + reader = Hdf5Reader(TEST_DATA) + infos = reader.datasets() + assert infos == [ + DatasetInfo(path="/labels", shape=(5,), dtype="string"), + DatasetInfo(path="/time", shape=(5,), dtype="float64"), + DatasetInfo(path="/meta/count", shape=(), dtype="int64"), + DatasetInfo(path="/observations/qpos", shape=(5, 3), dtype="float64"), + DatasetInfo(path="/observations/qvel", shape=(5,), dtype="float32"), + DatasetInfo(path="/observations/images/cam0", shape=(5, 2, 2, 3), dtype="uint8"), + ] + + under_group = reader.datasets("/observations/images") + assert under_group == [DatasetInfo(path="/observations/images/cam0", shape=(5, 2, 2, 3), dtype="uint8")] + + +def test_attributes() -> None: + reader = Hdf5Reader(TEST_DATA) + + assert reader.attributes() == { + "description": "canonical re_hdf5 test fixture", + "version": 1, + } + # Attribute order is not guaranteed by HDF5 — compare as a dict/set. + assert reader.attributes("/observations") == { + "frequency": 30.0, + "joints": [1.0, 2.0, 3.0], + } + assert reader.attributes("/observations/qpos") == {"unit": "rad"} + assert reader.attributes("/meta") == {} + + +def test_path_property_and_repr() -> None: + reader = Hdf5Reader(TEST_DATA) + assert reader.path == TEST_DATA + assert repr(reader) == f"Hdf5Reader({TEST_DATA})" + + +# --------------------------------------------------------------------------- +# error paths +# --------------------------------------------------------------------------- + + +def test_file_not_found(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError, match="not found"): + Hdf5Reader(tmp_path / "nonexistent.h5") + + +def test_misaligned_datasets_raise_value_error() -> None: + # Eager validation: the error surfaces at `stream()`, before iteration. + with pytest.raises(ValueError, match="not row-aligned"): + Hdf5Reader(TEST_DATA_MISALIGNED).stream() + + # Explicitly ignoring the offender resolves the mismatch. + chunks = Hdf5Reader(TEST_DATA_MISALIGNED).stream(ignore_datasets=["/b"]).to_chunks() + assert _by_entity(chunks, "/").num_rows == 4 + + +def test_bad_index_column_raises_value_error() -> None: + reader = Hdf5Reader(TEST_DATA) + + # (The timeline kind and unit are typed via `IndexColumn`, so a bad kind/unit + # is a type error rather than a runtime one; only path/shape issues reach here.) + with pytest.raises(ValueError, match="not found"): + reader.stream(index_column=IndexColumn.sequence("/missing")) + + with pytest.raises(ValueError, match="1-dimensional"): + reader.stream(index_column=IndexColumn.sequence("/observations/qpos")) + + +def test_missing_attribute_path_raises_key_error() -> None: + with pytest.raises(KeyError, match="not found"): + Hdf5Reader(TEST_DATA).attributes("/missing") + + with pytest.raises(KeyError, match="not found"): + Hdf5Reader(TEST_DATA).groups("/missing") diff --git a/rerun_py/tests/integration/test_headless_viewer.py b/rerun_py/tests/integration/test_headless_viewer.py new file mode 100644 index 000000000000..2749c34b6ce3 --- /dev/null +++ b/rerun_py/tests/integration/test_headless_viewer.py @@ -0,0 +1,99 @@ +"""Integration tests for the headless viewer spawned via the Python SDK.""" + +from __future__ import annotations + +import platform +import socket +import sys +import time +from typing import TYPE_CHECKING + +import pytest +import rerun as rr +from rerun.experimental import ViewerClient + +if TYPE_CHECKING: + from pathlib import Path + +# The wheel-test CI installs a software rasterizer only on linux-x64 (see +# `.github/workflows/rerun_reusable_test_wheels.yml`). On linux-arm64 the +# manylinux container has no Vulkan adapter, so the headless viewer panics on +# startup with "No graphics adapter was found". +pytestmark = pytest.mark.skipif( + sys.platform == "linux" and platform.machine() == "aarch64", + reason="no software rasterizer on linux-arm64 wheel-test runner", +) + + +def _find_free_port() -> int: + """Bind to port 0, read what the OS picked, then release it.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + port: int = s.getsockname()[1] + return port + + +def _wait_for_file(path: Path, timeout: float) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if path.exists() and path.stat().st_size > 0: + return + time.sleep(0.1) + raise TimeoutError(f"screenshot was never written to {path}") + + +def _wait_for_port(port: int, timeout: float) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(0.2) + try: + s.connect(("127.0.0.1", port)) + return + except OSError: + time.sleep(0.1) + raise TimeoutError(f"viewer never started listening on port {port}") + + +def _wait_for_port_closed(port: int, timeout: float) -> None: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(0.2) + try: + s.connect(("127.0.0.1", port)) + except OSError: + return + time.sleep(0.1) + raise TimeoutError(f"viewer still listening on port {port} after teardown") + + +@pytest.mark.skip(reason="RR-5124: linux wheel CI segfaults in llvmpipe/Mesa after the RunsOn AMI rollout") +def test_save_screenshot(tmp_path: Path) -> None: + """Log into a spawned headless viewer, then screenshot it to disk.""" + port = _find_free_port() + + with ViewerClient.spawn(headless=True, port=port, hide_welcome_screen=True) as viewer: + rec = rr.RecordingStream("rerun_example_headless_test") + rec.connect_grpc(url=viewer.url) + rec.log("points", rr.Points3D([[0, 0, 0], [1, 1, 1], [2, 0, 1]], colors=[255, 0, 0])) + rec.flush() + + out = tmp_path / "screenshot.png" + viewer.save_screenshot(str(out)) + _wait_for_file(out, timeout=5.0) + + # PNG magic number: 89 50 4E 47 0D 0A 1A 0A + with out.open("rb") as f: + assert f.read(8) == b"\x89PNG\r\n\x1a\n" + + +def test_viewer_dies_on_client_close() -> None: + """Closing the ViewerClient should kill the viewer it spawned.""" + port = _find_free_port() + viewer = ViewerClient.spawn(headless=True, port=port, hide_welcome_screen=True) + + _wait_for_port(port, timeout=30.0) + viewer.close() + # SIGTERM lands; the viewer should release the port within a few seconds. + _wait_for_port_closed(port, timeout=15.0) diff --git a/rerun_py/tests/integration/test_lazy_chunk_store.py b/rerun_py/tests/integration/test_lazy_chunk_store.py index e127a6e23952..534c6e62d021 100644 --- a/rerun_py/tests/integration/test_lazy_chunk_store.py +++ b/rerun_py/tests/integration/test_lazy_chunk_store.py @@ -1,4 +1,4 @@ -"""Integration tests for lazy ChunkStore loading.""" +"""Integration tests for LazyStore.""" from __future__ import annotations @@ -7,13 +7,16 @@ import pytest import rerun as rr from rerun.experimental import ( - OptimizationSettings, + LazyStore, + OptimizationProfile, RrdReader, ) if TYPE_CHECKING: from pathlib import Path + from syrupy.assertion import SnapshotAssertion + LAZY_RRD_APPLICATION_ID = "rerun_example_lazy_test_app" LAZY_RRD_RECORDING_ID = "lazy-rrd-rec-id" @@ -37,6 +40,12 @@ def lazy_rrd_path(tmp_path_factory: pytest.TempPathFactory) -> Path: return rrd_path +def test_store_returns_lazy_store(lazy_rrd_path: Path) -> None: + """RrdReader.store() returns a LazyStore.""" + store = RrdReader(lazy_rrd_path).store() + assert isinstance(store, LazyStore) + + def test_lazy_store_has_schema(lazy_rrd_path: Path) -> None: """Lazy store should have a schema even before loading chunk data.""" store = RrdReader(lazy_rrd_path).store() @@ -86,15 +95,74 @@ def test_lazy_store_filter(lazy_rrd_path: Path) -> None: assert str(chunk.entity_path) == "/entity_0" +def test_lazy_store_filter_only_loads_matching(lazy_rrd_path: Path) -> None: + """ + Filter pushdown must actually skip non-matching chunks at the I/O layer. + + Without pushdown, the engine would `load_chunks()` for every chunk in the manifest and + then drop non-matching ones in a post-source `FilterStream` — same observable output + (correct chunks returned), but every chunk paid I/O. The `_chunks_loaded` counter on + `LazyStore` distinguishes the two: pushdown means `_chunks_loaded == len(filtered)`. + """ + store = RrdReader(lazy_rrd_path).store() + total = len(store) + + # Nothing loaded yet — manifest is in memory but no chunk data has been read. + assert store._chunks_loaded == 0 + + filtered = store.stream().filter(content="/entity_0").to_chunks() + + assert len(filtered) > 0, "fixture should yield at least one /entity_0 chunk" + assert len(filtered) < total, "fixture should have non-/entity_0 chunks too" + assert store._chunks_loaded == len(filtered), ( + f"pushdown should have loaded only the {len(filtered)} matching chunks, " + f"but {store._chunks_loaded} of {total} were loaded" + ) + + +def test_lazy_store_filter_is_static(test_rrd_path: Path) -> None: + """ + `is_static=True` on a lazy store's stream returns only static chunks. + + Uses `test_rrd_path` (from `conftest.py`) because it includes a static `/config` entity; + `lazy_rrd_path` is temporal-only. + """ + chunks = RrdReader(test_rrd_path).store().stream().filter(is_static=True).to_chunks() + + assert chunks, "expected at least one static chunk (e.g. /config)" + for chunk in chunks: + assert chunk.is_static, f"unexpected non-static chunk at {chunk.entity_path}" + + def test_lazy_store_collect_optimize(lazy_rrd_path: Path) -> None: """Collecting a lazy store with optimization settings produces a materialized store.""" store = RrdReader(lazy_rrd_path).store() - optimized = store.stream().collect(optimize=OptimizationSettings()) + optimized = store.stream().collect(optimize=OptimizationProfile()) chunks = optimized.stream().to_chunks() assert len(chunks) > 0 +def test_summary_round_trip(lazy_rrd_path: Path) -> None: + """ + `lazy.summary()` matches `lazy.stream().collect().summary()` byte-for-byte. + + Caveat: `collect()` runs single-pass insert-time compaction at default config, + so this only holds when the source RRD is already optimized (no chunks + mergeable under default `ChunkStoreConfig`). The `lazy_rrd_path` fixture + uses one `send_columns` call per entity, producing exactly one chunk each + — already as merged as collect can make them. + """ + lazy = RrdReader(lazy_rrd_path).store() + assert lazy.summary() == lazy.stream().collect().summary() + + +def test_summary_format(lazy_rrd_path: Path, snapshot: SnapshotAssertion) -> None: + """Snapshot the manifest-derived summary so the format stays stable.""" + lazy = RrdReader(lazy_rrd_path).store() + assert lazy.summary() == snapshot + + def test_multiple_store_calls(lazy_rrd_path: Path) -> None: """Multiple .store() calls should return independent stores.""" reader = RrdReader(lazy_rrd_path) @@ -110,8 +178,10 @@ def test_multiple_store_calls(lazy_rrd_path: Path) -> None: def test_store_properties(lazy_rrd_path: Path) -> None: """Application and recording IDs should be accessible.""" reader = RrdReader(lazy_rrd_path) - assert reader.application_id == LAZY_RRD_APPLICATION_ID - assert reader.recording_id == LAZY_RRD_RECORDING_ID + recs = reader.recordings() + assert len(recs) == 1 + assert recs[0].application_id == LAZY_RRD_APPLICATION_ID + assert recs[0].recording_id == LAZY_RRD_RECORDING_ID # Store should also work. store = reader.store() diff --git a/rerun_py/tests/integration/test_lazy_chunk_stream.py b/rerun_py/tests/integration/test_lazy_chunk_stream.py index 3d5063cb78f8..c7dea1b2d963 100644 --- a/rerun_py/tests/integration/test_lazy_chunk_stream.py +++ b/rerun_py/tests/integration/test_lazy_chunk_stream.py @@ -9,7 +9,7 @@ import pytest import rerun as rr from inline_snapshot import snapshot as inline_snapshot -from rerun.experimental import Chunk, LazyChunkStream, Lens, LensOutput, RrdReader, Selector +from rerun.experimental import Chunk, DeriveLens, LazyChunkStream, MutateLens, RrdReader, Selector from .conftest import TEST_APP_ID as APP_ID, TEST_RECORDING_ID as RECORDING_ID @@ -24,8 +24,10 @@ def test_rrd_reader_properties(test_rrd_path: Path) -> None: reader = RrdReader(test_rrd_path) - assert reader.application_id == APP_ID - assert reader.recording_id == RECORDING_ID + recs = reader.recordings() + assert len(recs) == 1 + assert recs[0].application_id == APP_ID + assert recs[0].recording_id == RECORDING_ID def test_rrd_reader_file_not_found(tmp_path: Path) -> None: @@ -348,28 +350,22 @@ def test_terminal_does_not_consume(test_rrd_path: Path) -> None: def test_lenses_identity(test_rrd_path: Path) -> None: """A lens with Selector('.') passes through the struct component data unchanged.""" - lens = Lens( - "Imu:accel", - LensOutput().to_component("Imu:accel", Selector(".")), - ) + lens = MutateLens("Imu:accel", Selector(".")) store = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lens).collect() assert store.summary() == inline_snapshot( - "/sensors/imu rows=2 bytes=1.6 KiB static=False timelines=['my_index'] cols=['Imu:accel', 'my_index']" + "/sensors/imu rows=2 static=False timelines=['my_index'] cols=['Imu:accel', 'my_index']" ) def test_lenses_field_selector(test_rrd_path: Path) -> None: """A lens with Selector('.x') extracts a struct field and reinterprets it as a Rerun Scalar.""" - lens = Lens( - "Imu:accel", - LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".x")), - ) + lens = DeriveLens("Imu:accel").to_component(rr.Scalars.descriptor_scalars(), Selector(".x")) store = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lens).collect() assert store.summary() == inline_snapshot( - "/sensors/imu rows=2 bytes=1.5 KiB static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']" + "/sensors/imu rows=2 static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']" ) # Verify the extracted values are correct @@ -382,28 +378,22 @@ def test_lenses_field_selector(test_rrd_path: Path) -> None: def test_lenses_multiple_outputs(test_rrd_path: Path) -> None: """A single lens can produce multiple output groups at different entity paths.""" - lens = Lens( - "Imu:accel", - to_entity={ - "/out/x": LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".x")), - "/out/z": LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".z")), - }, - ) + lenses = [ + DeriveLens("Imu:accel", output_entity="/out/x").to_component(rr.Scalars.descriptor_scalars(), Selector(".x")), + DeriveLens("Imu:accel", output_entity="/out/z").to_component(rr.Scalars.descriptor_scalars(), Selector(".z")), + ] - store = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lens).collect() + store = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lenses).collect() assert store.summary() == inline_snapshot("""\ -/out/x rows=2 bytes=1.5 KiB static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index'] -/out/z rows=2 bytes=1.5 KiB static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']\ +/out/x rows=2 static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index'] +/out/z rows=2 static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']\ """) def test_lenses_drop_unmatched(test_rrd_path: Path) -> None: """With drop_unmatched (default), unmatched chunks are not forwarded.""" - lens = Lens( - "nonexistent:Component:foo", - LensOutput().to_component("out:Component:bar", Selector(".")), - ) + lens = DeriveLens("nonexistent:Component:foo").to_component("out:Component:bar", Selector(".")) store = RrdReader(test_rrd_path).stream().lenses(lens, output_mode="drop_unmatched").collect() assert store.summary() == inline_snapshot("") @@ -412,11 +402,8 @@ def test_lenses_drop_unmatched(test_rrd_path: Path) -> None: def test_lenses_forward_unmatched(test_rrd_path: Path) -> None: """With forward_unmatched, transformed chunks replace originals and unmatched chunks pass through.""" - lens = Lens( - "Imu:accel", - to_entity={ - "/transformed": LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".x")), - }, + lens = DeriveLens("Imu:accel", output_entity="/transformed").to_component( + rr.Scalars.descriptor_scalars(), Selector(".x") ) store = ( @@ -427,21 +414,18 @@ def test_lenses_forward_unmatched(test_rrd_path: Path) -> None: .collect() ) assert store.summary() == inline_snapshot("""\ -/cameras/front rows=1 bytes=1.5 KiB static=False timelines=['my_index'] cols=['TextLog:text', 'my_index'] -/config rows=1 bytes=1.1 KiB static=True timelines=[] cols=['TextLog:text'] -/robots/arm rows=2 bytes=1.6 KiB static=False timelines=['my_index', 'other_timeline'] cols=['Points3D:colors', 'Points3D:positions', 'my_index', 'other_timeline'] -/transformed rows=2 bytes=1.5 KiB static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']\ +/cameras/front rows=1 static=False timelines=['my_index'] cols=['TextLog:text', 'my_index'] +/config rows=1 static=True timelines=[] cols=['TextLog:text'] +/robots/arm rows=2 static=False timelines=['my_index', 'other_timeline'] cols=['Points3D:colors', 'Points3D:positions', 'my_index', 'other_timeline'] +/transformed rows=2 static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']\ """) def test_lenses_forward_all(test_rrd_path: Path) -> None: """With forward_all, both transformed and original data are forwarded.""" - lens = Lens( - "Imu:accel", - to_entity={ - "/transformed": LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".x")), - }, + lens = DeriveLens("Imu:accel", output_entity="/transformed").to_component( + rr.Scalars.descriptor_scalars(), Selector(".x") ) store = ( @@ -452,21 +436,18 @@ def test_lenses_forward_all(test_rrd_path: Path) -> None: .collect() ) assert store.summary() == inline_snapshot("""\ -/cameras/front rows=1 bytes=1.5 KiB static=False timelines=['my_index'] cols=['TextLog:text', 'my_index'] -/config rows=1 bytes=1.1 KiB static=True timelines=[] cols=['TextLog:text'] -/robots/arm rows=2 bytes=1.6 KiB static=False timelines=['my_index', 'other_timeline'] cols=['Points3D:colors', 'Points3D:positions', 'my_index', 'other_timeline'] -/sensors/imu rows=2 bytes=1.6 KiB static=False timelines=['my_index'] cols=['Imu:accel', 'my_index'] -/transformed rows=2 bytes=1.5 KiB static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']\ +/cameras/front rows=1 static=False timelines=['my_index'] cols=['TextLog:text', 'my_index'] +/config rows=1 static=True timelines=[] cols=['TextLog:text'] +/robots/arm rows=2 static=False timelines=['my_index', 'other_timeline'] cols=['Points3D:colors', 'Points3D:positions', 'my_index', 'other_timeline'] +/sensors/imu rows=2 static=False timelines=['my_index'] cols=['Imu:accel', 'my_index'] +/transformed rows=2 static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']\ """) def test_lenses_consumes_stream(test_rrd_path: Path) -> None: """Calling .lenses() consumes the stream (move semantics).""" - lens = Lens( - "Imu:accel", - LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".x")), - ) + lens = DeriveLens("Imu:accel").to_component(rr.Scalars.descriptor_scalars(), Selector(".x")) stream = RrdReader(test_rrd_path).stream() _transformed = stream.lenses(lens) @@ -478,25 +459,19 @@ def test_lenses_consumes_stream(test_rrd_path: Path) -> None: def test_lenses_chained_with_filter(test_rrd_path: Path) -> None: """Lenses can be composed with filter in a pipeline.""" - lens = Lens( - "Imu:accel", - LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".z")), - ) + lens = DeriveLens("Imu:accel").to_component(rr.Scalars.descriptor_scalars(), Selector(".z")) store = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lens).collect() assert store.summary() == inline_snapshot( - "/sensors/imu rows=2 bytes=1.5 KiB static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']" + "/sensors/imu rows=2 static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']" ) def test_lenses_content_filter_match(test_rrd_path: Path) -> None: """With `content` set to a matching path, lenses apply only to those chunks; others pass through.""" - lens = Lens( - "Imu:accel", - to_entity={ - "/transformed": LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".x")), - }, + lens = DeriveLens("Imu:accel", output_entity="/transformed").to_component( + rr.Scalars.descriptor_scalars(), Selector(".x") ) store = ( @@ -509,21 +484,18 @@ def test_lenses_content_filter_match(test_rrd_path: Path) -> None: # /sensors/imu was matched by content -> lens applied -> produced /transformed. # All other chunks pass through unchanged regardless of `drop_unmatched`. assert store.summary() == inline_snapshot("""\ -/cameras/front rows=1 bytes=1.5 KiB static=False timelines=['my_index'] cols=['TextLog:text', 'my_index'] -/config rows=1 bytes=1.1 KiB static=True timelines=[] cols=['TextLog:text'] -/robots/arm rows=2 bytes=1.6 KiB static=False timelines=['my_index', 'other_timeline'] cols=['Points3D:colors', 'Points3D:positions', 'my_index', 'other_timeline'] -/transformed rows=2 bytes=1.5 KiB static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']\ +/cameras/front rows=1 static=False timelines=['my_index'] cols=['TextLog:text', 'my_index'] +/config rows=1 static=True timelines=[] cols=['TextLog:text'] +/robots/arm rows=2 static=False timelines=['my_index', 'other_timeline'] cols=['Points3D:colors', 'Points3D:positions', 'my_index', 'other_timeline'] +/transformed rows=2 static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']\ """) def test_lenses_content_filter_excludes_lens_target(test_rrd_path: Path) -> None: """Chunks outside the `content` scope bypass the lens and pass through, regardless of output_mode.""" - lens = Lens( - "Imu:accel", - to_entity={ - "/transformed": LensOutput().to_component(rr.Scalars.descriptor_scalars(), Selector(".x")), - }, + lens = DeriveLens("Imu:accel", output_entity="/transformed").to_component( + rr.Scalars.descriptor_scalars(), Selector(".x") ) # Content scope only includes /robots/**, so /sensors/imu is bypassed entirely @@ -546,10 +518,7 @@ def test_lenses_content_filter_excludes_lens_target(test_rrd_path: Path) -> None def test_lenses_invalid_output_mode(test_rrd_path: Path) -> None: """Invalid output_mode string raises ValueError.""" - lens = Lens( - "Points3D:positions", - LensOutput().to_component("Points3D:positions", Selector(".")), - ) + lens = DeriveLens("Points3D:positions").to_component("Points3D:positions", Selector(".")) with pytest.raises(ValueError, match="Unknown output_mode"): RrdReader(test_rrd_path).stream().lenses(lens, output_mode="invalid") # type: ignore[arg-type] @@ -558,16 +527,15 @@ def test_lenses_invalid_output_mode(test_rrd_path: Path) -> None: def test_lenses_time_extraction(test_rrd_path: Path) -> None: """A lens can extract a timestamp field from a struct component as a new timeline.""" - lens = Lens( - "Imu:accel", - LensOutput() + lens = ( + DeriveLens("Imu:accel") .to_component(rr.Scalars.descriptor_scalars(), Selector(".x")) - .to_timeline("sensor_time", "timestamp_ns", Selector(".timestamp")), + .to_timeline("sensor_time", "timestamp_ns", Selector(".timestamp")) ) store = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lens).collect() assert store.summary() == inline_snapshot( - "/sensors/imu rows=2 bytes=1.5 KiB static=False timelines=['my_index', 'sensor_time'] cols=['Scalars:scalars', 'my_index', 'sensor_time']" + "/sensors/imu rows=2 static=False timelines=['my_index', 'sensor_time'] cols=['Scalars:scalars', 'my_index', 'sensor_time']" ) chunks = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lens).to_chunks() @@ -584,14 +552,11 @@ def test_lenses_dynamic_selector(test_rrd_path: Path) -> None: selector = Selector(".x").pipe(lambda arr: pc.multiply(arr, 2.0)) - lens = Lens( - "Imu:accel", - LensOutput().to_component(rr.Scalars.descriptor_scalars(), selector), - ) + lens = DeriveLens("Imu:accel").to_component(rr.Scalars.descriptor_scalars(), selector) store = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lens).collect() assert store.summary() == inline_snapshot( - "/sensors/imu rows=2 bytes=1.5 KiB static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']" + "/sensors/imu rows=2 static=False timelines=['my_index'] cols=['Scalars:scalars', 'my_index']" ) chunks = RrdReader(test_rrd_path).stream().filter(content="/sensors/**").lenses(lens).to_chunks() diff --git a/rerun_py/tests/integration/test_mcap_reader.py b/rerun_py/tests/integration/test_mcap_reader.py index 15699400495a..5fc7f8a7d587 100644 --- a/rerun_py/tests/integration/test_mcap_reader.py +++ b/rerun_py/tests/integration/test_mcap_reader.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +from collections import Counter from pathlib import Path from typing import TYPE_CHECKING @@ -191,6 +192,90 @@ def test_topic_filter_invalid_regex() -> None: McapReader(POINT_CLOUD_MCAP, exclude_topic_regex=["["]) +# --------------------------------------------------------------------------- +# Time-range filter +# --------------------------------------------------------------------------- + + +def _temporal_rows_by_entity(chunks: list[Chunk]) -> Counter[str]: + """Total non-static rows per entity — invariant under chunking and RowId regeneration.""" + counts: Counter[str] = Counter() + for chunk in chunks: + if not chunk.is_static: + counts[chunk.entity_path] += chunk.num_rows + return counts + + +def test_stream_time_range_override_matches_constructor() -> None: + """`stream(start/end)` restricts the scan identically to the constructor bounds.""" + lo, hi = McapReader(LOG_MCAP).time_bounds() + half = lo + (hi - lo) // 2 + 1 # exclusive end below `hi`, so at least the last message drops + + by_ctor = _temporal_rows_by_entity(McapReader(LOG_MCAP, start_time_ns=lo, end_time_ns=half).stream().to_chunks()) + by_override = _temporal_rows_by_entity(McapReader(LOG_MCAP).stream(start_time_ns=lo, end_time_ns=half).to_chunks()) + full = _temporal_rows_by_entity(McapReader(LOG_MCAP).stream().to_chunks()) + + assert by_override == by_ctor + assert 0 < sum(by_override.values()) < sum(full.values()) + + +def test_empty_time_range_rejected() -> None: + """A half-open `[t, t)` range is empty and rejected, on both the constructor and `stream`.""" + lo, _ = McapReader(LOG_MCAP).time_bounds() + with pytest.raises(ValueError, match="must be less than"): + McapReader(LOG_MCAP, start_time_ns=lo, end_time_ns=lo) + with pytest.raises(ValueError, match="must be less than"): + McapReader(LOG_MCAP).stream(start_time_ns=lo, end_time_ns=lo) + + +# --------------------------------------------------------------------------- +# Summary recovery (truncated / summary-less files) +# --------------------------------------------------------------------------- + + +def _truncate_before_summary(src: Path, dst: Path) -> None: + """ + Write `dst` as a copy of `src` with its summary section, footer, and end magic removed. + + The MCAP footer is a fixed record at the very end of the file: an 8-byte end magic + preceded by a 20-byte footer body whose first `u64` is `summary_start`. Cutting the + file at `summary_start` keeps the entire data section (all chunks and their message + indexes) but leaves no summary for the normal reader to find — the same shape as a + recording interrupted mid-write. + """ + data = src.read_bytes() + summary_start = int.from_bytes(data[-28:-20], "little") + assert 0 < summary_start < len(data), "unexpected footer layout in test asset" + dst.write_bytes(data[:summary_start]) + + +def test_recover_truncated_matches_healthy(tmp_path: Path) -> None: + """Truncated-before-summary file recovers to the same messages and time bounds as the intact file.""" + truncated = tmp_path / "truncated.mcap" + _truncate_before_summary(POINT_CLOUD_MCAP, truncated) + + healthy = McapReader(POINT_CLOUD_MCAP).stream().to_chunks() + recovered = McapReader(truncated, recover=True).stream().to_chunks() + + # Every message on every topic is recovered (invariant under chunking / RowId regeneration). + assert _temporal_rows_by_entity(recovered) == _temporal_rows_by_entity(healthy) + assert sum(_temporal_rows_by_entity(recovered).values()) > 0 + + # Time bounds come from a decompression-free chunk-index scan and must match the intact file. + assert McapReader(truncated, recover=True).time_bounds() == McapReader(POINT_CLOUD_MCAP).time_bounds() + + +def test_truncated_without_recover_raises(tmp_path: Path) -> None: + """Without `recover`, a missing summary is a hard error on both `stream` and `time_bounds`.""" + truncated = tmp_path / "truncated.mcap" + _truncate_before_summary(POINT_CLOUD_MCAP, truncated) + + with pytest.raises(ValueError): + McapReader(truncated).stream().to_chunks() + with pytest.raises(ValueError): + McapReader(truncated).time_bounds() + + # --------------------------------------------------------------------------- # StreamingReader protocol conformance # --------------------------------------------------------------------------- diff --git a/rerun_py/tests/integration/test_mp4_reader.py b/rerun_py/tests/integration/test_mp4_reader.py new file mode 100644 index 000000000000..4760c6c0661e --- /dev/null +++ b/rerun_py/tests/integration/test_mp4_reader.py @@ -0,0 +1,330 @@ +"""Integration tests for `rerun.experimental.Mp4Reader`.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +import pytest +from rerun.components import VideoCodec +from rerun.experimental import Chunk, Mp4Reader, Mp4TranscodeOptions, StreamingReader + +VIDEO_ASSETS_DIR = Path(__file__).resolve().parents[3] / "tests" / "assets" / "video" + +# H.264 fixture encoded without B-frames — usable in both modes. +H264_NO_BFRAMES = VIDEO_ASSETS_DIR / "Big_Buck_Bunny_1080_1s_h264_nobframes.mp4" + +# Same content but encoded with B-frames — stream mode transcodes it with ffmpeg +# to strip the B-frames; asset mode is unaffected. +H264_WITH_BFRAMES = VIDEO_ASSETS_DIR / "Big_Buck_Bunny_1080_1s_h264.mp4" + +_HAS_FFMPEG = shutil.which("ffmpeg") is not None + + +def _cols(chunk: Chunk) -> list[str]: + """Return component column names on a chunk (excludes time and control columns).""" + rb = chunk.to_record_batch() + timelines = set(chunk.timeline_names) + return sorted(f.name for f in rb.schema if f.name not in timelines and not f.name.startswith("rerun.controls")) + + +# --------------------------------------------------------------------------- +# Motivating example: parse an mp4 file into a VideoStream +# --------------------------------------------------------------------------- + + +def test_default_mode_produces_video_stream_chunks() -> None: + """The motivating example: point Mp4Reader at a video file and get back a VideoStream.""" + chunks = Mp4Reader(H264_NO_BFRAMES).stream().to_chunks() + + # Stream-mode output is structured as 1 static codec chunk + N GOP chunks. + assert len(chunks) >= 2, "stream mode should emit at least the static codec chunk plus one GOP" + + static_chunks = [c for c in chunks if c.is_static] + temporal_chunks = [c for c in chunks if not c.is_static] + + # Exactly one static chunk holding the codec. + assert len(static_chunks) == 1 + static = static_chunks[0] + assert static.entity_path.endswith("/Big_Buck_Bunny_1080_1s_h264_nobframes.mp4") + assert static.num_rows == 1 + assert any("codec" in name.lower() for name in _cols(static)), ( + f"expected a codec column on the static chunk; got {_cols(static)}" + ) + + # Every per-GOP chunk carries sample bytes + an is_keyframe flag on the + # "video" duration timeline, and the first row of each GOP is a keyframe. + for c in temporal_chunks: + assert c.timeline_names == ["video"], f"expected ['video'] timeline, got {c.timeline_names}" + assert c.num_rows >= 1 + col_names = _cols(c) + sample_col = next((n for n in col_names if "sample" in n.lower()), None) + keyframe_col = next((n for n in col_names if "keyframe" in n.lower()), None) + assert sample_col is not None, f"expected a sample column; got {col_names}" + assert keyframe_col is not None, f"expected an is_keyframe column; got {col_names}" + + rb = c.to_record_batch() + # `is_keyframe` is stored as a list-per-row component column (`List[bool]`). + first_keyframe = rb.column(keyframe_col)[0].as_py() + assert first_keyframe == [True], f"first row of a GOP chunk should be a keyframe, got {first_keyframe!r}" + + +# --------------------------------------------------------------------------- +# Asset mode — matches the existing `rerun video.mp4` behavior +# --------------------------------------------------------------------------- + + +def test_asset_mode_emits_asset_video() -> None: + """Asset mode produces an AssetVideo blob chunk plus a VideoFrameReference index chunk.""" + chunks = Mp4Reader(H264_NO_BFRAMES, mode="asset").stream().to_chunks() + + assert 1 <= len(chunks) <= 2, "asset mode emits 1 (blob only) or 2 (blob + index) chunks" + for c in chunks: + assert c.entity_path.endswith("/Big_Buck_Bunny_1080_1s_h264_nobframes.mp4") + + has_asset_video = any(any("AssetVideo" in name for name in _cols(c)) for c in chunks) + assert has_asset_video, "asset mode should emit an AssetVideo chunk" + + +# --------------------------------------------------------------------------- +# chunk_by_gop toggle +# --------------------------------------------------------------------------- + + +def test_stream_mode_chunk_by_gop_false_emits_one_sample_per_chunk() -> None: + """With chunk_by_gop=False, every temporal chunk is exactly one sample.""" + chunks = Mp4Reader(H264_NO_BFRAMES, chunk_by_gop=False).stream().to_chunks() + temporal = [c for c in chunks if not c.is_static] + assert len(temporal) > 0 + for c in temporal: + assert c.num_rows == 1, f"chunk_by_gop=False should give 1 row per chunk; got {c.num_rows}" + + +def test_stream_mode_chunk_by_gop_true_packs_multiple_samples() -> None: + """With chunk_by_gop=True (default), at least one GOP chunk should hold >1 sample.""" + chunks = Mp4Reader(H264_NO_BFRAMES).stream().to_chunks() + temporal = [c for c in chunks if not c.is_static] + assert any(c.num_rows > 1 for c in temporal), ( + "expected at least one GOP chunk with multiple samples — the test fixture has GOPs > 1 frame" + ) + + +# --------------------------------------------------------------------------- +# entity_path override +# --------------------------------------------------------------------------- + + +def test_custom_entity_path_applies_to_every_chunk() -> None: + chunks = Mp4Reader(H264_NO_BFRAMES, entity_path="/cameras/front").stream().to_chunks() + assert len(chunks) > 0 + for c in chunks: + assert c.entity_path == "/cameras/front" + + +def test_default_entity_path_derives_from_file_path() -> None: + """Default `entity_path=None` uses the absolute filesystem path as the entity hierarchy.""" + chunks = Mp4Reader(H264_NO_BFRAMES, mode="asset").stream().to_chunks() + for c in chunks: + assert c.entity_path.endswith("/Big_Buck_Bunny_1080_1s_h264_nobframes.mp4") + + +def test_relative_path_is_absolutized(monkeypatch: pytest.MonkeyPatch) -> None: + """A relative source path is resolved to absolute for both `.path` and the default entity path.""" + monkeypatch.chdir(H264_NO_BFRAMES.parent) + reader = Mp4Reader(Path(H264_NO_BFRAMES.name), mode="asset") + + assert Path(reader.path).is_absolute() + assert Path(reader.path) == H264_NO_BFRAMES + # The default entity path reflects the absolute path (parent dirs included), + # not just the bare filename that was passed in. + entity_path = reader.stream().to_chunks()[0].entity_path + assert "/tests/assets/video/" in entity_path + assert entity_path.endswith("/Big_Buck_Bunny_1080_1s_h264_nobframes.mp4") + + +# --------------------------------------------------------------------------- +# Error handling +# --------------------------------------------------------------------------- + + +def test_b_frames_without_ffmpeg_reports_missing_ffmpeg() -> None: + """ + A missing ffmpeg surfaces the "not installed" error rather than silently succeeding. + + We force the missing-ffmpeg case with a bogus `ffmpeg_override` so this is + deterministic regardless of whether ffmpeg is installed on the test machine. + """ + with pytest.raises(RuntimeError, match="Couldn't find an installation of the FFmpeg executable"): + # The error is raised eagerly inside the loader thread; we surface it on + # the first pull, so iterating is enough to trigger it. + list( + Mp4Reader( + H264_WITH_BFRAMES, + transcode=Mp4TranscodeOptions(ffmpeg_override="/definitely/not/a/real/ffmpeg"), + ).stream() + ) + + +def test_b_frames_in_asset_mode_are_fine() -> None: + """Asset mode is unaffected by B-frames.""" + chunks = Mp4Reader(H264_WITH_BFRAMES, mode="asset").stream().to_chunks() + assert len(chunks) >= 1 + + +def test_file_not_found(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError, match="not found"): + Mp4Reader(tmp_path / "nonexistent.mp4") + + +def test_invalid_mode() -> None: + with pytest.raises(ValueError, match="Invalid mode"): + Mp4Reader(H264_NO_BFRAMES, mode="bogus") # type: ignore[call-overload] + + +def test_chunk_by_gop_false_with_asset_mode_rejected() -> None: + """chunk_by_gop only makes sense in stream mode — passing it for asset mode is a user error.""" + with pytest.raises(ValueError, match="chunk_by_gop"): + Mp4Reader(H264_NO_BFRAMES, mode="asset", chunk_by_gop=False) # type: ignore[call-overload] + + +def test_invalid_timeline_type() -> None: + with pytest.raises(ValueError, match="Invalid timeline_type"): + Mp4Reader(H264_NO_BFRAMES, timeline_type="sequence") # type: ignore[call-overload] + + +# --------------------------------------------------------------------------- +# timeline_type — schema-level check +# --------------------------------------------------------------------------- + + +def test_timeline_type_timestamp_produces_timestamp_typed_column() -> None: + """`timeline_type="timestamp"` switches the time column from duration[ns] to timestamp[ns].""" + import pyarrow as pa + + chunks = Mp4Reader(H264_NO_BFRAMES, timeline_name="real_time", timeline_type="timestamp").stream().to_chunks() + temporal = [c for c in chunks if not c.is_static] + assert len(temporal) > 0 + rb = temporal[0].to_record_batch() + ts_field = next(f for f in rb.schema if f.name == "real_time") + # nanosecond-precision Arrow timestamp (with or without a tz attached). + assert pa.types.is_timestamp(ts_field.type), f"expected a timestamp[ns] column, got {ts_field.type}" + + +def test_asset_mode_timeline_type_timestamp_applies_to_index_chunk() -> None: + """`timeline_type` also types the asset-mode `VideoFrameReference` index timeline.""" + import pyarrow as pa + + chunks = ( + Mp4Reader(H264_NO_BFRAMES, mode="asset", timeline_name="real_time", timeline_type="timestamp") + .stream() + .to_chunks() + ) + # The index chunk is the one carrying the `real_time` timeline. + index_chunks = [c for c in chunks if "real_time" in c.timeline_names] + assert len(index_chunks) == 1, "asset mode should emit one VideoFrameReference index chunk" + rb = index_chunks[0].to_record_batch() + ts_field = next(f for f in rb.schema if f.name == "real_time") + assert pa.types.is_timestamp(ts_field.type), f"expected a timestamp[ns] column, got {ts_field.type}" + + +# --------------------------------------------------------------------------- +# B-frame sources are transcoded via ffmpeg +# --------------------------------------------------------------------------- + + +@pytest.mark.skipif(not _HAS_FFMPEG, reason="ffmpeg not installed") +def test_b_frames_are_transcoded_into_a_video_stream() -> None: + """ + A B-frame mp4 in stream mode is transcoded with ffmpeg into a normal `VideoStream`. + + It yields a static codec chunk plus per-GOP sample chunks — the same shape as + the no-B-frame happy path. + """ + chunks = Mp4Reader(H264_WITH_BFRAMES).stream().to_chunks() + static_chunks = [c for c in chunks if c.is_static] + temporal_chunks = [c for c in chunks if not c.is_static] + assert len(static_chunks) == 1 + assert len(temporal_chunks) > 0 + for c in temporal_chunks: + assert c.timeline_names == ["video"] + + +# --------------------------------------------------------------------------- +# Transcode transforms — output_codec / gop_size (stream mode only) +# --------------------------------------------------------------------------- + + +def _to_chunks_or_skip(reader: Mp4Reader) -> list[Chunk]: + """Materialize a transcoding stream, skipping when the encoder is unavailable.""" + try: + return reader.stream().to_chunks() + except RuntimeError as err: + if "encoder" in str(err) or "FFmpeg" in str(err): + pytest.skip(f"ffmpeg/encoder not available: {err}") + raise + + +def test_output_codec_same_as_source_stays_on_the_direct_path() -> None: + chunks = ( + Mp4Reader( + H264_NO_BFRAMES, + transcode=Mp4TranscodeOptions( + output_codec=VideoCodec.H264, + ffmpeg_override="/definitely/not/a/real/ffmpeg", + ), + ) + .stream() + .to_chunks() + ) + assert len([c for c in chunks if c.is_static]) == 1 + assert any(not c.is_static for c in chunks) + + +def test_invalid_output_codec_rejected() -> None: + with pytest.raises(TypeError, match="VideoCodec"): + Mp4TranscodeOptions(output_codec="av1") # type: ignore[arg-type] + + +def test_transcode_rejected_in_asset_mode() -> None: + """`transcode` is stream-only — passing it for asset mode is a user error.""" + with pytest.raises(ValueError, match="transcode"): + Mp4Reader(H264_NO_BFRAMES, mode="asset", transcode=Mp4TranscodeOptions(output_codec=VideoCodec.AV1)) # type: ignore[call-overload] + + +@pytest.mark.skipif(not _HAS_FFMPEG, reason="ffmpeg not installed") +@pytest.mark.parametrize("output_codec", [VideoCodec.AV1, VideoCodec.VP9, VideoCodec.H265]) +def test_output_codec_transcodes_to_requested_codec(output_codec: VideoCodec) -> None: + """Re-encoding a clean H.264 source to another codec yields a normal `VideoStream`.""" + chunks = _to_chunks_or_skip(Mp4Reader(H264_NO_BFRAMES, transcode=Mp4TranscodeOptions(output_codec=output_codec))) + static_chunks = [c for c in chunks if c.is_static] + temporal_chunks = [c for c in chunks if not c.is_static] + assert len(static_chunks) == 1 + assert len(temporal_chunks) > 0 + for c in temporal_chunks: + assert c.timeline_names == ["video"] + + +@pytest.mark.skipif(not _HAS_FFMPEG, reason="ffmpeg not installed") +def test_gop_size_forces_keyframe_spacing() -> None: + """ + `gop_size=N` forces a keyframe every N frames. + + With `chunk_by_gop=True` that means one temporal chunk per GOP, so every GOP + chunk but the last holds exactly N samples. + """ + gop = 10 + chunks = _to_chunks_or_skip(Mp4Reader(H264_NO_BFRAMES, transcode=Mp4TranscodeOptions(gop_size=gop))) + gop_sizes = [c.num_rows for c in chunks if not c.is_static] + assert len(gop_sizes) >= 2, f"gop_size={gop} should force multiple GOPs, got {gop_sizes}" + for n in gop_sizes[:-1]: + assert n == gop, f"every GOP but the last should hold {gop} samples, got {gop_sizes}" + assert 1 <= gop_sizes[-1] <= gop + + +# --------------------------------------------------------------------------- +# StreamingReader protocol conformance +# --------------------------------------------------------------------------- + + +def test_streaming_reader_protocol() -> None: + assert isinstance(Mp4Reader(H264_NO_BFRAMES), StreamingReader) diff --git a/rerun_py/tests/integration/test_parquet_reader.py b/rerun_py/tests/integration/test_parquet_reader.py new file mode 100644 index 000000000000..59b98f2c3328 --- /dev/null +++ b/rerun_py/tests/integration/test_parquet_reader.py @@ -0,0 +1,468 @@ +""" +Tests for rerun.experimental.ParquetReader. + +The reader turns raw parquet columns into grouped, time-indexed `Chunk`s — prefix / +individual / explicit-prefix grouping, index columns with unit scaling, static +columns, and error paths. Mapping the resulting struct components into archetypes is +done separately with lenses (see `test_lazy_chunk_stream.py`). +""" + +from __future__ import annotations + +import itertools +from typing import TYPE_CHECKING + +import pyarrow as pa +import pyarrow.parquet as pq +import pytest +import rerun as rr +from rerun.experimental import Chunk, DeriveLens, IndexColumn, LazyChunkStream, ParquetReader, StreamingReader + +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + + ParquetWriter = Callable[[dict[str, pa.Array]], Path] + + +# --------------------------------------------------------------------------- +# Fixtures / helpers +# --------------------------------------------------------------------------- + + +@pytest.fixture +def parquet_writer(tmp_path: Path) -> ParquetWriter: + """Return a callable that writes named Arrow columns to a fresh parquet file and returns its path.""" + counter = itertools.count() + + def write(columns: dict[str, pa.Array]) -> Path: + path = tmp_path / f"t{next(counter)}.parquet" + pq.write_table(pa.table(columns), str(path)) + return path + + return write + + +def _data_chunks(reader: ParquetReader) -> list[Chunk]: + """Run the reader, dropping the file-metadata `/__properties` chunk that parquet's schema metadata produces.""" + return reader.stream().drop(content="/__properties/**").to_chunks() + + +def _by_entity(chunks: list[Chunk], entity_path: str) -> Chunk: + matches = [c for c in chunks if c.entity_path == entity_path] + assert len(matches) == 1, f"expected exactly one chunk at {entity_path}, found {len(matches)}" + return matches[0] + + +def _struct_field_names(chunk: Chunk, component: str = "data") -> list[str]: + """Field names of a `List` component.""" + return list(chunk.to_record_batch().schema.field(component).type.value_type.names) + + +# --------------------------------------------------------------------------- +# prefix grouping +# --------------------------------------------------------------------------- + + +def test_prefix_grouping(parquet_writer: ParquetWriter) -> None: + """Multi-column prefixes become a single `data` struct; a lone column becomes a raw component.""" + + # Prefix grouping (delimiter `_`) yields: + # - `A_*` → entity `/A`, struct `data{pos_x..quat_w}` + # - `obs_*` → entity `/obs`, struct `data{x, y, z}` + # - `camera_*` → entity `/camera`, struct `data{rgb, depth}` + # - `speed` → entity `/speed`, a raw `speed` component (no delimiter → lone column) + path = parquet_writer({ + "frame_index": pa.array([0, 1, 2], pa.int64()), + "A_pos_x": pa.array([1.0, 2.0, 3.0]), + "A_pos_y": pa.array([4.0, 5.0, 6.0]), + "A_pos_z": pa.array([7.0, 8.0, 9.0]), + "A_quat_x": pa.array([0.0, 0.0, 0.0]), + "A_quat_y": pa.array([0.0, 0.0, 0.0]), + "A_quat_z": pa.array([0.0, 0.0, 0.0]), + "A_quat_w": pa.array([1.0, 1.0, 1.0]), + "obs_x": pa.array([1.0, 2.0, 3.0]), + "obs_y": pa.array([4.0, 5.0, 6.0]), + "obs_z": pa.array([7.0, 8.0, 9.0]), + "camera_rgb": pa.array([10.0, 20.0, 30.0]), + "camera_depth": pa.array([40.0, 50.0, 60.0]), + "speed": pa.array([100.0, 200.0, 300.0]), + }) + chunks = _data_chunks(ParquetReader(path, index_columns=[IndexColumn.sequence("frame_index")])) + + assert {c.entity_path for c in chunks} == {"/A", "/obs", "/camera", "/speed"} + + camera = _by_entity(chunks, "/camera") + assert camera.num_rows == 3 + assert camera.timeline_names == ["frame_index"] + assert _struct_field_names(camera) == ["rgb", "depth"] + assert camera.to_record_batch().column("data").to_pylist() == [ + [{"rgb": 10.0, "depth": 40.0}], + [{"rgb": 20.0, "depth": 50.0}], + [{"rgb": 30.0, "depth": 60.0}], + ] + + assert _struct_field_names(_by_entity(chunks, "/obs")) == ["x", "y", "z"] + assert _struct_field_names(_by_entity(chunks, "/A")) == [ + "pos_x", + "pos_y", + "pos_z", + "quat_x", + "quat_y", + "quat_z", + "quat_w", + ] + + # Lone column → its own raw component named after the column (not a `data` struct). + speed = _by_entity(chunks, "/speed") + assert "data" not in speed.to_record_batch().schema.names + assert speed.to_record_batch().column("speed").to_pylist() == [[100.0], [200.0], [300.0]] + + +def test_individual_grouping(parquet_writer: ParquetWriter) -> None: + """Individual grouping gives every column its own entity/component — no struct packing.""" + path = parquet_writer({ + "frame_index": pa.array([0, 1, 2], pa.int64()), + "camera_rgb": pa.array([1.0, 2.0, 3.0]), + "camera_depth": pa.array([4.0, 5.0, 6.0]), + }) + chunks = _data_chunks( + ParquetReader(path, column_grouping="individual", index_columns=[IndexColumn.sequence("frame_index")]) + ) + assert {c.entity_path for c in chunks} == {"/camera_rgb", "/camera_depth"} + for c in chunks: + assert "data" not in c.to_record_batch().schema.names + + +def test_explicit_prefixes(parquet_writer: ParquetWriter) -> None: + """Explicit prefixes group by exact prefix string; unmatched columns become individual groups.""" + path = parquet_writer({ + "fooa": pa.array([1.0, 2.0]), + "foob": pa.array([3.0, 4.0]), + "cata": pa.array([5.0, 6.0]), + "catb": pa.array([7.0, 8.0]), + "other": pa.array([9.0, 10.0]), + }) + chunks = _data_chunks(ParquetReader(path, column_grouping="explicit_prefixes", prefixes=["cat", "foo"])) + assert {c.entity_path for c in chunks} == {"/foo", "/cat", "/other"} + # The prefix is stripped from each struct field name. + assert _struct_field_names(_by_entity(chunks, "/foo")) == ["a", "b"] + assert _struct_field_names(_by_entity(chunks, "/cat")) == ["a", "b"] + + +# --------------------------------------------------------------------------- +# index columns +# --------------------------------------------------------------------------- + + +def test_index_sequence(parquet_writer: ParquetWriter) -> None: + path = parquet_writer({"frame_index": pa.array([0, 1, 2], pa.int64()), "value": pa.array([10.0, 20.0, 30.0])}) + chunk = _by_entity( + _data_chunks(ParquetReader(path, index_columns=[IndexColumn.sequence("frame_index")])), + "/value", + ) + rb = chunk.to_record_batch() + assert rb.schema.field("frame_index").type == pa.int64() + assert rb.column("frame_index").to_pylist() == [0, 1, 2] + + +def test_index_timestamp_unit_scaling(parquet_writer: ParquetWriter) -> None: + """A `ms` timestamp index is scaled to nanoseconds and typed `timestamp[ns]`.""" + path = parquet_writer({"ts_ms": pa.array([1, 2, 3], pa.int64()), "value": pa.array([1.0, 2.0, 3.0])}) + chunk = _by_entity( + _data_chunks(ParquetReader(path, index_columns=[IndexColumn.timestamp("ts_ms", input_unit="ms")])), + "/value", + ) + rb = chunk.to_record_batch() + assert rb.schema.field("ts_ms").type == pa.timestamp("ns") + assert rb.column("ts_ms").cast(pa.int64()).to_pylist() == [1_000_000, 2_000_000, 3_000_000] + + +def test_index_duration_unit_scaling(parquet_writer: ParquetWriter) -> None: + """A `us` duration index is scaled to nanoseconds and typed `duration[ns]`.""" + path = parquet_writer({"elapsed_us": pa.array([100, 200, 300], pa.int64()), "value": pa.array([1.0, 2.0, 3.0])}) + chunk = _by_entity( + _data_chunks(ParquetReader(path, index_columns=[IndexColumn.duration("elapsed_us", input_unit="us")])), + "/value", + ) + rb = chunk.to_record_batch() + assert rb.schema.field("elapsed_us").type == pa.duration("ns") + assert rb.column("elapsed_us").cast(pa.int64()).to_pylist() == [100_000, 200_000, 300_000] + + +# --------------------------------------------------------------------------- +# static columns +# --------------------------------------------------------------------------- + + +def test_static_columns(parquet_writer: ParquetWriter) -> None: + """Uniform static columns are emitted once as a separate static chunk.""" + path = parquet_writer({ + "frame_index": pa.array([0, 1, 2], pa.int64()), + "value": pa.array([1.0, 2.0, 3.0]), + "suite": pa.array(["s", "s", "s"]), + "agg": pa.array(["mean", "mean", "mean"]), + }) + chunks = _data_chunks( + ParquetReader( + path, + column_grouping="individual", + index_columns=[IndexColumn.sequence("frame_index")], + static_columns=["suite", "agg"], + ) + ) + static = [c for c in chunks if c.is_static] + assert len(static) == 1 + assert static[0].num_rows == 1 + assert {c for c in static[0].to_record_batch().schema.names if not c.startswith("rerun.controls")} == { + "suite", + "agg", + } + + temporal = [c for c in chunks if not c.is_static] + assert {c.entity_path for c in temporal} == {"/value"} + + +def test_static_column_non_uniform_is_error(parquet_writer: ParquetWriter) -> None: + """A static column with varying values raises when the stream runs.""" + path = parquet_writer({"x": pa.array([1.0, 2.0]), "suite": pa.array(["a", "b"])}) + with pytest.raises(Exception, match=r"non-uniform|static"): + ParquetReader(path, column_grouping="individual", static_columns=["suite"]).stream().to_chunks() + + +# --------------------------------------------------------------------------- +# error paths +# --------------------------------------------------------------------------- + + +def test_file_not_found(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError, match="not found"): + ParquetReader(tmp_path / "nonexistent.parquet") + + +def test_invalid_column_grouping(parquet_writer: ParquetWriter) -> None: + path = parquet_writer({"x": pa.array([1.0])}) + with pytest.raises(ValueError, match="Unknown column_grouping"): + ParquetReader(path, column_grouping="bogus") + + +def test_prefixes_without_explicit_grouping_is_error(parquet_writer: ParquetWriter) -> None: + path = parquet_writer({"x": pa.array([1.0])}) + with pytest.raises(ValueError, match="explicit_prefixes"): + ParquetReader(path, prefixes=["foo"]) + + +def test_missing_index_column_is_error(parquet_writer: ParquetWriter) -> None: + path = parquet_writer({"x": pa.array([1.0])}) + with pytest.raises(Exception, match="not found"): + ParquetReader(path, index_columns=[IndexColumn.sequence("missing")]).stream().to_chunks() + + +# --------------------------------------------------------------------------- +# Archetype mapping via lenses +# --------------------------------------------------------------------------- + + +def test_transform3d_via_lenses(parquet_writer: ParquetWriter) -> None: + """ + Reproduce the old `ColumnRule` mapping — a `Transform3D` (translation + rotation) — with lens helpers. + + `to_translation` / `to_quaternion` pack the reader's `data` struct fields and cast them to the + `FixedSizeList` arrays the Transform3D components expect; chaining them on one lens builds a + full transform. This also exercises the FSL→FSL `f64`→`f32` auto-cast end to end. + """ + # A pose table: per-row translation (`pos_*`) and rotation quaternion (`quat_*`) under prefix `A`. + path = parquet_writer({ + "frame_index": pa.array([0, 1], pa.int64()), + "A_pos_x": pa.array([1.0, 2.0]), + "A_pos_y": pa.array([3.0, 4.0]), + "A_pos_z": pa.array([5.0, 6.0]), + "A_quat_x": pa.array([0.0, 0.0]), + "A_quat_y": pa.array([0.0, 0.0]), + "A_quat_z": pa.array([0.0, 0.0]), + "A_quat_w": pa.array([1.0, 1.0]), + }) + + lens = ( + DeriveLens("data", output_entity="/pose") + .to_translation("pos_x", "pos_y", "pos_z") + .to_quaternion("quat_x", "quat_y", "quat_z", "quat_w") + ) + + chunks = ( + ParquetReader(path, index_columns=[IndexColumn.sequence("frame_index")]) + .stream() + .lenses([lens], content="/A", output_mode="drop_unmatched") + .to_chunks() + ) + pose = _by_entity(chunks, "/pose") + rb = pose.to_record_batch() + + # The emitted Arrow types match the real Transform3D components exactly (incl. the f32 cast). + translation = rb.column("Transform3D:translation") + quaternion = rb.column("Transform3D:quaternion") + assert translation.type.value_type == rr.components.Translation3D.arrow_type() + assert quaternion.type.value_type == rr.components.RotationQuat.arrow_type() + + # Values packed row-major from the source columns; timeline preserved. + assert translation.to_pylist() == [[[1.0, 3.0, 5.0]], [[2.0, 4.0, 6.0]]] + assert quaternion.to_pylist() == [[[0.0, 0.0, 0.0, 1.0]], [[0.0, 0.0, 0.0, 1.0]]] + assert rb.column("frame_index").to_pylist() == [0, 1] + + +def test_to_packed_component_generic(parquet_writer: ParquetWriter) -> None: + """The generic `to_packed_component` maps struct fields onto an arbitrary fixed-size-list component.""" + # Prefix `p` → entity `/p`, struct `data{x, y, z}`. + path = parquet_writer({ + "frame_index": pa.array([0, 1], pa.int64()), + "p_x": pa.array([1.0, 2.0]), + "p_y": pa.array([3.0, 4.0]), + "p_z": pa.array([5.0, 6.0]), + }) + + lens = DeriveLens("data", output_entity="/points").to_packed_component( + rr.Points3D.descriptor_positions(), "x", "y", "z" + ) + + chunks = ( + ParquetReader(path, index_columns=[IndexColumn.sequence("frame_index")]) + .stream() + .lenses([lens], content="/p", output_mode="drop_unmatched") + .to_chunks() + ) + positions = _by_entity(chunks, "/points").to_record_batch().column("Points3D:positions") + + assert positions.type.value_type == rr.components.Position3D.arrow_type() + assert positions.to_pylist() == [[[1.0, 3.0, 5.0]], [[2.0, 4.0, 6.0]]] + + +def test_to_rotation_axis_angle(parquet_writer: ParquetWriter) -> None: + """`to_rotation_axis_angle` builds the `Struct{axis, angle}` a `RotationAxisAngle` expects.""" + # Prefix `r` → entity `/r`, struct `data{ax, ay, az, angle}`. + path = parquet_writer({ + "frame_index": pa.array([0, 1], pa.int64()), + "r_ax": pa.array([1.0, 0.0]), + "r_ay": pa.array([0.0, 1.0]), + "r_az": pa.array([0.0, 0.0]), + "r_angle": pa.array([1.5, 3.0]), + }) + + lens = DeriveLens("data", output_entity="/rot").to_rotation_axis_angle("ax", "ay", "az", "angle") + + chunks = ( + ParquetReader(path, index_columns=[IndexColumn.sequence("frame_index")]) + .stream() + .lenses([lens], content="/r", output_mode="drop_unmatched") + .to_chunks() + ) + rot = _by_entity(chunks, "/rot").to_record_batch().column("Transform3D:rotation_axis_angle") + + assert rot.type.value_type == rr.components.RotationAxisAngle.arrow_type() + assert rot.to_pylist() == [ + [{"axis": [1.0, 0.0, 0.0], "angle": 1.5}], + [{"axis": [0.0, 1.0, 0.0], "angle": 3.0}], + ] + + +def test_to_scalars(parquet_writer: ParquetWriter) -> None: + """`to_scalars` maps struct fields to a multi-instance `Scalars:scalars` column (one series each).""" + # Prefix `obs` → entity `/obs`, struct `data{vx, vy, vz}`. + path = parquet_writer({ + "frame_index": pa.array([0, 1], pa.int64()), + "obs_vx": pa.array([1.0, 2.0]), + "obs_vy": pa.array([3.0, 4.0]), + "obs_vz": pa.array([5.0, 6.0]), + }) + + lens = DeriveLens("data", output_entity="/obs").to_scalars("vx", "vy", "vz") + + chunks = ( + ParquetReader(path, index_columns=[IndexColumn.sequence("frame_index")]) + .stream() + .lenses([lens], content="/obs", output_mode="drop_unmatched") + .to_chunks() + ) + scalars = _by_entity(chunks, "/obs").to_record_batch().column("Scalars:scalars") + + # Plain `List` with one instance (series) per field — *not* a nested fixed-size list. + assert scalars.type.value_type == rr.components.Scalar.arrow_type() + assert scalars.to_pylist() == [[1.0, 3.0, 5.0], [2.0, 4.0, 6.0]] + + +def test_to_scalars_single_field_is_plain_scalar(parquet_writer: ParquetWriter) -> None: + """A single field is read as a plain scalar, not packed into a 1-element fixed-size list.""" + # Prefix `obs` → entity `/obs`, struct `data{vx, vy}`. + path = parquet_writer({ + "frame_index": pa.array([0, 1], pa.int64()), + "obs_vx": pa.array([1.0, 2.0]), + "obs_vy": pa.array([3.0, 4.0]), + }) + + lens = DeriveLens("data", output_entity="/obs").to_scalars("vx") + + chunks = ( + ParquetReader(path, index_columns=[IndexColumn.sequence("frame_index")]) + .stream() + .lenses([lens], content="/obs", output_mode="drop_unmatched") + .to_chunks() + ) + scalars = _by_entity(chunks, "/obs").to_record_batch().column("Scalars:scalars") + + # Plain scalar per row — the canonical Scalar datatype — and crucially *not* a fixed-size list. + assert scalars.type.value_type == rr.components.Scalar.arrow_type() + assert scalars.to_pylist() == [[1.0], [2.0]] + + +def test_named_scalar_series_via_lenses(parquet_writer: ParquetWriter) -> None: + """ + End-to-end: map a timeseries to multi-value `Scalars` and co-locate static per-series names. + + `to_scalars` only produces the scalar values; `SeriesLines:names` is static metadata that must be + injected by hand. We build that static chunk with `Chunk.from_columns(..., indexes=[])` and merge + it into the reader stream, so both live at the same entity. + """ + path = parquet_writer({ + "t": pa.array([0, 1, 2], pa.int64()), + "obs_vx": pa.array([1.0, 2.0, 3.0]), + "obs_vy": pa.array([4.0, 5.0, 6.0]), + "obs_vz": pa.array([7.0, 8.0, 9.0]), + }) + + lens = DeriveLens("data", output_entity="/obs").to_scalars("vx", "vy", "vz") + reader_stream = ( + ParquetReader(path, index_columns=[IndexColumn.sequence("t")]) + .stream() + .lenses([lens], content="/obs", output_mode="drop_unmatched") + ) + + # Static names: empty `indexes` ⇒ static chunk; partition all 3 names into a single row. + names_chunk = Chunk.from_columns( + "/obs", + indexes=[], + columns=rr.SeriesLines.columns(names=["vx", "vy", "vz"]).partition(lengths=[3]), + ) + + store = LazyChunkStream.merge(reader_stream, LazyChunkStream.from_iter([names_chunk])).collect() + + obs_chunks = [c for c in store.stream().to_chunks() if c.entity_path == "/obs"] + temporal = [c for c in obs_chunks if not c.is_static] + static = [c for c in obs_chunks if c.is_static] + + assert len(temporal) == 1 + assert len(static) == 1 + + scalars = temporal[0].to_record_batch().column("Scalars:scalars") + assert scalars.to_pylist() == [[1.0, 4.0, 7.0], [2.0, 5.0, 8.0], [3.0, 6.0, 9.0]] + + names = static[0].to_record_batch().column("SeriesLines:names") + assert names.to_pylist() == [["vx", "vy", "vz"]] + + +# --------------------------------------------------------------------------- +# StreamingReader protocol conformance +# --------------------------------------------------------------------------- + + +def test_streaming_reader_protocol(parquet_writer: ParquetWriter) -> None: + path = parquet_writer({"x": pa.array([1.0])}) + assert isinstance(ParquetReader(path), StreamingReader) diff --git a/rerun_py/tests/integration/test_rrd_reader_multi_store.py b/rerun_py/tests/integration/test_rrd_reader_multi_store.py new file mode 100644 index 000000000000..745dcf295bc0 --- /dev/null +++ b/rerun_py/tests/integration/test_rrd_reader_multi_store.py @@ -0,0 +1,251 @@ +"""Tests for multi-store RRD support in RrdReader.""" + +from __future__ import annotations + +import subprocess +from typing import TYPE_CHECKING + +import pytest +import rerun as rr +from inline_snapshot import snapshot as inline_snapshot +from rerun.experimental import RrdReader + +if TYPE_CHECKING: + from pathlib import Path + + +MULTI_APP_ID = "rerun_example_test_app" +REC_ID_1 = "recording_1" +REC_ID_2 = "recording_2" + + +@pytest.fixture(scope="session") +def blueprint_only_rrd_path(tmp_path_factory: pytest.TempPathFactory) -> Path: + """An RRD containing only a blueprint store — no recording stores.""" + path = tmp_path_factory.mktemp("blueprint_only") / "blueprint.rbl" + rr.blueprint.Blueprint(auto_layout=False, auto_views=False).save(MULTI_APP_ID, path) + return path + + +@pytest.fixture(scope="session") +def multi_store_rrd_path(tmp_path_factory: pytest.TempPathFactory) -> Path: + """ + Build a multi-store RRD with 2 recordings + 1 blueprint. + + Each source store is written to its own file, then combined into a single RRD + (with one footer listing all three manifests) using `rerun rrd merge`. + """ + tmp_dir = tmp_path_factory.mktemp("multi_store") + rec1_path = tmp_dir / "rec1.rrd" + rec2_path = tmp_dir / "rec2.rrd" + bp_path = tmp_dir / "blueprint.rbl" + out_path = tmp_dir / "multi.rrd" + + with rr.RecordingStream(MULTI_APP_ID, recording_id=REC_ID_1) as rec: + rec.save(rec1_path) + rec.send_columns( + "/entity_a", + indexes=[rr.TimeColumn("frame", sequence=[1, 2])], + columns=rr.Points3D.columns(positions=[[1, 2, 3], [4, 5, 6]]), + ) + + with rr.RecordingStream(MULTI_APP_ID, recording_id=REC_ID_2) as rec: + rec.save(rec2_path) + rec.send_columns( + "/entity_b", + indexes=[rr.TimeColumn("frame", sequence=[10])], + columns=rr.Points3D.columns(positions=[[7, 8, 9]]), + ) + + rr.blueprint.Blueprint(rr.blueprint.Spatial3DView(origin="/entity_a")).save(MULTI_APP_ID, bp_path) + + subprocess.run( + ["rerun", "rrd", "merge", str(rec1_path), str(rec2_path), str(bp_path), "-o", str(out_path)], + check=True, + capture_output=True, + ) + return out_path + + +# --------------------------------------------------------------------------- +# Store enumeration +# --------------------------------------------------------------------------- + + +def test_recordings(multi_store_rrd_path: Path) -> None: + reader = RrdReader(multi_store_rrd_path) + recs = reader.recordings() + assert len(recs) == 2 + assert all(s.kind == "recording" for s in recs) + assert {s.recording_id for s in recs} == {REC_ID_1, REC_ID_2} + + +def test_blueprints(multi_store_rrd_path: Path) -> None: + reader = RrdReader(multi_store_rrd_path) + bps = reader.blueprints() + assert len(bps) == 1 + assert bps[0].kind == "blueprint" + + +def test_single_store_rrd(test_rrd_path: Path) -> None: + """The existing single-store fixture should have exactly one recording.""" + reader = RrdReader(test_rrd_path) + recs = reader.recordings() + assert len(recs) == 1 + assert reader.blueprints() == [] + + +# --------------------------------------------------------------------------- +# StoreEntry properties +# --------------------------------------------------------------------------- + + +def test_store_entry_properties(multi_store_rrd_path: Path) -> None: + reader = RrdReader(multi_store_rrd_path) + entry = reader.recordings()[0] + assert entry.application_id == MULTI_APP_ID + assert entry.recording_id in {REC_ID_1, REC_ID_2} + assert entry.kind == "recording" + + +def test_store_entry_equality(multi_store_rrd_path: Path) -> None: + reader = RrdReader(multi_store_rrd_path) + entries_a = reader.recordings() + reader.blueprints() + entries_b = reader.recordings() + reader.blueprints() + for a, b in zip(entries_a, entries_b, strict=True): + assert a == b + + +def test_store_entry_hashable(multi_store_rrd_path: Path) -> None: + reader = RrdReader(multi_store_rrd_path) + entries = reader.recordings() + reader.blueprints() + entry_set = set(entries) + assert len(entry_set) == len(entries) + + +def test_store_entry_repr(multi_store_rrd_path: Path) -> None: + reader = RrdReader(multi_store_rrd_path) + entry = reader.recordings()[0] + assert repr(entry) == inline_snapshot( + "StoreEntry(kind='recording', application_id='rerun_example_test_app', recording_id='recording_1')" + ) + + +# --------------------------------------------------------------------------- +# Store selection on stream() / store() +# --------------------------------------------------------------------------- + + +def test_stream_default(multi_store_rrd_path: Path) -> None: + """Default stream() uses first recording store.""" + reader = RrdReader(multi_store_rrd_path) + with pytest.warns(match="implicitly using"): + chunks = list(reader.stream()) + assert len(chunks) > 0 + + +def test_stream_specific_recording(multi_store_rrd_path: Path) -> None: + """Can stream a specific recording by passing its StoreEntry.""" + reader = RrdReader(multi_store_rrd_path) + recs = reader.recordings() + assert len(recs) == 2 + + chunks_0 = list(reader.stream(store=recs[0])) + chunks_1 = list(reader.stream(store=recs[1])) + assert len(chunks_0) > 0 + assert len(chunks_1) > 0 + + +def test_stream_blueprint(multi_store_rrd_path: Path) -> None: + """Streaming a blueprint store yields its chunks.""" + reader = RrdReader(multi_store_rrd_path) + bps = reader.blueprints() + chunks = list(reader.stream(store=bps[0])) + assert len(chunks) > 0 + + +def test_store_default(multi_store_rrd_path: Path) -> None: + """Default store() loads first recording.""" + reader = RrdReader(multi_store_rrd_path) + with pytest.warns(match="implicitly using"): + cs = reader.store() + assert len(cs) > 0 + + +def test_store_specific(multi_store_rrd_path: Path) -> None: + """Can load a specific recording as a LazyStore.""" + reader = RrdReader(multi_store_rrd_path) + recs = reader.recordings() + cs = reader.store(store=recs[1]) + assert len(cs) > 0 + + +def test_implicit_pick_warns_for_stream(multi_store_rrd_path: Path) -> None: + """stream() without `store=` should warn when there are multiple recordings.""" + reader = RrdReader(multi_store_rrd_path) + with pytest.warns(match="implicitly using"): + list(reader.stream()) + + +def test_implicit_pick_silent_for_single_recording(test_rrd_path: Path) -> None: + """No warning when the file has exactly one recording — the pick is unambiguous.""" + import warnings + + reader = RrdReader(test_rrd_path) + with warnings.catch_warnings(): + warnings.simplefilter("error") + list(reader.stream()) + reader.store() + + +def test_stream_nonexistent_store(multi_store_rrd_path: Path, test_rrd_path: Path) -> None: + """Streaming with a StoreEntry that doesn't belong to this file fails fast.""" + other_reader = RrdReader(test_rrd_path) + foreign_entry = other_reader.recordings()[0] + + reader = RrdReader(multi_store_rrd_path) + with pytest.raises(ValueError, match="not found"): + reader.stream(store=foreign_entry) + + +def test_store_nonexistent_store(multi_store_rrd_path: Path, test_rrd_path: Path) -> None: + """Loading a store with a StoreEntry that doesn't belong to this file fails fast.""" + other_reader = RrdReader(test_rrd_path) + foreign_entry = other_reader.recordings()[0] + + reader = RrdReader(multi_store_rrd_path) + with pytest.raises(ValueError, match="not found"): + reader.store(store=foreign_entry) + + +def test_stream_default_no_recording_raises(blueprint_only_rrd_path: Path) -> None: + """stream() with no explicit store and no recording in the file must fail fast.""" + reader = RrdReader(blueprint_only_rrd_path) + with pytest.raises(ValueError, match="No recording store"): + reader.stream() + + +def test_store_default_no_recording_raises(blueprint_only_rrd_path: Path) -> None: + """store() with no explicit store and no recording in the file must fail fast.""" + reader = RrdReader(blueprint_only_rrd_path) + with pytest.raises(ValueError, match="No recording store"): + reader.store() + + +# --------------------------------------------------------------------------- +# Backward compatibility with the single-store fixture +# --------------------------------------------------------------------------- + + +def test_existing_stream_unchanged(test_rrd_path: Path) -> None: + """Existing single-store usage still works.""" + reader = RrdReader(test_rrd_path) + chunks = list(reader.stream()) + assert len(chunks) > 0 + + +def test_existing_store_unchanged(test_rrd_path: Path) -> None: + """Existing single-store store() still works.""" + reader = RrdReader(test_rrd_path) + cs = reader.store() + assert len(cs) > 0 diff --git a/rerun_py/tests/integration/test_send_chunks.py b/rerun_py/tests/integration/test_send_chunks.py new file mode 100644 index 000000000000..00a9e7e83395 --- /dev/null +++ b/rerun_py/tests/integration/test_send_chunks.py @@ -0,0 +1,161 @@ +"""Tests for rerun.experimental.send_chunks.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol + +import pytest +import rerun as rr +from rerun.experimental import Chunk, RrdReader + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + from pathlib import Path + + from rerun.experimental import ChunkStore, LazyChunkStream, LazyStore + + +class SendChunksAndRead(Protocol): + """Send `chunks` to a fresh recording and return a reader for the result.""" + + def __call__( + self, + chunks: Chunk | LazyChunkStream | LazyStore | ChunkStore | Iterable[Chunk], + ) -> RrdReader: ... + + +def _make_chunk(entity_path: str, value: int) -> Chunk: + return Chunk.from_columns( + entity_path, + indexes=[rr.TimeColumn("frame", sequence=[value])], + columns=rr.Scalars.columns(scalars=[float(value)]), + ) + + +@pytest.fixture +def send_chunks_and_read(tmp_path: Path) -> SendChunksAndRead: + """Fixture: send chunks into a fresh dest recording (one per call) and return a reader.""" + counter = 0 + + def _impl( + chunks: Chunk | LazyChunkStream | LazyStore | ChunkStore | Iterable[Chunk], + ) -> RrdReader: + nonlocal counter + counter += 1 + out_path = tmp_path / f"out_{counter}.rrd" + with rr.RecordingStream( + "rerun_example_dest_app", + recording_id="dest_rec", + send_properties=False, + ) as rec: + rec.save(out_path) + rec.send_chunks(chunks) + return RrdReader(out_path) + + return _impl + + +# --------------------------------------------------------------------------- +# Single chunk +# --------------------------------------------------------------------------- + + +def test_send_single_chunk(send_chunks_and_read: SendChunksAndRead) -> None: + chunk = _make_chunk("/single", 0) + + reader = send_chunks_and_read(chunk) + + paths = set(reader.store().schema().entity_paths()) + assert paths == {"/single"} + + +# --------------------------------------------------------------------------- +# Iterables +# --------------------------------------------------------------------------- + + +def test_send_iterable_chunks(send_chunks_and_read: SendChunksAndRead) -> None: + chunks = [_make_chunk("/a", 0), _make_chunk("/b", 1)] + + reader = send_chunks_and_read(chunks) + + paths = set(reader.store().schema().entity_paths()) + assert paths == {"/a", "/b"} + + +def test_send_generator(send_chunks_and_read: SendChunksAndRead) -> None: + def gen() -> Iterator[Chunk]: + yield _make_chunk("/g0", 0) + yield _make_chunk("/g1", 1) + + reader = send_chunks_and_read(gen()) + + paths = set(reader.store().schema().entity_paths()) + assert paths == {"/g0", "/g1"} + + +# --------------------------------------------------------------------------- +# LazyChunkStream / LazyStore / ChunkStore +# --------------------------------------------------------------------------- + + +def test_send_lazy_chunk_stream(send_chunks_and_read: SendChunksAndRead, test_rrd_path: Path) -> None: + src_paths = set(RrdReader(test_rrd_path).store().schema().entity_paths()) + + stream = RrdReader(test_rrd_path).stream() + reader = send_chunks_and_read(stream) + + dest_paths = set(reader.store().schema().entity_paths()) + assert dest_paths == src_paths + + +def test_send_lazy_chunk_stream_filtered(send_chunks_and_read: SendChunksAndRead, test_rrd_path: Path) -> None: + stream = RrdReader(test_rrd_path).stream().filter(content="/robots/**") + reader = send_chunks_and_read(stream) + + dest_paths = set(reader.store().schema().entity_paths()) + assert dest_paths == {"/robots/arm"} + + +def test_send_lazy_store(send_chunks_and_read: SendChunksAndRead, test_rrd_path: Path) -> None: + via_store_reader = send_chunks_and_read(RrdReader(test_rrd_path).store()) + via_stream_reader = send_chunks_and_read(RrdReader(test_rrd_path).store().stream()) + + via_store_paths = set(via_store_reader.store().schema().entity_paths()) + via_stream_paths = set(via_stream_reader.store().schema().entity_paths()) + assert via_store_paths == via_stream_paths + + +def test_send_chunk_store(send_chunks_and_read: SendChunksAndRead, test_rrd_path: Path) -> None: + via_store_reader = send_chunks_and_read(RrdReader(test_rrd_path).stream().collect()) + via_stream_reader = send_chunks_and_read(RrdReader(test_rrd_path).stream().collect().stream()) + + via_store_paths = set(via_store_reader.store().schema().entity_paths()) + via_stream_paths = set(via_stream_reader.store().schema().entity_paths()) + assert via_store_paths == via_stream_paths + + +# --------------------------------------------------------------------------- +# Edge cases +# --------------------------------------------------------------------------- + + +def test_send_chunks_iterable_type_error(tmp_path: Path) -> None: + """Non-Chunk items in an iterable raise TypeError when drained.""" + out = tmp_path / "out.rrd" + with rr.RecordingStream("rerun_example_dest_app", recording_id="dest_rec") as rec: + rec.save(out) + with pytest.raises(TypeError, match="Chunk"): + rec.send_chunks(["not a chunk"]) # type: ignore[list-item] + + +def test_send_chunks_consumed_lazy_stream(tmp_path: Path, test_rrd_path: Path) -> None: + """A LazyChunkStream consumed by a builder cannot be re-sent.""" + stream = RrdReader(test_rrd_path).stream() + stream.filter(content="/robots/**") # consumes `stream` + + out = tmp_path / "out.rrd" + with rr.RecordingStream("rerun_example_dest_app", recording_id="dest_rec") as rec: + rec.save(out) + with pytest.raises(ValueError): + rec.send_chunks(stream) diff --git a/rerun_py/tests/test_types/components/.gitattributes b/rerun_py/tests/test_types/components/.gitattributes index eba4ad1ee06c..ebd21acd3ee6 100644 --- a/rerun_py/tests/test_types/components/.gitattributes +++ b/rerun_py/tests/test_types/components/.gitattributes @@ -25,3 +25,4 @@ affix_fuzzer6.py linguist-generated=true affix_fuzzer7.py linguist-generated=true affix_fuzzer8.py linguist-generated=true affix_fuzzer9.py linguist-generated=true +many_vec3.py linguist-generated=true diff --git a/rerun_py/tests/test_types/components/__init__.py b/rerun_py/tests/test_types/components/__init__.py index 1ba7b2698eaa..3f4645180892 100644 --- a/rerun_py/tests/test_types/components/__init__.py +++ b/rerun_py/tests/test_types/components/__init__.py @@ -25,6 +25,7 @@ from .affix_fuzzer21 import AffixFuzzer21, AffixFuzzer21Batch from .affix_fuzzer22 import AffixFuzzer22, AffixFuzzer22Batch from .affix_fuzzer23 import AffixFuzzer23, AffixFuzzer23Batch +from .many_vec3 import ManyVec3, ManyVec3Batch __all__ = [ "AffixFuzzer1", @@ -93,4 +94,6 @@ "AffixFuzzer22Batch", "AffixFuzzer23", "AffixFuzzer23Batch", + "ManyVec3", + "ManyVec3Batch", ] diff --git a/rerun_py/tests/test_types/components/many_vec3.py b/rerun_py/tests/test_types/components/many_vec3.py new file mode 100644 index 000000000000..c16432be70f3 --- /dev/null +++ b/rerun_py/tests/test_types/components/many_vec3.py @@ -0,0 +1,30 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/fuzzy.fbs". + +# You can extend this class by creating a "ManyVec3Ext" class in "many_vec3_ext.py". + +from __future__ import annotations + +from rerun._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +from .. import datatypes + +__all__ = ["ManyVec3", "ManyVec3Batch"] + + +class ManyVec3(datatypes.ManyVec3, ComponentMixin): + _BATCH_TYPE = None + # You can define your own __init__ function as a member of ManyVec3Ext in many_vec3_ext.py + + # Note: there are no fields here because ManyVec3 delegates to datatypes.ManyVec3 + + +class ManyVec3Batch(datatypes.ManyVec3Batch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.testing.components.ManyVec3" + + +# This is patched in late to avoid circular dependencies. +ManyVec3._BATCH_TYPE = ManyVec3Batch # type: ignore[assignment] diff --git a/rerun_py/tests/test_types/datatypes/.gitattributes b/rerun_py/tests/test_types/datatypes/.gitattributes index 89155232533c..8986324a7bcc 100644 --- a/rerun_py/tests/test_types/datatypes/.gitattributes +++ b/rerun_py/tests/test_types/datatypes/.gitattributes @@ -11,8 +11,12 @@ affix_fuzzer3.py linguist-generated=true affix_fuzzer4.py linguist-generated=true affix_fuzzer5.py linguist-generated=true enum_test.py linguist-generated=true +fixed_size_enum_array.py linguist-generated=true +fixed_size_wide_enum_array.py linguist-generated=true flattened_scalar.py linguist-generated=true +many_vec3.py linguist-generated=true multi_enum.py linguist-generated=true primitive_component.py linguist-generated=true string_component.py linguist-generated=true valued_enum.py linguist-generated=true +wide_enum.py linguist-generated=true diff --git a/rerun_py/tests/test_types/datatypes/__init__.py b/rerun_py/tests/test_types/datatypes/__init__.py index 3347267d0f63..3e2132840f8f 100644 --- a/rerun_py/tests/test_types/datatypes/__init__.py +++ b/rerun_py/tests/test_types/datatypes/__init__.py @@ -11,7 +11,20 @@ from .affix_fuzzer21 import AffixFuzzer21, AffixFuzzer21ArrayLike, AffixFuzzer21Batch, AffixFuzzer21Like from .affix_fuzzer22 import AffixFuzzer22, AffixFuzzer22ArrayLike, AffixFuzzer22Batch, AffixFuzzer22Like from .enum_test import EnumTest, EnumTestArrayLike, EnumTestBatch, EnumTestLike +from .fixed_size_enum_array import ( + FixedSizeEnumArray, + FixedSizeEnumArrayArrayLike, + FixedSizeEnumArrayBatch, + FixedSizeEnumArrayLike, +) +from .fixed_size_wide_enum_array import ( + FixedSizeWideEnumArray, + FixedSizeWideEnumArrayArrayLike, + FixedSizeWideEnumArrayBatch, + FixedSizeWideEnumArrayLike, +) from .flattened_scalar import FlattenedScalar, FlattenedScalarArrayLike, FlattenedScalarBatch, FlattenedScalarLike +from .many_vec3 import ManyVec3, ManyVec3ArrayLike, ManyVec3Batch, ManyVec3Like from .multi_enum import MultiEnum, MultiEnumArrayLike, MultiEnumBatch, MultiEnumLike from .primitive_component import ( PrimitiveComponent, @@ -21,6 +34,7 @@ ) from .string_component import StringComponent, StringComponentArrayLike, StringComponentBatch, StringComponentLike from .valued_enum import ValuedEnum, ValuedEnumArrayLike, ValuedEnumBatch, ValuedEnumLike +from .wide_enum import WideEnum, WideEnumArrayLike, WideEnumBatch, WideEnumLike __all__ = [ "AffixFuzzer1", @@ -59,10 +73,22 @@ "EnumTestArrayLike", "EnumTestBatch", "EnumTestLike", + "FixedSizeEnumArray", + "FixedSizeEnumArrayArrayLike", + "FixedSizeEnumArrayBatch", + "FixedSizeEnumArrayLike", + "FixedSizeWideEnumArray", + "FixedSizeWideEnumArrayArrayLike", + "FixedSizeWideEnumArrayBatch", + "FixedSizeWideEnumArrayLike", "FlattenedScalar", "FlattenedScalarArrayLike", "FlattenedScalarBatch", "FlattenedScalarLike", + "ManyVec3", + "ManyVec3ArrayLike", + "ManyVec3Batch", + "ManyVec3Like", "MultiEnum", "MultiEnumArrayLike", "MultiEnumBatch", @@ -79,4 +105,8 @@ "ValuedEnumArrayLike", "ValuedEnumBatch", "ValuedEnumLike", + "WideEnum", + "WideEnumArrayLike", + "WideEnumBatch", + "WideEnumLike", ] diff --git a/rerun_py/tests/test_types/datatypes/fixed_size_enum_array.py b/rerun_py/tests/test_types/datatypes/fixed_size_enum_array.py new file mode 100644 index 000000000000..608d357c554f --- /dev/null +++ b/rerun_py/tests/test_types/datatypes/fixed_size_enum_array.py @@ -0,0 +1,100 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +# You can extend this class by creating a "FixedSizeEnumArrayExt" class in "fixed_size_enum_array_ext.py". + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +import pyarrow as pa +from attrs import define, field +from rerun._baseclasses import ( + BaseBatch, +) + +if TYPE_CHECKING: + from .. import datatypes + +__all__ = ["FixedSizeEnumArray", "FixedSizeEnumArrayArrayLike", "FixedSizeEnumArrayBatch", "FixedSizeEnumArrayLike"] + + +def _fixed_size_enum_array__values__special_field_converter_override(x: Any) -> list[datatypes.EnumTest]: + from .. import datatypes + + if isinstance(x, datatypes.FixedSizeEnumArray): + return x.values + + try: + values = list(x) + except TypeError as err: + raise ValueError("values must be an iterable of EnumTest values") from err + + if len(values) != 3: + raise ValueError(f"values must be a 3-element array. Got: {len(values)}") + + def convert_value(value: Any) -> datatypes.EnumTest: + if isinstance(value, (datatypes.EnumTest, str)): + return datatypes.EnumTest.auto(value) + return datatypes.EnumTest.auto(int(value)) + + return [convert_value(value) for value in values] + + +@define(init=False) +class FixedSizeEnumArray: + """**Datatype**: Test datatype for fixed-size enum arrays.""" + + def __init__(self: Any, values: FixedSizeEnumArrayLike) -> None: + """ + Create a new instance of the FixedSizeEnumArray datatype. + + Parameters + ---------- + values: + Fixed-size enum array. + + """ + + # You can define your own __init__ function as a member of FixedSizeEnumArrayExt in fixed_size_enum_array_ext.py + self.__attrs_init__(values=values) + + values: list[datatypes.EnumTest] = field(converter=_fixed_size_enum_array__values__special_field_converter_override) + # Fixed-size enum array. + # + # (Docstring intentionally commented out to hide this field from the docs) + + def __len__(self) -> int: + # You can define your own __len__ function as a member of FixedSizeEnumArrayExt in fixed_size_enum_array_ext.py + return len(self.values) + + +FixedSizeEnumArrayLike = FixedSizeEnumArray +"""A type alias for any FixedSizeEnumArray-like object.""" + +FixedSizeEnumArrayArrayLike = FixedSizeEnumArray | Sequence[FixedSizeEnumArrayLike] +"""A type alias for any FixedSizeEnumArray-like array object.""" + + +class FixedSizeEnumArrayBatch(BaseBatch[FixedSizeEnumArrayArrayLike]): + _ARROW_DATATYPE = pa.list_(pa.field("item", pa.uint8(), nullable=False, metadata={}), 3) + + @staticmethod + def _native_to_pa_array(data: FixedSizeEnumArrayArrayLike, data_type: pa.DataType) -> pa.Array: + from typing import cast + + if isinstance(data, FixedSizeEnumArray): + typed_data = [data.values] + else: + data = cast("FixedSizeEnumArrayArrayLike", data) + try: + typed_data = [FixedSizeEnumArray(data).values] # type: ignore[arg-type] + except (AttributeError, TypeError, ValueError): + typed_data = [ + datum.values if isinstance(datum, FixedSizeEnumArray) else FixedSizeEnumArray(datum).values + for datum in data # type: ignore[union-attr] # ty: ignore[not-iterable] + ] + + flat_data = [axis.value for item in typed_data for axis in item] + return pa.FixedSizeListArray.from_arrays(pa.array(flat_data, type=pa.uint8()), type=data_type) diff --git a/rerun_py/tests/test_types/datatypes/fixed_size_wide_enum_array.py b/rerun_py/tests/test_types/datatypes/fixed_size_wide_enum_array.py new file mode 100644 index 000000000000..2ceabe148a1e --- /dev/null +++ b/rerun_py/tests/test_types/datatypes/fixed_size_wide_enum_array.py @@ -0,0 +1,107 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +# You can extend this class by creating a "FixedSizeWideEnumArrayExt" class in "fixed_size_wide_enum_array_ext.py". + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +import pyarrow as pa +from attrs import define, field +from rerun._baseclasses import ( + BaseBatch, +) + +if TYPE_CHECKING: + from .. import datatypes + +__all__ = [ + "FixedSizeWideEnumArray", + "FixedSizeWideEnumArrayArrayLike", + "FixedSizeWideEnumArrayBatch", + "FixedSizeWideEnumArrayLike", +] + + +def _fixed_size_wide_enum_array__values__special_field_converter_override(x: Any) -> list[datatypes.WideEnum]: + from .. import datatypes + + if isinstance(x, datatypes.FixedSizeWideEnumArray): + return x.values + + try: + values = list(x) + except TypeError as err: + raise ValueError("values must be an iterable of WideEnum values") from err + + if len(values) != 2: + raise ValueError(f"values must be a 2-element array. Got: {len(values)}") + + def convert_value(value: Any) -> datatypes.WideEnum: + if isinstance(value, (datatypes.WideEnum, str)): + return datatypes.WideEnum.auto(value) + return datatypes.WideEnum.auto(int(value)) + + return [convert_value(value) for value in values] + + +@define(init=False) +class FixedSizeWideEnumArray: + """**Datatype**: Test datatype for fixed-size arrays of wide enums.""" + + def __init__(self: Any, values: FixedSizeWideEnumArrayLike) -> None: + """ + Create a new instance of the FixedSizeWideEnumArray datatype. + + Parameters + ---------- + values: + Fixed-size wide enum array. + + """ + + # You can define your own __init__ function as a member of FixedSizeWideEnumArrayExt in fixed_size_wide_enum_array_ext.py + self.__attrs_init__(values=values) + + values: list[datatypes.WideEnum] = field( + converter=_fixed_size_wide_enum_array__values__special_field_converter_override + ) + # Fixed-size wide enum array. + # + # (Docstring intentionally commented out to hide this field from the docs) + + def __len__(self) -> int: + # You can define your own __len__ function as a member of FixedSizeWideEnumArrayExt in fixed_size_wide_enum_array_ext.py + return len(self.values) + + +FixedSizeWideEnumArrayLike = FixedSizeWideEnumArray +"""A type alias for any FixedSizeWideEnumArray-like object.""" + +FixedSizeWideEnumArrayArrayLike = FixedSizeWideEnumArray | Sequence[FixedSizeWideEnumArrayLike] +"""A type alias for any FixedSizeWideEnumArray-like array object.""" + + +class FixedSizeWideEnumArrayBatch(BaseBatch[FixedSizeWideEnumArrayArrayLike]): + _ARROW_DATATYPE = pa.list_(pa.field("item", pa.uint32(), nullable=False, metadata={}), 2) + + @staticmethod + def _native_to_pa_array(data: FixedSizeWideEnumArrayArrayLike, data_type: pa.DataType) -> pa.Array: + from typing import cast + + if isinstance(data, FixedSizeWideEnumArray): + typed_data = [data.values] + else: + data = cast("FixedSizeWideEnumArrayArrayLike", data) + try: + typed_data = [FixedSizeWideEnumArray(data).values] # type: ignore[arg-type] + except (AttributeError, TypeError, ValueError): + typed_data = [ + datum.values if isinstance(datum, FixedSizeWideEnumArray) else FixedSizeWideEnumArray(datum).values + for datum in data # type: ignore[union-attr] # ty: ignore[not-iterable] + ] + + flat_data = [axis.value for item in typed_data for axis in item] + return pa.FixedSizeListArray.from_arrays(pa.array(flat_data, type=pa.uint32()), type=data_type) diff --git a/rerun_py/tests/test_types/datatypes/many_vec3.py b/rerun_py/tests/test_types/datatypes/many_vec3.py new file mode 100644 index 000000000000..f39ab71299bc --- /dev/null +++ b/rerun_py/tests/test_types/datatypes/many_vec3.py @@ -0,0 +1,71 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/testing/datatypes/fuzzy.fbs". + +# You can extend this class by creating a "ManyVec3Ext" class in "many_vec3_ext.py". + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +import pyarrow as pa +from attrs import define, field +from rerun._baseclasses import ( + BaseBatch, +) +from rerun._converters import ( + to_np_float32, +) +from rerun._numpy_compatibility import asarray + +if TYPE_CHECKING: + import numpy as np + import numpy.typing as npt + +__all__ = ["ManyVec3", "ManyVec3ArrayLike", "ManyVec3Batch", "ManyVec3Like"] + + +@define(init=False) +class ManyVec3: + """**Datatype**: A fixed-size array of arrays — exercises nested fixed-size lists in Arrow.""" + + def __init__(self: Any, triples: ManyVec3Like) -> None: + """Create a new instance of the ManyVec3 datatype.""" + + # You can define your own __init__ function as a member of ManyVec3Ext in many_vec3_ext.py + self.__attrs_init__(triples=triples) + + triples: npt.NDArray[np.float32] = field(converter=to_np_float32) + + def __array__(self, dtype: npt.DTypeLike = None, copy: bool | None = None) -> npt.NDArray[Any]: + # You can define your own __array__ function as a member of ManyVec3Ext in many_vec3_ext.py + return asarray(self.triples, dtype=dtype, copy=copy) + + def __len__(self) -> int: + # You can define your own __len__ function as a member of ManyVec3Ext in many_vec3_ext.py + return len(self.triples) + + +ManyVec3Like = ManyVec3 +"""A type alias for any ManyVec3-like object.""" + +ManyVec3ArrayLike = ManyVec3 | Sequence[ManyVec3Like] +"""A type alias for any ManyVec3-like array object.""" + + +class ManyVec3Batch(BaseBatch[ManyVec3ArrayLike]): + _ARROW_DATATYPE = pa.list_( + pa.field( + "item", + pa.list_(pa.field("item", pa.float32(), nullable=False, metadata={}), 3), + nullable=False, + metadata={}, + ), + 2, + ) + + @staticmethod + def _native_to_pa_array(data: ManyVec3ArrayLike, data_type: pa.DataType) -> pa.Array: + raise NotImplementedError( + "Arrow serialization of ManyVec3 not implemented: We lack codegen for arrow-serialization of general structs" + ) # You need to implement native_to_pa_array_override in many_vec3_ext.py diff --git a/rerun_py/tests/test_types/datatypes/wide_enum.py b/rerun_py/tests/test_types/datatypes/wide_enum.py new file mode 100644 index 000000000000..e4a124140670 --- /dev/null +++ b/rerun_py/tests/test_types/datatypes/wide_enum.py @@ -0,0 +1,69 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_sdk_types/definitions/rerun/testing/components/enum_test.fbs". + +# You can extend this class by creating a "WideEnumExt" class in "wide_enum_ext.py". + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Literal + +import pyarrow as pa +from rerun._baseclasses import ( + BaseBatch, +) + +__all__ = ["WideEnum", "WideEnumArrayLike", "WideEnumBatch", "WideEnumLike"] + + +from enum import Enum + + +class WideEnum(Enum): + """**Datatype**: A test enum with values that require more than one byte.""" + + Low = 0x1 + """Low value.""" + + High = 0x10000 + """High value.""" + + @classmethod + def auto(cls, val: str | int | WideEnum) -> WideEnum: + """Best-effort converter, including a case-insensitive string matcher.""" + if isinstance(val, WideEnum): + return val + if isinstance(val, int): + return cls(val) + try: + return cls[val] + except KeyError: + val_lower = val.lower() + for variant in cls: + if variant.name.lower() == val_lower: + return variant + raise ValueError(f"Cannot convert {val} to {cls.__name__}") + + def __str__(self) -> str: + """Returns the variant name.""" + return self.name + + +WideEnumLike = WideEnum | Literal["High", "Low", "high", "low"] | int +"""A type alias for any WideEnum-like object.""" + +WideEnumArrayLike = WideEnum | Literal["High", "Low", "high", "low"] | int | Sequence[WideEnumLike] +"""A type alias for any WideEnum-like array object.""" + + +class WideEnumBatch(BaseBatch[WideEnumArrayLike]): + _ARROW_DATATYPE = pa.uint32() + + @staticmethod + def _native_to_pa_array(data: WideEnumArrayLike, data_type: pa.DataType) -> pa.Array: + if isinstance(data, (WideEnum, int, str)): + data = [data] + + pa_data = [WideEnum.auto(v).value if v is not None else None for v in data] # type: ignore[redundant-expr] # ty: ignore[not-iterable] + + return pa.array(pa_data, type=data_type) diff --git a/rerun_py/tests/unit/__snapshots__/test_recording.ambr b/rerun_py/tests/unit/__snapshots__/test_recording.ambr index 8c57cb4a6256..847ba12a0e17 100644 --- a/rerun_py/tests/unit/__snapshots__/test_recording.ambr +++ b/rerun_py/tests/unit/__snapshots__/test_recording.ambr @@ -44,7 +44,6 @@ # --- # name: test_schema_recording ''' - Index(timeline:log_tick) Index(timeline:log_time) Index(timeline:my_index) Column name: /points:Points3D:colors diff --git a/rerun_py/tests/unit/__snapshots__/test_send_dataframe.ambr b/rerun_py/tests/unit/__snapshots__/test_send_dataframe.ambr index 7a452292017d..dec76ff36b73 100644 --- a/rerun_py/tests/unit/__snapshots__/test_send_dataframe.ambr +++ b/rerun_py/tests/unit/__snapshots__/test_send_dataframe.ambr @@ -3,7 +3,6 @@ ''' pyarrow.Table my_index: int64 - log_tick: int64 /points:Points3D:colors: list child 0, item: uint32 /points:Points3D:positions: list[3]> @@ -13,7 +12,6 @@ child 0, item: float ---- my_index: [[1,7]] - log_tick: [[0,1]] /points:Points3D:colors: [[null,[4278190335]]] /points:Points3D:positions: [[[[1,2,3],[4,5,6],[7,8,9]],[[10,11,12]]]] /points:Points3D:radii: [[[0.5],null]] diff --git a/rerun_py/tests/unit/test_bar_chart.py b/rerun_py/tests/unit/test_bar_chart.py new file mode 100644 index 000000000000..91f89ad08bd5 --- /dev/null +++ b/rerun_py/tests/unit/test_bar_chart.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import numpy as np +import pytest +import rerun as rr + + +def test_bar_chart_shapes() -> None: + """`BarChart` accepts only 1D data.""" + rr.set_strict_mode(True) + + # Single-element 1D array. + rr.BarChart(np.array([1.0])) + # Regular 1D array. + rr.BarChart(np.array([1.0, 2.0, 3.0])) + # Leading singleton dimension. + rr.BarChart(np.array([[1.0, 2.0, 3.0]])) + + with pytest.raises(ValueError, match="Bar chart data should only be 1D"): + rr.BarChart(np.array(1.0)) + + with pytest.raises(ValueError, match="Bar chart data should only be 1D"): + rr.BarChart(np.ones((2, 2))) diff --git a/rerun_py/tests/unit/test_batcher_config.py b/rerun_py/tests/unit/test_batcher_config.py index 3e4e84c76cde..3f8773e8e55d 100644 --- a/rerun_py/tests/unit/test_batcher_config.py +++ b/rerun_py/tests/unit/test_batcher_config.py @@ -77,7 +77,7 @@ def test_flush_always() -> None: rec = rr.RecordingStream( "rerun_example_multi_stream", - batcher_config=rr.ChunkBatcherConfig.ALWAYS(), + batcher_config=rr.ChunkBatcherConfig.ALWAYS_TEST_ONLY(), ) rec.save(rec_path) diff --git a/rerun_py/tests/unit/test_binary_stream.py b/rerun_py/tests/unit/test_binary_stream.py index 8f9c2492b642..b308fe5287c6 100644 --- a/rerun_py/tests/unit/test_binary_stream.py +++ b/rerun_py/tests/unit/test_binary_stream.py @@ -2,7 +2,6 @@ from __future__ import annotations -import os import queue import subprocess import tempfile @@ -16,6 +15,8 @@ if TYPE_CHECKING: from collections.abc import Iterator + import pytest + @rr.thread_local_stream("rerun_example_binary_stream") def job(name: str) -> Iterator[tuple[str, bytes | None]]: @@ -25,7 +26,7 @@ def job(name: str) -> Iterator[tuple[str, bytes | None]]: rr.send_blueprint(blueprint) - for i in range(100): + for i in range(30): time.sleep(0.01) rr.log("test", rr.TextLog(f"Message {i}")) @@ -37,10 +38,11 @@ def queue_results(generator: Iterator[Any], out_queue: queue.Queue[tuple[str, by out_queue.put(item) -def test_binary_stream() -> None: - # Flush num rows must be 0 to avoid inconsistencies in the stream - prev_flush_num_rows = os.environ.get("RERUN_FLUSH_NUM_ROWS") - os.environ["RERUN_FLUSH_NUM_ROWS"] = "0" +def test_binary_stream(monkeypatch: pytest.MonkeyPatch) -> None: + # Flush num rows must be 0 to avoid inconsistencies in the stream. + # Use `monkeypatch` so the env var is reverted even if the test fails: leaking it pollutes the + # rest of the session (e.g. it gets inherited by the viewer spawned in the integration tests). + monkeypatch.setenv("RERUN_FLUSH_NUM_ROWS", "0") results_queue: queue.Queue[tuple[str, bytes | None]] = queue.Queue() @@ -73,9 +75,3 @@ def test_binary_stream() -> None: if process.returncode != 0: print(process.stderr.decode("utf-8")) raise Exception("Rerun failed") - - # Restore the previous value of RERUN_FLUSH_NUM_ROWS - if prev_flush_num_rows is not None: - os.environ["RERUN_FLUSH_NUM_ROWS"] = prev_flush_num_rows - else: - del os.environ["RERUN_FLUSH_NUM_ROWS"] diff --git a/rerun_py/tests/unit/test_dataloader_decoder_helpers.py b/rerun_py/tests/unit/test_dataloader_decoder_helpers.py index 385d603bb5f7..68c00db8035d 100644 --- a/rerun_py/tests/unit/test_dataloader_decoder_helpers.py +++ b/rerun_py/tests/unit/test_dataloader_decoder_helpers.py @@ -2,107 +2,46 @@ from __future__ import annotations +import pickle +from fractions import Fraction +from typing import cast + +import av import numpy as np import pyarrow as pa import pytest +import torch +from rerun.experimental.dataloader import Field from rerun.experimental.dataloader._decoders import ( - _avcc_to_annex_b, + VideoFrameDecoder, _flatten_blob, - _is_annex_b, - _is_av1_keyframe_packet, + _starts_with, _unwrap_to_numpy, ) +from rerun.experimental.dataloader._utils import _field_index_range, _prior_keyframe -@pytest.mark.parametrize( - ("data", "expected"), - [ - (b"\x00\x00\x00\x01\xab\xcd", True), # 4-byte start code - (b"\x00\x00\x01\xab\xcd", True), # 3-byte short start code - (b"\x00\x00\x00\x01", True), # exactly the start code - (b"\x00\x00\x01", True), # exactly the short start code - (b"\x00\x00\x02\xab", False), - (b"\xab\xcd\xef\x01", False), - (b"", False), - (b"\x00", False), - (b"\x00\x00", False), - ], -) -def test_is_annex_b(data: bytes, expected: bool) -> None: - assert _is_annex_b(data) is expected - - -def _make_obu_header(obu_type: int) -> int: - """Return a byte whose OBU-type field (bits [3:6]) matches *obu_type*.""" - return (obu_type & 0xF) << 3 - - -@pytest.mark.parametrize( - ("obu_type", "expected"), - [ - (1, True), # OBU_SEQUENCE_HEADER - (2, True), # OBU_TEMPORAL_DELIMITER - (3, False), # OBU_FRAME_HEADER - (6, False), # OBU_FRAME - (0, False), - (15, False), - ], -) -def test_is_av1_keyframe_packet(obu_type: int, expected: bool) -> None: - sample = bytes([_make_obu_header(obu_type), 0x00, 0x00]) - assert _is_av1_keyframe_packet(sample) is expected - - -def test_is_av1_keyframe_packet_empty() -> None: - assert _is_av1_keyframe_packet(b"") is False +def _encoder_available(name: str) -> bool: + """True if this PyAV build can encode with *name*.""" + try: + av.codec.Codec(name, "w") + except Exception: + return False + return True -def test_is_av1_keyframe_packet_ignores_low_bits() -> None: - # Low three bits (extension/has-size/reserved) must not affect detection. - header = _make_obu_header(1) | 0b111 - assert _is_av1_keyframe_packet(bytes([header])) is True - - -def _avcc_encode(nal_units: list[bytes], nal_length_size: int = 4) -> bytes: +def _h264_annex_b(nal_units: list[tuple[int, bytes]], use_4byte: bool = True) -> bytes: + """Build an Annex B H.264 stream from `(nal_unit_type, payload)` pairs.""" + start = b"\x00\x00\x00\x01" if use_4byte else b"\x00\x00\x01" out = bytearray() - for unit in nal_units: - out.extend(len(unit).to_bytes(nal_length_size, "big")) - out.extend(unit) + for nal_type, payload in nal_units: + out.extend(start) + # nal_ref_idc=3, forbidden_zero_bit=0 + out.append((3 << 5) | (nal_type & 0x1F)) + out.extend(payload) return bytes(out) -def test_avcc_to_annex_b_single_unit() -> None: - unit = b"\x67\x42\xc0\x1f" - result = _avcc_to_annex_b(_avcc_encode([unit])) - assert result == b"\x00\x00\x00\x01" + unit - - -def test_avcc_to_annex_b_multiple_units() -> None: - units = [b"\x67\x42\xc0\x1f", b"\x68\xce\x38\x80", b"\x65\x88\x84"] - result = _avcc_to_annex_b(_avcc_encode(units)) - expected = b"".join(b"\x00\x00\x00\x01" + u for u in units) - assert result == expected - - -def test_avcc_to_annex_b_length_size_2() -> None: - units = [b"\xaa\xbb", b"\xcc\xdd\xee"] - result = _avcc_to_annex_b(_avcc_encode(units, nal_length_size=2), nal_length_size=2) - expected = b"".join(b"\x00\x00\x00\x01" + u for u in units) - assert result == expected - - -def test_avcc_to_annex_b_truncated_stops_early() -> None: - # Well-formed first unit, then a length that claims more data than is left. - first = b"\x67\x42\xc0" - buf = len(first).to_bytes(4, "big") + first + (0xFF).to_bytes(4, "big") + b"\x00\x01" - result = _avcc_to_annex_b(buf) - assert result == b"\x00\x00\x00\x01" + first - - -def test_avcc_to_annex_b_empty() -> None: - assert _avcc_to_annex_b(b"") == b"" - - def test_unwrap_plain_numeric() -> None: arr = pa.array([1.0, 2.0, 3.0], type=pa.float64()) np.testing.assert_array_equal(_unwrap_to_numpy(arr), np.array([1.0, 2.0, 3.0])) @@ -168,3 +107,272 @@ def test_flatten_blob_binary_respects_offsets() -> None: ) for row, expected in enumerate([b"AAAA", b"BB", b"CCCCCC"]): np.testing.assert_array_equal(_flatten_blob(arr, row), np.frombuffer(expected, dtype=np.uint8)) + + +def test_video_frame_decoder_returns_none_without_keyframe() -> None: + """`decode` returns `None` when the prefetched window contains no keyframe.""" + p_slice_only = _h264_annex_b([(1, b"\xab\xcd\xef\x01\x02\x03")]) + raw = pa.chunked_array([pa.array([[p_slice_only]], type=pa.list_(pa.binary()))]) + + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=2) + assert decoder.decode(raw, 0, "seg") is None + + +def test_video_frame_decoder_is_keyframe_h264() -> None: + gop = 4 + samples = _encode_h264(num_frames=8, gop=gop) + decoder = VideoFrameDecoder(codec="h264") + assert decoder._is_keyframe(samples[0]) is True + assert decoder._is_keyframe(samples[1]) is False + assert decoder._is_keyframe(samples[gop]) is True + + +def test_video_frame_decoder_is_keyframe_h264_idr_without_sps() -> None: + # An IDR NAL alone can't bootstrap a decoder (no SPS): not a keyframe. + idr_only = _h264_annex_b([(5, b"\x88")]) + assert VideoFrameDecoder(codec="h264")._is_keyframe(idr_only) is False + + +@pytest.mark.skipif(not _encoder_available("libx265"), reason="PyAV build lacks the libx265 encoder") +def test_video_frame_decoder_is_keyframe_hevc() -> None: + samples = _encode_hevc(num_frames=4, gop=4) + decoder = VideoFrameDecoder(codec="hevc") + assert decoder._is_keyframe(samples[0]) is True + assert decoder._is_keyframe(samples[1]) is False + + +def test_video_frame_decoder_is_keyframe_undetectable_codec_returns_none() -> None: + assert VideoFrameDecoder(codec="mjpeg")._is_keyframe(b"\x00") is None + + +def test_video_frame_decoder_is_keyframe_vp9_classifies_garbage() -> None: + # vp9 has a detector, so garbage is classified rather than passed through as None. + assert VideoFrameDecoder(codec="vp9")._is_keyframe(b"\x00") is False + + +def test_video_frame_decoder_has_keyframe_h264() -> None: + samples = _encode_h264(num_frames=4, gop=4) + keyframe, p_slice = samples[0], samples[1] + decoder = VideoFrameDecoder(codec="h264") + assert decoder._has_keyframe([]) is False + assert decoder._has_keyframe([p_slice]) is False + assert decoder._has_keyframe([p_slice, keyframe]) is True + + +def test_video_frame_decoder_has_keyframe_undetectable_codec_trusts_decoder() -> None: + # Undetectable codec: `_is_keyframe` returns None and `_has_keyframe` returns True so + # failures surface from the decoder rather than being swallowed as cold-start. + assert VideoFrameDecoder(codec="mjpeg")._has_keyframe([b"\x00"]) is True + + +def test_video_frame_decoder_derives_keyframe_path() -> None: + decoder = VideoFrameDecoder(codec="h264") + assert decoder.prior_keyframe_path("/camera:VideoStream:sample") == "/camera:VideoStream:is_keyframe" + assert ( + decoder.prior_keyframe_path("/robot/cam_left:VideoStream:sample") == "/robot/cam_left:VideoStream:is_keyframe" + ) + + +def test_video_frame_decoder_keyframe_path_no_separator() -> None: + # Defensive: a path with no `:` is non-canonical; return None rather than guessing. + assert VideoFrameDecoder(codec="h264").prior_keyframe_path("/just_an_entity") is None + + +def test_field_index_range_window_beats_anchor_and_heuristic() -> None: + field = Field(path="/camera:VideoStream:sample", decode=VideoFrameDecoder(codec="h264"), window=(-3, 5)) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=10) + # Anchor and heuristic must lose to the explicit window. + assert _field_index_range(100, field, decoder, prior_keyframe=42) == (97, 105) + + +def test_field_index_range_anchor_beats_heuristic_integer() -> None: + field = Field(path="/camera:VideoStream:sample", decode=VideoFrameDecoder(codec="h264")) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=10) + assert _field_index_range(100, field, decoder, prior_keyframe=87) == (87, 100) + + +def test_field_index_range_anchor_beats_heuristic_timestamp() -> None: + field = Field(path="/camera:VideoStream:sample", decode=VideoFrameDecoder(codec="h264")) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=30, fps_estimate=30.0) + target = np.datetime64(1_000_000_000, "ns") + result = _field_index_range(target, field, decoder, prior_keyframe=500_000_000) + assert result is not None + lo, hi = result + assert lo == np.datetime64(500_000_000, "ns") + assert hi == target + + +def test_field_index_range_falls_back_to_heuristic_when_anchor_missing() -> None: + # Simulates "no prior keyframe yet in this segment" — the prefetcher drops the + # entry and the field falls back to the decoder's heuristic context_range. + field = Field(path="/camera:VideoStream:sample", decode=VideoFrameDecoder(codec="h264")) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=10) + assert _field_index_range(100, field, decoder, prior_keyframe=None) == (90, 100) + + +def test_field_index_range_default_kwarg_is_none() -> None: + # Existing call sites that don't pass `prior_keyframe` keep the same behavior. + field = Field(path="/camera:VideoStream:sample", decode=VideoFrameDecoder(codec="h264")) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=5) + assert _field_index_range(20, field, decoder) == (15, 20) + + +def test_prior_keyframe_none_or_empty_returns_none() -> None: + assert _prior_keyframe(None, 100) is None + assert _prior_keyframe(np.array([], dtype=np.int64), 100) is None + + +def test_prior_keyframe_target_before_first_returns_none() -> None: + assert _prior_keyframe(np.array([50, 100, 150], dtype=np.int64), 49) is None + + +def test_prior_keyframe_target_equals_keyframe_returns_keyframe() -> None: + assert _prior_keyframe(np.array([50, 100, 150], dtype=np.int64), 100) == 100 + + +def test_prior_keyframe_target_between_returns_largest_leq() -> None: + kfs = np.array([50, 100, 150], dtype=np.int64) + assert _prior_keyframe(kfs, 99) == 50 + assert _prior_keyframe(kfs, 149) == 100 + + +def test_prior_keyframe_target_after_last_returns_last() -> None: + assert _prior_keyframe(np.array([50, 100, 150], dtype=np.int64), 9999) == 150 + + +def test_starts_with() -> None: + assert _starts_with([b"a", b"b", b"c"], []) + assert _starts_with([b"a", b"b", b"c"], [b"a", b"b"]) + assert _starts_with([b"a", b"b"], [b"a", b"b"]) + assert not _starts_with([b"a"], [b"a", b"b"]) + assert not _starts_with([b"a", b"x"], [b"a", b"b"]) + + +def _encode_h264(num_frames: int, gop: int, b_frames: int = 0) -> list[bytes]: + """One Annex B sample per frame, keyframes every *gop* frames.""" + encoder = av.CodecContext.create("libx264", "w") + encoder.width, encoder.height = 64, 64 + encoder.pix_fmt = "yuv420p" + encoder.time_base = Fraction(1, 30) + encoder.framerate = Fraction(30, 1) + encoder.options = {"g": str(gop), "bf": str(b_frames), "tune": "zerolatency" if b_frames == 0 else "psnr"} + samples: list[bytes] = [] + for i in range(num_frames): + pixels = np.empty((64, 64, 3), dtype=np.uint8) + pixels[:, :, 0] = ((np.arange(64) + i) % 256)[np.newaxis, :] + pixels[:, :, 1] = ((np.arange(64) + i * 3) % 256)[:, np.newaxis] + pixels[:, :, 2] = (i * 7) % 256 + frame = av.VideoFrame.from_ndarray(pixels, format="rgb24").reformat(format="yuv420p") + frame.pts = i + samples.extend(bytes(p) for p in encoder.encode(frame)) + samples.extend(bytes(p) for p in encoder.encode(None)) + assert len(samples) == num_frames + return samples + + +def _encode_hevc(num_frames: int, gop: int) -> list[bytes]: + """One Annex B HEVC sample per frame, keyframes every *gop* frames, headers repeated on each keyframe.""" + # The PyAV stubs' video-codec-name literal doesn't know libx265, so the overload needs help. + encoder = cast("av.VideoCodecContext", av.CodecContext.create("libx265", "w")) + encoder.width, encoder.height = 64, 64 + encoder.pix_fmt = "yuv420p" + encoder.time_base = Fraction(1, 30) + encoder.framerate = Fraction(30, 1) + encoder.options = { + "x265-params": f"keyint={gop}:min-keyint={gop}:bframes=0:repeat-headers=1:log-level=none", + } + samples: list[bytes] = [] + for i in range(num_frames): + pixels = np.full((64, 64, 3), (i * 31) % 256, dtype=np.uint8) + frame = av.VideoFrame.from_ndarray(pixels, format="rgb24").reformat(format="yuv420p") + frame.pts = i + samples.extend(bytes(p) for p in encoder.encode(frame)) + samples.extend(bytes(p) for p in encoder.encode(None)) + assert len(samples) == num_frames + return samples + + +def _raw_window(samples: list[bytes]) -> pa.ChunkedArray: + return pa.chunked_array([pa.array([[s] for s in samples], type=pa.list_(pa.binary()))]) + + +def _session_contexts(decoder: VideoFrameDecoder) -> list[av.VideoCodecContext]: + return [session.context for session in decoder._sessions.values()] + + +def test_video_frame_decoder_sequential_reads_reuse_session() -> None: + gop = 6 + samples = _encode_h264(num_frames=12, gop=gop) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=gop) + + contexts = [] + for target in range(12): + keyframe = (target // gop) * gop + window = _raw_window(samples[keyframe : target + 1]) + got = decoder.decode(window, target, "seg") + expected = VideoFrameDecoder(codec="h264", keyframe_interval=gop).decode(window, target, "seg") + assert got is not None and expected is not None + assert torch.equal(got, expected) + contexts.extend(_session_contexts(decoder)) + + # One context per GOP; without sessions this would be one per target. + assert len(set(map(id, contexts))) == 2 + + +def test_video_frame_decoder_repeated_target_hits_session() -> None: + samples = _encode_h264(num_frames=4, gop=4) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=4) + + window = _raw_window(samples[:3]) + first = decoder.decode(window, 2, "seg") + context = _session_contexts(decoder)[0] + second = decoder.decode(window, 2, "seg") + assert first is not None and second is not None + assert torch.equal(first, second) + assert _session_contexts(decoder) == [context] + + +def test_video_frame_decoder_backward_step_restarts_session() -> None: + gop = 6 + samples = _encode_h264(num_frames=6, gop=gop) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=gop) + + decoder.decode(_raw_window(samples[:5]), 4, "seg") + context = _session_contexts(decoder)[0] + # A shorter window is not an extension: a fresh context must replay it. + got = decoder.decode(_raw_window(samples[:3]), 2, "seg") + expected = VideoFrameDecoder(codec="h264", keyframe_interval=gop).decode(_raw_window(samples[:3]), 2, "seg") + assert got is not None and expected is not None + assert torch.equal(got, expected) + assert _session_contexts(decoder) != [context] + + +def test_video_frame_decoder_segments_get_separate_sessions() -> None: + samples = _encode_h264(num_frames=4, gop=4) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=4) + + a = decoder.decode(_raw_window(samples[:2]), 1, "seg_a") + b = decoder.decode(_raw_window(samples[:2]), 1, "seg_b") + assert a is not None and b is not None + assert torch.equal(a, b) + assert len(decoder._sessions) == 2 + + +def test_video_frame_decoder_delayed_stream_falls_back_to_flush() -> None: + # B-frames make the decoder hold frames back, so no session can be kept. + samples = _encode_h264(num_frames=8, gop=8, b_frames=2) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=8) + + assert decoder.decode(_raw_window(samples[:8]), 7, "seg") is not None + assert len(decoder._sessions) == 0 + + +def test_video_frame_decoder_pickle_drops_sessions() -> None: + samples = _encode_h264(num_frames=4, gop=4) + decoder = VideoFrameDecoder(codec="h264", keyframe_interval=4) + assert decoder.decode(_raw_window(samples[:2]), 1, "seg") is not None + assert len(decoder._sessions) == 1 + + restored = pickle.loads(pickle.dumps(decoder)) + assert len(restored._sessions) == 0 + assert restored.decode(_raw_window(samples[:2]), 1, "seg") is not None diff --git a/rerun_py/tests/unit/test_dataloader_query_indices.py b/rerun_py/tests/unit/test_dataloader_query_indices.py new file mode 100644 index 000000000000..7385687101a3 --- /dev/null +++ b/rerun_py/tests/unit/test_dataloader_query_indices.py @@ -0,0 +1,67 @@ +"""Tests for `_build_query_indices` in `rerun.experimental.dataloader._utils`.""" + +from __future__ import annotations + +import numpy as np +import pyarrow as pa +import pytest +from rerun.experimental.dataloader._sample_index import SampleIndex, SegmentMetadata +from rerun.experimental.dataloader._utils import Target, _build_query_indices + + +def _segment(segment_id: str, index_start: int, num_samples: int, ns_per_sample: int) -> SegmentMetadata: + return SegmentMetadata( + segment_id=segment_id, + index_start=index_start, + index_end=index_start + (num_samples - 1) * ns_per_sample, + num_samples=num_samples, + ) + + +def _targets(sample_index: SampleIndex, count: int) -> list[Target]: + """`Target`s for the first `count` global indices, with no keyframe anchors.""" + located = (sample_index.global_to_local(i) for i in range(count)) + return [Target(segment=segment, index_value=value, anchors={}) for segment, value in located] + + +@pytest.mark.parametrize( + ("ns_dtype", "expected_arrow_type"), + [ + ("datetime64[ns]", pa.timestamp("ns")), + ("timedelta64[ns]", pa.duration("ns")), + ], +) +def test_build_query_indices_temporal_returns_pyarrow(ns_dtype: str, expected_arrow_type: pa.DataType) -> None: + """ + Temporal timelines must hand values to the Rust binding as pyarrow arrays. + + `IndexValuesLike::extract_bound` accepts `datetime64` ndarrays but not + `timedelta64`, so the dataloader routes both temporal kinds through + `pa.array(…, timestamp("ns") | duration("ns"))` instead. + """ + ns_per_sample = 10_000_000 # 100 Hz + segment = _segment("seg-a", index_start=0, num_samples=3, ns_per_sample=ns_per_sample) + sample_index = SampleIndex([segment], ns_per_sample=ns_per_sample, ns_dtype=ns_dtype) + + targets = _targets(sample_index, 3) + result = _build_query_indices(targets, fields={}, decoders={}, sample_index=sample_index) + + assert set(result.keys()) == {"seg-a"} + values = result["seg-a"] + assert isinstance(values, pa.Array) + assert values.type == expected_arrow_type + assert values.cast(pa.int64()).to_pylist() == [0, ns_per_sample, 2 * ns_per_sample] + + +def test_build_query_indices_integer_returns_ndarray() -> None: + """Integer timelines keep the int64 ndarray path.""" + segment = SegmentMetadata(segment_id="seg-a", index_start=10, index_end=12, num_samples=3) + sample_index = SampleIndex([segment]) + + targets = _targets(sample_index, 3) + result = _build_query_indices(targets, fields={}, decoders={}, sample_index=sample_index) + + values = result["seg-a"] + assert isinstance(values, np.ndarray) + assert values.dtype == np.int64 + assert values.tolist() == [10, 11, 12] diff --git a/rerun_py/tests/unit/test_disconnect_on_cleanup.py b/rerun_py/tests/unit/test_disconnect_on_cleanup.py index 694b8f075136..9dda8b802905 100644 --- a/rerun_py/tests/unit/test_disconnect_on_cleanup.py +++ b/rerun_py/tests/unit/test_disconnect_on_cleanup.py @@ -35,5 +35,22 @@ def create_recording() -> None: assert rerun_bindings.check_for_rrd_footer(rec_path) +def test_footer_written_at_context_exit() -> None: + """The footer must be present as soon as the `with`-block exits — before `rec` is GC'd.""" + with tempfile.TemporaryDirectory() as dirpath: + rec_path = f"{dirpath}/rec.rrd" + + rec = rr.RecordingStream("rerun_example_finalize_at_exit") + with rec: + rec.save(rec_path) + rec.log("x", rr.Points2D(positions=[(1, 2), (3, 4)])) + + # `rec` is still alive here — no GC has happened. The footer must already be on disk. + assert rerun_bindings.check_for_rrd_footer(rec_path) + + # Keep `rec` alive past the assertion so `__del__` can't sneak in and rescue a missing footer. + del rec + + if __name__ == "__main__": test_disconnect_on_cleanup() diff --git a/rerun_py/tests/unit/test_exceptions.py b/rerun_py/tests/unit/test_exceptions.py index e8e0ab8100bd..c6dbe9e026c9 100644 --- a/rerun_py/tests/unit/test_exceptions.py +++ b/rerun_py/tests/unit/test_exceptions.py @@ -1,7 +1,6 @@ from __future__ import annotations import inspect -import os from typing import Any import pytest @@ -52,9 +51,11 @@ def expected_warnings(warnings: Any, mem: Any, starting_msgs: int, count: int, e assert "some value error" in str(w.message), f"mem: {mem}, starting_msgs: {starting_msgs}, count: {count}" -def test_stack_tracking() -> None: - # Force flushing so we can count the messages - os.environ["RERUN_FLUSH_NUM_ROWS"] = "0" +def test_stack_tracking(monkeypatch: pytest.MonkeyPatch) -> None: + # Force flushing so we can count the messages. + # Use `monkeypatch` so the env var is reverted at the end of the test: leaking it pollutes the + # rest of the session (e.g. it gets inherited by the viewer spawned in the integration tests). + monkeypatch.setenv("RERUN_FLUSH_NUM_ROWS", "0") rr.init("rerun_example_strict_mode", strict=False, spawn=False) mem = rr.memory_recording() diff --git a/rerun_py/tests/unit/test_file_sink.py b/rerun_py/tests/unit/test_file_sink.py new file mode 100644 index 000000000000..8d2bab762cbb --- /dev/null +++ b/rerun_py/tests/unit/test_file_sink.py @@ -0,0 +1,87 @@ +"""Tests for the `FileSink` / `save` / `stdout` `write_footer` opt-out.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import rerun as rr + +if TYPE_CHECKING: + import pathlib + +APP_ID = "rerun_example_test_file_sink" + +# The trailing RRD `StreamFooter` frame always ends with the bytes `RRF2` followed by +# `FOOT`, located at `file_len - 12 .. file_len - 4`. +# See `re_log_encoding::rrd::frames::StreamFooter` for the definition. +_STREAM_FOOTER_FOURCC = b"RRF2" +_STREAM_FOOTER_IDENTIFIER = b"FOOT" + + +def _has_stream_footer(path: pathlib.Path) -> bool: + """Return True if the file at `path` ends with a valid RRD `StreamFooter` trailer.""" + data = path.read_bytes() + if len(data) < 12: + return False + return data[-12:-8] == _STREAM_FOOTER_FOURCC and data[-8:-4] == _STREAM_FOOTER_IDENTIFIER + + +def _log_some(rec: rr.RecordingStream) -> None: + for i in range(10): + rec.log("signal", rr.Scalars(float(i))) + + +def test_save_default_writes_footer(tmp_path: pathlib.Path) -> None: + """`RecordingStream.save(path)` defaults to writing a footer.""" + rrd = tmp_path / "default.rrd" + rec = rr.RecordingStream(APP_ID) + rec.save(rrd) + _log_some(rec) + rec.disconnect() + + assert _has_stream_footer(rrd), "default save() must produce a footer-bearing file" + + +def test_save_write_footer_false_omits_footer(tmp_path: pathlib.Path) -> None: + """`RecordingStream.save(path, write_footer=False)` produces a footer-less file.""" + rrd = tmp_path / "no_footer.rrd" + rec = rr.RecordingStream(APP_ID) + rec.save(rrd, write_footer=False) + _log_some(rec) + rec.disconnect() + + assert not _has_stream_footer(rrd), "save(…, write_footer=False) must produce a footer-less file" + + +def test_module_save_write_footer_false(tmp_path: pathlib.Path) -> None: + """The module-level `rr.save(…, write_footer=False)` honours the flag.""" + rrd = tmp_path / "module_no_footer.rrd" + rr.init(APP_ID + "_module") + rr.save(rrd, write_footer=False) + for i in range(10): + rr.log("signal", rr.Scalars(float(i))) + rr.disconnect() + + assert not _has_stream_footer(rrd) + + +def test_filesink_class_default_writes_footer(tmp_path: pathlib.Path) -> None: + """The `rr.FileSink(path)` class defaults to writing a footer (legacy call shape).""" + rrd = tmp_path / "filesink_default.rrd" + rec = rr.RecordingStream(APP_ID) + rec.set_sinks(rr.FileSink(rrd)) + _log_some(rec) + rec.disconnect() + + assert _has_stream_footer(rrd) + + +def test_filesink_class_write_footer_false(tmp_path: pathlib.Path) -> None: + """The `rr.FileSink(path, write_footer=False)` class honours the kw-only flag.""" + rrd = tmp_path / "filesink_no_footer.rrd" + rec = rr.RecordingStream(APP_ID) + rec.set_sinks(rr.FileSink(rrd, write_footer=False)) + _log_some(rec) + rec.disconnect() + + assert not _has_stream_footer(rrd) diff --git a/rerun_py/tests/unit/test_filesink_threading.py b/rerun_py/tests/unit/test_filesink_threading.py new file mode 100644 index 000000000000..a5a8f16008db --- /dev/null +++ b/rerun_py/tests/unit/test_filesink_threading.py @@ -0,0 +1,53 @@ +"""Regression guard: multiple threads must be able to log to a shared FileSink inside a `with` block.""" + +from __future__ import annotations + +import threading +from typing import TYPE_CHECKING + +import rerun as rr +from rerun.experimental import RrdReader + +if TYPE_CHECKING: + from pathlib import Path + +NUM_THREADS = 8 +MESSAGES_PER_THREAD = 50 + + +def _worker(rec: rr.RecordingStream, thread_id: int) -> None: + base = thread_id * MESSAGES_PER_THREAD + for i in range(MESSAGES_PER_THREAD): + rec.set_time("seq", sequence=base + i) + rec.log(f"thread_{thread_id}", rr.Scalars(float(base + i))) + + +def test_multithreaded_filesink_in_context_manager(tmp_path: Path) -> None: + rrd_path = tmp_path / "multithread.rrd" + + def run() -> None: + with rr.RecordingStream("rerun_example_multithread_filesink") as rec: + rec.save(rrd_path) + + threads = [threading.Thread(target=_worker, args=(rec, tid)) for tid in range(NUM_THREADS)] + for t in threads: + t.start() + for t in threads: + t.join() + + run() + + # `RrdReader.store()` rejects footer-less files, so a successful call here implicitly + # verifies the file was finalized. + reader = RrdReader(rrd_path) + reader.store() + + seen = dict.fromkeys(range(NUM_THREADS), 0) + for chunk in reader.stream(): + for tid in seen: + if chunk.entity_path == f"/thread_{tid}": + seen[tid] += chunk.num_rows + break + + for tid, count in seen.items(): + assert count == MESSAGES_PER_THREAD, f"thread {tid}: got {count} rows, expected {MESSAGES_PER_THREAD}" diff --git a/rerun_py/tests/unit/test_fixed_size_enum_array.py b/rerun_py/tests/unit/test_fixed_size_enum_array.py new file mode 100644 index 000000000000..35e3c4b9ae9d --- /dev/null +++ b/rerun_py/tests/unit/test_fixed_size_enum_array.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import pyarrow as pa +from test_types.datatypes import ( + FixedSizeEnumArray, + FixedSizeEnumArrayBatch, + FixedSizeWideEnumArray, + FixedSizeWideEnumArrayBatch, +) + + +def test_fixed_size_enum_array_batch() -> None: + a = FixedSizeEnumArray(["Up", "Down", "Right"]) # type: ignore[arg-type] + b = FixedSizeEnumArray(["Left", "Forward", "Back"]) # type: ignore[arg-type] + values = [[1, 2, 3], [4, 5, 6]] + + single = FixedSizeEnumArrayBatch(a).as_arrow_array() + raw = FixedSizeEnumArrayBatch(values).as_arrow_array() # type: ignore[arg-type] + instances = FixedSizeEnumArrayBatch([a, b]).as_arrow_array() + + assert single.type.value_type == pa.uint8() + + assert single.to_pylist() == values[:1] + assert raw.to_pylist() == values + assert instances.to_pylist() == values + + +def test_fixed_size_enum_array_accepts_existing_instance() -> None: + original = FixedSizeEnumArray(["Up", "Down", "Right"]) # type: ignore[arg-type] + FixedSizeEnumArray(original) # Does not raise an exception. + + +def test_fixed_size_wide_enum_array_batch() -> None: + a = FixedSizeWideEnumArray(["Low", "High"]) # type: ignore[arg-type] + b = FixedSizeWideEnumArray(["High", "Low"]) # type: ignore[arg-type] + values = [[1, 65536], [65536, 1]] + + single = FixedSizeWideEnumArrayBatch(a).as_arrow_array() + raw = FixedSizeWideEnumArrayBatch(values).as_arrow_array() # type: ignore[arg-type] + instances = FixedSizeWideEnumArrayBatch([a, b]).as_arrow_array() + + assert single.type.value_type == pa.uint32() + + assert single.to_pylist() == values[:1] + assert raw.to_pylist() == values + assert instances.to_pylist() == values diff --git a/rerun_py/tests/unit/test_multi_stream.py b/rerun_py/tests/unit/test_multi_stream.py index 495759188af5..a8d5814c3786 100644 --- a/rerun_py/tests/unit/test_multi_stream.py +++ b/rerun_py/tests/unit/test_multi_stream.py @@ -55,10 +55,10 @@ def test_isolated_streams(tmp_path: Path) -> None: server = rr.server.Server(datasets={"test_dataset": tmp_path}) ds = server.client().get_dataset("test_dataset") - assert ds.filter_segments("rec1").filter_contents("/data1").reader(index="log_tick").count() == 1 - assert ds.filter_segments("rec2").filter_contents("/data2").reader(index="log_tick").count() == 1 + assert ds.filter_segments("rec1").filter_contents("/data1").reader(index="log_time").count() == 1 + assert ds.filter_segments("rec2").filter_contents("/data2").reader(index="log_time").count() == 1 assert ( - ds.filter_segments(["rec1", "rec2"]).filter_contents(["/data1", "/data2"]).reader(index="log_tick").count() == 2 + ds.filter_segments(["rec1", "rec2"]).filter_contents(["/data1", "/data2"]).reader(index="log_time").count() == 2 ) diff --git a/rerun_py/tests/unit/test_optimization_profile.py b/rerun_py/tests/unit/test_optimization_profile.py new file mode 100644 index 000000000000..39c9ec376835 --- /dev/null +++ b/rerun_py/tests/unit/test_optimization_profile.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import dataclasses + +from rerun.experimental import OptimizationProfile + + +def test_optimization_profile_custom() -> None: + """OptimizationProfile fields are individually overridable.""" + p = OptimizationProfile(max_rows=100, extra_passes=3, gop_batching=False, split_size_ratio=10.0) + assert p.max_rows == 100 + assert p.extra_passes == 3 + assert p.gop_batching is False + assert p.split_size_ratio == 10.0 + assert p.max_bytes is None # unchanged from default + + +def test_optimization_profile_equality() -> None: + """Dataclass-derived equality: field-by-field comparison.""" + assert OptimizationProfile() == OptimizationProfile() + assert OptimizationProfile(extra_passes=10) == OptimizationProfile(extra_passes=10) + assert OptimizationProfile() != OptimizationProfile(gop_batching=False) + + +def test_replace_works() -> None: + """`dataclasses.replace` produces a new profile with overridden fields.""" + derived = dataclasses.replace(OptimizationProfile.OBJECT_STORE, gop_batching=False) + assert derived.gop_batching is False + # Other fields preserved from OBJECT_STORE + assert derived.max_bytes == OptimizationProfile.OBJECT_STORE.max_bytes + assert derived.max_rows == OptimizationProfile.OBJECT_STORE.max_rows + # Original is untouched + assert OptimizationProfile.OBJECT_STORE.gop_batching is True diff --git a/rerun_py/tests/unit/test_optimization_profile_parity.py b/rerun_py/tests/unit/test_optimization_profile_parity.py new file mode 100644 index 000000000000..3d9b4a9f6335 --- /dev/null +++ b/rerun_py/tests/unit/test_optimization_profile_parity.py @@ -0,0 +1,58 @@ +""" +Parity test for `OptimizationProfile`. + +Python `OptimizationProfile.{LIVE,OBJECT_STORE}` must agree with the +Rust `OptimizationProfile::{LIVE,OBJECT_STORE}` constants byte-for-byte. +""" + +from __future__ import annotations + +import math + +import pytest +from rerun.experimental import OptimizationProfile + +from rerun_bindings import _optimization_profile_values # noqa: TID251 + +# Mapping: Python field -> Rust dict key. Names diverge intentionally +# (Rust mirrors `ChunkStoreConfig::chunk_max_*`; Python keeps the existing +# public `max_*` field set). +FIELD_MAP = { + "max_bytes": "chunk_max_bytes", + "max_rows": "chunk_max_rows", + "max_rows_if_unsorted": "chunk_max_rows_if_unsorted", + "extra_passes": "num_extra_passes", + "gop_batching": "gop_batching", + "split_size_ratio": "split_size_ratio", +} + + +def _python_dict(p: OptimizationProfile) -> dict[str, object]: + return {rust_key: getattr(p, py_key) for py_key, rust_key in FIELD_MAP.items()} + + +@pytest.mark.parametrize( + "name,profile", + [ + ("LIVE", OptimizationProfile.LIVE), + ("OBJECT_STORE", OptimizationProfile.OBJECT_STORE), + ], +) +def test_profile_parity(name: str, profile: OptimizationProfile) -> None: + rust = _optimization_profile_values(name) + py = _python_dict(profile) + + # Bidirectional: a missing field on either side fails first with a clear diff. + assert set(rust.keys()) == set(py.keys()), f"key set diverged: rust={set(rust.keys())} py={set(py.keys())}" + + for key in sorted(rust): + rv, pv = rust[key], py[key] + if isinstance(rv, float) or isinstance(pv, float): + assert math.isclose(rv, pv, rel_tol=1e-9), f"{name}.{key}: rust={rv!r} py={pv!r}" # type: ignore[arg-type] + else: + assert rv == pv, f"{name}.{key}: rust={rv!r} py={pv!r}" + + +def test_unknown_profile_name_raises() -> None: + with pytest.raises(ValueError): + _optimization_profile_values("BOGUS") diff --git a/rerun_py/tests/unit/test_optimization_settings.py b/rerun_py/tests/unit/test_optimization_settings.py deleted file mode 100644 index 0bca26fe69e5..000000000000 --- a/rerun_py/tests/unit/test_optimization_settings.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -from rerun.experimental import OptimizationSettings - - -def test_optimization_settings_defaults() -> None: - """OptimizationSettings() mirrors `rerun rrd optimize` defaults.""" - s = OptimizationSettings() - # Threshold fields default to None — resolved to ChunkStoreConfig::DEFAULT by Rust. - assert s.max_bytes is None - assert s.max_rows is None - assert s.max_rows_if_unsorted is None - assert s.extra_passes == 50 - assert s.gop_batching is True - assert s.split_size_ratio is None - - -def test_optimization_settings_custom() -> None: - """OptimizationSettings fields are individually overridable.""" - s = OptimizationSettings(max_rows=100, extra_passes=3, gop_batching=False, split_size_ratio=10.0) - assert s.max_rows == 100 - assert s.extra_passes == 3 - assert s.gop_batching is False - assert s.split_size_ratio == 10.0 - assert s.max_bytes is None # unchanged from default - - -def test_optimization_settings_equality() -> None: - """Dataclass-derived equality: field-by-field comparison.""" - assert OptimizationSettings() == OptimizationSettings() - assert OptimizationSettings(extra_passes=10) == OptimizationSettings(extra_passes=10) - assert OptimizationSettings() != OptimizationSettings(gop_batching=False) diff --git a/rerun_py/tests/unit/test_python_objects_to_record_batch.py b/rerun_py/tests/unit/test_python_objects_to_record_batch.py index 211b339c1f86..ae941586bf4c 100644 --- a/rerun_py/tests/unit/test_python_objects_to_record_batch.py +++ b/rerun_py/tests/unit/test_python_objects_to_record_batch.py @@ -26,7 +26,7 @@ def test_list_column_mismatch_error_message() -> None: assert "3 rows" in error_message assert "expected 1" in error_message assert "Hint" in error_message - assert "[[...]]" in error_message # NOLINT + assert "[[…]]" in error_message def test_list_column_properly_wrapped() -> None: diff --git a/rerun_py/tests/unit/test_query_metrics.py b/rerun_py/tests/unit/test_query_metrics.py new file mode 100644 index 000000000000..dc46070f00c9 --- /dev/null +++ b/rerun_py/tests/unit/test_query_metrics.py @@ -0,0 +1,438 @@ +from __future__ import annotations + +import contextvars +import datetime +import threading +from types import SimpleNamespace +from typing import TYPE_CHECKING, Any + +import pytest +from rerun.experimental import MetricsCollector, QueryMetrics, query_metrics +from rerun.experimental._query_metrics import _active_collectors + +if TYPE_CHECKING: + from collections.abc import Iterator + + +# --------------------------------------------------------------------------- +# Fake handle infrastructure +# +# The `query_metrics()` context manager imports `_new_metrics_collector` +# lazily from `rerun_bindings` on every call. Monkeypatching that symbol on +# `rerun_bindings` is what gets picked up at scope entry — same pattern as +# `test_tracing_session.py`. +# --------------------------------------------------------------------------- + + +def _fake_query_metrics(**overrides: Any) -> SimpleNamespace: + """ + Build a stand-in for the Rust-side `_QueryMetrics` PyO3 class. + + Only the attributes the Python wrapper reads in `_from_rust` need to be + present; default values are chosen so the resulting `QueryMetrics` + dataclass is internally consistent. + """ + defaults: dict[str, Any] = { + "dataset_id": "ds-unit", + "query_chunks": 3, + "query_segments": 1, + "query_layers": 1, + "query_columns": 4, + "query_entities": 2, + "query_bytes": 1024, + "query_chunks_per_segment_min": 3, + "query_chunks_per_segment_max": 3, + "query_chunks_per_segment_mean": 3.0, + "query_type": "full_scan", + "primary_index_name": "time_2", + "time_to_first_chunk_info": datetime.timedelta(microseconds=200), + "filters_pushed_down": 1, + "filters_applied_client_side": 0, + "entity_path_narrowing_applied": True, + "total_duration": datetime.timedelta(microseconds=500), + "time_to_first_chunk": None, + "error_kind": None, + "direct_terminal_reason": None, + "fetch_grpc_requests": 1, + "fetch_grpc_bytes": 2048, + "fetch_direct_requests": 0, + "fetch_direct_bytes": 0, + "fetch_direct_retries": 0, + "fetch_direct_requests_retried": 0, + "fetch_direct_retry_sleep": datetime.timedelta(0), + "fetch_direct_max_attempt": 0, + "fetch_direct_original_ranges": 0, + "fetch_direct_merged_ranges": 0, + "planned_fetch_batches": 1, + "planned_segment_waves": 1, + "segment_admission_limit": 3, + "max_segments_per_fetch_batch": 1, + "max_segments_per_wave": 1, + "peak_active_segments": 1, + "pipeline_budget_bytes": 4 * 1024 * 1024 * 1024, + "pipeline_peak_decoded_bytes": 64 * 1024 * 1024, + "pipeline_byte_waits": 0, + "segment_admission_waits": 0, + "pipeline_stall_breaker_activations": 0, + } + defaults.update(overrides) + return SimpleNamespace(**defaults) + + +class _FakeHandle: + """ + Stand-in for the Rust `_MetricsCollectorHandle`. + + Honors the contract the Python wrapper depends on: + - `snapshot()` is non-destructive — returns a copy of the current buffer. + - `drain()` returns the buffer and clears it. + + Tests poke `pending` directly to simulate snapshots arriving from the + (here-absent) Rust capture path. + """ + + def __init__(self) -> None: + self.pending: list[SimpleNamespace] = [] + self.drain_calls = 0 + self.snapshot_calls = 0 + + def snapshot(self) -> list[SimpleNamespace]: + self.snapshot_calls += 1 + return list(self.pending) + + def drain(self) -> list[SimpleNamespace]: + self.drain_calls += 1 + out = list(self.pending) + self.pending.clear() + return out + + +@pytest.fixture +def install_fake_handles(monkeypatch: pytest.MonkeyPatch) -> Iterator[list[_FakeHandle]]: + """ + Install a fake `_new_metrics_collector` that hands out fresh `_FakeHandle`s. + + Yields the list of handles that have been allocated, in the order they + were requested. Each `with query_metrics()` scope pulls one handle. + """ + import rerun_bindings # noqa: TID251 + + handles: list[_FakeHandle] = [] + + def factory() -> _FakeHandle: + h = _FakeHandle() + handles.append(h) + return h + + monkeypatch.setattr(rerun_bindings, "_new_metrics_collector", factory) + yield handles + + +# --------------------------------------------------------------------------- +# C1. Empty scope → empty collector. +# --------------------------------------------------------------------------- + + +def test_empty_scope_yields_empty_collector(install_fake_handles: list[_FakeHandle]) -> None: + with query_metrics() as m: + assert isinstance(m, MetricsCollector) + assert m.queries == [] + assert m.last_query() is None + + assert m.queries == [] + assert m.last_query() is None + assert len(install_fake_handles) == 1 + + +# --------------------------------------------------------------------------- +# C2. A pending Rust-side snapshot surfaces through `.queries` / `.last_query()`. +# --------------------------------------------------------------------------- + + +def test_fake_handle_populates_collector(install_fake_handles: list[_FakeHandle]) -> None: + with query_metrics() as m: + handle = install_fake_handles[-1] + handle.pending.append( + _fake_query_metrics( + query_chunks=7, + fetch_grpc_bytes=9_000, + planned_fetch_batches=16, + planned_segment_waves=1_332, + segment_admission_limit=3, + max_segments_per_fetch_batch=2, + max_segments_per_wave=3, + peak_active_segments=3, + pipeline_budget_bytes=8 * 1024 * 1024 * 1024, + pipeline_peak_decoded_bytes=96 * 1024 * 1024, + pipeline_byte_waits=4, + segment_admission_waits=20, + pipeline_stall_breaker_activations=1, + ) + ) + qs = m.queries + + assert len(qs) == 1 + assert isinstance(qs[0], QueryMetrics) + assert qs[0].query_chunks == 7 + assert qs[0].fetch_grpc_bytes == 9_000 + assert qs[0].entity_path_narrowing_applied is True + assert qs[0].planned_fetch_batches == 16 + assert qs[0].planned_segment_waves == 1_332 + assert qs[0].segment_admission_limit == 3 + assert qs[0].max_segments_per_fetch_batch == 2 + assert qs[0].max_segments_per_wave == 3 + assert qs[0].peak_active_segments == 3 + assert qs[0].pipeline_budget_bytes == 8 * 1024 * 1024 * 1024 + assert qs[0].pipeline_peak_decoded_bytes == 96 * 1024 * 1024 + assert qs[0].pipeline_byte_waits == 4 + assert qs[0].segment_admission_waits == 20 + assert qs[0].pipeline_stall_breaker_activations == 1 + assert m.last_query() == qs[0] + + +# --------------------------------------------------------------------------- +# C3. After `__exit__`, the collector still surfaces queries. +# Regression guard: `drain()` is called on exit and the result kept on the +# Python side, so `.queries` keeps working past the `with` block. +# --------------------------------------------------------------------------- + + +def test_drain_on_exit_preserves_queries(install_fake_handles: list[_FakeHandle]) -> None: + with query_metrics() as m: + handle = install_fake_handles[-1] + # Mid-scope: snapshot() is empty (no captures yet). + assert m.queries == [] + # The Rust side surfaces a snapshot via the buffer between mid-scope + # read and `__exit__`. The wrapper drains this on exit. + handle.pending.append(_fake_query_metrics(query_chunks=11)) + + # After exit, `.queries` must still return the drained snapshot. + assert len(m.queries) == 1 + assert m.queries[0].query_chunks == 11 + + # And it must continue to return the same content on repeated reads — + # i.e. the post-exit path doesn't itself drain anything. + assert m.queries[0].query_chunks == 11 + + +# --------------------------------------------------------------------------- +# C4. `clear()` empties both the Rust handle buffer and the Python side. +# --------------------------------------------------------------------------- + + +def test_clear_empties_both_buffers(install_fake_handles: list[_FakeHandle]) -> None: + with query_metrics() as m: + handle = install_fake_handles[-1] + handle.pending.append(_fake_query_metrics(query_chunks=1)) + handle.pending.append(_fake_query_metrics(query_chunks=2)) + assert len(m.queries) == 2 + + m.clear() + + # Rust side drained. + assert handle.pending == [] + # Python side also empty. + assert m.queries == [] + assert m.last_query() is None + + +# --------------------------------------------------------------------------- +# C5. ImportError on bindings → inert collector + warning, no propagation. +# --------------------------------------------------------------------------- + + +def test_inert_fallback_on_import_error( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """ + Yield an inert collector if the bindings are missing. + + If `rerun_bindings` is missing the symbol, the context manager logs a + warning and yields an inert collector instead of raising. This matches + `tracing_session`'s behavior for a missing telemetry stack. + """ + import rerun_bindings # noqa: TID251 + + monkeypatch.delattr(rerun_bindings, "_new_metrics_collector", raising=False) + + with caplog.at_level("WARNING", logger="rerun"): + with query_metrics() as m: + assert m.queries == [] + assert m.last_query() is None + + assert m.queries == [] + assert any("query_metrics" in r.getMessage() for r in caplog.records), ( + f"expected a WARNING about query_metrics, got: {[r.getMessage() for r in caplog.records]}" + ) + + +# --------------------------------------------------------------------------- +# C6. Allocation failure → inert collector + log, no propagation. +# --------------------------------------------------------------------------- + + +def test_allocation_failure_yields_inert_collector( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + import rerun_bindings # noqa: TID251 + + def boom() -> None: + raise RuntimeError("simulated allocation failure") + + monkeypatch.setattr(rerun_bindings, "_new_metrics_collector", boom) + + with caplog.at_level("ERROR", logger="rerun"): + with query_metrics() as m: + assert m.queries == [] + assert m.last_query() is None + + # The wrapper logs `exception`, which records at ERROR level. + assert any("query_metrics" in r.getMessage() for r in caplog.records), ( + f"expected an error log about query_metrics, got: {[r.getMessage() for r in caplog.records]}" + ) + + +# --------------------------------------------------------------------------- +# C7. ContextVar lifecycle: scope enter pushes, scope exit pops. +# --------------------------------------------------------------------------- + + +def test_context_var_pushes_and_pops(install_fake_handles: list[_FakeHandle]) -> None: + assert _active_collectors.get() == () + + with query_metrics(): + active = _active_collectors.get() + assert len(active) == 1 + assert active[0] is install_fake_handles[0] + + # After exit the stack is back to its pre-scope value. + assert _active_collectors.get() == () + + +@pytest.mark.usefixtures("install_fake_handles") +def test_context_var_pops_on_exception() -> None: + class _Boom(Exception): + pass + + with pytest.raises(_Boom): + with query_metrics(): + assert len(_active_collectors.get()) == 1 + raise _Boom + + # Even on early exit via exception, the ContextVar resets cleanly. + assert _active_collectors.get() == () + + +# --------------------------------------------------------------------------- +# C8. Nested `query_metrics()` scopes both end up on the stack; a query +# observed mid-inner-scope is visible to both via the ContextVar. +# --------------------------------------------------------------------------- + + +def test_nested_scopes_stack(install_fake_handles: list[_FakeHandle]) -> None: + with query_metrics() as outer: + outer_handle = install_fake_handles[-1] + assert _active_collectors.get() == (outer_handle,) + + with query_metrics() as inner: + inner_handle = install_fake_handles[-1] + # Both collectors are on the stack while the inner scope is open. + assert _active_collectors.get() == (outer_handle, inner_handle) + + # Simulate the Rust capture path: it reads the ContextVar and + # fans the snapshot out to every collector currently active. + snap = _fake_query_metrics(query_chunks=42) + for h in _active_collectors.get(): + h.pending.append(snap) # type: ignore[attr-defined] + + # The inner scope sees the snapshot mid-scope. + inner_last = inner.last_query() + assert inner_last is not None + assert inner_last.query_chunks == 42 + + # After the inner scope exits, only the outer is on the stack. + assert _active_collectors.get() == (outer_handle,) + + # Both scopes should have seen the snapshot — fan-out is observable. + outer_last = outer.last_query() + assert outer_last is not None + assert outer_last.query_chunks == 42 + inner_last = inner.last_query() + assert inner_last is not None + assert inner_last.query_chunks == 42 + + +# --------------------------------------------------------------------------- +# C9. Sibling scopes in detached contexts do not pollute each other. +# A `query_metrics()` scope opened in one `contextvars.Context` is invisible +# to a sibling context — which is the whole point of moving off the global +# registry. +# --------------------------------------------------------------------------- + + +@pytest.mark.usefixtures("install_fake_handles") +def test_sibling_contexts_are_isolated() -> None: + barrier_after_enter = threading.Event() + barrier_before_exit = threading.Event() + observed_in_thread: list[tuple[object, ...]] = [] + + def worker() -> None: + # No `contextvars.copy_context()` here — the raw thread inherits an + # empty default ContextVar value. The parent's scope must be + # invisible. + observed_in_thread.append(_active_collectors.get()) + barrier_after_enter.set() + barrier_before_exit.wait(timeout=5.0) + + t = threading.Thread(target=worker) + + with query_metrics(): + assert len(_active_collectors.get()) == 1 + t.start() + barrier_after_enter.wait(timeout=5.0) + # The worker thread observed the default empty stack, not the + # parent's scope. + assert observed_in_thread == [()] + barrier_before_exit.set() + + t.join(timeout=5.0) + + +# --------------------------------------------------------------------------- +# C10. `contextvars.copy_context()` *does* carry the scope into a child task. +# --------------------------------------------------------------------------- + + +@pytest.mark.usefixtures("install_fake_handles") +def test_copy_context_inherits_scope() -> None: + captured: list[tuple[object, ...]] = [] + + def child() -> None: + captured.append(_active_collectors.get()) + + with query_metrics(): + ctx = contextvars.copy_context() + ctx.run(child) + + # The child saw the same single-element stack as the parent. + assert len(captured) == 1 + assert len(captured[0]) == 1 + + +# --------------------------------------------------------------------------- +# C11. `.queries` is non-destructive — repeated reads return the same content. +# --------------------------------------------------------------------------- + + +def test_repeated_reads_are_non_destructive(install_fake_handles: list[_FakeHandle]) -> None: + with query_metrics() as m: + handle = install_fake_handles[-1] + handle.pending.append(_fake_query_metrics(query_chunks=5)) + first = m.queries + second = m.queries + + assert first == second + assert len(first) == 1 + assert first[0].query_chunks == 5 diff --git a/rerun_py/tests/unit/test_recording.py b/rerun_py/tests/unit/test_recording.py deleted file mode 100644 index 395d919779fc..000000000000 --- a/rerun_py/tests/unit/test_recording.py +++ /dev/null @@ -1,429 +0,0 @@ -"""Tests for rerun.recording module (non-deprecated Recording functionality).""" - -from __future__ import annotations - -import pathlib -import subprocess -import uuid -from typing import TYPE_CHECKING - -import rerun as rr - -if TYPE_CHECKING: - import syrupy - -APP_ID = "rerun_example_test_recording" - - -def test_recording_info(tmp_path: pathlib.Path) -> None: - """Test Recording.application_id() and Recording.recording_id().""" - - rrd = tmp_path / "tmp.rrd" - - expected_recording_id = uuid.uuid4() - with rr.RecordingStream(APP_ID, recording_id=expected_recording_id) as rec: - rec.save(rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3]])) - - recording = rr.recording.load_recording(rrd) - - assert recording.application_id() == APP_ID - assert recording.recording_id() == str(expected_recording_id) - - -def test_schema_recording(tmp_path: pathlib.Path, snapshot: syrupy.SnapshotAssertion) -> None: - """Test Recording.schema() returns correct index and component columns.""" - - rrd = tmp_path / "tmp.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3], [4, 5, 6], [7, 8, 9]], radii=[])) - rec.set_time("my_index", sequence=7) - rec.log("points", rr.Points3D([[10, 11, 12]], colors=[[255, 0, 0]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - - recording = rr.recording.load_recording(rrd) - schema = recording.schema() - - # log_tick, log_time, my_index - assert len(schema.index_columns()) == 3 - # Timestamp, Color, Position3D, Radius, Text - assert len(schema.component_columns()) == 5 - - # Index columns - assert schema.index_columns()[0].name == "log_tick" - assert schema.index_columns()[1].name == "log_time" - assert schema.index_columns()[2].name == "my_index" - - assert str(schema) == snapshot() - - col = 0 - - # Content columns - assert schema.component_columns()[col].entity_path == "/points" - assert schema.component_columns()[col].archetype == "rerun.archetypes.Points3D" - assert schema.component_columns()[col].component == "Points3D:colors" - assert schema.component_columns()[col].component_type == "rerun.components.Color" - assert schema.component_columns()[col].is_static is False - col += 1 - - assert schema.component_columns()[col].entity_path == "/points" - assert schema.component_columns()[col].archetype == "rerun.archetypes.Points3D" - assert schema.component_columns()[col].component == "Points3D:positions" - assert schema.component_columns()[col].component_type == "rerun.components.Position3D" - assert schema.component_columns()[col].is_static is False - col += 1 - - assert schema.component_columns()[col].entity_path == "/points" - assert schema.component_columns()[col].archetype == "rerun.archetypes.Points3D" - assert schema.component_columns()[col].component == "Points3D:radii" - assert schema.component_columns()[col].component_type == "rerun.components.Radius" - assert schema.component_columns()[col].is_static is False - col += 1 - - assert schema.component_columns()[col].entity_path == "/static_text" - assert schema.component_columns()[col].archetype == "rerun.archetypes.TextLog" - assert schema.component_columns()[col].component == "TextLog:text" - assert schema.component_columns()[col].component_type == "rerun.components.Text" - assert schema.component_columns()[col].is_static is True - col += 1 - - assert schema.component_columns()[col].entity_path == "/__properties" - assert schema.component_columns()[col].archetype == "rerun.archetypes.RecordingInfo" - assert schema.component_columns()[col].component == "RecordingInfo:start_time" - assert schema.component_columns()[col].component_type == "rerun.components.Timestamp" - assert schema.component_columns()[col].is_static is True - - -def test_schema_entity_paths(tmp_path: pathlib.Path) -> None: - """Test Schema.entity_paths() returns a sorted list of unique entity paths.""" - - rrd = tmp_path / "tmp.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - rec.send_property("my_prop", rr.AnyValues(prop=123)) - - recording = rr.recording.load_recording(rrd) - schema = recording.schema() - - assert schema.entity_paths() == ["/points", "/static_text"] - assert schema.entity_paths(include_properties=True) == [ - "/__properties", - "/__properties/my_prop", - "/points", - "/static_text", - ] - - -def test_schema_archetypes(tmp_path: pathlib.Path) -> None: - """Test Schema.archetypes() returns a sorted list of unique archetype names.""" - - rrd = tmp_path / "tmp.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - rec.send_property("my_prop", rr.Points2D([[0, 2]])) - - recording = rr.recording.load_recording(rrd) - schema = recording.schema() - - assert schema.archetypes() == ["rerun.archetypes.Points3D", "rerun.archetypes.TextLog"] - assert schema.archetypes(include_properties=True) == [ - "rerun.archetypes.Points2D", - "rerun.archetypes.Points3D", - "rerun.archetypes.RecordingInfo", - "rerun.archetypes.TextLog", - ] - - -def test_schema_component_types(tmp_path: pathlib.Path) -> None: - """Test Schema.component_types() returns a sorted list of unique component types.""" - - rrd = tmp_path / "tmp.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - rec.send_property("my_prop", rr.Points2D([[0, 2]])) - - recording = rr.recording.load_recording(rrd) - schema = recording.schema() - - assert schema.component_types() == [ - "rerun.components.Position3D", - "rerun.components.Text", - ] - assert schema.component_types(include_properties=True) == [ - "rerun.components.Position2D", - "rerun.components.Position3D", - "rerun.components.Text", - "rerun.components.Timestamp", - ] - - -def test_schema_columns_for(tmp_path: pathlib.Path) -> None: - """Test Schema.columns_for() filters component columns by entity_path, archetype, and component_type.""" - - rrd = tmp_path / "tmp.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - rec.send_property("my_prop", rr.Points2D([[0, 2]])) - - recording = rr.recording.load_recording(rrd) - schema = recording.schema() - - def names(cols: list) -> list[str]: # type: ignore[type-arg] - return sorted(col.name for col in cols) - - # Filter by entity_path - assert names(schema.columns_for(entity_path="/points")) == ["/points:Points3D:positions"] - assert names(schema.columns_for(entity_path="/static_text")) == ["/static_text:TextLog:text"] - - # Filter by archetype (fully-qualified) - assert names(schema.columns_for(archetype="rerun.archetypes.Points3D")) == ["/points:Points3D:positions"] - assert names(schema.columns_for(archetype="rerun.archetypes.TextLog")) == ["/static_text:TextLog:text"] - - # Filter by archetype (short form) - assert names(schema.columns_for(archetype="Points3D")) == ["/points:Points3D:positions"] - assert names(schema.columns_for(archetype="TextLog")) == ["/static_text:TextLog:text"] - - # Filter by archetype (class) - assert names(schema.columns_for(archetype=rr.Points3D)) == ["/points:Points3D:positions"] - assert names(schema.columns_for(archetype=rr.TextLog)) == ["/static_text:TextLog:text"] - - # Filter by component_type - assert names(schema.columns_for(component_type="rerun.components.Text")) == ["/static_text:TextLog:text"] - assert names(schema.columns_for(component_type="rerun.components.Position3D")) == ["/points:Points3D:positions"] - - # Combined filter - assert names(schema.columns_for(entity_path="/points", archetype="rerun.archetypes.Points3D")) == [ - "/points:Points3D:positions", - ] - - # Properties excluded by default - assert names(schema.columns_for()) == ["/points:Points3D:positions", "/static_text:TextLog:text"] - assert names(schema.columns_for(include_properties=True)) == [ - "/points:Points3D:positions", - "/static_text:TextLog:text", - "property:RecordingInfo:start_time", - "property:my_prop:Points2D:positions", - ] - - # Properties included - assert names(schema.columns_for(include_properties=True, archetype="rerun.archetypes.RecordingInfo")) == [ - "property:RecordingInfo:start_time", - ] - - # No matches - assert schema.columns_for(entity_path="/nonexistent") == [] - - -def test_schema_column_names_for(tmp_path: pathlib.Path) -> None: - """Test Schema.column_names_for() returns filtered column name strings.""" - - rrd = tmp_path / "tmp.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - - recording = rr.recording.load_recording(rrd) - schema = recording.schema() - - # Filter by archetype (fully-qualified) - assert schema.column_names_for(archetype="rerun.archetypes.Points3D") == ["/points:Points3D:positions"] - - # Filter by archetype (short form) - assert schema.column_names_for(archetype="Points3D") == ["/points:Points3D:positions"] - - # Filter by archetype (class) - assert schema.column_names_for(archetype=rr.Points3D) == ["/points:Points3D:positions"] - - # Filter by component_type - assert schema.column_names_for(component_type="rerun.components.Position3D") == ["/points:Points3D:positions"] - assert schema.column_names_for(component_type="rerun.components.Text") == ["/static_text:TextLog:text"] - - # No matches - assert schema.column_names_for(entity_path="/nonexistent") == [] - - -def test_load_recording_path_types(tmp_path: pathlib.Path) -> None: - """Test that load_recording accepts both str and Path.""" - - rrd = tmp_path / "tmp.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(rrd) - rec.log("test", rr.TextLog("Hello")) - - # Test with string path - recording = rr.recording.load_recording(rrd) - assert recording is not None - - # Test with Path object - recording = rr.recording.load_recording(pathlib.Path(tmp_path) / "tmp.rrd") - assert recording is not None - - -def test_chunk_record_batch(tmp_path: pathlib.Path, snapshot: syrupy.SnapshotAssertion) -> None: - """Test that Chunk.format() returns a human-readable table with expected data.""" - - rrd = tmp_path / "tmp.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(rrd) - # Use send_columns to avoid auto-injected log_time/log_tick timelines - rec.send_columns( - "points", - indexes=[rr.TimeColumn("my_index", sequence=[1, 2])], - columns=rr.Points3D.columns(positions=[[1, 2, 3], [4, 5, 6]], colors=[[0, 0, 0], [255, 0, 0]]), - ) - rec.send_columns( - "static_text", - indexes=[], - columns=rr.TextLog.columns(text=["Hello"]), - ) - - recording = rr.recording.load_recording(rrd) - chunks = sorted(recording.chunks(), key=lambda c: c.entity_path) - - parts = [] - for chunk in chunks: - if chunk.entity_path.startswith("/__"): - continue - parts.append(chunk.format(width=200, redact=True)) - - parts.sort() - result = "\n".join(parts) - assert result == snapshot() - - -def test_save_roundtrip(tmp_path: pathlib.Path) -> None: - """Test that save() produces an RRD that preserves metadata and schema.""" - - original_rrd = tmp_path / "original.rrd" - roundtrip_rrd = tmp_path / "roundtrip.rrd" - - expected_recording_id = uuid.uuid4() - - with rr.RecordingStream(APP_ID, recording_id=expected_recording_id) as rec: - rec.save(original_rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3], [4, 5, 6]])) - rec.set_time("my_index", sequence=2) - rec.log("points", rr.Points3D([[7, 8, 9]], colors=[[255, 0, 0]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - - recording = rr.recording.load_recording(original_rrd) - recording.save(roundtrip_rrd) - - # Load the roundtripped recording and verify metadata is preserved - roundtripped = rr.recording.load_recording(roundtrip_rrd) - - assert roundtripped.application_id() == APP_ID - assert roundtripped.recording_id() == str(expected_recording_id) - - # Verify schema is preserved - original_schema = recording.schema() - roundtrip_schema = roundtripped.schema() - - assert str(original_schema) == str(roundtrip_schema) - - -def test_save_roundtrip_compare(tmp_path: pathlib.Path) -> None: - """Test that optimizing then roundtripping produces an identical RRD.""" - - original_rrd = tmp_path / "original.rrd" - optimized_rrd = tmp_path / "optimized.rrd" - roundtrip_rrd = tmp_path / "roundtrip.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(original_rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - - # Optimize the original so chunk boundaries match what ChunkStore produces - process = subprocess.run( - ["rerun", "rrd", "optimize", str(original_rrd), "-o", str(optimized_rrd)], - check=False, - capture_output=True, - ) - assert process.returncode == 0, f"RRD optimize failed: {process.stderr.decode('utf-8')}" - - # Roundtrip via load + save - rr.recording.load_recording(optimized_rrd).save(roundtrip_rrd) - - # Compare optimized vs roundtripped - process = subprocess.run( - ["rerun", "rrd", "compare", "--unordered", str(optimized_rrd), str(roundtrip_rrd)], - check=False, - capture_output=True, - ) - if process.returncode != 0: - print(process.stdout.decode("utf-8")) - print(process.stderr.decode("utf-8")) - assert process.returncode == 0, f"RRD compare failed: {process.stderr.decode('utf-8')}" - - -def test_chunk_roundtrip_compare(tmp_path: pathlib.Path) -> None: - """Test that roundtripping through chunks produces an identical RRD.""" - - original_rrd = tmp_path / "original.rrd" - optimized_rrd = tmp_path / "optimized.rrd" - roundtrip_rrd = tmp_path / "roundtrip.rrd" - - with rr.RecordingStream(APP_ID, recording_id=uuid.uuid4()) as rec: - rec.save(original_rrd) - rec.set_time("my_index", sequence=1) - rec.log("points", rr.Points3D([[1, 2, 3]])) - rec.set_time("my_index", sequence=2) - rec.set_time("other_timeline", sequence=10) - rec.log("points", rr.Points3D([[4, 5, 6]])) - rec.log("static_text", rr.TextLog("Hello"), static=True) - - # Optimize the original so chunk boundaries match what ChunkStore produces - process = subprocess.run( - ["rerun", "rrd", "optimize", str(original_rrd), "-o", str(optimized_rrd)], - check=False, - capture_output=True, - ) - assert process.returncode == 0, f"RRD optimize failed: {process.stderr.decode('utf-8')}" - - # Load, roundtrip through chunks, and save - recording = rr.recording.load_recording(optimized_rrd) - reconstructed = rr.recording.Recording.from_chunks( - recording.chunks(), - application_id=recording.application_id(), - recording_id=recording.recording_id(), - ) - reconstructed.save(roundtrip_rrd) - - # Compare optimized vs roundtripped - process = subprocess.run( - ["rerun", "rrd", "compare", "--unordered", str(optimized_rrd), str(roundtrip_rrd)], - check=False, - capture_output=True, - ) - if process.returncode != 0: - print(process.stdout.decode("utf-8")) - print(process.stderr.decode("utf-8")) - assert process.returncode == 0, f"RRD compare failed: {process.stderr.decode('utf-8')}" diff --git a/rerun_py/tests/unit/test_sample_index.py b/rerun_py/tests/unit/test_sample_index.py index 64f792688b91..8807b5707fe0 100644 --- a/rerun_py/tests/unit/test_sample_index.py +++ b/rerun_py/tests/unit/test_sample_index.py @@ -1,4 +1,4 @@ -"""Tests for `SampleIndex.global_to_local` in `rerun.experimental.dataloader._sample_index`.""" +"""Tests for `SampleIndex` in `rerun.experimental.dataloader._sample_index`.""" from __future__ import annotations @@ -68,9 +68,11 @@ def test_global_to_local_fixed_rate_timestamp() -> None: ns_per_sample = 10_000_000 # 100 Hz seg_a = _fixed_rate_segment("seg-a", index_start=1_000_000_000, num_samples=3, ns_per_sample=ns_per_sample) seg_b = _fixed_rate_segment("seg-b", index_start=2_000_000_000, num_samples=2, ns_per_sample=ns_per_sample) - sample_index = SampleIndex([seg_a, seg_b], ns_per_sample=ns_per_sample, is_timestamp=True) + sample_index = SampleIndex([seg_a, seg_b], ns_per_sample=ns_per_sample, ns_dtype="datetime64[ns]") assert sample_index.total_samples == 5 + assert sample_index.is_timestamp + assert not sample_index.is_duration expected = [ (seg_a, np.datetime64(1_000_000_000, "ns")), @@ -86,6 +88,30 @@ def test_global_to_local_fixed_rate_timestamp() -> None: assert value == expected_value +def test_global_to_local_fixed_rate_duration() -> None: + ns_per_sample = 10_000_000 # 100 Hz + seg_a = _fixed_rate_segment("seg-a", index_start=0, num_samples=3, ns_per_sample=ns_per_sample) + seg_b = _fixed_rate_segment("seg-b", index_start=500_000_000, num_samples=2, ns_per_sample=ns_per_sample) + sample_index = SampleIndex([seg_a, seg_b], ns_per_sample=ns_per_sample, ns_dtype="timedelta64[ns]") + + assert sample_index.total_samples == 5 + assert sample_index.is_duration + assert not sample_index.is_timestamp + + expected = [ + (seg_a, np.timedelta64(0, "ns")), + (seg_a, np.timedelta64(10_000_000, "ns")), + (seg_a, np.timedelta64(20_000_000, "ns")), + (seg_b, np.timedelta64(500_000_000, "ns")), + (seg_b, np.timedelta64(510_000_000, "ns")), + ] + for global_idx, (expected_seg, expected_value) in enumerate(expected): + resolved_seg, value = sample_index.global_to_local(global_idx) + assert resolved_seg is expected_seg + assert isinstance(value, np.timedelta64) + assert value == expected_value + + @pytest.mark.parametrize("bad_idx", [-1, 6, 100]) def test_global_to_local_out_of_range_raises(bad_idx: int) -> None: seg_a = _integer_segment("seg-a", index_start=0, index_end=2) diff --git a/rerun_py/tests/unit/test_selector.py b/rerun_py/tests/unit/test_selector.py index 648e01e430a1..1c7cacb6ad1a 100644 --- a/rerun_py/tests/unit/test_selector.py +++ b/rerun_py/tests/unit/test_selector.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pickle + import pyarrow as pa import pyarrow.compute as pc import pytest @@ -109,3 +111,36 @@ def test_pipe_chaining() -> None: result = Selector(".data").pipe(Selector(".val")).pipe(lambda a: pc.add(a, 1)).execute(outer) assert result is not None assert result == pa.array([6.0, 11.0]) + + +def test_pickle_roundtrip_simple() -> None: + arr = pa.StructArray.from_arrays( + [pa.array([1.0, 2.0, 3.0])], + names=["x"], + ) + original = Selector(".x") + restored = pickle.loads(pickle.dumps(original)) + assert isinstance(restored, Selector) + assert str(restored) == str(original) + assert restored.execute(arr) == pa.array([1.0, 2.0, 3.0]) + + +def test_pickle_roundtrip_piped_selectors() -> None: + inner = pa.StructArray.from_arrays( + [pa.array([10, 20])], + names=["value"], + ) + outer = pa.StructArray.from_arrays( + [inner], + names=["nested"], + ) + original = Selector(".nested").pipe(Selector(".value")) + restored = pickle.loads(pickle.dumps(original)) + assert isinstance(restored, Selector) + assert restored.execute(outer) == pa.array([10, 20]) + + +def test_pickle_rejects_callable_pipe() -> None: + sel = Selector(".x").pipe(lambda a: pc.multiply(a, 2)) + with pytest.raises(TypeError, match="Cannot pickle Selector"): + pickle.dumps(sel) diff --git a/rerun_py/tests/unit/test_send_dataframe.py b/rerun_py/tests/unit/test_send_dataframe.py index ebeea5b3f21c..439c9c256474 100644 --- a/rerun_py/tests/unit/test_send_dataframe.py +++ b/rerun_py/tests/unit/test_send_dataframe.py @@ -1,20 +1,31 @@ -""" -Tests for rr.send_dataframe and rr.send_record_batch. - -These tests verify the send_dataframe functionality using the Server + Catalog API. -""" +"""Tests for rr.send_dataframe and rr.send_record_batch.""" from __future__ import annotations import uuid from typing import TYPE_CHECKING +import pyarrow as pa +import pytest import rerun as rr +from inline_snapshot import snapshot as inline_snapshot +from rerun import ( + RERUN_KIND, + RERUN_KIND_CONTROL, + RERUN_KIND_INDEX, + SORBET_ARCHETYPE_NAME, + SORBET_COMPONENT, + SORBET_COMPONENT_TYPE, + SORBET_ENTITY_PATH, + SORBET_INDEX_NAME, +) +from rerun.experimental import RrdReader if TYPE_CHECKING: + from collections.abc import Callable from pathlib import Path - import pyarrow as pa + from rerun.experimental import Chunk from syrupy import SnapshotAssertion APP_ID = "rerun_example_test_send_dataframe" @@ -70,3 +81,248 @@ def test_send_dataframe_roundtrip(tmp_path: Path, snapshot: SnapshotAssertion) - assert original_table == roundtrip_table assert str(original_table) == snapshot() + + +# A simple list-of-floats component column, two rows. +_VALUES = pa.array([[1.0], [2.0]], type=pa.list_(pa.float32())) + + +@pytest.fixture +def send_dataframe_and_get_chunks(tmp_path: Path) -> Callable[..., list[Chunk]]: + """Send a table/reader via `send_dataframe`, then read the result back as sorted chunks.""" + counter = 0 + + def _impl(df: pa.Table | pa.RecordBatchReader, **kwargs: object) -> list[Chunk]: + nonlocal counter + counter += 1 + out_path = tmp_path / f"out_{counter}.rrd" + with rr.RecordingStream(APP_ID, recording_id="characterization", send_properties=False) as rec: + rec.save(out_path) + rr.send_dataframe(df, recording=rec, **kwargs) # type: ignore[arg-type] + chunks = RrdReader(out_path).stream().to_chunks() + return sorted(chunks, key=lambda c: c.entity_path) + + return _impl + + +def _summary(chunks: list[Chunk]) -> list[str]: + """One compact, redacted line per chunk — entity path, timelines, and components.""" + return [ + f"{c.entity_path} static={c.is_static} timelines={sorted(c.timeline_names)} ncols={c.num_columns}" + for c in chunks + ] + + +def test_full_metadata_single_entity( + send_dataframe_and_get_chunks: Callable[[pa.Table | pa.RecordBatchReader], list[Chunk]], +) -> None: + """Fully-tagged index + component column, mirroring the `send_dataframe` doc snippet.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame", RERUN_KIND: RERUN_KIND_INDEX}), + pa.field( + "/points:Points3D:positions", + _VALUES.type, + metadata={ + SORBET_ENTITY_PATH: b"/points", + SORBET_ARCHETYPE_NAME: b"rerun.archetypes.Points3D", + SORBET_COMPONENT: b"Points3D:positions", + SORBET_COMPONENT_TYPE: b"rerun.components.Position3D", + RERUN_KIND: b"data", + }, + ), + ]) + [chunk] = send_dataframe_and_get_chunks(pa.Table.from_arrays([index, _VALUES], schema=schema)) + assert chunk.format(redact=True) == inline_snapshot("""\ +┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ METADATA: │ +│ * entity_path: /points │ +│ * id: [**REDACTED**] │ +│ * version: [**REDACTED**] │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ +│ ┌───────────────────────────────────────────────┬───────────────────┬───────────────────────────────┐ │ +│ │ RowId ┆ frame ┆ Points3D:positions │ │ +│ │ --- ┆ --- ┆ --- │ │ +│ │ type: non-null FixedSizeBinary(16) ┆ type: Int64 ┆ type: List(Float32) │ │ +│ │ ARROW:extension:metadata: {"namespace":"row"} ┆ index_name: frame ┆ archetype: Points3D │ │ +│ │ ARROW:extension:name: TUID ┆ is_sorted: true ┆ component: Points3D:positions │ │ +│ │ is_sorted: true ┆ kind: index ┆ component_type: Position3D │ │ +│ │ kind: control ┆ ┆ kind: data │ │ +│ ╞═══════════════════════════════════════════════╪═══════════════════╪═══════════════════════════════╡ │ +│ │ row_[**REDACTED**] ┆ 0 ┆ [1.0] │ │ +│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ │ +│ │ row_[**REDACTED**] ┆ 1 ┆ [2.0] │ │ +│ └───────────────────────────────────────────────┴───────────────────┴───────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────────────────────────────────────────────┘\ +""") + + +def test_index_kind_without_index_name( + send_dataframe_and_get_chunks: Callable[[pa.Table | pa.RecordBatchReader], list[Chunk]], +) -> None: + """A `kind=index` column with no `index_name` becomes a timeline named after the column.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("my_time", index.type, metadata={RERUN_KIND: RERUN_KIND_INDEX}), + pa.field( + "/e:C:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e", SORBET_COMPONENT: b"C:c", RERUN_KIND: b"data"} + ), + ]) + [chunk] = send_dataframe_and_get_chunks(pa.Table.from_arrays([index, _VALUES], schema=schema)) + assert chunk.timeline_names == inline_snapshot(["my_time"]) + + +def test_entity_path_from_column_name( + send_dataframe_and_get_chunks: Callable[[pa.Table | pa.RecordBatchReader], list[Chunk]], +) -> None: + """A leading-`/` column name is split into entity path + component; otherwise it lands on root.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame", RERUN_KIND: RERUN_KIND_INDEX}), + pa.field("/points:Points3D:positions", _VALUES.type, metadata={SORBET_COMPONENT: b"Points3D:positions"}), + ]) + [chunk] = send_dataframe_and_get_chunks(pa.Table.from_arrays([index, _VALUES], schema=schema)) + assert chunk.entity_path == inline_snapshot("/points") + + +def test_entity_path_non_leading_slash_is_root( + send_dataframe_and_get_chunks: Callable[[pa.Table | pa.RecordBatchReader], list[Chunk]], +) -> None: + """A column name without a leading `/` is no longer parsed for an entity path; it lands on root.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame", RERUN_KIND: RERUN_KIND_INDEX}), + pa.field("foo:bar", _VALUES.type, metadata={}), + ]) + [chunk] = send_dataframe_and_get_chunks(pa.Table.from_arrays([index, _VALUES], schema=schema)) + assert chunk.entity_path == inline_snapshot("/") + + +def test_multiple_entities( + send_dataframe_and_get_chunks: Callable[[pa.Table | pa.RecordBatchReader], list[Chunk]], +) -> None: + """Component columns with different entity paths split into one chunk per entity.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame", RERUN_KIND: RERUN_KIND_INDEX}), + pa.field( + "/a:C:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/a", SORBET_COMPONENT: b"C:c", RERUN_KIND: b"data"} + ), + pa.field( + "/b:C:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/b", SORBET_COMPONENT: b"C:c", RERUN_KIND: b"data"} + ), + ]) + chunks = send_dataframe_and_get_chunks(pa.Table.from_arrays([index, _VALUES, _VALUES], schema=schema)) + assert _summary(chunks) == inline_snapshot([ + "/a static=False timelines=['frame'] ncols=3", + "/b static=False timelines=['frame'] ncols=3", + ]) + + +def test_control_kind_is_treated_as_row_id() -> None: + """ + A `kind=control` column is interpreted as a row-id column, not a component. + + Without a chunk id the batch is only *partially* identified, so it takes the mint path: the + control column is dropped (rather than carried as a component) and fresh row ids are minted. + """ + from rerun.experimental import Chunk + + index = pa.array([0, 1], type=pa.int64()) + control = pa.array([10, 20], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame", RERUN_KIND: RERUN_KIND_INDEX}), + pa.field("ctrl", control.type, metadata={RERUN_KIND: RERUN_KIND_CONTROL}), + pa.field( + "/e:C:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e", SORBET_COMPONENT: b"C:c", RERUN_KIND: b"data"} + ), + ]) + rb = pa.RecordBatch.from_arrays([index, control, _VALUES], schema=schema) + [chunk] = Chunk.from_record_batch(rb) + formatted = chunk.format(redact=True, trim_metadata_keys=False) + # The control column was consumed as a row-id and dropped, not carried as a component, and the + # chunk carries a freshly-minted `RowId` column instead. + assert "ctrl" not in formatted + assert "rerun:component: C:c" in formatted + assert "RowId" in formatted + + +def test_no_component_type_is_left_unset( + send_dataframe_and_get_chunks: Callable[[pa.Table | pa.RecordBatchReader], list[Chunk]], +) -> None: + """A component column with no `component_type` metadata leaves it unset (no `Unknown` default).""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame", RERUN_KIND: RERUN_KIND_INDEX}), + pa.field( + "/e:thing", + _VALUES.type, + metadata={SORBET_ENTITY_PATH: b"/e", SORBET_COMPONENT: b"thing", RERUN_KIND: b"data"}, + ), + ]) + [chunk] = send_dataframe_and_get_chunks(pa.Table.from_arrays([index, _VALUES], schema=schema)) + formatted = chunk.format(redact=True, trim_metadata_keys=False) + assert "rerun:component: thing" in formatted + assert "rerun:component_type" not in formatted + + +def test_no_index_is_ambiguous() -> None: + """With `index` left at the default (AUTO) and no index metadata, the batch is rejected.""" + from rerun.experimental import Chunk + + schema = pa.schema([ + pa.field( + "/e:C:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e", SORBET_COMPONENT: b"C:c", RERUN_KIND: b"data"} + ), + ]) + with pytest.raises(ValueError): + Chunk.from_record_batch(pa.RecordBatch.from_arrays([_VALUES], schema=schema)) + + +def test_static_index_none( + send_dataframe_and_get_chunks: Callable[..., list[Chunk]], +) -> None: + """`index=None` produces a static chunk.""" + one_value = pa.array([[1.0]], type=pa.list_(pa.float32())) + schema = pa.schema([ + pa.field( + "/e:C:c", + one_value.type, + metadata={SORBET_ENTITY_PATH: b"/e", SORBET_COMPONENT: b"C:c", RERUN_KIND: b"data"}, + ), + ]) + table = pa.Table.from_arrays([one_value], schema=schema) + [chunk] = send_dataframe_and_get_chunks(table, index=None) + assert chunk.is_static == inline_snapshot(True) + assert chunk.timeline_names == inline_snapshot([]) + + +def test_static_index_none_with_index_metadata_is_contradiction( + send_dataframe_and_get_chunks: Callable[..., list[Chunk]], +) -> None: + """`index=None` plus index metadata in the batch is a contradiction and is rejected.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame", RERUN_KIND: RERUN_KIND_INDEX}), + pa.field( + "/e:C:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e", SORBET_COMPONENT: b"C:c", RERUN_KIND: b"data"} + ), + ]) + with pytest.raises(ValueError): + send_dataframe_and_get_chunks(pa.Table.from_arrays([index, _VALUES], schema=schema), index=None) + + +def test_record_batch_reader_input( + send_dataframe_and_get_chunks: Callable[[pa.Table | pa.RecordBatchReader], list[Chunk]], +) -> None: + """A `RecordBatchReader` produces the same result as the equivalent `Table`.""" + index = pa.array([0, 1], type=pa.int64()) + schema = pa.schema([ + pa.field("frame", index.type, metadata={SORBET_INDEX_NAME: b"frame", RERUN_KIND: RERUN_KIND_INDEX}), + pa.field( + "/e:C:c", _VALUES.type, metadata={SORBET_ENTITY_PATH: b"/e", SORBET_COMPONENT: b"C:c", RERUN_KIND: b"data"} + ), + ]) + table = pa.Table.from_arrays([index, _VALUES], schema=schema) + [chunk] = send_dataframe_and_get_chunks(table.to_reader()) + assert _summary([chunk]) == inline_snapshot(["/e static=False timelines=['frame'] ncols=3"]) diff --git a/rerun_py/tests/unit/test_tracing_session.py b/rerun_py/tests/unit/test_tracing_session.py new file mode 100644 index 000000000000..4b1963a2a2e0 --- /dev/null +++ b/rerun_py/tests/unit/test_tracing_session.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +import pytest +from rerun._tracing_session import ( + _generate_session_id, + _is_valid_session_id, + tracing_session, +) + + +def test_generated_id_is_valid() -> None: + for _ in range(8): + sid = _generate_session_id() + assert _is_valid_session_id(sid), f"generated invalid id: {sid!r}" + + +def test_validation_rejects_malformed_ids() -> None: + bad_ids = [ + "", + "rs_", + "rs_cafebab", # 7 hex chars + "rs_cafebabe1", # 9 hex chars + "rs_CAFEBABE", # uppercase + "rs_cafebabz", # non-hex + "xx_cafebabe", # wrong prefix + "cafebabe", # missing prefix + ] + for sid in bad_ids: + assert not _is_valid_session_id(sid), f"unexpectedly accepted: {sid!r}" + + +def test_logs_session_id_at_scope_entry(monkeypatch: pytest.MonkeyPatch) -> None: + """The session id must be forwarded to the Rust `tracing` stack on scope entry.""" + import rerun_bindings # noqa: TID251 + + captured: list[str] = [] + + def fake_log(sid: str) -> None: + captured.append(sid) + + # The context manager imports its bindings lazily from `rerun_bindings`, so + # patching the symbols on that module is what gets picked up at scope entry. + # `_is_telemetry_active` is forced to `True` so the test exercises the + # active-telemetry branch regardless of whether `TELEMETRY_ENABLED=true` + # was set for the running process (CI does not set it). + monkeypatch.setattr(rerun_bindings, "_is_telemetry_active", lambda: True) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_started", fake_log) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_finished", lambda *_args: None) + + with tracing_session() as sid: + pass + + assert captured == [sid], f"expected Rust logger to be called once with {sid!r}, got: {captured!r}" + + +def test_logs_metrics_at_scope_exit(monkeypatch: pytest.MonkeyPatch) -> None: + """A normal exit must invoke `_log_tracing_session_finished` once with the active session id.""" + import rerun_bindings # noqa: TID251 + + finished_calls: list[tuple[object, ...]] = [] + + monkeypatch.setattr(rerun_bindings, "_is_telemetry_active", lambda: True) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_started", lambda _sid: None) + monkeypatch.setattr( + rerun_bindings, + "_log_tracing_session_finished", + lambda *args: finished_calls.append(args), + ) + + with tracing_session() as sid: + pass + + assert len(finished_calls) == 1, f"expected one finished call, got: {finished_calls!r}" + args = finished_calls[0] + # Signature: (sid, elapsed_s, cpu_user_s, cpu_system_s, cpu_iowait_s, net_rx_mb) + assert args[0] == sid + assert isinstance(args[1], float) and args[1] >= 0.0 + # Remaining four fields are float|None depending on psutil/platform availability. + for field in args[2:]: + assert field is None or isinstance(field, float) + + +def test_skips_metrics_log_when_block_raises(monkeypatch: pytest.MonkeyPatch) -> None: + """If the `with` body raises, the finished-log must not fire (simpler control flow).""" + import rerun_bindings # noqa: TID251 + + finished_calls: list[tuple[object, ...]] = [] + + monkeypatch.setattr(rerun_bindings, "_is_telemetry_active", lambda: True) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_started", lambda _sid: None) + monkeypatch.setattr( + rerun_bindings, + "_log_tracing_session_finished", + lambda *args: finished_calls.append(args), + ) + + class _Boom(Exception): + pass + + with pytest.raises(_Boom): + with tracing_session(): + raise _Boom + + assert finished_calls == [], f"expected no finished call on early exit, got: {finished_calls!r}" + + +def test_psutil_failure_does_not_propagate(monkeypatch: pytest.MonkeyPatch) -> None: + """A psutil failure during snapshot or delta must never break the `with` block.""" + import rerun._tracing_session as ts + + import rerun_bindings # noqa: TID251 + + finished_calls: list[tuple[object, ...]] = [] + + monkeypatch.setattr(rerun_bindings, "_is_telemetry_active", lambda: True) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_started", lambda _sid: None) + monkeypatch.setattr( + rerun_bindings, + "_log_tracing_session_finished", + lambda *args: finished_calls.append(args), + ) + + class _BrokenPsutil: + @staticmethod + def Process() -> None: + raise OSError("simulated AccessDenied") + + @staticmethod + def net_io_counters() -> None: + raise OSError("simulated permission failure") + + monkeypatch.setattr(ts, "psutil", _BrokenPsutil) + + body_ran = False + with tracing_session() as sid: + body_ran = True + + assert body_ran, "with-block body must execute even when psutil snapshots fail" + assert len(finished_calls) == 1, f"expected one finished call, got: {finished_calls!r}" + args = finished_calls[0] + assert args[0] == sid + assert isinstance(args[1], float) + # All four metric fields must be None when psutil fails. + assert args[2:] == (None, None, None, None) + + +def test_finished_log_failure_does_not_propagate(monkeypatch: pytest.MonkeyPatch) -> None: + """If `_log_tracing_session_finished` itself raises, the `with` block must still complete cleanly.""" + import rerun_bindings # noqa: TID251 + + def boom(*_args: object) -> None: + raise RuntimeError("simulated tracing failure") + + monkeypatch.setattr(rerun_bindings, "_is_telemetry_active", lambda: True) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_started", lambda _sid: None) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_finished", boom) + + # No exception should escape the context manager. + with tracing_session() as sid: + assert sid.startswith("rs_") + + +def test_warns_and_no_ops_when_telemetry_inactive( + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Warn and yield an empty id when `TELEMETRY_ENABLED` is not truthy.""" + import rerun_bindings # noqa: TID251 + + # Force the inactive-telemetry branch regardless of process-wide env so + # this case exercises in CI even when telemetry happens to be active. + monkeypatch.setattr(rerun_bindings, "_is_telemetry_active", lambda: False) + + with caplog.at_level("WARNING", logger="rerun"): + with tracing_session() as sid: + assert sid == "" + + assert any("TELEMETRY_ENABLED=true" in r.getMessage() for r in caplog.records), ( + f"expected a WARNING about TELEMETRY_ENABLED, got: {[r.getMessage() for r in caplog.records]}" + ) + + +def test_nested_sessions_shadow_and_restore(monkeypatch: pytest.MonkeyPatch) -> None: + """ + Nested `with tracing_session()` blocks shadow the outer id while open and restore it on exit. + + Standard `ContextVar` token-reset semantics. + """ + import rerun_bindings # noqa: TID251 + + monkeypatch.setattr(rerun_bindings, "_is_telemetry_active", lambda: True) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_started", lambda _sid: None) + monkeypatch.setattr(rerun_bindings, "_log_tracing_session_finished", lambda *_args: None) + + var = rerun_bindings._get_tracing_session_var() + assert var.get(None) is None, "expected no active session before any scope" + + with tracing_session() as t1: + assert var.get(None) == t1, f"outer scope should see its own id, got {var.get(None)!r}" + + with tracing_session() as t2: + assert t2 != t1, "nested scope should generate a distinct id" + assert var.get(None) == t2, f"inner scope should shadow outer, got {var.get(None)!r}" + + assert var.get(None) == t1, f"outer id should be restored after inner exits, got {var.get(None)!r}" + + assert var.get(None) is None, f"session should be cleared after outermost exits, got {var.get(None)!r}" diff --git a/rerun_py/tests/unit/test_urdf_tree.py b/rerun_py/tests/unit/test_urdf_tree.py index 468b7ad868ae..563a37f63bce 100644 --- a/rerun_py/tests/unit/test_urdf_tree.py +++ b/rerun_py/tests/unit/test_urdf_tree.py @@ -3,10 +3,11 @@ import math from pathlib import Path +import pyarrow as pa import pytest import rerun as rr import rerun.urdf as rru -from rerun.experimental import RrdReader, StreamingReader +from rerun.experimental import Chunk, DeriveLens, LazyChunkStream, RrdReader, Selector, StreamingReader REPO_ROOT = Path(__file__).resolve().parents[3] URDF_PATH = REPO_ROOT / "examples" / "rust" / "animated_urdf" / "data" / "so100.urdf" @@ -32,7 +33,7 @@ def test_urdf_tree_loading() -> None: child_link = tree.get_joint_child(joint) assert child_link.name == "shoulder" - # We expect flat, neighbouring paths for visual and collision geometries of links, + # We expect flat, neighboring paths for visual and collision geometries of links, # queryable by either link name or link object. visual_paths = tree.get_visual_geometry_paths(child_link) assert visual_paths[0] == "/so_arm100/visual_geometries/shoulder/visual_0" @@ -144,7 +145,7 @@ def test_urdf_tree_custom_static_transform_entity_path(tmp_path: Path) -> None: tree = rru.UrdfTree.from_file_path(URDF_PATH, static_transform_entity_path="custom_tf_static") tree.log_urdf_to_recording(rec) - paths = RrdReader(rrd_path).store().schema().entity_paths() + paths = RrdReader(rrd_path).stream().collect().schema().entity_paths() assert "/custom_tf_static" in paths assert "/tf_static" not in paths @@ -205,6 +206,83 @@ def test_urdf_compute_transform_columns() -> None: joint.compute_transform_columns([joint.limit_upper + 1.0], clamp=True) +def test_urdf_transform_batches_in_lens_pipeline() -> None: + """Integration test for computing transform batches from joint states in a chunk pipeline using lenses.""" + + frame_prefix = "prefix_" + urdf_tree = rru.UrdfTree.from_file_path(URDF_PATH, frame_prefix=frame_prefix) + + # Create a chunk with joint names and values. + messages = pa.StructArray.from_arrays( + [ + # Note: "1", "2" are joint names from the test URDF file we use here. + pa.array([["1", "2"], ["1"]], type=pa.list_(pa.string())), + pa.array([[0.0, 0.5], [1.0]], type=pa.list_(pa.float64())), + ], + names=["joint_names", "actuator_readings"], + ) + chunk = Chunk.from_columns( + "/joint_states", + indexes=[rr.TimeColumn("frame", sequence=[0, 1])], + columns=rr.DynamicArchetype.columns( + archetype="schemas.SomeCustomJointState", + components={"message": messages}, + ), + ) + + # Apply a two-stage pipeline using lenses: + + # 1. Compute joint transforms in batch using `UrdfTree.compute_joint_transform_batches`. + # N input rows with multiple joint states per row -> N output rows with multiple transforms per row. + compute_joints = DeriveLens("schemas.SomeCustomJointState:message").to_component( + "rerun.urdf.JointTransformBatch", + Selector(".").pipe( + lambda joint_state_messages: urdf_tree.compute_joint_transform_batches( + names=Selector(".joint_names").execute(joint_state_messages), + values=Selector(".actuator_readings").execute(joint_state_messages), + ) + ), + ) + # 2. Scatter the batch data into final `Transform3D` rows. + # N input rows with multiple transforms per row -> one output row per transform. + output_transforms = ( + DeriveLens("rerun.urdf.JointTransformBatch", output_entity="/tf", scatter=True) + .to_component(rr.Transform3D.descriptor_translation(), Selector(".[].translation")) + .to_component(rr.Transform3D.descriptor_quaternion(), Selector(".[].quaternion")) + .to_component(rr.Transform3D.descriptor_parent_frame(), Selector(".[].parent_frame")) + .to_component(rr.Transform3D.descriptor_child_frame(), Selector(".[].child_frame")) + ) + + chunks = LazyChunkStream.from_iter([chunk]).lenses(compute_joints).lenses(output_transforms).to_chunks() + + assert len(chunks) == 1 + assert chunks[0].entity_path == "/tf" + assert chunks[0].num_rows == 3 + + batch = chunks[0].to_record_batch() + assert batch.column("frame").to_pylist() == [0, 0, 1] + assert set(batch.column_names) >= { + "Transform3D:translation", + "Transform3D:quaternion", + "Transform3D:parent_frame", + "Transform3D:child_frame", + } + + joint1 = urdf_tree.get_joint_by_name("1") + joint2 = urdf_tree.get_joint_by_name("2") + assert joint1 is not None and joint2 is not None + assert batch.column("Transform3D:parent_frame").to_pylist() == [ + [f"{frame_prefix}{joint1.parent_link}"], + [f"{frame_prefix}{joint2.parent_link}"], + [f"{frame_prefix}{joint1.parent_link}"], + ] + assert batch.column("Transform3D:child_frame").to_pylist() == [ + [f"{frame_prefix}{joint1.child_link}"], + [f"{frame_prefix}{joint2.child_link}"], + [f"{frame_prefix}{joint1.child_link}"], + ] + + def assert_quat_equivalent(actual: rr.components.RotationQuatBatch, expected: list[float]) -> None: actual_values = actual.pa_array.to_pylist()[0] dot = sum(a * b for a, b in zip(actual_values, expected, strict=False)) diff --git a/rerun_py/tests/unit/test_video_utils.py b/rerun_py/tests/unit/test_video_utils.py new file mode 100644 index 000000000000..52d82190f2a2 --- /dev/null +++ b/rerun_py/tests/unit/test_video_utils.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import pytest +from rerun.components import VideoCodec +from rerun.experimental.video import ( + detect_gop_start, + is_annex_b, + length_prefixed_to_annex_b, +) + +ANNEX_B_START_CODE = b"\x00\x00\x00\x01" + +# H.264 SPS and IDR NAL units, matching the `re_video` GOP detection tests. +SPS_NALU = bytes([ + 0x67, 0x64, 0x00, 0x0A, 0xAC, 0x72, 0x84, 0x44, 0x26, 0x84, 0x00, 0x00, 0x03, + 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xCA, 0x3C, 0x48, 0x96, 0x11, 0x80, +]) # fmt: skip +IDR_NALU = bytes([ + 0x65, 0x88, 0x84, 0x21, 0x43, 0x02, 0x4C, 0x82, 0x54, 0x2B, 0x8F, 0x2C, 0x8C, + 0x54, 0x4A, 0x92, 0x54, 0x2B, 0x8F, 0x2C, 0x8C, 0x54, 0x4A, 0x92, +]) # fmt: skip + +H264_KEYFRAME_ANNEX_B = ANNEX_B_START_CODE + SPS_NALU + ANNEX_B_START_CODE + IDR_NALU + + +def _length_prefixed(nalus: list[bytes], length_prefix_size: int = 4) -> bytes: + return b"".join(len(nalu).to_bytes(length_prefix_size, "big") + nalu for nalu in nalus) + + +@pytest.mark.parametrize( + ("data", "expected"), + [ + (b"\x00\x00\x00\x01\xab\xcd", True), # 4-byte start code + (b"\x00\x00\x01\xab\xcd", True), # 3-byte short start code + (b"\x00\x00\x00\x01", True), # exactly the start code + (b"\x00\x00\x01", True), # exactly the short start code + (b"\x00\x00\x02\xab", False), + (b"\xab\xcd\xef\x01", False), + (b"", False), + (b"\x00", False), + (b"\x00\x00", False), + ], +) +def test_is_annex_b(data: bytes, expected: bool) -> None: + assert is_annex_b(data) is expected + + +def test_detect_gop_start_h264_keyframe() -> None: + assert detect_gop_start(H264_KEYFRAME_ANNEX_B, VideoCodec.H264) + + +def test_detect_gop_start_h264_non_keyframe() -> None: + assert not detect_gop_start(ANNEX_B_START_CODE + SPS_NALU, VideoCodec.H264) + assert not detect_gop_start(bytes(range(1, 11)), VideoCodec.H264) + + +def test_detect_gop_start_h264_broken_sps() -> None: + broken_sps = bytes([0x67, 0x00]) + SPS_NALU[2:] + with pytest.raises(ValueError, match="Failed reading SPS"): + detect_gop_start(ANNEX_B_START_CODE + broken_sps + ANNEX_B_START_CODE + IDR_NALU, VideoCodec.H264) + + +def test_length_prefixed_to_annex_b() -> None: + length_prefixed = _length_prefixed([SPS_NALU, IDR_NALU]) + assert length_prefixed_to_annex_b(length_prefixed) == H264_KEYFRAME_ANNEX_B + + +def test_length_prefixed_to_annex_b_short_prefix() -> None: + length_prefixed = _length_prefixed([SPS_NALU, IDR_NALU], length_prefix_size=2) + assert length_prefixed_to_annex_b(length_prefixed, length_prefix_size=2) == H264_KEYFRAME_ANNEX_B + + +def test_length_prefixed_to_annex_b_truncated() -> None: + length_prefixed = _length_prefixed([SPS_NALU, IDR_NALU]) + with pytest.raises(ValueError, match="incomplete NAL unit"): + length_prefixed_to_annex_b(length_prefixed[:-1]) + + +def test_length_prefixed_round_trip_detects_gop() -> None: + annex_b = length_prefixed_to_annex_b(_length_prefixed([SPS_NALU, IDR_NALU])) + assert detect_gop_start(annex_b, VideoCodec.H264) diff --git a/run_wasm/src/main.rs b/run_wasm/src/main.rs index 1685da2fdeac..b8da9c29751b 100644 --- a/run_wasm/src/main.rs +++ b/run_wasm/src/main.rs @@ -92,7 +92,7 @@ fn main() { std::thread::sleep(Duration::from_millis(500)); // Open browser tab. - let viewer_url = format!("http://{host}:{port}",); + let viewer_url = format!("http://{host}:{port}"); webbrowser::open(&viewer_url).ok(); println!("Opening browser at {viewer_url}"); diff --git a/rust-toolchain b/rust-toolchain index ce5b509874b5..7fb2669f3e5b 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -5,6 +5,6 @@ # to the user in the error, instead of "error: invalid channel name '[toolchain]'". [toolchain] -channel = "1.92.0" +channel = "1.95.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/accept_snapshots.sh b/scripts/accept_snapshots.sh index 9e02eadbcf17..e677d4f87f8c 100755 --- a/scripts/accept_snapshots.sh +++ b/scripts/accept_snapshots.sh @@ -5,8 +5,8 @@ set -eu # rename the .new.png files to .png -find . -type d -path "*/tests/snapshots*" | while read dir; do - find "$dir" -type f -name "*.new.png" | while read file; do +find . -type d -path "*/tests/snapshots*" | while read -r dir; do + find "$dir" -type f -name "*.new.png" | while read -r file; do mv -f "$file" "${file%.new.png}.png" done done diff --git a/scripts/append_web_viewer.py b/scripts/append_web_viewer.py index 7102e0e9473e..d0cff2f194c8 100755 --- a/scripts/append_web_viewer.py +++ b/scripts/append_web_viewer.py @@ -33,7 +33,8 @@ def create_web_viewer_zip(web_viewer_dir: Path) -> bytes: """Create a zip archive of the web viewer assets.""" required_files = [ "index.html", - "favicon.svg", + "favicon.ico", + "apple-touch-icon.png", "sw.js", "re_viewer.js", "re_viewer_bg.wasm", diff --git a/scripts/check_env.py b/scripts/check_env.py index 1bd2e618211d..6562482edbed 100755 --- a/scripts/check_env.py +++ b/scripts/check_env.py @@ -7,9 +7,9 @@ import subprocess import sys -PIXI_VERSION = "0.55.0" -CARGO_VERSION = "1.92.0" -RUST_VERSION = "1.92.0" +PIXI_VERSION = "0.71.3" +CARGO_VERSION = "1.95.0" +RUST_VERSION = "1.95.0" def check_version(cmd: str, expected: str, update: str, install: str) -> bool: diff --git a/scripts/ci/buf.yaml b/scripts/ci/buf.yaml new file mode 100644 index 000000000000..3930182462e1 --- /dev/null +++ b/scripts/ci/buf.yaml @@ -0,0 +1,15 @@ +version: v2 +modules: + - path: crates/store/re_protos/proto + - path: examples/python/objectron/objectron/proto +lint: + use: + - STANDARD + except: + - DIRECTORY_SAME_PACKAGE + - PACKAGE_DIRECTORY_MATCH + ignore: + - examples/python/objectron/objectron/proto +breaking: + use: + - FILE diff --git a/scripts/ci/build_and_upload_wheels.py b/scripts/ci/build_and_upload_wheels.py index 6409c54d959a..4ad7491eabac 100755 --- a/scripts/ci/build_and_upload_wheels.py +++ b/scripts/ci/build_and_upload_wheels.py @@ -64,12 +64,18 @@ def __str__(self) -> str: def build_and_upload( - bucket: Bucket | None, mode: BuildMode, gcs_dir: str, target: str, compatibility: str | None + bucket: Bucket | None, + mode: BuildMode, + gcs_dir: str, + target: str, + compatibility: str | None, + use_zig: bool, ) -> None: # pypi / extra builds require a web build if mode in (BuildMode.PYPI, BuildMode.EXTRA): run("pixi run rerun-build-web-release") + # Grep for these feature names before changing them. if mode is BuildMode.PYPI: maturin_feature_flags = "--no-default-features --features perf_telemetry,pypi" elif mode is BuildMode.PR: @@ -80,10 +86,12 @@ def build_and_upload( dist = f"dist/{target}" compatibility = f"--compatibility {compatibility}" if compatibility is not None else "" + zig = "--zig" if use_zig else "" run( "maturin build " f"{compatibility} " + f"{zig} " "--manifest-path rerun_py/Cargo.toml " "--release " f"--target {target} " @@ -128,6 +136,7 @@ def main() -> None: type=str, help='The platform tag for linux, e.g. "manylinux_2_28"', ) + parser.add_argument("--zig", action="store_true", help="Use Zig to target the requested manylinux version") parser.add_argument("--upload-gcs", action="store_true", default=False, help="Upload the wheel to GCS") parser.add_argument( "--upload-only", @@ -152,6 +161,7 @@ def main() -> None: args.dir, args.target or detect_target(), args.compat, + args.zig, ) diff --git a/scripts/ci/bundle_macos_app.py b/scripts/ci/bundle_macos_app.py new file mode 100755 index 000000000000..3c2f12c79b71 --- /dev/null +++ b/scripts/ci/bundle_macos_app.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Assemble a macOS `.app` bundle around the rerun-cli binary. + +Produces `/Rerun.app/` with: + Contents/ + Info.plist (with __VERSION__ substituted) + MacOS/rerun (the binary, executable bit set) + Resources/Rerun.icns (multi-resolution icns derived from the PNG) + +The bundle is what gives macOS the right dock label ("Rerun" instead of "rerun"), +proper "About Rerun" menu, and a hook for future file associations. +""" + +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + +# Apple requires CFBundleShortVersionString to be three integers separated by periods. +# Map e.g. "0.33.0-alpha.1+dev" → "0.33.0". +_VERSION_PREFIX = re.compile(r"^(\d+\.\d+\.\d+)") + +ICNS_SIZES = [ + (16, "icon_16x16.png"), + (32, "icon_16x16@2x.png"), + (32, "icon_32x32.png"), + (64, "icon_32x32@2x.png"), + (128, "icon_128x128.png"), + (256, "icon_128x128@2x.png"), + (256, "icon_256x256.png"), + (512, "icon_256x256@2x.png"), + (512, "icon_512x512.png"), + (1024, "icon_512x512@2x.png"), +] + + +def run(args: list[str]) -> None: + print(f"> {' '.join(args)}", flush=True) + subprocess.run(args, check=True) + + +def build_icns(png: Path, out: Path) -> None: + """Build a multi-resolution .icns from a square source PNG using macOS native tools.""" + iconset = out.parent / f"{out.stem}.iconset" + if iconset.exists(): + shutil.rmtree(iconset) + iconset.mkdir(parents=True) + for size, name in ICNS_SIZES: + run(["sips", "-z", str(size), str(size), str(png), "--out", str(iconset / name)]) + run(["iconutil", "--convert", "icns", "--output", str(out), str(iconset)]) + shutil.rmtree(iconset) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--binary", required=True, type=Path, help="Path to the rerun-cli binary") + parser.add_argument("--icon", required=True, type=Path, help="Path to the source square PNG icon") + parser.add_argument("--info-plist", required=True, type=Path, help="Path to the Info.plist template") + parser.add_argument("--version", required=True, help="Version string (e.g. 0.21.0)") + parser.add_argument("--output-dir", required=True, type=Path, help="Directory to write Rerun.app into") + args = parser.parse_args() + + if sys.platform != "darwin": + print("error: bundle_macos_app.py must run on macOS (uses sips and iconutil)", file=sys.stderr) + return 1 + + for path in [args.binary, args.icon, args.info_plist]: + if not path.exists(): + print(f"error: {path} does not exist", file=sys.stderr) + return 1 + + app = args.output_dir / "Rerun.app" + if app.exists(): + shutil.rmtree(app) + macos_dir = app / "Contents" / "MacOS" + resources_dir = app / "Contents" / "Resources" + macos_dir.mkdir(parents=True) + resources_dir.mkdir(parents=True) + + # Binary — named with a capital R inside the bundle so macOS's + # NSProcessInfo.processName resolves to "Rerun", which winit then uses to + # build the app menu items ("About Rerun", "Hide Rerun", "Quit Rerun"). + binary_dst = macos_dir / "Rerun" + shutil.copy2(args.binary, binary_dst) + binary_dst.chmod(0o755) + + # Info.plist with version substituted (sanitized to Apple's x.y.z form) + match = _VERSION_PREFIX.match(args.version) + short_version = match.group(1) if match else "0.0.0" + plist_text = args.info_plist.read_text(encoding="utf-8").replace("__VERSION__", short_version) + (app / "Contents" / "Info.plist").write_text(plist_text, encoding="utf-8") + + # Icon + build_icns(args.icon, resources_dir / "Rerun.icns") + + print(f"Wrote {app}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/ci/check_d2_diagrams.py b/scripts/ci/check_d2_diagrams.py new file mode 100755 index 000000000000..869ad19084ba --- /dev/null +++ b/scripts/ci/check_d2_diagrams.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +"""Checks that no D2 diagrams are stored as fenced code blocks in markdown. + +D2 diagrams must be rendered to SVG and embedded as `` elements, not +stored as ```d2 code blocks. Use `scripts/render_d2.py` to render a diagram +and produce a ready-to-paste HTML block. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +# Matches the opening fence of a ```d2 code block, allowing leading +# whitespace (indented blocks) and trailing info-string after `d2`. +D2_FENCE_RE = re.compile(r"^[ \t]*(`{3,}|~{3,})[ \t]*d2\b", re.IGNORECASE) + +SEARCH_ROOTS = ("docs/content", "examples") + + +def find_d2_code_blocks(text: str) -> list[int]: + """Return the 1-based line numbers of every ```d2 opening fence.""" + hits: list[int] = [] + for i, line in enumerate(text.splitlines(), start=1): + if D2_FENCE_RE.match(line): + hits.append(i) + return hits + + +def all_markdown_files() -> list[Path]: + files: list[Path] = [] + for root in SEARCH_ROOTS: + files.extend(sorted(Path(root).rglob("*.md"))) + return files + + +def check() -> None: + offenders: list[tuple[Path, int]] = [] + for path in all_markdown_files(): + text = path.read_text(encoding="utf-8") + for line in find_d2_code_blocks(text): + offenders.append((path, line)) + + if offenders: + for path, line in offenders: + print(f"{path}:{line}: D2 diagram stored as a code block") + print() + print( + "D2 diagrams must not be stored as ```d2 code blocks. " + "Render them to SVG with `scripts/render_d2.py` and embed the " + 'resulting `
` HTML block instead.' + ) + sys.exit(1) + + print("✔ no D2 code blocks found") + + +def main() -> None: + check() + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/check_doc_order.py b/scripts/ci/check_doc_order.py new file mode 100755 index 000000000000..6e10ca7ab81f --- /dev/null +++ b/scripts/ci/check_doc_order.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +"""Check docs frontmatter order values.""" + +from __future__ import annotations + +import argparse +import sys +from collections import defaultdict +from pathlib import Path +from typing import Any + +import yaml + + +def parse_frontmatter(path: Path) -> dict[str, Any]: + text = path.read_text(encoding="utf-8") + if not text.startswith("---"): + return {} + + end = text.find("\n---", 3) + if end == -1: + raise ValueError(f"{path}: unterminated YAML frontmatter") + + frontmatter = yaml.safe_load(text[3:end].strip()) or {} + if not isinstance(frontmatter, dict): + raise ValueError(f"{path}: frontmatter is not a mapping") + + return frontmatter + + +def check(root: Path) -> bool: + docs_by_order: dict[Path, dict[Any, list[Path]]] = defaultdict(lambda: defaultdict(list)) + + for path in sorted(root.rglob("*.md")): + frontmatter = parse_frontmatter(path) + if frontmatter.get("redirect") is not None: + continue + + if "order" in frontmatter: + docs_by_order[path.parent][frontmatter["order"]].append(path) + + duplicates = [] + for parent, paths_by_order in docs_by_order.items(): + for order, paths in paths_by_order.items(): + if len(paths) > 1: + duplicates.append((parent, order, paths)) + + for parent, order, paths in sorted(duplicates, key=lambda item: (item[0].as_posix(), str(item[1]))): + print(f"{parent.relative_to(root)} has multiple docs with order {order}:") + for path in paths: + print(f" {path.relative_to(root)}") + print() + + if duplicates: + print("Docs in the same directory must have unique `order` values.") + return False + + return True + + +def main() -> None: + parser = argparse.ArgumentParser(description="Check docs frontmatter order values") + parser.add_argument("--root", type=Path, default=Path("docs/content"), help="Docs content root") + args = parser.parse_args() + + if not check(args.root): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/check_large_files.py b/scripts/ci/check_large_files.py index a48157a06f63..7415755c3b42 100644 --- a/scripts/ci/check_large_files.py +++ b/scripts/ci/check_large_files.py @@ -15,11 +15,13 @@ "crates/store/re_dataframe/src/query.rs", "crates/store/re_protos/proto/schema_snapshot.yaml", "crates/store/re_protos/src/v1alpha1/rerun.cloud.v1alpha1.rs", + "crates/store/re_protos/src/v1alpha1/rerun.cloud.v1alpha1.ext.rs", "crates/store/re_query/src/range_zip/generated.rs", "crates/store/re_sdk_types/src/datatypes/tensor_buffer.rs", "crates/store/re_sdk_types/src/reflection/mod.rs", "crates/top/re_sdk/src/recording_stream.rs", "crates/viewer/re_ui/data/Inter-Medium.otf", + "crates/viewer/re_viewer/data/app_icon_mac.png", "crates/viewer/re_viewer/src/app.rs", # TODO(emilk): break this up into smaller files "docs/snippets/INDEX.md", "pixi.lock", @@ -27,7 +29,7 @@ "uv.lock", # Examples excluded from the uv workspace so they maintain standalone lockfiles. "examples/python/dataloader/uv.lock", - "examples/python/rerun_export/uv.lock", + "examples/python/droid_semantic_search/uv.lock", } # Paths with the following prefixes are allowed to contain PNG files that are not checked into LFS @@ -35,8 +37,9 @@ "crates/viewer/re_ui/data/icons/", "crates/viewer/re_ui/data/logo_dark_mode.png", "crates/viewer/re_ui/data/logo_light_mode.png", + "crates/viewer/re_viewer/data/app_icon.png", "crates/viewer/re_viewer/data/app_icon_mac.png", - "crates/viewer/re_viewer/data/app_icon_windows.png", + "crates/viewer/re_web_viewer_server/web_viewer/apple-touch-icon.png", "docs/snippets/all/archetypes/ferris.png", "docs/snippets/all/archetypes/encoded_depth.png", "docs/snippets/src/snippets/ferris.png", diff --git a/scripts/ci/check_skills.py b/scripts/ci/check_skills.py new file mode 100644 index 000000000000..dc32298898b4 --- /dev/null +++ b/scripts/ci/check_skills.py @@ -0,0 +1,99 @@ +"""Sanity-check the skills under `skills/*`. + +Runs a set of independent checks against every skill directory and reports all +failures at once. Add new checks by writing a `(skill_dir) -> list[str]` +function (returning one message per problem) and appending it to `CHECKS`. +""" + +from __future__ import annotations + +import argparse +import sys +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import yaml + +REQUIRED_KEYS = ("name", "description") + + +def _parse_frontmatter(path: Path) -> dict[str, Any]: + text = path.read_text(encoding="utf-8") + if not text.startswith("---"): + raise ValueError("missing YAML frontmatter (file must start with `---`)") + + end = text.find("\n---", 3) + if end == -1: + raise ValueError("unterminated YAML frontmatter (no closing `---`)") + + frontmatter = yaml.safe_load(text[3:end].strip()) + if not isinstance(frontmatter, dict): + raise ValueError("frontmatter is not a mapping") + + return frontmatter + + +def check_frontmatter(skill_dir: Path) -> list[str]: + """Verify the front matter is valid yaml""" + skill_md = skill_dir / "SKILL.md" + if not skill_md.is_file(): + return ["missing SKILL.md"] + + try: + frontmatter = _parse_frontmatter(skill_md) + except yaml.YAMLError as err: + # Flatten the multi-line PyYAML message so the failure is a single grep-able line. + detail = " ".join(str(err).split()) + return [f"SKILL.md: invalid YAML frontmatter: {detail}"] + except ValueError as err: + return [f"SKILL.md: {err}"] + + errors = [] + missing = [key for key in REQUIRED_KEYS if not frontmatter.get(key)] + if missing: + errors.append(f"SKILL.md: missing required frontmatter key(s): {', '.join(missing)}") + + if "name" in frontmatter and frontmatter["name"] != skill_dir.name: + errors.append(f"SKILL.md: `name: {frontmatter['name']}` does not match directory name `{skill_dir.name}`") + + return errors + + +# Each check takes a skill directory and returns one message per problem found (empty = pass). +CHECKS: list[Callable[[Path], list[str]]] = [ + check_frontmatter, +] + + +def check(root: Path) -> bool: + skill_dirs = sorted(p for p in root.glob("*") if p.is_dir()) + if not skill_dirs: + print(f"No skill directories found under {root}") + return False + + ok = True + for skill_dir in skill_dirs: + errors = [msg for check_fn in CHECKS for msg in check_fn(skill_dir)] + if errors: + ok = False + for msg in errors: + print(f"FAIL {skill_dir.name}: {msg}") + else: + print(f"ok {skill_dir.name}") + + return ok + + +def main() -> None: + parser = argparse.ArgumentParser(description="Sanity-check the skills under skills/*") + parser.add_argument("--root", type=Path, default=Path("skills"), help="Skills root directory") + args = parser.parse_args() + + if not check(args.root): + print("\nSkill checks failed. See errors above.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/crates.py b/scripts/ci/crates.py index 8e323b2b1ee6..64701753022d 100755 --- a/scripts/ci/crates.py +++ b/scripts/ci/crates.py @@ -660,7 +660,7 @@ def get_version(target: Target | None, skip_prerelease: bool = False) -> Version current_version = VersionInfo.parse(branch_name) # ensures that it is a valid version except ValueError: print(f"the current branch `{branch_name}` does not specify a valid version.") - print("this script expects the format `prepare-release-x.y.z-meta.N`") + print("this script expects the format `prepare-release-x.y.z` or `prepare-release-x.y.z-alpha.N`") sys.exit(1) elif target is Target.CratesIo: latest_published_version = get_latest_published_version("rerun", skip_prerelease) diff --git a/scripts/ci/fetch_artifact.py b/scripts/ci/fetch_artifact.py index d373f41a38ec..578e9f2496a9 100644 --- a/scripts/ci/fetch_artifact.py +++ b/scripts/ci/fetch_artifact.py @@ -5,6 +5,7 @@ import argparse import os import stat +import tarfile from pathlib import Path from google.cloud import storage @@ -13,7 +14,9 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("--commit-sha", required=True, help="Which sha are we fetching artifacts for") - parser.add_argument("--artifact", choices=["rerun-cli"], help="Which artifact are we fetching") + parser.add_argument( + "--artifact", choices=["rerun-cli", "rerun-cli-macos-app"], help="Which artifact are we fetching" + ) parser.add_argument( "--platform", choices=[ @@ -27,6 +30,24 @@ def main() -> None: args = parser.parse_args() + # rerun-cli-macos-app fetches the macOS .app bundle (tarball) and extracts it into --dest. + if args.artifact == "rerun-cli-macos-app": + if args.platform != "macos-arm64": + raise SystemExit("--artifact rerun-cli-macos-app is only valid with --platform macos-arm64") + bucket_path = f"commit/{args.commit_sha}/rerun-cli/{args.platform}/Rerun.app.tar.gz" + print(f"Fetching artifact from {bucket_path} to {args.dest}") + gcs = storage.Client() + bucket = gcs.bucket("rerun-builds") + artifact = bucket.blob(bucket_path) + os.makedirs(args.dest, exist_ok=True) + tarball = Path(args.dest) / "Rerun.app.tar.gz" + with open(tarball, "wb") as f: + artifact.download_to_file(f) + with tarfile.open(tarball, "r:gz") as tar: + tar.extractall(args.dest) + tarball.unlink() + return + artifact_names: dict[tuple[str, str], str] = {} artifact_names["rerun-cli", "linux-arm64"] = "rerun" artifact_names["rerun-cli", "linux-x64"] = "rerun" diff --git a/scripts/ci/generate_prerelease_pip_index.py b/scripts/ci/generate_prerelease_pip_index.py index 630c503271ce..6ed7f5078fb3 100755 --- a/scripts/ci/generate_prerelease_pip_index.py +++ b/scripts/ci/generate_prerelease_pip_index.py @@ -30,7 +30,7 @@ def generate_pip_index(title: str, dir: str, upload: bool, check: bool) -> None: # Initialize the GCS clients t0 = time.time() - gcs_client = storage.Client() + gcs_client = storage.Client(project="rerun-open") print(f"GCS client initialized in {time.time() - t0:.2f}s") # Prepare the found_builds list diff --git a/scripts/ci/isolated_examples.py b/scripts/ci/isolated_examples.py index a36a62bea7e4..25fd7ecb9a23 100644 --- a/scripts/ci/isolated_examples.py +++ b/scripts/ci/isolated_examples.py @@ -1,7 +1,7 @@ """CI helpers for isolated Python example projects. An "isolated" example is one that has its own uv project (separate pyproject.toml and uv.lock) -because its dependency closure conflicts with the workspace .venv (e.g. LeRobot pinning an incompatible rerun-sdk). +because its dependency closure conflicts with the workspace .venv (e.g. LeRobot requiring Python >=3.12). Each such example opts in by setting `[tool.rerun-example] isolated = true` in its pyproject.toml. Fails on the first non-zero exit. @@ -69,6 +69,12 @@ def cmd_lint(examples_dir: Path, repo_root: Path) -> int: for project in projects: print(f"\n=== {project.relative_to(repo_root)} ===", flush=True) subprocess.run(["uv", "sync"], cwd=project, check=True) + # rerun-dev-fixup is not in pyproject.toml (uv 0.7.x resolves all groups + # unconditionally — a path-only package would block standalone --no-sources). + # Install it explicitly here when running inside the monorepo. + shim_dir = (project / "../../../rerun_py/rerun_dev_fixup").resolve() + if shim_dir.exists(): + subprocess.run(["uv", "pip", "install", str(shim_dir)], cwd=project, check=True) merged = build_merged_config(shared_base, project / "pyproject.toml") with tempfile.NamedTemporaryFile( mode="w", diff --git a/scripts/ci/macos/Info.plist b/scripts/ci/macos/Info.plist new file mode 100644 index 000000000000..eb39c2a6e98d --- /dev/null +++ b/scripts/ci/macos/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Rerun + CFBundleExecutable + Rerun + CFBundleIconFile + Rerun.icns + CFBundleIdentifier + io.rerun.Rerun + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Rerun + CFBundlePackageType + APPL + CFBundleShortVersionString + __VERSION__ + CFBundleSignature + ???? + CFBundleVersion + __VERSION__ + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 11.0 + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + NSSupportsAutomaticGraphicsSwitching + + + diff --git a/scripts/ci/render_bench.py b/scripts/ci/render_bench.py index df9b11f7c0c4..9c27dfdbfafc 100755 --- a/scripts/ci/render_bench.py +++ b/scripts/ci/render_bench.py @@ -105,7 +105,7 @@ def duplicate(self, date: datetime) -> BenchmarkEntry: Benchmarks = dict[str, list[BenchmarkEntry]] -FORMAT_BENCHER_RE = re.compile(r"test\s+(\S+).*bench:\s+(\d+)\s+ns\/iter") +FORMAT_BENCHER_RE = re.compile(r"test\s+(\S+).*bench:\s+([\d,]+)\s+ns\/iter") def parse_bencher_line(data: str) -> Measurement: @@ -113,7 +113,7 @@ def parse_bencher_line(data: str) -> Measurement: if match is None: raise ValueError(f"invalid bencher line: {data}") name, ns_iter = match.groups() - return Measurement(name, float(ns_iter), "ns/iter") + return Measurement(name, float(ns_iter.replace(",", "")), "ns/iter") def parse_bencher_text(data: str) -> list[Measurement]: diff --git a/scripts/ci/rust_checks.py b/scripts/ci/rust_checks.py index bd9a2d409ebe..a2c3bcd1d62d 100755 --- a/scripts/ci/rust_checks.py +++ b/scripts/ci/rust_checks.py @@ -220,7 +220,14 @@ def main() -> None: def base_checks(results: list[Result]) -> None: # First check with --locked to make sure Cargo.lock is up to date. results.append(run_cargo("check", "--locked --all-features")) - results.append(run_cargo("fmt", "--all -- --check")) + + fmt_result = run_cargo("fmt", "--all -- --check") + if sys.platform == "win32" and not fmt_result.success: + # TODO(rust-lang/rustfmt#6934): cargo-fmt passes all target paths for an edition to one + # rustfmt spawn, which can exceed the Windows command-line length limit in large workspaces. + fmt_result.success = True + results.append(fmt_result) + results.append(run_cargo("clippy", "--all-targets --all-features -- --deny warnings")) @@ -229,6 +236,10 @@ def sdk_variations(results: list[Result]) -> None: results.append(run_cargo("check", "-p rerun --no-default-features")) results.append(run_cargo("check", "-p rerun --no-default-features --features sdk")) + # `re_server` is built without the optional `lance` feature in many configurations + # (e.g. when pulled in by `rerun`'s `--all-features`, which does not propagate `re_server/lance`). + results.append(run_cargo("check", "-p re_server")) + deny_targets = [ "aarch64-apple-darwin", diff --git a/scripts/ci/setup_software_rasterizer.py b/scripts/ci/setup_software_rasterizer.py index ab95b99319dc..967ca829ac26 100644 --- a/scripts/ci/setup_software_rasterizer.py +++ b/scripts/ci/setup_software_rasterizer.py @@ -291,12 +291,63 @@ def vulkan_info(extra_env_vars: dict[str, str]) -> None: print(run([vulkaninfo_path, "--summary"], env=env).stdout) +def _looks_like_vulkan_sdk(path: Path) -> bool: + """Whether `path` holds a usable Vulkan SDK, i.e. ships the vulkaninfo utility.""" + exe = "vulkaninfoSDK.exe" if os.name == "nt" else "vulkaninfo" + return (path / "bin" / exe).exists() + + +def print_vulkan_sdk_candidate_locations() -> None: + """ + Dump the places the Vulkan SDK might have ended up, to diagnose a cache mismatch. + """ + workspace = os.environ.get("GITHUB_WORKSPACE") or os.getcwd() + version = os.environ.get("VULKAN_SDK_VERSION", "") + + print("Vulkan SDK diagnostics:") + print(f" VULKAN_SDK = {os.environ.get('VULKAN_SDK')}") + print(f" VULKAN_SDK_VERSION = {version or ''}") + print(f" GITHUB_WORKSPACE = {workspace}") + print(f" cwd = {os.getcwd()}") + + if not version: + print(" (VULKAN_SDK_VERSION unset — cannot enumerate versioned locations)") + return + + candidates: list[Path] = [Path(workspace) / "vulkan_sdk" / version] # intended destination + if os.name == "nt": + candidates.append(Path(f"C:\\VulkanSDK\\{version}")) # the action's default destination + # A relative `..` chain that under/overshoots lands the SDK next to some ancestor of + # the workspace; probe each, using both the casings the action and default use. + for ancestor in [Path(workspace), *Path(workspace).parents]: + candidates.append(ancestor / "vulkan_sdk" / version) + candidates.append(ancestor / "VulkanSDK" / version) + + print(" candidate locations:") + seen: set[Path] = set() + for path in candidates: + if path in seen: + continue + seen.add(path) + if not path.exists(): + print(f" [missing] {path}") + elif _looks_like_vulkan_sdk(path): + print(f" [SDK!] {path}") + else: + print(f" [dir] {path} (exists, but no vulkaninfo)") + + def check_for_vulkan_sdk() -> None: vulkan_sdk_path = os.environ.get("VULKAN_SDK") if vulkan_sdk_path is None: print( "ERROR: VULKAN_SDK is not set. The sdk needs to be installed prior including runtime & vulkaninfo utility.", ) + print_vulkan_sdk_candidate_locations() + sys.exit(1) + if not Path(vulkan_sdk_path).exists(): + print("ERROR: VULKAN_SDK points to a path that does not exist (likely a cache restored to the wrong location).") + print_vulkan_sdk_candidate_locations() sys.exit(1) diff --git a/scripts/ci/sync_release_assets.py b/scripts/ci/sync_release_assets.py index 8198e21755de..9251bb8f2b43 100644 --- a/scripts/ci/sync_release_assets.py +++ b/scripts/ci/sync_release_assets.py @@ -26,6 +26,7 @@ from github.Repository import Repository Assets = dict[str, storage.Blob] +MissingAssets = list[tuple[str, str]] def get_any_release(repo: Repository, tag_name: str) -> GitRelease | None: @@ -63,7 +64,11 @@ def fetch_binary_assets( if do_rerun_js: print(" - JS package") - all_found = True + missing_assets: MissingAssets = [] + + def report_missing_blob(asset_name: str, blob_url: str) -> None: + print(f"Missing {asset_name}: gs://{bucket.name}/{blob_url}") + missing_assets.append((asset_name, blob_url)) # Python wheels if do_wheels: @@ -92,8 +97,7 @@ def fetch_binary_assets( assets[name] = blob if not found: - all_found = False - print("Python wheels not found") + report_missing_blob("Python wheels", f"commit/{commit_short}/wheels/*.whl") # rerun_c if do_rerun_c: @@ -121,8 +125,7 @@ def fetch_binary_assets( print(f"Found Rerun C library: {name}") assets[name] = blob else: - all_found = False - print(f"Failed to fetch blob {blob_url} ({name})") + report_missing_blob(name, blob_url) # rerun_cpp_sdk if do_rerun_cpp_sdk: @@ -142,8 +145,10 @@ def fetch_binary_assets( # -> The name should *not* contain the version number. assets["rerun_cpp_sdk.zip"] = blob else: - all_found = False - print("Rerun cross-platform bundle not found") + report_missing_blob( + f"rerun_cpp_sdk-{tag}-multiplatform.zip", + f"commit/{commit_short}/rerun_cpp_sdk.zip", + ) # rerun-cli if do_rerun_cli: @@ -164,6 +169,10 @@ def fetch_binary_assets( f"rerun-cli-{tag}-aarch64-apple-darwin", f"commit/{commit_short}/rerun-cli/macos-arm64/rerun", ), + ( + f"Rerun-{tag}-aarch64-apple-darwin.app.tar.gz", + f"commit/{commit_short}/rerun-cli/macos-arm64/rerun", + ), ] for name, blob_url in rerun_cli_blobs: blob = bucket.get_blob(blob_url) @@ -171,8 +180,7 @@ def fetch_binary_assets( print(f"Found Rerun CLI binary: {name}") assets[name] = blob else: - all_found = False - print(f"Failed to fetch blob {blob_url} ({name})") + report_missing_blob(name, blob_url) # rerun-js if do_rerun_js: @@ -194,11 +202,18 @@ def fetch_binary_assets( print(f"Found Rerun JS package: {name}") assets[name] = blob else: - all_found = False - print(f"Failed to fetch blob {blob_url} ({name})") + report_missing_blob(name, blob_url) - if not all_found: - raise Exception("Some requested assets were not found") + if missing_assets: + missing_blob_paths = "\n".join( + f" - {asset_name}: gs://{bucket.name}/{blob_url}" for asset_name, blob_url in missing_assets + ) + raise RuntimeError( + f"Failed to fetch {len(missing_assets)} requested release asset(s) from GCS for " + f"tag {tag!r} at commit {commit_short}.\n" + f"Missing blobs:\n{missing_blob_paths}\n" + "Check that the release build workflow completed successfully and uploaded these artifacts." + ) return assets diff --git a/scripts/ci/upload_docs.py b/scripts/ci/upload_docs.py new file mode 100755 index 000000000000..66f536c19b1d --- /dev/null +++ b/scripts/ci/upload_docs.py @@ -0,0 +1,607 @@ +#!/usr/bin/env python3 + +""" +Manage Rerun documentation and examples in GCS for the rerun.io website. + +The website (rerun-io/landing) reads its docs and examples from GCS at +`gs://rerun-docs/prose/`, exposed publicly as `https://ref.rerun.io/prose/`. + +Subcommands: + upload Build and upload a version (files + index.json), optionally + promote to `latest`, and trigger revalidation. + delete Remove a version (files + entry in versions.json) and + trigger revalidation. + +Usable both from CI and locally. Run from anywhere; paths are resolved +relative to the script's monorepo location. + +Install dependencies (already present in this repo's uv workspace): + uv sync + +Examples: + uv run scripts/ci/upload_docs.py upload --version 0.21.0 --mark-latest --purge-token "$ISR_BYPASS_TOKEN" + uv run scripts/ci/upload_docs.py upload --version pr-1234 --skip-purge + uv run scripts/ci/upload_docs.py delete --version test-local --skip-purge +""" + +from __future__ import annotations + +import argparse +import gzip +import io +import json +import mimetypes +import re +import subprocess +import sys +from collections.abc import Iterable +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import requests +import requests.adapters +import yaml +from google.cloud import storage + +SCHEMA_VERSION = 1 + +BUCKET_NAME = "rerun-docs" +GCS_PREFIX = "prose" +DEFAULT_SITE_URL = "https://rerun.io" + +# Files that should be uploaded for each subtree. The website only reads +# these paths; uploading anything else just wastes space. +DOC_CONTENT_GLOBS = ("**/*.md",) +DOC_CONTENT_EXTRA_FILES = ("_redirects.yaml",) +SNIPPET_FILES = ("snippets.toml",) +SNIPPET_SOURCE_EXTS = (".py", ".rs", ".cpp") +EXAMPLE_FILES = ("manifest.toml",) +EXAMPLE_README_GLOB = "*/*/README.md" + +PR_VERSION_RE = re.compile(r"^pr-\d+$") + + +# --------------------------------------------------------------------------- +# index.json construction +# --------------------------------------------------------------------------- + + +@dataclass +class FileEntry: + """A single file to upload, plus its archive-relative path.""" + + archive_path: str # e.g. "docs/content/getting-started/quick-start.md" + source: Path # absolute path on disk + is_doc_markdown: bool # true for docs/content/**.md (parse frontmatter) + + +def collect_files(rerun_root: Path) -> list[FileEntry]: + files: list[FileEntry] = [] + + docs_content = rerun_root / "docs" / "content" + for pattern in DOC_CONTENT_GLOBS: + for md in docs_content.glob(pattern): + if md.is_file(): + rel = md.relative_to(rerun_root).as_posix() + files.append(FileEntry(rel, md, is_doc_markdown=True)) + for name in DOC_CONTENT_EXTRA_FILES: + path = docs_content / name + if path.is_file(): + rel = path.relative_to(rerun_root).as_posix() + files.append(FileEntry(rel, path, is_doc_markdown=False)) + + snippets_dir = rerun_root / "docs" / "snippets" + for name in SNIPPET_FILES: + path = snippets_dir / name + if path.is_file(): + rel = path.relative_to(rerun_root).as_posix() + files.append(FileEntry(rel, path, is_doc_markdown=False)) + snippets_all = snippets_dir / "all" + if snippets_all.is_dir(): + for path in snippets_all.rglob("*"): + if path.is_file() and path.suffix in SNIPPET_SOURCE_EXTS: + rel = path.relative_to(rerun_root).as_posix() + files.append(FileEntry(rel, path, is_doc_markdown=False)) + + examples_dir = rerun_root / "examples" + for name in EXAMPLE_FILES: + path = examples_dir / name + if path.is_file(): + rel = path.relative_to(rerun_root).as_posix() + files.append(FileEntry(rel, path, is_doc_markdown=False)) + for readme in examples_dir.glob(EXAMPLE_README_GLOB): + if readme.is_file(): + rel = readme.relative_to(rerun_root).as_posix() + files.append(FileEntry(rel, readme, is_doc_markdown=False)) + + return files + + +def parse_doc_frontmatter(md_path: Path) -> dict[str, Any]: + """Parse YAML frontmatter from a docs markdown file. + + Returns the metadata dict expected by the website: title, order, + sort_children, hidden, expand, redirect. + """ + text = md_path.read_text(encoding="utf-8") + if not text.startswith("---"): + raise ValueError(f"{md_path}: missing YAML frontmatter") + end = text.find("\n---", 3) + if end == -1: + raise ValueError(f"{md_path}: unterminated YAML frontmatter") + raw = text[3:end].strip() + fm = yaml.safe_load(raw) or {} + if not isinstance(fm, dict): + raise ValueError(f"{md_path}: frontmatter is not a mapping") + + if "title" not in fm: + raise ValueError(f"{md_path}: frontmatter missing required 'title'") + + metadata: dict[str, Any] = { + "title": fm["title"], + "hidden": bool(fm.get("hidden", False)), + "expand": bool(fm.get("expand", False)), + } + if "order" in fm and fm["order"] is not None: + metadata["order"] = fm["order"] + if "sort_children" in fm and fm["sort_children"] is not None: + metadata["sort_children"] = fm["sort_children"] + if "redirect" in fm and fm["redirect"] is not None: + metadata["redirect"] = fm["redirect"] + return metadata + + +def load_redirects(rerun_root: Path) -> dict[str, str]: + path = rerun_root / "docs" / "content" / "_redirects.yaml" + if not path.is_file(): + return {} + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + if not isinstance(data, dict): + raise ValueError(f"{path}: top-level value must be a mapping") + return {str(k): str(v) for k, v in data.items()} + + +def build_index( + *, + version: str, + rerun_commit: str, + files: list[FileEntry], + redirects: dict[str, str], +) -> dict[str, Any]: + entries: dict[str, dict[str, Any]] = {} + dir_children: dict[str, set[str]] = {} + + def ensure_dir(dir_path: str) -> None: + """Make sure `dir_path` (ending in '/') and all its parents exist + in the entries map, and that each parent dir lists the child.""" + if dir_path in entries: + return + # Add this dir. + entries[dir_path] = {"kind": "dir"} + dir_children.setdefault(dir_path, set()) + # Recurse up to root and register this as a child of its parent. + without_trailing = dir_path[:-1] + if "/" in without_trailing: + parent = without_trailing.rsplit("/", 1)[0] + "/" + ensure_dir(parent) + child_basename = without_trailing.rsplit("/", 1)[1] + "/" + dir_children[parent].add(child_basename) + # Top-level dirs have no parent in the index. + + for f in files: + # Register all parent directories. + if "/" in f.archive_path: + parent = f.archive_path.rsplit("/", 1)[0] + "/" + ensure_dir(parent) + dir_children[parent].add(f.archive_path.rsplit("/", 1)[1]) + + entry: dict[str, Any] = {"kind": "file"} + if f.is_doc_markdown: + entry["metadata"] = parse_doc_frontmatter(f.source) + entries[f.archive_path] = entry + + # Flush children lists into the dir entries. + for dir_path, children in dir_children.items(): + entries[dir_path]["children"] = sorted(children) + + return { + "schema_version": SCHEMA_VERSION, + "version": version, + "last_update": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "rerun_commit": rerun_commit, + "entries": entries, + "redirects": redirects, + } + + +# --------------------------------------------------------------------------- +# Upload +# --------------------------------------------------------------------------- + + +def content_type_for(path: str) -> str: + if path.endswith(".md"): + return "text/markdown; charset=utf-8" + if path.endswith(".toml"): + return "application/toml; charset=utf-8" + if path.endswith((".yaml", ".yml")): + return "application/yaml; charset=utf-8" + if path.endswith(".json"): + return "application/json; charset=utf-8" + if path.endswith((".py", ".rs", ".cpp")): + return "text/plain; charset=utf-8" + guessed, _ = mimetypes.guess_type(path) + return guessed or "application/octet-stream" + + +def gzip_bytes(data: bytes) -> bytes: + buf = io.BytesIO() + # mtime=0 so re-uploading the same content produces identical bytes. + with gzip.GzipFile(fileobj=buf, mode="wb", mtime=0) as gz: + gz.write(data) + return buf.getvalue() + + +def upload_blob( + bucket: storage.Bucket, + *, + gcs_path: str, + payload: bytes, + content_type: str, + cache_control: str = "no-cache", +) -> None: + """Upload `payload` (already gzipped) under `gcs_path`.""" + blob = bucket.blob(gcs_path) + blob.content_encoding = "gzip" + blob.content_type = content_type + blob.cache_control = cache_control + blob.upload_from_string(payload, content_type=content_type) + + +def upload_files( + bucket: storage.Bucket, + version: str, + files: list[FileEntry], + *, + dry_run: bool, + concurrency: int = 128, +) -> None: + prefix = f"{GCS_PREFIX}/{version}/" + + def upload_one(f: FileEntry) -> str: + gcs_path = prefix + f.archive_path + if dry_run: + return f"DRY-RUN: {gcs_path}" + payload = gzip_bytes(f.source.read_bytes()) + upload_blob( + bucket, + gcs_path=gcs_path, + payload=payload, + content_type=content_type_for(f.archive_path), + ) + return gcs_path + + with ThreadPoolExecutor(max_workers=concurrency) as pool: + futures = [pool.submit(upload_one, f) for f in files] + for i, fut in enumerate(as_completed(futures), 1): + path = fut.result() + if i % 50 == 0 or i == len(futures): + print(f" [{i}/{len(futures)}] {path}") + + +def upload_index(bucket: storage.Bucket, version: str, index: dict[str, Any], *, dry_run: bool) -> None: + gcs_path = f"{GCS_PREFIX}/{version}/index.json" + if dry_run: + print(f"DRY-RUN: would write {gcs_path} ({len(index['entries'])} entries)") + return + payload = gzip_bytes(json.dumps(index, indent=2).encode("utf-8")) + upload_blob( + bucket, + gcs_path=gcs_path, + payload=payload, + content_type="application/json; charset=utf-8", + ) + print(f"wrote {gcs_path}") + + +def update_versions_manifest( + bucket: storage.Bucket, + *, + version: str, + rerun_commit: str, + mark_latest: bool, + dry_run: bool, +) -> None: + gcs_path = f"{GCS_PREFIX}/versions.json" + blob = bucket.blob(gcs_path) + + if blob.exists(): + # GCS auto-decompresses on download. + raw = blob.download_as_bytes() + manifest = json.loads(raw) + else: + manifest = {"schema_version": SCHEMA_VERSION, "latest": version, "versions": {}} + + manifest.setdefault("schema_version", SCHEMA_VERSION) + manifest.setdefault("versions", {}) + manifest["versions"][version] = {"rerun_commit": rerun_commit} + if mark_latest or "latest" not in manifest: + manifest["latest"] = version + + if dry_run: + print(f"DRY-RUN: would update {gcs_path}: {json.dumps(manifest, indent=2)}") + return + + payload = gzip_bytes(json.dumps(manifest, indent=2).encode("utf-8")) + upload_blob( + bucket, + gcs_path=gcs_path, + payload=payload, + content_type="application/json; charset=utf-8", + ) + print(f"wrote {gcs_path} (latest={manifest['latest']})") + + +# --------------------------------------------------------------------------- +# Revalidation webhook +# --------------------------------------------------------------------------- + + +def trigger_revalidate( + *, + site_url: str, + token: str, + target: dict[str, Any], + dry_run: bool, +) -> None: + endpoint = site_url.rstrip("/") + "/api/revalidate" + + if dry_run: + print(f"DRY-RUN: would POST {endpoint} body={target}") + return + + resp = requests.post( + endpoint, + json=target, + headers={"Authorization": f"Bearer {token}"}, + timeout=30, + ) + resp.raise_for_status() + print(f"revalidation OK: {resp.status_code}") + + +def delete_version_files(bucket: storage.Bucket, version: str, *, dry_run: bool, concurrency: int = 128) -> int: + prefix = f"{GCS_PREFIX}/{version}/" + blobs = list(bucket.list_blobs(prefix=prefix)) + if not blobs: + print(f" no objects under {prefix}") + return 0 + + def delete_one(blob: storage.Blob) -> str: + if dry_run: + return f"DRY-RUN: {blob.name}" + blob.delete() + return str(blob.name) + + with ThreadPoolExecutor(max_workers=concurrency) as pool: + futures = [pool.submit(delete_one, b) for b in blobs] + for i, fut in enumerate(as_completed(futures), 1): + name = fut.result() + if i % 100 == 0 or i == len(futures): + print(f" [{i}/{len(futures)}] {name}") + return len(blobs) + + +def remove_version_from_manifest(bucket: storage.Bucket, *, version: str, dry_run: bool) -> bool: + """Remove `version` from versions.json. Returns True if `latest` was + affected (caller should also purge the unversioned routes).""" + gcs_path = f"{GCS_PREFIX}/versions.json" + blob = bucket.blob(gcs_path) + if not blob.exists(): + print(f" {gcs_path} does not exist; nothing to update") + return False + + manifest = json.loads(blob.download_as_bytes()) + versions = manifest.get("versions", {}) + if version not in versions: + print(f" {version} not in versions.json; nothing to update") + return False + + if manifest.get("latest") == version: + raise SystemExit( + f"refusing to delete {version}: it is currently `latest`. " + f"Promote another version with `upload --mark-latest` first." + ) + + del versions[version] + + if dry_run: + print(f"DRY-RUN: would update {gcs_path}: {json.dumps(manifest, indent=2)}") + return False + + payload = gzip_bytes(json.dumps(manifest, indent=2).encode("utf-8")) + upload_blob( + bucket, + gcs_path=gcs_path, + payload=payload, + content_type="application/json; charset=utf-8", + ) + print(f"wrote {gcs_path} (removed {version})") + return False + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def make_storage_client(pool_size: int) -> storage.Client: + """Storage client whose HTTP session has a connection pool big enough + for `pool_size` concurrent in-flight requests. The default `requests` + pool is 10, so without this the worker threads serialize behind it.""" + client = storage.Client() + adapter = requests.adapters.HTTPAdapter(pool_connections=pool_size, pool_maxsize=pool_size, max_retries=3) + client._http.mount("https://", adapter) + client._http.mount("http://", adapter) + return client + + +def detect_rerun_root(script_path: Path) -> Path: + # scripts/ci/upload_docs.py -> ../../ + return script_path.resolve().parent.parent.parent + + +def detect_rerun_commit(rerun_root: Path) -> str: + try: + sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=rerun_root, text=True).strip() + return sha + except Exception as e: + raise SystemExit(f"failed to detect rerun commit via git: {e}") + + +def add_common_args(p: argparse.ArgumentParser) -> None: + p.add_argument("--version", required=True, help="version label, e.g. 0.21.0 / main / nightly / pr-1234") + p.add_argument("--purge-token", help="bearer token for the website's /api/revalidate endpoint") + p.add_argument("--skip-purge", action="store_true", help="skip the revalidation webhook (local testing)") + p.add_argument("--site-url", default=DEFAULT_SITE_URL, help=f"website base URL (default: {DEFAULT_SITE_URL})") + p.add_argument("--bucket", default=BUCKET_NAME, help=f"GCS bucket (default: {BUCKET_NAME})") + p.add_argument("--concurrency", type=int, default=128, help="parallel GCS operations (default: 128)") + p.add_argument("--dry-run", action="store_true", help="do not upload, delete, or call any webhook") + + +def cmd_upload(args: argparse.Namespace) -> int: + is_pr_preview = bool(PR_VERSION_RE.match(args.version)) + if is_pr_preview and args.mark_latest: + raise SystemExit("--mark-latest is incompatible with a pr-* version") + + rerun_root = Path(args.rerun_root).resolve() if args.rerun_root else detect_rerun_root(Path(__file__)) + if not (rerun_root / "docs" / "content").is_dir(): + raise SystemExit(f"could not locate docs/content under {rerun_root}") + rerun_commit = args.rerun_commit or detect_rerun_commit(rerun_root) + + print(f"rerun root: {rerun_root}") + print(f"version: {args.version}{' (PR preview)' if is_pr_preview else ''}") + print(f"rerun commit: {rerun_commit}") + print(f"bucket: gs://{args.bucket}/{GCS_PREFIX}/{args.version}/") + print(f"site: {args.site_url}") + print(f"dry-run: {args.dry_run}") + print() + + print("scanning files…") + files = collect_files(rerun_root) + print(f" found {len(files)} files") + + print("building index.json…") + redirects = load_redirects(rerun_root) + index = build_index( + version=args.version, + rerun_commit=rerun_commit, + files=files, + redirects=redirects, + ) + print(f" {len(index['entries'])} entries, {len(redirects)} redirects") + + client = make_storage_client(args.concurrency) + bucket = client.bucket(args.bucket) + + print("uploading files…") + upload_files(bucket, args.version, files, dry_run=args.dry_run, concurrency=args.concurrency) + + print("uploading index.json…") + upload_index(bucket, args.version, index, dry_run=args.dry_run) + + if is_pr_preview: + print("PR preview: skipping versions.json update") + else: + print("updating versions.json…") + update_versions_manifest( + bucket, + version=args.version, + rerun_commit=rerun_commit, + mark_latest=args.mark_latest, + dry_run=args.dry_run, + ) + + if args.skip_purge: + print("skipping revalidation webhook (--skip-purge)") + else: + target: dict[str, Any] = ( + {"target": "latest"} if args.mark_latest else {"target": "version", "version": args.version} + ) + print("triggering revalidation…") + trigger_revalidate(site_url=args.site_url, token=args.purge_token, target=target, dry_run=args.dry_run) + + print("done.") + return 0 + + +def cmd_delete(args: argparse.Namespace) -> int: + is_pr_preview = bool(PR_VERSION_RE.match(args.version)) + + print(f"version: {args.version}{' (PR preview)' if is_pr_preview else ''}") + print(f"bucket: gs://{args.bucket}/{GCS_PREFIX}/{args.version}/") + print(f"site: {args.site_url}") + print(f"dry-run: {args.dry_run}") + print() + + client = make_storage_client(args.concurrency) + bucket = client.bucket(args.bucket) + + # Update manifest first so concurrent readers stop discovering the + # version before we start tearing down its files. (PR previews aren't + # in versions.json, so this is a no-op for them.) + if not is_pr_preview: + print("updating versions.json…") + remove_version_from_manifest(bucket, version=args.version, dry_run=args.dry_run) + + print("deleting files…") + deleted = delete_version_files(bucket, args.version, dry_run=args.dry_run, concurrency=args.concurrency) + print(f" deleted {deleted} objects") + + if args.skip_purge: + print("skipping revalidation webhook (--skip-purge)") + else: + print("triggering revalidation…") + trigger_revalidate( + site_url=args.site_url, + token=args.purge_token, + target={"target": "version", "version": args.version}, + dry_run=args.dry_run, + ) + + print("done.") + return 0 + + +def main(argv: Iterable[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Manage Rerun docs/examples in GCS for the rerun.io website.") + sub = p.add_subparsers(dest="command", required=True) + + pu = sub.add_parser("upload", help="build & upload a version") + add_common_args(pu) + pu.add_argument("--mark-latest", action="store_true", help="set this version as `latest` in versions.json") + pu.add_argument("--rerun-commit", help="override the rerun commit SHA (default: git HEAD)") + pu.add_argument( + "--rerun-root", help="path to a rerun checkout to read docs/examples from (default: this monorepo's rerun/)" + ) + + pd = sub.add_parser("delete", help="delete a version") + add_common_args(pd) + + args = p.parse_args(list(argv) if argv is not None else None) + + if not args.skip_purge and not args.purge_token: + raise SystemExit("either --purge-token or --skip-purge is required") + + if args.command == "upload": + return cmd_upload(args) + if args.command == "delete": + return cmd_delete(args) + raise SystemExit(f"unknown command: {args.command}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index 0fc049a5f99a..ab8e400fbe0a 100644 --- a/scripts/clippy_wasm/clippy.toml +++ b/scripts/clippy_wasm/clippy.toml @@ -8,7 +8,7 @@ # ----------------------------------------------------------------------------- # Section identical to the main clippy.toml: -msrv = "1.92" +msrv = "1.95" allow-unwrap-in-tests = true @@ -31,6 +31,7 @@ too-many-lines-threshold = 600 # TODO(emilk): decrease this disallowed-macros = [ 'std::dbg', + { path = "cfg_if::cfg_if", reason = "Use the standard library's `cfg_select!` instead" }, { path = "egui::hex_color", reason = "Do not hard-code colors - declare them design_tokens.rs instead, and define in light/dark_theme.json" }, { path = "std::debug_assert", reason = "Use `re_log::debug_assert` instead" }, diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 31721ec54f1c..959659a2a389 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -27,8 +27,31 @@ REPO = "rerun" INCLUDE_LABELS = False # It adds quite a bit of visual noise -# Cache for organization members to avoid repeated API calls -_org_members_cache: set[str] | None = None +# Cache for contributor classification to avoid repeated API calls +_external_contributor_cache: dict[str, bool] = {} + + +# Coding-agent accounts that GitHub registers as ordinary users, not as a `Bot` and with no +# `[bot]` suffix or `app/` prefix, so neither the account `type` nor the login itself flags them. +# `claude` is Anthropic's account, committing via `noreply@anthropic.com`. `cursoragent` is Cursor's. +_AGENT_USER_NAMES = {"claude", "cursoragent"} + + +def is_bot_account(user_name: str, account_type: str | None = None) -> bool: + """Return whether a GitHub account is a bot rather than a human contributor. + + The account `type` from the GitHub API is the reliable signal, and the only one that catches + bots with a plain login like the `Copilot` agent, so callers pass it whenever they have it. + Without a `type` we judge by the login alone, which still covers GitHub App bots in both + formats we see. The REST API uses a `[bot]` suffix, as in `dependabot[bot]` or `claude[bot]`. + The `gh` CLI uses an `app/` prefix, as in `app/copilot-swe-agent`. + A few coding agents run as ordinary user accounts with no such marker, so we also match + `claude` and `cursoragent` by name. + """ + if account_type == "Bot": + return True + name = user_name.lower() + return name.endswith("[bot]") or name.startswith("app/") or name in _AGENT_USER_NAMES def eprint(*args: Any, **kwargs: Any) -> None: @@ -37,7 +60,7 @@ def eprint(*args: Any, **kwargs: Any) -> None: @dataclass class PrInfo: - gh_user_name: str | None + gh_user_names: list[str] pr_title: str labels: list[str] @@ -50,41 +73,96 @@ class CommitInfo: source_ref_hash: str | None -def get_rerun_org_members() -> set[str]: - """Fetch all members of the rerun-io GitHub organization.""" - global _org_members_cache +def unique_user_names(user_names: list[str]) -> list[str]: + """Deduplicate GitHub usernames while preserving order.""" + seen = set() + unique = [] + for user_name in user_names: + if user_name not in seen and not is_bot_account(user_name): + seen.add(user_name) + unique.append(user_name) + return unique + + +def is_external_contributor(user_name: str) -> bool: + """Return whether a GitHub user looks external to Rerun. + + We intentionally use the repository permissions endpoint instead of the org members endpoint, + since the latter only returns public org memberships. + + Bots are never external contributors. The permissions endpoint reports the account `type`, + so a resolvable bot like `dependabot[bot]` is caught from its type without a name list. + Bots whose login never resolves through the API are caught earlier by `is_bot_account`, + including the Copilot agent's `app/…` login. + """ + if is_bot_account(user_name): + return False - if _org_members_cache is not None: - return _org_members_cache + if user_name in _external_contributor_cache: + return _external_contributor_cache[user_name] try: - # Use gh CLI to fetch organization members - # Note: only PUBLIC members will be fetched! - # You can see which members are public and private at https://github.com/orgs/rerun-io/people - # That's also where members can change themselves from Private to Public. result = subprocess.run( - ["gh", "api", f"/orgs/{OWNER}/members", "--paginate", "--jq", ".[].login"], + [ + "gh", + "api", + f"/repos/{OWNER}/{REPO}/collaborators/{user_name}/permission", + "--jq", + "[.user.type, .permission] | @tsv", + ], capture_output=True, text=True, check=True, ) + account_type, _, permission = result.stdout.strip().partition("\t") + is_external = not is_bot_account(user_name, account_type) and permission not in { + "admin", + "maintain", + "write", + "triage", + } + except subprocess.CalledProcessError as e: + eprint( + f"ERROR fetching repository permission for @{user_name}: {e.stderr.strip()}. Assuming external contributor." + ) + is_external = True - members = set() - for line in result.stdout.strip().split("\n"): - if line.strip(): # Skip empty lines - members.add(line.strip()) + _external_contributor_cache[user_name] = is_external + return is_external - _org_members_cache = members - eprint(f"Fetched {len(members)} members from rerun-io organization") - return members - except subprocess.CalledProcessError as e: - eprint( - f"ERROR fetching org members: {e.stderr.strip()}. You need to install the GitHub CLI tools: https://cli.github.com/ and authenticate with github." +# Slow +def fetch_pr_commit_authors(repo: str, pr_number: int) -> list[str]: + """Return the GitHub logins of every human who authored a commit in a PR. + + GitHub resolves each commit's author email to a login server-side, so this recovers the real + contributors even when their `Co-authored-by` trailers use a private email we cannot map. + We pass each author's account `type` to `is_bot_account` so bot commit authors get dropped too, + such as the `Copilot` agent whose plain login alone would not look like a bot. + """ + try: + result = subprocess.run( + [ + "gh", + "api", + f"/repos/{OWNER}/{repo}/pulls/{pr_number}/commits?per_page=100", + "--jq", + ".[] | select(.author != null) | [.author.login, .author.type] | @tsv", + ], + capture_output=True, + text=True, + check=True, ) - # Return empty set as fallback to avoid breaking the script - _org_members_cache = set() - return _org_members_cache + except subprocess.CalledProcessError as e: + eprint(f"ERROR fetching commits for {repo} PR #{pr_number}: {e.stderr.strip()}") + return [] + + user_names = [] + for line in result.stdout.splitlines(): + login, _, account_type = line.partition("\t") + if not is_bot_account(login, account_type): + user_names.append(login) + return unique_user_names(user_names) # Slow @@ -100,7 +178,7 @@ def fetch_reality_pr_info(commit_hash: str) -> PrInfo | None: "api", f"/repos/{OWNER}/reality/commits/{commit_hash}/pulls", "--jq", - ".[0] | {title: .title, labels: [.labels[].name], author: (.author.login // .user.login)}", + ".[0] | {number: .number, title: .title, labels: [.labels[].name]}", ], capture_output=True, text=True, @@ -113,8 +191,16 @@ def fetch_reality_pr_info(commit_hash: str) -> PrInfo | None: if not pr_data or "title" not in pr_data: return None - labels = pr_data["labels"] - return PrInfo(gh_user_name=None, pr_title=pr_data["title"], labels=labels) + pr_number = pr_data.get("number") + # The PR's commit authors are the human contributors. We take them rather than the PR author + # because synced PRs are opened by the sync bot, with the real people only on the commits. + gh_user_names = fetch_pr_commit_authors("reality", pr_number) if pr_number is not None else [] + + return PrInfo( + gh_user_names=gh_user_names, + pr_title=pr_data["title"], + labels=pr_data["labels"], + ) except subprocess.CalledProcessError as e: # Commit doesn't exist in Reality repo, or API error @@ -131,36 +217,25 @@ def fetch_pr_info_from_commit_info(commit_info: CommitInfo) -> PrInfo | None: Fetch PR info with Reality-first, Rerun-fallback strategy. Priority order: - 1. Try Reality repo using Source-Ref commit hash (if present) - use for title and labels - 2. Always try to get author from the original Rerun PR (if PR number exists) - 3. Fallback to Rerun repo entirely if no Source-Ref + 1. Try the Reality repo using the Source-Ref commit hash, if present. + 2. Fall back to the Rerun repo using the PR number. + + Either way the contributors come from the PR's commit authors, which the API resolves to real + logins and tags with an account `type`, so private emails are recovered and bots are dropped. """ # Priority 1: Try Reality repo if Source-Ref is present if commit_info.source_ref_hash is not None: reality_pr_info = fetch_reality_pr_info(commit_info.source_ref_hash) if reality_pr_info is not None: - # Got Reality PR info, but we want the author from the original Rerun PR - if commit_info.pr_number is not None: - rerun_pr_info = fetch_pr_info(commit_info.pr_number) - if rerun_pr_info is not None: - # Use title and labels from Reality, author from Rerun - return PrInfo( - gh_user_name=rerun_pr_info.gh_user_name, - pr_title=reality_pr_info.pr_title, - labels=reality_pr_info.labels, - ) - # No Rerun PR number, so just use Reality info without author attribution - # Set author to None so it won't be attributed - return PrInfo( - gh_user_name=None, - pr_title=reality_pr_info.pr_title, - labels=reality_pr_info.labels, - ) + return reality_pr_info # If Reality lookup fails, fall through to Rerun repo fallback # Priority 2: Fallback to Rerun repo using PR number if commit_info.pr_number is not None: - return fetch_pr_info(commit_info.pr_number) + pr_info = fetch_pr_info(commit_info.pr_number) + if pr_info is not None: + pr_info.gh_user_names = fetch_pr_commit_authors(REPO, commit_info.pr_number) + return pr_info # No PR info available return None @@ -189,7 +264,7 @@ def fetch_pr_info(pr_number: int) -> PrInfo | None: pr_data = json.loads(result.stdout) labels = [label["name"] for label in pr_data["labels"]] gh_user_name = pr_data["author"]["login"] - return PrInfo(gh_user_name=gh_user_name, pr_title=pr_data["title"], labels=labels) + return PrInfo(gh_user_names=unique_user_names([gh_user_name]), pr_title=pr_data["title"], labels=labels) except subprocess.CalledProcessError as e: eprint( @@ -336,13 +411,14 @@ def main() -> None: # Generate summary - prefer PR number, fallback to commit hash if pr_number is not None: summary = f"{title} [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})" - dup_check = f"[#{pr_number}]" + dup_checks = [f"[#{pr_number}]"] else: # No PR number in title, but we have Reality PR info - use hash of synced commit in Rerun repo summary = f"{title} [{hexsha_short}](https://github.com/{OWNER}/{REPO}/commit/{hexsha_full})" - dup_check = f"[{hexsha_full}]" + # The changelog records the short hash in brackets and the full hash in the URL, so check both. + dup_checks = [f"[{hexsha_short}]", f"[{hexsha_full}]"] - if dup_check in previous_changelog: + if any(dup_check in previous_changelog for dup_check in dup_checks): eprint(f"Ignoring dup: {summary}") continue @@ -352,9 +428,18 @@ def main() -> None: summary += f" ({', '.join(labels)})" if pr_info is not None: - gh_user_name = pr_info.gh_user_name - if gh_user_name is not None and gh_user_name not in get_rerun_org_members(): + external_contributors = [ + gh_user_name for gh_user_name in pr_info.gh_user_names if is_external_contributor(gh_user_name) + ] + if len(external_contributors) == 1: + gh_user_name = external_contributors[0] summary += f" (thanks [@{gh_user_name}](https://github.com/{gh_user_name})!)" + elif 1 < len(external_contributors): + thanks = ", ".join( + f"[@{gh_user_name}](https://github.com/{gh_user_name})" + for gh_user_name in external_contributors + ) + summary += f" (thanks {thanks}!)" if labels == ["⛴ release"]: continue # Ignore release PRs diff --git a/scripts/lint.py b/scripts/lint.py index 24d3b6e5adb6..194ec115c63b 100755 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -46,7 +46,8 @@ wasm_caps = re.compile(r"\bWASM\b") nb_prefix = re.compile(r"nb_") else_return = re.compile(r"else\s*{\s*return;?\s*};") -explicit_quotes = re.compile(r'[^(]\\"\{\w*\}\\"') # looks for: \"{foo}\" +# Looks for: \"{foo}\" (manual quotes), including \"{foo:?}\" (excess quotes). +explicit_quotes = re.compile(r'[^(]\\"\{[^}]*\}\\"') ellipsis = re.compile(r"[^.]\.\.\.([^\-.0-9a-zA-Z]|$)") ellipsis_expression = re.compile(r"[\[\]\(\)<>\{\}]?.*\.\.\..*[\[\]\(\)<>\{\}]") ellipsis_import = re.compile(r"from \.\.\.") @@ -239,6 +240,17 @@ def lint_line( if re.search(r"\b3d\b", line_without_link_targets): return "we prefer '3D' over '3d'" + # Em dash should be spaced (` — `, not `word—word`). See DESIGN.md. + # The UI placeholder literal `"—"` (em dash inside quotes) is naturally exempt + # since the regex requires word/paren/asterisk characters on both sides. + if re.search(r"[\w\)\*]—[\w\(\*]", line): + return "Use a spaced em dash (' — '), not 'word—word'. See DESIGN.md." + + # En dash (`–`) is for numeric ranges only; as a sentence dash, use an em dash (` — `). + # Detection: en dash with spaces, flanked by letters (digit-flanked allows `100 – 200`). + if re.search(r"[A-Za-z\"'\)]\s–\s[A-Za-z]", line): + return "Use an em dash (' — '), not an en dash, as a sentence dash. See DESIGN.md." + if ( "recording=rec" in line and "rr." not in line @@ -304,7 +316,10 @@ def lint_line( return "Don't use nb_things - use num_things or thing_count instead" if explicit_quotes.search(line): - return "Prefer using {:?} - it will also escape newlines etc" + return ( + "Prefer using {:?} over explicit quotes - it will also escape newlines etc. " + "See: https://github.com/rerun-io/rerun/blob/main/CODE_STYLE.md#misc" + ) if m := re.search(r'"([^"]*)"', line): if err := check_string(m.group(1)): @@ -344,12 +359,29 @@ def lint_line( ) if is_in_oss_rerun_repo: - # Check for specific data platform phrases that should be capitalized. + # Deprecated brand names. Replacement is context-dependent: + # - 'Rerun Hub' → commercial managed offering + # - 'catalog server' → generic OSS or managed + # - or rephrase to avoid naming the product + # Matched case-insensitively so that lowercase variants (e.g. 'rerun cloud') are also caught. + deprecated_msg = "is a deprecated name. Use 'Rerun Hub' (commercial), 'catalog server' (generic), or rephrase." + if re.search(r"\bRerun\s+Cloud\b", line, re.IGNORECASE): + return f"'Rerun Cloud' {deprecated_msg}" + if re.search(r"\bRerun\s+Base\b", line, re.IGNORECASE): + return f"'Rerun Base' {deprecated_msg}" + if re.search(r"\bRerun\s+Data\s+Platform\b", line, re.IGNORECASE): + return f"'Rerun Data Platform' {deprecated_msg}" + if re.search(r"\bData\s+Platform\b", line, re.IGNORECASE): + return f"'Data Platform' {deprecated_msg}" # Skip URL paths (`/dataplatform/`) and python package extras specifiers - if re.search(r"(the\s+data\s+platform|Rerun\s+data\s+platform)", line) or re.search( - r"(?` itself implements `Any`, making it easy to accidentally pass the wrong object. Expect purpose defined traits instead.""" + if file_extension == "rs": + if re.search(r"\.zip\(", line): + return ( + "Prefer `std::iter::zip(a, b)` (iterators), `itertools::izip!(a, b, …)` (3+ iterators), " + "or `Option::zip(a, b)` (options) over `a.zip(b)`" + ) + if re.search(r"\.chain\(", line): + return "Prefer `std::iter::chain(a, b)` or `itertools::chain!(a, b, …)` over `a.chain(b)`" + return None @@ -474,13 +515,16 @@ def test_lint_line() -> None: """, "fn ret_any() -> &dyn std::any::Any", "fn ret_any_mut() -> &mut dyn std::any::Any", + # URL paths and python package extras still reference the feature name. "Visit /dataplatform/docs for more info", "The https://example.com/dataplatform/api endpoint", 'dependencies = ["rerun-sdk[dataloader,dataplatform]"]', 'override-dependencies = ["rerun-sdk[dataplatform]"]', 'extras = ["dataplatform,extra"]', - "We need a data platform solution", - "Building data platform infrastructure", + # New approved names. + "Connect to Rerun Hub for hosted catalogs.", + "Spin up a catalog server locally.", + "We use the catalog server in production.", # %err (Display) in tracing macros is good 'tracing::warn!(%err, "something failed");', 're_log::error!(%err, "something failed");', @@ -507,6 +551,25 @@ def test_lint_line() -> None: '#[error("Failed to open {path}: {err}")]', '#[error("Something went wrong: {0}")]', # single unnamed is fine '#[error("Simple error message")]', + # Spaced em dash is the convention. + "Use a spaced em dash (` — `) for parenthetical breaks.", + "foo — bar — baz", + # En dash is fine in numeric/character ranges (no spaces, or digit-flanked). + "Range: 2020–2025", + "pp. 10–15", + "100 KB–10 MB", + "Chunks 100 – 200", # digit on right side — allowed range with spaces + "A–Z and a–z and 0–9", + # Em dash as a UI placeholder literal in a string (not prose). + '"—".to_owned()', + 'return sha[:8] if sha else "—"', + # Mathematical/UI display with en dash, no spaces. + 'ui.button("–∞")', + # Preferred zip/chain alternatives. + "let it = std::iter::zip(a, b);", + "let it = std::iter::chain(a, b);", + "for (x, y, z) in izip!(a, b, c) {", + "for x in itertools::chain!(a, b, c) {", ] should_error = [ @@ -575,10 +638,27 @@ def test_lint_line() -> None: "fn take_any_mut(thing: &mut dyn std::any::Any)", "fn take_any(thing: &dyn Any)", "fn take_any_mut(thing: &mut dyn Any)", + # Deprecated brand names — must use 'Rerun Hub' or 'catalog server' instead. + # Matched case-insensitively, so lowercase variants must also error. "The dataplatform is powerful", "Using dataplatform for analytics", + "Using DATAPLATFORM in caps", "I love the data platform", "The Rerun data platform is great", + "We use the Rerun Data Platform.", + "We use the RERUN DATA PLATFORM.", + "Connect via Rerun Cloud today.", + "Connect via rerun cloud today.", + "Connect via RERUN CLOUD today.", + "The Data Platform stores recordings.", + "The data platform stores recordings.", + "Rerun Base is the new commercial offering.", + "rerun base is the new commercial offering.", + # Wrong 'Rerun Hub' capitalization. + "Connect to Rerun hub today.", + "Use rerun Hub for catalogs.", + "Use rerun hub for catalogs.", + "USE RERUN HUB FOR CATALOGS.", # Inline sensitive data in log messages (bad pattern) - only error/warn are linted 're_log::warn!("Failed to open URL {url}: {err}");', 're_log::error!("Failed to read file at {path}: {err}");', @@ -590,6 +670,18 @@ def test_lint_line() -> None: # thiserror with multiple unnamed fields (bad) '#[error("Failed to do {0}: {1}")]', '#[error("{0} failed with {1} at {2}")]', + # Unspaced em dash (should be spaced). + "the layout—are computed", + "components—the viewer no longer", + "*data blueprints*—the entity", + "(SN)—and the storage node", + # En dash used as a sentence dash (should be em dash). + "Foo – the description", + "[Python](./install-rerun/python.md) – the Python SDK", + "done – next step", + # Method `.zip(` / `.chain(` — prefer `std::iter::*` or `itertools::izip!/chain!`. + "let it = a.iter().zip(b.iter());", + "let it = a.iter().chain(b.iter());", ] for test in should_pass: @@ -1172,6 +1264,7 @@ def test_lint_pyclass_requirements() -> None: "UIs", "UX", "Wasm", + "Windows", # "Arrow", # Would be nice to capitalize in the right context, but it's a too common word. # "Windows", # Consider "multiple plot windows" ] @@ -1181,9 +1274,8 @@ def test_lint_pyclass_requirements() -> None: # Referring to the Rerun Viewer as just "the Viewer" is fine, but not all mentions of "viewer" are capitalized. "Arrow", # Referring to the Apache Arrow project as just "Arrow" is fine, but not all mentions of "arrow" are capitalized. - "Data", - "Platform", - # In the context of "Data Platform" we want capitalization, but not for all mentions + "Hub", + # Referring to Rerun Hub as just "Hub" is fine, but "hub" as a common noun isn't capitalized. ] force_capitalized_as_lower = [word.lower() for word in force_capitalized] @@ -1303,16 +1395,6 @@ def is_acronym_or_pascal_case(s: str) -> bool: return " ".join(new_words) -def fix_dataplatform(s: str) -> str: - """Fix specific data platform phrases to proper capitalization unless it's part of a URL path or package extras.""" - # Skip URL paths (`/dataplatform/`) and package extras specifiers - # (`rerun-sdk[dataloader,dataplatform]`) — those reference the feature name, not prose. - s = re.sub(r"the\s+data\s+platform", "the Data Platform", s) - s = re.sub(r"Rerun\s+data\s+platform", "Rerun Data Platform", s) - s = re.sub(r"(? str: new_words: list[str] = [] inline_code_block = False @@ -1394,14 +1476,6 @@ def lint_markdown(filepath: str, source: SourceFile) -> tuple[list[str], list[st errors.append(f"{line_nr}: Certain words should be capitalized. This should be '{new_line}'.") line = new_line - # Fix dataplatform to Data Platform - new_line = fix_dataplatform(line) - if new_line != line: - errors.append( - f"{line_nr}: Use 'Data Platform' instead of 'dataplatform'. This should be '{new_line}'." - ) - line = new_line - if in_example_readme and not in_metadata: # Check that

is not used in example READMEs if line.startswith("#") and not line.startswith("##"): @@ -1593,6 +1667,14 @@ def lint_file(filepath: str, args: Any) -> int: print(source.error("Prefer using tonic::Result<>", line_nr=line_nr)) num_errors += 1 + if filepath.endswith(".proto"): + for line_nr, line in enumerate(source.lines): + if source.should_ignore(line_nr): + continue + if "/// " in line: + print(source.error("Use `//` not `///` for comments in .proto files", line_nr=line_nr)) + num_errors += 1 + if filepath.endswith((".rs", ".fbs")): errors, lines_out = lint_vertical_spacing(source.lines) for error in errors: @@ -1738,10 +1820,13 @@ def main() -> None: "html", "js", "md", + "mjs", + "proto", "py", "rs", "sh", "toml", + "ts", "txt", "wgsl", "yaml", @@ -1772,6 +1857,8 @@ def rerun(path: str) -> str: return f"{rerun_prefix}{path}" exclude_paths = ( + "./dataplatform/crates/redap_protos/Cargo.toml", # intentional [lints.clippy] override (see file header) + "./dataplatform/crates/redap_protos/src/v1alpha1", # auto-generated rerun(".github/workflows/reusable_checks.yml"), # zombie TODO hunting job rerun(".nox"), rerun(".pytest_cache"), @@ -1780,6 +1867,7 @@ def rerun(path: str) -> str: rerun("crates/store/re_protos/proto/schema_snapshot.yaml"), # auto-generated rerun("crates/store/re_protos/src/v0"), # auto-generated rerun("crates/store/re_protos/src/v1alpha1"), # auto-generated + rerun("crates/viewer/re_ui/data/Inter-README.txt"), # third-party font readme (Inter) rerun("crates/viewer/re_web_viewer_server/web_viewer/re_viewer.js"), # auto-generated by wasm_bindgen rerun("docs/content/concepts/app-model.md"), # this really needs custom letter casing rerun("docs/content/reference/cli.md"), # auto-generated @@ -1831,11 +1919,15 @@ def rerun(path: str) -> str: filepath = "./" + filepath filepath = filepath.replace("\\", "/") - # Only lint files inside the rerun directory. - # In the standalone rerun repo rerun_prefix is "./" so everything matches. - # In the monorepo (reality) rerun_prefix is "./rerun/" which keeps us - # from accidentally linting dataplatform/ or other top-level directories. - if not filepath.startswith(rerun_prefix): + # Only lint files inside the rerun or dataplatform directories. + # In the standalone rerun repo `rerun_prefix` is "./" so everything matches. + # In the monorepo (reality) we explicitly include both top-level Rust + # workspaces (`./rerun/` and `./dataplatform/`) so they share the same + # custom lints, and skip everything else (`node_modules/`, `landing/`, …). + allowed_prefixes: tuple[str, ...] = (rerun_prefix,) + if rerun_prefix != "./": + allowed_prefixes = allowed_prefixes + ("./dataplatform/",) + if not filepath.startswith(allowed_prefixes): continue extension = filepath.split(".")[-1] diff --git a/scripts/pre-push.sh b/scripts/pre-push.sh index 1388a6029008..a08fc73f0853 100755 --- a/scripts/pre-push.sh +++ b/scripts/pre-push.sh @@ -9,7 +9,7 @@ if ! command -v "pixi" > /dev/null 2>&1; then exit 1 fi -while read local_ref local_sha remote_ref remote_sha; do +while read -r local_ref _local_sha _remote_ref _remote_sha; do # Extract the branch name from the local reference branch_name=$(echo "$local_ref" | sed 's/^refs\/heads\///') @@ -18,7 +18,7 @@ while read local_ref local_sha remote_ref remote_sha; do # Check if the pushed branch matches the active branch if [ "$branch_name" = "$active_branch" ]; then - exec pixi run fast-lint $@ + exec pixi run fast-lint "$@" else echo "Skipping fast-lint because the pushed branch ($branch_name) does not match the active branch ($active_branch)." fi diff --git a/scripts/render_d2.py b/scripts/render_d2.py new file mode 100755 index 000000000000..300ef01be86f --- /dev/null +++ b/scripts/render_d2.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +""" +Render a D2 diagram in both light and dark themes, upload the two SVGs to +static.rerun.io, and print a `
` HTML block that can +be pasted directly into the docs markdown. + +The rendered SVGs match the look the rerun.io docs site produces for D2 +diagrams: monospaced uppercase labels, muted strokes, transparent node +fills, emerald accent for `` runs inside labels. + +Requires: + - the `d2` CLI on PATH (https://d2lang.com). + - `scripts/upload_image.py` (same directory) and its dependencies; see + that file's docstring for GCS authentication setup. + +Usage: + python3 scripts/render_d2.py path/to/diagram.d2 + cat diagram.d2 | python3 scripts/render_d2.py + +or via pixi: + pixi run python scripts/render_d2.py path/to/diagram.d2 + +The final HTML block is printed to stdout (and stderr when interactive), +mirroring how `upload_image.py` reports its results. + +Per theme, this is roughly equivalent to: + cat diagram.d2 | d2 --pad=0 --scale=0.8 --stdout-format=svg - - +followed by SVG post-processing: + - Strip D2's embedded " + + body = re.sub(r"", _wrap, body, count=1, flags=re.DOTALL) + return '\n' + body + + +def main() -> None: + ap = argparse.ArgumentParser( + description="Render a D2 diagram in light+dark, upload both SVGs to " + "static.rerun.io, print an HTML
block ready to paste " + "into docs markdown.", + ) + ap.add_argument( + "source", + nargs="?", + type=Path, + help="Path to a .d2 source file. If omitted, source is read from stdin.", + ) + args = ap.parse_args() + + import logging + import sys + import tempfile + + logging.basicConfig(level=logging.INFO) + + if args.source is None: + if sys.stdin.isatty(): + ap.error("no source file given and stdin is a tty") + source_text = sys.stdin.read() + else: + source_text = args.source.read_text() + + # Imported lazily so the render API can be used as a library without + # pulling in upload_image's heavier dependency tree (PIL, gcloud, …). + from upload_image import Uploader + + uploader = Uploader() + + urls: dict[str, str] = {} + with tempfile.TemporaryDirectory() as td: + for theme in ("light", "dark"): + logging.info(f"rendering {theme} theme") + svg_path = Path(td) / f"d2-{theme}.svg" + svg_path.write_text(render(source_text, theme)) + object_name = uploader.upload_file(svg_path) + urls[theme] = f"https://static.rerun.io/{object_name}" + + html = ( + '
\n' + f' \n' + f' \n' + "
" + ) + print(f"\n{html}", file=sys.stderr) + if not sys.stdout.isatty(): + # Allow piping into pbcopy/xclip without the stderr banner. + print(html) + + +if __name__ == "__main__": + main() diff --git a/scripts/roundtrip_utils.py b/scripts/roundtrip_utils.py index 5b394ad95bfa..69d25fc6c06b 100755 --- a/scripts/roundtrip_utils.py +++ b/scripts/roundtrip_utils.py @@ -77,10 +77,17 @@ def roundtrip_env(*, save_path: str | None = None) -> dict[str, str]: return env -def run_comparison(rrd0_path: Path | str, rrd1_path: Path | str, full_dump: bool) -> None: +def run_comparison( + rrd0_path: Path | str, + rrd1_path: Path | str, + full_dump: bool, + ignore_timelines: list[str] | None = None, +) -> None: cmd = ["rerun", "rrd", "compare", "--unordered", "--ignore-chunks-without-components"] if full_dump: cmd += ["--full-dump"] + for timeline in ignore_timelines or []: + cmd += ["--ignore-timeline", timeline] cmd += [str(rrd0_path), str(rrd1_path)] run(cmd, env=roundtrip_env(), timeout=60) diff --git a/scripts/rs_coverage.sh b/scripts/rs_coverage.sh new file mode 100755 index 000000000000..f2e7ba46d48a --- /dev/null +++ b/scripts/rs_coverage.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Rust test coverage via cargo-llvm-cov + nextest. +# +# Run through pixi so the coverage tools are on PATH (auto-selects the `coverage` environment): +# pixi run rs-coverage # whole workspace +# pixi run rs-coverage re_dataframe # scoped to one crate +# +# Runs the test suite once under source-based coverage instrumentation, then emits every +# report format from that single run so the suite isn't executed more than once: +# * lcov.info — line coverage consumed by the "Coverage Gutters" +# VS Code extension (recommended in +# `.vscode/extensions.json`; it finds `lcov.info` +# out of the box — open a file and run +# "Coverage Gutters: Watch" to see inline gutters). +# * target/llvm-cov/html/index.html — browsable HTML report (path printed at the end). +# * a per-file coverage summary table printed to the terminal. +# +# `cargo-llvm-cov` needs the `llvm-tools-preview` rustup component matching the pinned +# toolchain, which we add on the fly (idempotent, same idea as `rerun-build-web` adding +# the wasm target). +# +# `--ignore-run-fail` means a failing test still produces a coverage report (test failures +# are printed but don't abort the run) — useful because a whole-workspace run includes GPU +# image-snapshot tests (e.g. re_integration_test) that are environment-sensitive locally. +# For fast, clean local coverage, scope to a crate: `pixi run rs-coverage re_dataframe`. + +set -eu + +sel=("${1:---workspace}") +case "${sel[0]}" in + --*) ;; + *) sel=(-p "${sel[0]}") ;; +esac + +# Install the component `cargo-llvm-cov` needs to instrument builds (see comment above). +rustup component add llvm-tools-preview + +# Run the tests once under coverage instrumentation, without generating a report yet. +cargo llvm-cov nextest --ignore-run-fail --all-features --no-report "${sel[@]}" +# Emit the report formats below from that single run's coverage data. +cargo llvm-cov report --lcov --output-path lcov.info # for Coverage Gutters +cargo llvm-cov report --html # browsable HTML report +cargo llvm-cov report # per-file summary table to stdout +echo "HTML report: $PWD/target/llvm-cov/html/index.html" diff --git a/scripts/update_snapshots_from_ci.sh b/scripts/update_snapshots_from_ci.sh index 54299a26b160..06729d0490aa 100755 --- a/scripts/update_snapshots_from_ci.sh +++ b/scripts/update_snapshots_from_ci.sh @@ -16,14 +16,14 @@ else fi # remove any existing .new.png that might have been left behind -find . -type d -path "*/tests/snapshots*" | while read dir; do - find "$dir" -type f -name "*.new.png" | while read file; do +find . -type d -path "*/tests/snapshots*" | while read -r dir; do + find "$dir" -type f -name "*.new.png" | while read -r file; do rm "$file" done done -gh run download "$RUN_ID" --name "test-results-linux" --dir tmp_artefacts +gh run download "$RUN_ID" --name "test-results-rust-checks-tests-linux" --dir tmp_artefacts # move the snapshots to the correct location, overwriting the existing ones rsync -a tmp_artefacts/ . diff --git a/skills/rerun-blueprint/SKILL.md b/skills/rerun-blueprint/SKILL.md new file mode 100644 index 000000000000..4192bbd7f004 --- /dev/null +++ b/skills/rerun-blueprint/SKILL.md @@ -0,0 +1,145 @@ +--- +name: rerun-blueprint +description: Design a Rerun blueprint from the data, then iterate on it from headless screenshots. Read this when laying out a recording or dataset in the viewer, designing a default blueprint, or deciding which views show which entities. Covers archetype-to-view mapping, layout reasoning, the rrb construction API, the contents grammar, and the screenshot loop. +user_invocable: true +allowed-tools: Read, Grep, Bash, WebFetch +--- + +# Rerun blueprint + +A blueprint decides how a recording is shown; the recording decides what exists. +Read the data, design a first layout, then **iterate from screenshots** until it +reads at a glance. The constructors are discoverable (`dir(rrb)`, +`help(rrb.Spatial3DView)`); this skill covers what you can't guess. Import as +`import rerun.blueprint as rrb`. + +## 1. Read the data + +Enumerate every `(entity_path, archetype)` pair first — the archetype picks the +view, the entity path scopes it. For a dataset (see `rerun-catalog-queries`): + +```python +for c in dataset.schema().component_columns(): + print(c.entity_path, c.archetype, c.component_name) +``` + +For a local recording, stream it with `RrdReader` and read `entity_path` and +the archetype off each chunk (see `rerun-chunk-processing`). + +## 2. Map archetype to view + +| Archetype family | View | +| ------------------------------------------------------------------------------------- | --------------------------------------------------- | +| `Points2D`, `Image`, `EncodedImage`, `Boxes2D`, `LineStrips2D`, `Pinhole` projections | `Spatial2DView` | +| `Points3D`, `Mesh3D`, `Boxes3D`, `LineStrips3D`, `Transform3D`, `Asset3D` | `Spatial3DView` | +| `Scalars`, `SeriesLines`, `SeriesPoints` | `TimeSeriesView` | +| `TextLog` / `TextDocument` / `BarChart` | `TextLogView` / `TextDocumentView` / `BarChartView` | +| `Tensor`, `DepthImage` (heatmap) | `TensorView` | +| `GeoPoints`, `GeoLineStrings` | `MapView` | +| `GraphNodes`, `GraphEdges` | `GraphView` | +| tabular / catalog data | `DataframeView` | + +## 3. Group entities into views + +This step is just deciding which entities share a view. Sizing and placement are +mechanics you tune by eye in the screenshot loop, not rules to settle up front. + +- **Group by shared path prefix.** Entities under a common prefix usually belong + in one view. Don't give every entity its own pane: dozens of raw message + entities go in one `DataframeView`, or get left out, rather than a pane each. +- **One spatial frame per spatial view.** A whole `Transform3D` tree goes in one + `Spatial3DView` at the common ancestor; a `Pinhole` camera gets its own + `Spatial2DView` rooted at the camera so images inherit the projection. +- **Collapse scalars.** Many `Scalars` under a prefix → one `TimeSeriesView` over + the prefix, not one each. Split only when value ranges clash. +- **`column_shares`/`row_shares` are relative weights** on a container's + children, equal by default. Tune them from the screenshots. + +## 4. Construct it + +Containers (`Grid`, `Horizontal`, `Vertical`, `Tabs`) hold views. Default a flat +set to `Grid`; use the others for a deliberate split, `Tabs` for alternatives +competing for one slot (left/right/depth cameras). **Always set an explicit +`origin` and `name`** — `origin` defaults to `/`, which dumps the whole tree into +one view (the usual cause of an unreadable blob). + +```python +blueprint = rrb.Blueprint( + rrb.Horizontal( + rrb.Spatial3DView(origin="/world", name="Scene"), + rrb.Vertical( + rrb.Spatial2DView(origin="/world/camera", name="Camera"), + rrb.TimeSeriesView(origin="/sensors", name="Sensors"), + ), + column_shares=[3, 2], + ), + rrb.TextLogView(origin="/logs", name="Logs"), + collapse_panels=True, +) +``` + +`contents` defaults to `"$origin/**"`. Scope a view with include/exclude rules, +e.g. `contents=["+ $origin/**", "- $origin/internal/**"]`. A bare line is an +include; `/**` is the only wildcard (matches a subtree). Most-specific rule wins, +ties go to the last, unmatched paths are excluded. + +**Coordinate frames.** A spatial view only renders entities it can place relative +to its target frame. `Transform3D`/`Pinhole` logged on entities compose down the +tree, so `origin` is enough. But **named frames** (`CoordinateFrame(frame=...)`, +common in ROS/MCAP) live in a separate frame graph — point the view at a frame +the data occupies via `spatial_information=rrb.SpatialInformation(target_frame="")`. +The tell is an empty 3D view with "No transform path from ``…" errors: +`origin="/"` targets the root `tf#/`, which connects to nothing if the tf tree +was never materialized. Read the `CoordinateFrame:frame` values, target the one +the main 3D content sits in, and exclude entities in unconnected frames. + +## 5. Iterate from screenshots + +Spawn the viewer and load the recording once, then re-send blueprints into the +same viewer; each send + screenshot is one cheap iteration. Render, look, revise. + +The blueprint binds to the data by store identity, **both application id and +recording id**. A loaded `.rrd` keeps its own identity, so build the stream from +the rrd's values (set both; `recording_id` otherwise defaults to a random one). +Mismatch either and the viewer keeps blueprint and data as separate recordings +and never applies the blueprint. + +```python +import time +import rerun as rr +import rerun.blueprint as rrb +from rerun.experimental import ViewerClient, RrdReader + +store = RrdReader("segment.rrd").recordings()[0] # the rrd's application id + recording id + +with ViewerClient.spawn(headless=True, port=9879) as viewer: + rec = rr.RecordingStream(store.application_id, recording_id=store.recording_id) + rec.connect_grpc(url=viewer.url) + rec.log_file_from_path("segment.rrd") + + def shot(blueprint, path): + rec.send_blueprint(blueprint, make_active=True, make_default=True) + time.sleep(2) # let the import finish and a frame render; bump if a view is blank + viewer.save_screenshot(path) # view_id=view.id for a single view + + shot(blueprint_v1, "bp_v1.png") + # Read bp_v1.png, revise, send the next one into the same viewer. + shot(blueprint_v2, "bp_v2.png") +``` + +To bake a finished blueprint in instead, pass `default_blueprint=` to `rr.init` / +`spawn` / `connect_grpc` / `save`, or `blueprint=` to `notebook_show`. + +## Gotchas + +- A `DataframeView` shows "Unknown timeline" without a query: + `query=rrb.archetypes.DataframeQuery(timeline="", apply_latest_at=True)`. +- A view back blank? The importer may not have finished (bump the settle) or the + cursor sits before the data (add `rrb.TimePanel(play_state=rrb.components.PlayState.Following)`). +- `rrb` views and `ViewerClient` (`rerun.experimental`) are unstable. Check + `help()` if a constructor argument is rejected. + +## See also + +- `rerun-data-model` — entities, archetypes, timelines. +- `rerun-catalog-queries` — enumerate entities in a dataset. diff --git a/skills/rerun-catalog-queries/SKILL.md b/skills/rerun-catalog-queries/SKILL.md new file mode 100644 index 000000000000..16b228994fb2 --- /dev/null +++ b/skills/rerun-catalog-queries/SKILL.md @@ -0,0 +1,306 @@ +--- +name: rerun-catalog-queries +description: Performance patterns and gotchas for querying a Rerun catalog from Python. Reach for this when a CatalogClient/dataset query is unexpectedly slow, or when shaping a per-segment / per-episode pipeline that hits the catalog from many places. +user_invocable: true +allowed-tools: Read, Grep, Bash +--- + +# Rerun catalog queries + +Practical performance patterns for querying a Rerun catalog +from Python (`rerun.catalog.CatalogClient` → +`dataset.reader(...)` → DataFusion `DataFrame`). The DataFusion side of +the stack is covered by the **`datafusion-python`** skill — load that +for `DataFrame` / `SessionContext` / expression-API references. This +skill focuses on catalog-specific behaviors and the round-trip costs +that catch teams off guard. + +--- + +## The query cost model in one sentence + +Every materialization of a `dataset.reader(...)` DataFrame is **one +cloud round-trip**, with most of the cost being the network/decode +pair, not the compute. Plan for round-trip count and payload bytes, in +that order. + +A typical catalog round-trip is **a few seconds** even for a tiny +result. So: +- 30 segments × 1 query each ≈ 90s. (Naive per-segment loop.) +- 30 segments × 4 queries each ≈ 6 minutes. (A splitter that runs + `count` + `collect_column` for both starts and stops.) +- 1 query covering all 30 segments ≈ 3s. + +The same fan-out happens along the **entity** axis: 10 entities × +1 `filter_contents([one_entity]).reader()` each ≈ 30s, vs one +`filter_contents([all_entities]).reader()` ≈ 3s. Push as much as you +can into one round-trip — across segments and across entities. + +--- + +## Always apply `filter_contents` and time-window filters before `.reader(...)` + +The single biggest lever: + +- `filter_contents([entity_globs])` restricts which entity-path columns + the reader produces. Without it, every entity in the dataset is + read. +- For Scalars-typed columns this also reduces array nesting depth from + `list>` to `list`. +- Time-window filters (`df.filter(col(index).cast(int64) >= start) + .filter(col(index).cast(int64) <= end)`) push down to storage and + dramatically reduce bytes scanned. The order matters: filter then + reader-bound projection, never the other way around. + +Combined: `dataset.filter_segments(seg).filter_contents(entities).reader(...) +.filter(in_window).select(...)`. + +--- + +## `df.cache()` is your friend for repeated probes + +When the same materialization gets used by multiple downstream +filter/count/collect calls, materialize once with `DataFrame.cache()` +and operate on the cached frame: + +```python +cached = ( + dataset + .filter_segments(seg) + .filter_contents([entity]) + .reader(index=index_col) + .select(col(index_col).cast(pa.int64()).alias(index_col), value.alias("v")) + .cache() # one network round-trip, materializes into in-memory batches +) +starts = cached.filter(col("v") == start_val).collect_column(index_col) +stops = cached.filter(stop_pred(col("v"))).collect_column(index_col) +``` + +Without `cache()`, each `count()` / `collect_column()` re-executes the +whole reader chain. + +**When NOT to cache.** `cache()` forces materialization into Arrow +batches, breaking laziness. If downstream code keeps composing more +DataFusion ops on top (joins, windows, further filters) and only +materializes once at the end, caching mid-pipeline turns one execution +into two and pre-empts whatever physical-plan optimizations the engine +could have done across the boundary. Reach for `cache()` when the +consumers are terminal (`count()`, `collect_column()`, `to_arrow_table()`), +not when they're another lazy `DataFrame`. + +--- + +## Cross-segment batching: drop `filter_segments`, group by `rerun_segment_id` + +For pipelines that need the same query on many segments, omit +`filter_segments(...)` entirely and pull a single cross-segment table. +Every reader row carries a `rerun_segment_id` column — group locally: + +```python +df = dataset.filter_contents(entities).reader(index=index_col) +cached = df.select( + "rerun_segment_id", + col(index_col).cast(pa.int64()).alias(index_col), + value.alias("v"), +).cache() + +# Now N filter/aggregate calls are local, not network. +starts = cached.filter(col("v") == start_val).select("rerun_segment_id", index_col).to_arrow_table() +``` + +Trigger / event columns are tiny enough that pulling all segments at +once dominates per-segment looping by an order of magnitude. + +--- + +## Per-entity fan-out within a segment + +Symmetric to cross-segment batching, along the entity axis. If you +need data from N entities of a single segment, **don't** loop: + +```python +# Anti-pattern: N reader setups, N round-trips. +for entity in entities: + df = dataset.filter_segments(seg).filter_contents([entity]).reader(index=ix) + ... +``` + +Instead pull all N at once and project per entity locally. Per "Reader +row layout" below, every row carries data for one entity and NULLs +for the others, so `col("::").is_not_null()` +is the per-entity filter: + +```python +shared = ( + dataset + .filter_segments(seg) + .filter_contents(sorted(set(entities))) + .reader(index=ix) + .filter(col(ix).cast(pa.int64()).between(start_ns, end_ns)) +) +# Each downstream consumer narrows to its entity's rows lazily. +src_a = shared.filter(col(f"{ent_a}:{comp_a}").is_not_null()).select(ix, f"{ent_a}:{comp_a}") +src_b = shared.filter(col(f"{ent_b}:{comp_b}").is_not_null()).select(ix, f"{ent_b}:{comp_b}") +``` + +DataFusion can share the underlying scan across the per-entity +projections when it builds the physical plan, so this stays a single +catalog round-trip even though there are N logical consumers. Works +inside generators that build per-source DataFusion plans (resampling, +bracket lookup, nearest-in-time joins) — collapse the network fan-out +without changing the per-source logic. + +A trap when refactoring: if a downstream query uses a reader column's +fully-qualified name (`col(f"{entity}:{archetype}:{component}")`), +you don't need to alias the column in `shared`. The shared reader's +output schema preserves native column names, so existing per-entity +projection helpers keep working unchanged. + +--- + +## `count()` is *not* free + +Counter-intuitive: `df.count()` and `df.aggregate([], [F.count(col)])` +do not always push down. Aggregate plans can force the engine to +materialize the underlying column data server-side, then count on the +client. `F.count(col)` over wide entity-columns can ship full struct or +blob payloads to count nullity. + +Alternatives, in order of preference for "is anything here": + +| Need | Use | +|---|---| +| "any row in this filter?" | `df.select(col(index)).limit(1).to_arrow_table().num_rows > 0` — server short-circuits on first match | +| "count rows in a tiny window" | `df.filter(window).select(col(index)).count()` after the time filter | +| "count *each* entity in a wide query" | per-entity `limit(1)` probes, threaded — *not* one big `count(col)` aggregate | + +A trap that fooled us: assuming `bool_or(col.is_not_null())` would only +need nullity buffers. It does not — the operator still touches payload +data on most plans. + +--- + +## `using_index_values` + `fill_latest_at` is great for resampling, not for presence + +```python +.reader(index=index_col, using_index_values=targets, fill_latest_at=True) +``` + +- Returns one row per target timestamp, each entity column carrying its + latest non-null value at-or-before the target. +- Excellent for nearest-prior resampling (no DataFusion required). +- **Don't** use it as a presence check. Two reasons: + 1. Semantics are "ever emitted before T", not "emitted in [start, T]". + 2. The server still ships the full struct/blob payload for every + entity to compute the latest-known value; a downstream + `is_not_null()` projection runs post-transfer and doesn't reduce + wire bytes. + +For a strict in-window presence check, prefer per-entity `limit(1)` +over the time-filtered reader, run concurrently. + +--- + +## Schema introspection is cheap; use it before probing + +```python +schema = dataset.filter_segments(seg).schema() +available = {(c.entity_path, c.component) for c in schema.component_columns()} +``` + +This is a single round-trip and tells you which `(entity, component)` +pairs the segment registered. If a column isn't in the schema, you can +drop it from your manifest without any further cloud queries. This is +often a complete substitute for "does this entity have events" +probing. + +Caveat: schema presence ≠ events. The MCAP records the topic schema +even for unused topics. If your pipeline cares about distinguishing +"registered but never emitted" vs "registered with events", you have +to probe — see "is anything here" patterns above. + +--- + +## Reader row layout: entities are columns, not rows + +Every row from `dataset..reader(...)` corresponds to a single +event on a single entity. Other entities' columns are null on that +row. Implications: + +- `select("rerun_segment_id", "::")` + works — quote the entity column when using SQL. +- There is **no** `rerun_entity_path` row attribute. To attribute rows + to entities you either filter to one entity at a time, or pick a + per-entity column (e.g. `:McapChannel:id`) whose non-null pattern + identifies the source. +- `df.count()` returns *total events across all entities*, not + per-entity counts. + +--- + +## Field access on a null struct returns `0.0`, not null + +A DataFusion gotcha that bites pipelines reading struct messages: + +```python +col("/some/entity:msg.MyType:message")[0]["sub"]["x"] +# When the parent struct is null on a row, this evaluates to 0.0 +# (and "" for strings), not null. +``` + +Wrap struct-walk projections with a null guard: + +```python +parent = col("/some/entity:msg.MyType:message") +leaf = parent[0]["sub"]["x"] +guard = parent.is_null() | parent[0].is_null() | parent[0]["sub"].is_null() +expr = F.when(guard, lit(None)).otherwise(leaf) +``` + +**Only apply this to struct sources.** For scalar / blob columns +(`Scalars:scalars`, `EncodedImage:blob`, etc.) the wrap is a no-op at +best and at worst rewrites the plan in ways that change downstream +join behavior. Gate the guard on whether the source actually walks +through a struct boundary. + +--- + +## Common debug recipe + +When a query stage is slower than expected: + +1. **Count round-trips.** Wrap each `to_arrow_table()` / + `collect_column()` / `count()` with `time.perf_counter()`. Each is + a round-trip. If you see N segment queries, that's N × few-seconds + minimum. +2. **Split build vs materialize timing.** Time the lazy DataFrame + construction *separately* from the terminal `to_arrow_table()` call. + If "build" takes seconds, something inside is materializing eagerly + (an Arrow round-trip in a join helper, a `cache()` in a generator, + a `.collect()` hidden in a chained-join utility). A correctly lazy + plan should build in ~milliseconds regardless of result size. +3. **Measure bytes.** `tbl.nbytes` after `to_arrow_table()` reveals + when "I projected `is_not_null()`" actually shipped megabytes. If + bytes are large despite a small projection, the operator didn't + push down. +4. **Cross-segment first, then cross-entity.** If the per-segment query + is fundamentally the same (just scoped by id), drop `filter_segments` + and group by `rerun_segment_id` locally. If you also have a + per-entity loop within a segment, collapse it the same way (one + `filter_contents([all])` reader, per-entity `is_not_null()` filters + downstream). +5. **Cache before terminal re-use.** If two `count()` / `collect_column()` + calls share the same reader, `df.cache()` between them. Don't cache + if the consumers are themselves lazy DataFrames being composed + further — caching breaks plan-wide optimization. +6. **Window first.** Always push the time filter before any projection + or aggregate that touches payload columns. + +--- + +## See also + +- `datafusion-python` skill — DataFrame API, SQL parity, expression + building, common pitfalls (boolean operators, immutability, etc.). + Not installed? Ask the user to install it globally: + `npx skills add apache/datafusion-python`. diff --git a/skills/rerun-chunk-processing/SKILL.md b/skills/rerun-chunk-processing/SKILL.md new file mode 100644 index 000000000000..bcabc893af0a --- /dev/null +++ b/skills/rerun-chunk-processing/SKILL.md @@ -0,0 +1,273 @@ +--- +name: rerun-chunk-processing +description: "Core mechanics of the Rerun Chunk Processing API (rerun.experimental) — LazyChunkStream pipelines, Chunk, lenses (MutateLens/DeriveLens/Selector), RrdReader, writing optimized RRDs. Read BEFORE writing any ingestion/conversion/preprocessing code (convert an MCAP, build a recording from a dataset, preprocess an .rrd, port an old converter): it mandates reader+lens pipelines and steers away from hand-built chunks — no Chunk.from_columns for data a reader/lens can produce, no per-message rr.log, no manual pa.array assembly. Source-specific knowledge lives in the importer skills (rerun-mcap, rerun-urdf, rerun-parquet, rerun-lerobot); read rerun-data-model first to decide what the data should become." +user_invocable: true +allowed-tools: Read, Grep, Bash, WebFetch +--- + +# Rerun chunk processing + +The pipeline layer between raw data and an RRD: readers produce `Chunk`s, +streams transform them, terminal calls execute. This skill is the generic +mechanics only. Decide the data model first (`rerun-data-model`), then pick the +importer skill for each source: + +| Source | Reader | Skill | +| ----------------------------------------- | --------------------------------------- | --------------- | +| MCAP file (ROS2, protobuf, Foxglove) | `McapReader(path).stream()` | `rerun-mcap` | +| URDF robot model (+ joint states → FK) | `UrdfTree.from_file_path(...).stream()` | `rerun-urdf` | +| Parquet table (trajectories, sensor logs) | `ParquetReader(path).stream()` | `rerun-parquet` | +| LeRobot dataset directory | built-in importer, then `RrdReader` | `rerun-lerobot` | +| Existing RRD | `RrdReader(path)` | here, below | +| Sidecar files (JSON calib, metadata) | `Chunk.from_columns` + `from_iter` | here, below | + +The API is `rerun.experimental`; when +behavior matters, check the installed surface: +`python -c "from rerun.experimental import LazyChunkStream; help(LazyChunkStream)"`. + +## Decision rule: where does each component come from? + +Default: **a reader produces the chunks; lenses shape them.** Walk this before +writing any conversion code — most "build it by hand" instincts are wrong here: + +1. **Source a reader supports?** Use the reader's `.stream()`; never hand-parse + and re-log. MCAP→`McapReader`, URDF→`UrdfTree`, parquet→`ParquetReader`, + RRD→`RrdReader`, LeRobot dir→`log_file_from_path`. +2. **A decoder already emits the archetype?** Foxglove gives `Transform3D`, + `Pinhole`, `VideoStream` (real sample bytes) ready-made — **pass it through**, + do not re-derive. Only custom-protobuf topics arrive as `:message` and + need a lens (see `rerun-mcap`). +3. **Fix an existing component in place** (swapped resolution, recolor, unit + convert)? `MutateLens`, `output_mode="forward_unmatched"`. +4. **Derive a new component/entity** (FK→`/tf`, scalars from a message)? + `DeriveLens`. To scatter one row into N (a joint batch → per-joint `/tf`), + use the **two-lens pair**: derive the batch with `output_mode="forward_all"` + (keeps the originals, e.g. the joint states), then a second + `DeriveLens` with `scatter=True` and `output_mode="drop_unmatched"` (emits + only the scattered rows). See the `robot_data_preprocessing` example. +5. **Genuine sidecar** no reader or lens can produce (JSON calibration offsets, + hand-measured extrinsics, external metadata)? `Chunk.from_columns` + `from_iter`. +6. Finish with `LazyChunkStream.merge(...)` → + `.collect(optimize=OptimizationProfile.OBJECT_STORE)` → + `write_rrd(application_id, recording_id)`. + +Why this order: the pipeline stays lazy, columnar, multithreaded, and +`OBJECT_STORE`-optimizable. A hand-built row loop or an out-of-lens `pa.array` +throws all of that away — that is the path we are deliberately avoiding. + +## Anti-patterns (use a reader + lens instead) + +If you are writing the left, stop and use the right: + +- **`for`-loop building rows/components** → a lens with a `Selector(...).pipe(...)` + PyArrow-compute callback. +- **`rr.init` + `rr.log` per message for conversion** → that is _live_ logging; + for ingestion, read with a reader and `write_rrd`. +- **`chunk.to_record_batch()` + `pc.filter` then rebuilding via + `Chunk.from_columns`** (row-thinning by hand) → `stream.drop(content=...)`, + `.split(...)`, or a `MutateLens` returning a filtered `pa.array`. +- **`pa.array` / `pa.RecordBatch` / `np.frombuffer` assembled OUTSIDE a lens** → + move the transform inside a `MutateLens`/`DeriveLens` selector callback. +- **`rr.send_columns` hand-assembled from a custom parser** → use the matching + reader; it produces chunks directly. +- **Parsing MCAP/URDF with a non-Rerun library then re-logging** → `McapReader` + / `UrdfTree`. +- **`Chunk.from_columns` for data a reader already decodes** (`Pinhole` + intrinsics, `VideoStream`, `Transform3D` from a transforms topic) → keep it in + the reader stream; fix with a `MutateLens` if needed. + +A wall of `pyarrow.compute` "missing-attribute" type errors (`pc.filter`, +`pc.list_element`) usually means `pc.*` calls sit in module-level helpers instead +of inside `Selector.pipe` lens callbacks. Refactor into a lens before suppressing +the checker — the errors are a smell that the hand-building should not exist. + +**Porting an existing converter?** Hand-built converters predate decoder +improvements and are not ground truth. Re-verify the decoder output (step 2) and +check every `Chunk.from_columns` / for-loop against this list before copying. + +## Core model + +- `LazyChunkStream` is a lazy pipeline DAG, not a collection. Building + filters, lenses, maps, splits, and merges reads no source data. +- Execution starts at terminal calls: `write_rrd(...)`, `collect()`, + `to_chunks()`, or iterating the stream. +- Execution is streaming, multithreaded, and mostly GIL-free. Prefer + stream/lens operations and PyArrow compute over Python row loops. +- **Move semantics**: builder calls (`filter`, `drop`, `lenses`, `map`, + `flat_map`) consume the input stream; reusing a consumed stream raises. + Reassign after each step. Terminal calls do not consume, but each terminal + call re-executes the whole pipeline; `collect()` once if that is too costly. +- `ChunkStore` is materialized in memory (`stream.collect()`, + `ChunkStore.from_chunks`). `LazyStore` is manifest-indexed, loads chunks on + demand (`RrdReader(path).store()`, catalog segment stores). Both have + `schema()`, `summary()`, `stream()`, and `write_rrd(...)`. + +## Stream composition + +```python +from rerun.experimental import Chunk, LazyChunkStream, OptimizationProfile +``` + +- `stream.filter(content=, has_timeline=, is_static=, components=)` keeps the + matching portion of each chunk; `stream.drop(...)` is its complement, same + keyword filters. `content` takes an entity-path glob or a list of them. +- `stream.map(fn)` applies `Chunk -> Chunk`; `stream.flat_map(fn)` applies + `Chunk -> Iterable[Chunk]`. Escape hatches for chunk-level Python logic; + prefer lenses for columnar work. +- `stream.split(content=, ...)` returns `(matching, non_matching)`; both + branches share the same upstream. +- `LazyChunkStream.merge(*streams)` fans in any number of sources. +- `LazyChunkStream.from_iter(chunks)` wraps hand-built chunks. + +```python +stream = source_stream() # any importer skill +stream = stream.drop(content="/video_raw/**") +stream = stream.lenses(fix_lens, content="/cam/**", output_mode="forward_unmatched") +merged = LazyChunkStream.merge(stream, sidecar_stream) +merged.write_rrd(out_path, application_id="my_app", recording_id=recording_id) +``` + +## Hand-built chunks — sidecar only + +Use `Chunk.from_columns` ONLY for data no reader or lens can emit — JSON/CSV +calibration, frame offsets, external metadata. If a reader +(`McapReader`/`UrdfTree`/`ParquetReader`) decodes the topic or a lens can derive +it, that is the idiomatic path; do not hand-assemble it here. In the +`robot_data_preprocessing` example the _only_ hand-built chunk is the JSON +offsets sidecar; the camera fix, FK→`/tf`, meshes, and recolor are all +readers + lenses. + +`Chunk.from_columns(entity_path, indexes, columns)` mirrors +`rr.send_columns(...)` and accepts the same archetype `.columns(...)` helpers. +Empty `indexes` means static. + +```python +chunk = Chunk.from_columns( + "/tf_static/robot_offsets", + indexes=[], # static + columns=rr.Transform3D.columns( + translation=translations, + quaternion=quaternions_xyzw, + parent_frame=parents, + child_frame=children, + ), +) +sidecar_stream = LazyChunkStream.from_iter([chunk]) +``` + +`rr.AnyValues.columns(...)` covers non-standard metadata fields. For +inspection, a `Chunk` exposes `entity_path`, `num_rows`, `is_static`, +`timeline_names`, `to_record_batch()`, and `format()` (human-readable table). + +## Lenses + +Lenses reshape, fix, or derive components without iterating rows. Apply with +`stream.lenses(lenses, output_mode=..., content=...)`. + +- `MutateLens(component, selector, keep_row_ids=False)` modifies an existing + component in place. +- `DeriveLens(component, output_entity=None, scatter=False)` creates new + columns, optionally at another entity. Chain `.to_component(descriptor, +selector)` per output; `.to_timeline(name, "sequence" | "duration_ns" | +"timestamp_ns", selector)` extracts a time column from the data itself. + `scatter=True` explodes one input row into N output rows (one per list + element). +- Scope with `content=` whenever the same component name exists under multiple + entities. + +Output modes, and **the default is `drop_unmatched`**: + +- `drop_unmatched` (default): only lens outputs survive. Right for derive-only + intermediate streams; silently discards everything else if applied broadly. +- `forward_unmatched`: lens outputs plus the original components no lens + consumed. Right for targeted fixes that preserve the rest of the stream. +- `forward_all`: lens outputs plus all originals, including consumed ones. Can + duplicate data. + +In-place fix (keep Arrow type and length intact): + +```python +stream = stream.lenses( + MutateLens( + "Pinhole:resolution", + Selector(".").pipe( + lambda res: pa.array( + [(h, w) for w, h in res.to_pylist()], + type=res.type, + ) + ), + ), + content=["/external/cam_low", "/external/cam_high"], + output_mode="forward_unmatched", +) +``` + +Derive with unit conversion (PyArrow compute, no Python loop): + +```python +DeriveLens("schemas.proto.JointState:message", output_entity="/joints_deg/waist").to_component( + rr.Scalars.descriptor_scalars(), + Selector(".joint_positions").pipe(lambda arr: pc.multiply(pc.list_element(arr, 0), 180.0 / math.pi)), +) +``` + +## Selector grammar + +`Selector("")` navigates nested Arrow data, jq-style: + +- `.` current value; `.field` struct field +- `[]` iterate list elements; `[N]` index a list +- `?` suppress errors / skip missing optionals; `!` assert non-null +- `|` pipe one expression into another + +`.pipe(fn)` chains a Python/PyArrow transform (or another Selector). +`.execute(array)` runs it eagerly; `.execute_per_row(array)` guarantees the +output row count matches the input (use inside lens callbacks that must stay +row-aligned). + +## Writing RRDs + +- `stream.write_rrd(path, application_id=..., recording_id=...)` executes and + writes in one streaming pass. +- `stream.collect(optimize=OptimizationProfile.OBJECT_STORE).write_rrd(...)` + materializes, optimizes chunk layout, then writes. Memory scales with the + materialized chunks. +- Profiles: `OBJECT_STORE` (large chunks, for storage/query/catalog) and + `LIVE` (small chunks, low-latency viewer). +- Multiple physical RRDs form one logical recording when they share a + `recording_id`; use this to separate base data, model/URDF data, and layers. + +**Always use `OptimizationProfile.OBJECT_STORE`** when the RRD is headed for a +Rerun catalog or Hub, unless explicitly asked otherwise. + +## Chunk API vs logging API + +- Logging (`rr.log`, `rr.send_columns`, `RecordingStream`) is for live logging + from user code; chunk processing is for ingestion, conversion, and + postprocessing existing recordings. +- Logging → chunks: write an RRD, read it back with `RrdReader`. + `RrdReader(path)` lists `recordings()` / `blueprints()` (each a `StoreEntry` + with `kind`, `application_id`, `recording_id`); `.stream(store=entry)` for + sequential passes, `.store(store=entry)` for indexed access. +- Chunks → logging: `rerun.experimental.send_chunks(chunks, recording=...)` + accepts a `Chunk`, `LazyChunkStream`, `LazyStore`, `ChunkStore`, or any + iterable of chunks. The source store's `application_id`/`recording_id` are + **not** preserved; the active recording's identity wins. + +## Common gotchas + +- The default lens `output_mode` is `drop_unmatched`; forgetting to set + `forward_unmatched` on a targeted fix silently drops the rest of the stream. +- Do not reuse a consumed `LazyChunkStream`; reassign or `split` deliberately. +- Scope lenses with `content=`; the same component name often exists under + many entities. +- Preserve Arrow array type and length in `MutateLens` transforms. +- For catalog layers, the layer `recording_id` must equal the segment id. +- This is `rerun.experimental`; pin-check signatures when upgrading. + +## References + +- End-to-end example (MCAP + URDF + JSON sidecar, lenses, merge, optimize): + `https://github.com/rerun-io/rerun/tree/main/examples/python/robot_data_preprocessing` +- Docs: `https://rerun.io/docs/concepts/logging-and-ingestion/chunk-processing-api`, + `https://rerun.io/docs/concepts/query-and-transform/lenses` diff --git a/skills/rerun-data-model/SKILL.md b/skills/rerun-data-model/SKILL.md new file mode 100644 index 000000000000..f6d2a0c09e00 --- /dev/null +++ b/skills/rerun-data-model/SKILL.md @@ -0,0 +1,89 @@ +--- +name: rerun-data-model +description: "How raw multimodal robot data maps onto the Rerun data model. Read FIRST, before modeling or converting a dataset — and whenever you are about to convert/ingest/preprocess robot data into an .rrd or build a Rerun recording, even if not asked for the data model. Resolves the entity-vs-component, property-vs-component-vs-layer, and static-vs-temporal decisions and routes to the mechanism (do it with readers and lenses, not hand-built chunks or per-message rr.log): rerun-chunk-processing and the importer skills rerun-mcap, rerun-urdf, rerun-parquet, rerun-lerobot." +user_invocable: true +allowed-tools: Read, Grep, Bash, WebFetch +--- + +# Rerun data model + +The hard part of ingesting a dataset is the **modeling decision**, not the API call. +Get the model right and any mechanism works; get it wrong and queries, views, and training all break. +This skill is just the decisions. +For mechanism details see `rerun-chunk-processing` (pipeline mechanics) and the importer skills it routes to: `rerun-mcap`,`rerun-urdf`,`rerun-parquet`, `rerun-lerobot`. +For exact signatures, the docs at `rerun.io/docs/concepts/logging-and-ingestion`. + +**Before writing conversion code, fill in the mapping table below.** It is the design, and a human can review it in seconds. + +## Pick the mechanism before you model the bytes + +Modeling decides _what_ each datum becomes; this decides _how_ it gets there — and the default is **a reader + lenses, not hand-built chunks**: + +- **Does a reader exist for this source?** MCAP→`McapReader`, URDF→`UrdfTree`, parquet→`ParquetReader`, RRD→`RrdReader`, LeRobot dir→`log_file_from_path`. **Yes →** `reader.stream()` + lenses; the reader produces the chunks, you do not. +- **No, and it is genuine external metadata** (JSON calibration, offsets) or a specific custom use case that can't be covered by a generic reader**→** `Chunk.from_columns`. +- **Otherwise**: consider if you are about to hand-build something a reader or lens should produce and ask for clarification. + +For MCAP specifically: a Foxglove- or ROS-decodable file emits archetypes like `Transform3D`, `Pinhole`, and `VideoStream` for certain supported message schemas ready-made — pass those through, never re-derive them; only custom protobuf signal topics need lenses. The full decision tree and the anti-pattern list are in `rerun-chunk-processing`. + +## The model + +``` +Dataset → Segment → Layer → Recording → Entity → Component → Chunk +``` + +- **Entity** = a thing, named by a path (`/robot/arm/camera`). + **Component** = one typed field on it (positions, image, a scalar). + **Archetype** = a standard bundle of components that log and render together (`Points3D`, `Image`, `Transform3D`). +- Every value you log sits on two axes: **where** (entity path + component) and **when** (which timeline, or _static_ = all time). +- **Segment** = one episode; on registration its `recording_id` becomes the `segment_id`. **Layer** = an extra `.rrd` on top of a segment. It attaches by matching the segment's `recording_id` and nothing else: that shared id is the whole layering mechanism. + +## The decisions + +**Entity vs component on an existing entity** +New entity if it has its own spatial frame (`Transform3D`/`Pinhole`), its own annotation context, or should be shown/cleared/shared independently (each robot link, each camera, each sensor). Same entity + extra component for auxiliary data at the same instances (per-point confidence). +Use `AnyValues` for non-standard fields. + +**Property vs component vs layer** (the most common confusion) + +- **Component**: per-timestamp signal on the timeline (joint angle, image). +- **Segment property**: one-per-episode metadata for catalog filter/search (operator, robot, site, task, date). Not per timestamp. +- **Layer**: a whole derived `.rrd` over a base segment (FK transforms, point clouds, gripper state, labels, quality scores). Queryable as if part of base. + +**Static vs temporal** +Static belongs to all timelines and shadows any temporal value of the same component on the same entity. Use it for invariants (calibration, coordinate frames, robot meshes, annotation context, a video asset). Never make per-frame data static. + +**Which timeline** +A `timestamp` timeline (ns since epoch) for cross-sensor clock alignment, a `sequence` timeline for frame/ordinal alignment; stamp on both when useful. +**Do not resample to a common rate.** Latest-at reconciles multi-rate streams at query time by holding each component's last sample (no interpolation). + +**Base vs layer** +Base = faithful conversion of the raw streams, nothing computed. +Layer = anything derived (FK from URDF + joint states, clouds from depth + intrinsics). +Keep them separate `.rrd`s. + +## The mapping table (produce before coding) + +| Source (topic/column/key) | Entity path | Archetype | Component(s) | Timeline | Static/temporal | Base/layer | Property? | +| ------------------------- | -------------------- | ------------------------------------- | -------------------- | ------------- | --------------------------- | ---------- | --------- | +| mcap `/joint_states` | `/robot/` | `Scalars` | `scalars` | `sensor_time` | temporal | base | no | +| `cam0/color.mp4` | `/camera/cam0/video` | `AssetVideo`+`VideoFrameReference` | asset+refs | `video_time` | asset static, refs temporal | base | no | +| `calibration.json` | `/camera/cam0` | `Pinhole` (+`Transform3D` extrinsics) | `image_from_camera` | — | static | base | no | +| URDF + joints (computed) | `/robot/` | `Transform3D` | translation/rotation | `sensor_time` | temporal | **layer** | no | +| `episode.json` operator | segment | — | — | — | — | — | **yes** | + +## Patterns worth knowing + +- **Transforms / FK trees**: log a `Transform3D` per link entity; it relates to the **parent path** and composes down the tree. (Named `CoordinateFrame` + `child_frame`/`parent_frame` only when topology must be a flexible graph.) +- **Cameras**: extrinsics (`Transform3D`) + intrinsics (`Pinhole`) on the camera entity, image/depth as **children** so they inherit the projection. +- **Video**: `VideoStream` for raw H.264/H.265 samples. +- **Columnar ingest**: for an existing _file_, use the matching reader (`rerun-mcap`/`-parquet`/`-lerobot`), which produces chunks directly — do not hand-assemble `send_columns` from a custom parser. When you do log columns directly (live logging), `send_columns` adds **no** automatic timelines, so pass every timeline you want. + +## Gotchas that cause real failures + +1. Component columns come back as `ListArray` in queries: index `[0]`/`[0][0]` (0-based DataFrame, 1-based SQL). See `rerun-catalog-queries`. +2. A layer must share the segment's `recording_id`, or it won't attach. `application_id` is discarded on registration. +3. `send_columns`/`send_chunks` add no `log_time`/`log_tick`; only the timelines you pass exist. +4. Static shadows all temporal data of that component for all time; static is overwritten in the viewer but every write stays on disk until `rerun rrd optimize`. +5. One `Transform3D`/`Pinhole` relation per frame pair; logging the same relation on a second entity is rejected. +6. Entity paths are not file paths (`..` is meaningless, `__` is reserved). +7. A catalog **layer** written with a default `RecordingStream` injects a `/__properties` (`RecordingInfo`) chunk that can collide with the base segment's properties when the catalog merges layers by `recording_id`; the dataset's default blueprint then stops applying on open. Construct the layer's stream with `send_properties=False`. diff --git a/skills/rerun-lerobot/SKILL.md b/skills/rerun-lerobot/SKILL.md new file mode 100644 index 000000000000..0e16c28498ad --- /dev/null +++ b/skills/rerun-lerobot/SKILL.md @@ -0,0 +1,73 @@ +--- +name: rerun-lerobot +description: Ingest a LeRobot (HuggingFace) dataset into Rerun. Read when converting a LeRobot dataset to RRDs, splitting it into per-episode segments, or registering it on a Rerun catalog. Covers the built-in directory importer (log_file_from_path), the RrdReader + send_chunks per-episode split, and when to drop to ParquetReader for custom control. +user_invocable: true +allowed-tools: Read, Grep, Bash, WebFetch +--- + +# Rerun LeRobot ingestion + +Rerun has a **built-in LeRobot importer**: point `log_file_from_path` (or the viewer, or `rerun ` on the CLI) at the dataset _directory_ and it ingests episodes, camera videos, and state/action tables with no conversion code. +There is no chunk-level `LeRobotReader`; the chunk-processing route is to import first, then reprocess the resulting RRD with `RrdReader`. + +The download step needs +`huggingface_hub`. + +## Step 1: dataset -> one combined RRD + +```python +from huggingface_hub import snapshot_download +import rerun as rr + +dataset_dir = snapshot_download(repo_id="rerun/so101-pick-and-place", repo_type="dataset", local_dir=dest) + +with rr.RecordingStream("rerun_example_lerobot") as rec: + rec.save(str(combined_rrd)) + rec.log_file_from_path(str(dataset_dir)) # the built-in importer +``` + +The importer emits one recording per episode (recording ids like `episode_1`), plus a metadata-only root recording, all into the single RRD. + +`rr.RecordingStream` + `log_file_from_path` here is the **importer bootstrap** — the one place `RecordingStream` is correct in an ingestion pipeline (it drives the built-in importer, not per-message logging). Do not generalize it to `rr.log`-per-message loops; for everything after import, reprocess the RRD with `RrdReader` + lenses (see `rerun-chunk-processing`: Chunk API vs logging API). + +## Step 2: split into per-episode RRDs + +Catalog segments are one-recording-per-file, and `recording_id` becomes the segment id on registration. +Split with `RrdReader`: + +```python +reader = rr.experimental.RrdReader(str(combined_rrd)) +for entry in reader.recordings(): + store = reader.store(store=entry) + if not store.schema().entity_paths(): # skip the metadata-only root recording + continue + episode_id = zero_pad(entry.recording_id) # episode_1 -> episode_00001 + with rr.RecordingStream("rerun_example_lerobot", recording_id=episode_id, send_properties=False) as rec: + rec.save(str(rrd_dir / f"{episode_id}.rrd")) + rec.send_chunks(store) +``` + +Two non-obvious moves: + +- **Zero-pad the episode id.** `episode_10` sorts before `episode_2` + lexicographically; segment tables and viewers sort lexicographically. Pad to + a fixed width when re-assigning `recording_id`. +- **`send_properties=False`** on the new stream, so the copy doesn't inject + fresh recording properties on top of the copied chunks. + +`send_chunks` does not preserve the source store's identity; the new stream's +`recording_id` wins, which is exactly what makes the rename work. + +If episodes need cleanup (drop topics, fix data, add derived components), run the store through lenses between read and write: `reader.stream(store=entry).drop(...).lenses(...)` then `collect().write_rrd(..., recording_id=episode_id)` (see `rerun-chunk-processing`). + +Computed layers and per-episode properties then follow the standard patterns in `rerun-data-model` (layer `recording_id` must equal the episode segment id). + +## Gotchas + +1. `log_file_from_path` must target the dataset **root directory**, not a file inside it. +2. Unpadded episode ids sort incorrectly downstream; pad before registering. +3. The combined RRD contains a metadata-only root recording; skip stores with no entity paths or you register an empty segment. + +## References + +- `https://github.com/rerun-io/rerun/tree/main/examples/python/dataloader` `prepare_dataset.py` (download → import → split → register, complete and runnable) and `train.py` (training-side consumption via `rerun.experimental.dataloader`) diff --git a/skills/rerun-mcap/SKILL.md b/skills/rerun-mcap/SKILL.md new file mode 100644 index 000000000000..a0bcece7707e --- /dev/null +++ b/skills/rerun-mcap/SKILL.md @@ -0,0 +1,86 @@ +--- +name: rerun-mcap +description: Ingest MCAP files into Rerun chunk streams with rerun.experimental.McapReader. Read when converting an MCAP recording, selecting topics or decoders, decoding custom protobuf messages, or when an MCAP-derived stream comes out empty. Builds on rerun-chunk-processing (stream mechanics) and rerun-data-model (what the topics should become). +user_invocable: true +allowed-tools: Read, Grep, Bash, WebFetch +--- + +# Rerun MCAP ingestion + +`McapReader` turns an MCAP file into a lazy chunk stream: one entity per topic +at the topic's path, message payloads decoded by pluggable decoders. This +skill covers the reader's options, what each topic becomes, and the failure +modes that yield an empty stream with no error. Stream mechanics (filter, drop, +lenses, merge, write) are in `rerun-chunk-processing`. + +## The API + +```python +from rerun.experimental import McapReader + +reader = McapReader(mcap_path) # see help(McapReader) for the full option set +stream = reader.stream() +``` + +A URDF embedded in the MCAP can be ingested as well (then see `rerun-urdf`). + +## What a topic becomes + +With the default decoders (`decoders=None`), the message **schema name** decides what a topic becomes — **pass archetypes through, lens only the raw `:message` topics**: + +| MCAP schema name | decodes to | what to do | +| --- | --- | --- | +| `foxglove.FrameTransforms` | `Transform3D` | pass through; do **not** hand-build | +| `foxglove.CameraCalibration` | `Pinhole` | pass through | +| `foxglove.CompressedVideo` | `VideoStream` (real sample bytes) + `CoordinateFrame` | pass through | +| other `foxglove.*` well-known types | the matching archetype | pass through | +| ros2 well-known types (`ros2msg` / `ros2_reflection`) | archetype | pass through | +| your own `schemas.proto.*` / custom protobuf | one `:message` struct | attach semantics with a `DeriveLens` + `Selector` | + +So a camera topic already arrives as `Pinhole`, its video as `VideoStream`, and a `frame_transforms` topic as `Transform3D` — only custom messages (e.g. a custom joint states schema, a custom gripper status enum) come through reflection or raw only and need lenses. + +The `foxglove` decoder does the schema→archetype mapping; because foxglove messages are protobuf-_encoded_ it rides on the `protobuf` decoder, so keep `decoders=None` (verified: `decoders=["protobuf"]` alone leaves `foxglove.CameraCalibration` a raw `:message`; adding `foxglove` makes it a `Pinhole`). Confirm on your file: `McapReader(path).stream()`, then read `McapSchema:name` and a few `Chunk.format()` before deciding anything is missing or needs rebuilding. + +- Entity path = topic name (`/sensors/joint_states` stays `/sensors/joint_states`). + Filter early: `McapReader(path).stream().filter(content="/sensors/**")`. +- A reflection-decoded message lands as one struct component named `:message`. + Navigate it with `Selector` (`Selector(".joint_positions")`) inside lenses; this is how custom messages + get Rerun semantics attached (see the DeriveLens patterns in `rerun-chunk-processing`). +- Topic regexes use RE2 syntax and are **not anchored**: `cam` matches + `/external/cam_low` and `/camera_info`. + Anchor explicitly (`^/external/cam`) when it matters. Prefer reader-level topic filtering over `.filter(...)` + when you can, so excluded topics are never decoded at all. + +## When to use the low-level `mcap` package instead + +`McapReader` keeps payloads in columnar chunk streams; that is almost always what you want. +Drop to `mcap.reader.make_reader` only when you need raw record metadata without payloads, or when you need to rewrite the container itself (re-registering schemas, channels, and messages). + +## Gotchas + +1. Empty stream, no error: a topic regex that matched nothing, or a channel + whose decoder produced no rows. Check `Chunk.format()` on a few chunks of + `reader.stream().to_chunks()` against a tiny test file, or compare topic + names with the `mcap` CLI / package first. +2. Topic regexes are unanchored RE2; excludes run after includes. +3. `timeline_type="timestamp"` interprets MCAP log times as wall-clock ns + since epoch. If the recording's clock is wrong, fix it at the reader with + `timestamp_offset_ns` rather than mutating timestamps downstream. +4. Decoder subsets silently skip topics no decoder claims; when a topic is + missing, retry with `decoders=None` to rule out decoder selection. +5. Example fix-lenses are dataset-specific. Before copying a `MutateLens` like + the `Pinhole:resolution` swap from the `robot_data_preprocessing` example, + read the raw component from `McapReader(path).stream()` and confirm the defect + exists in _your_ data — applied blindly it corrupts correct calibration (a + correct 648×480 flipped to 480×648). +6. `foxglove` derives both the camera's `Pinhole:child_frame` and the video's + `CoordinateFrame:frame` from each message's `.frame_id` (plus an image-plane + suffix), so they **match** when the calibration and video topics share a + `frame_id`. Only when those topics carry _different_ `frame_id`s does the + video frame diverge and orphan the video from its image plane — re-home it + then with a per-camera `MutateLens` on `CoordinateFrame:frame`. + +## References + +- End-to-end MCAP pipeline: `https://github.com/rerun-io/rerun/tree/main/examples/python/robot_data_preprocessing` +- `rerun-chunk-processing` (stream/lens mechanics), `rerun-urdf` (FK from joint-state topics), `rerun-data-model` (modeling decisions) diff --git a/skills/rerun-parquet/SKILL.md b/skills/rerun-parquet/SKILL.md new file mode 100644 index 000000000000..97a27a70d43c --- /dev/null +++ b/skills/rerun-parquet/SKILL.md @@ -0,0 +1,244 @@ +--- +name: rerun-parquet +description: Ingest tabular Parquet files into Rerun chunk streams with rerun.experimental.ParquetReader. Read when converting trajectory or sensor tables (LeRobot-style parquet, exported logs) into entities and components — column grouping, timeline/index columns, static columns, and lenses (DeriveLens) that assemble the typed components (Transform3D, Scalars) from the reader's grouped struct/scalar output. Builds on rerun-chunk-processing and rerun-data-model. +user_invocable: true +allowed-tools: Read, Grep, Bash, WebFetch +--- + +# Rerun parquet ingestion + +`ParquetReader` is a **pure reader**: it maps a flat table onto the Rerun +model by turning raw columns into grouped, time-indexed chunks of struct and +scalar components. Column-name prefixes become entities, grouped columns +become a single struct component, designated columns become timelines. The +reader does **not** assemble archetypes anymore — mapping struct fields into +typed Rerun components (Transform3D, Scalars, Points3D) is done with lenses on +the reader's `.stream()`. The whole reader job is configuration; fill in the +`rerun-data-model` mapping table first, then express it through the +constructor. Stream mechanics after `.stream()` are in +`rerun-chunk-processing`. + +**The whole table is configuration, not code.** If you find yourself building +`Chunk.from_columns` from a parquet, or munging it in pandas first, stop — +`ParquetReader` plus a lens almost certainly expresses it. Anything the reader +cannot express (per-row entity routing, derived values, unit conversion) +belongs in lenses downstream, not in pre-pandas munging; keep the pipeline +columnar. + +## The API + +```python +from rerun.experimental import ParquetReader, DeriveLens + +reader = ParquetReader( + table_path, + entity_path_prefix="/world", # prepended to every entity path + column_grouping="prefix", # "prefix" | "individual" | "explicit_prefixes" + delimiter="_", # split for column_grouping="prefix" + prefixes=None, # required for "explicit_prefixes" + use_structs=True, # pack grouped columns into one struct component + static_columns=["robot_type"], # constant-per-file values, logged static + index_columns=[("timestamp", "timestamp", "us"), ("frame_index", "sequence")], +) +stream = reader.stream() +``` + +Every parameter after `path` is keyword-only. There is no `column_rules` +kwarg — typed-component assembly moved to lenses (below). + +## What the reader emits + +The reader turns the table into chunks, one chunk per group, then leaves the +data as generic struct/scalar components for lenses to map. The naming is the +key thing the rest of the pipeline keys off: + +- **A grouped multi-column prefix `X`** → entity `/X`, with a single struct + component named **`data`**. The struct's fields are the column names with the + prefix (and delimiter, for `"prefix"`) stripped. So `A_pos_x`, `A_quat_w` + under prefix `A` land as struct `data` with fields `pos_x`, `quat_w` on + entity `/A`. +- **A lone column with no group** → its own entity named after the column, and + a raw component named after the column — *not* a `data` struct. So a `speed` + column becomes entity `/speed`, component `speed`. +- **A `/__properties` metadata chunk** built from the parquet file's schema + metadata. You typically drop it right after `.stream()`: + + ```python + stream = reader.stream().drop(content="/__properties/**") + ``` + +## Column grouping: which columns share an entity + +- `"prefix"` (default): split each column name on `delimiter`, group by the + first segment. `gripper_pos_x`, `gripper_pos_y` → entity `/gripper`, struct + `data{pos_x, pos_y}`. +- `"explicit_prefixes"`: group by the exact strings in `prefixes`, tried + longest-first; the prefix is stripped from each struct field name (a raw + string match, no delimiter — `foo` + `a` → field `a`). Columns matching no + prefix become individual groups. Use this when names contain the delimiter + ambiguously (`observation.state` vs `observation.images.top`: pass the full + prefixes). +- `"individual"`: every column is its own chunk/entity with a raw component + named after the column — no struct packing at all, even for columns sharing a + prefix. `use_structs` is ignored here. Rarely the model you want; reach for + it only as a debugging baseline. + +`use_structs=True` (default) packs a group's columns into a single Arrow +struct component (the `data` field) for `"prefix"`/`"explicit_prefixes"`; +`False` emits one component per column (the pre-struct flat layout, what +queries see as separate columns). + +## Timelines: `index_columns` + +Each entry is `(name, type)` or `(name, type, unit)`: + +- `type`: `"timestamp"` (since epoch), `"duration"` (elapsed), `"sequence"` + (ordinal int). +- `unit` describes what the raw integers in the column *are* (`"ns"` default, + `"us"`, `"ms"`, `"s"`); Rerun rescales to ns internally. Ignored for + `"sequence"`. + +**If omitted, a synthetic `row_index` sequence timeline is generated.** That +is almost never the timeline you want to query or align against; always name +the real time columns. Stamp both a timestamp and a sequence timeline when the +table has both (multi-rate alignment, see `rerun-data-model`). + +## Static columns: `static_columns` + +Listed columns are constant across all rows; they are emitted once as a single +static (timeless) chunk, separate from the temporal data. A listed column that +actually varies raises an error when the stream runs — that error is a +data-quality signal, not a reason to drop the static declaration. + +## Typed components via lenses + +The reader's grouped output is generic struct (`data`) and scalar data. A +`DeriveLens` reads that struct's fields, packs and casts them into real Rerun +components, and writes them to an output entity — this is what the old +`column_rules` API used to do, now done downstream on the stream. + +Construct a lens against the reader's struct component (`"data"` for grouped +prefixes, or the column name for a lone/individual column), then add one or +more `.to_*` builder methods. Each builder returns a fresh lens, so they chain. + +| Builder | Produces | Argument order | +|---|---|---| +| `to_translation(x, y, z)` | `Transform3D:translation` | x, y, z | +| `to_quaternion(x, y, z, w)` | `Transform3D:quaternion` | x, y, z, w (xyzw) | +| `to_scale(x, y, z)` | `Transform3D:scale` | x, y, z | +| `to_rotation_axis_angle(axis_x, axis_y, axis_z, angle)` | `Transform3D:rotation_axis_angle` | axis_x, axis_y, axis_z, angle (radians) | +| `to_scalars(*fields)` | `Scalars:scalars` | one or more field names | +| `to_packed_component(component, *fields)` | the given component | descriptor, then field names | +| `to_component(component, selector)` | the given component | descriptor, then a `Selector` | +| `to_timeline(name, type, selector)` | a timeline (not a component) | name, `"sequence"`/`"duration_ns"`/`"timestamp_ns"`, selector | + +`to_packed_component` packs the named struct fields (in order, at least one +required) into the fixed-size list the component expects, and by default +**auto-casts `f64`→`f32`** to match component types. The `to_translation`, +`to_quaternion`, `to_scale` helpers are convenience wrappers over it, so they +all auto-cast. `to_rotation_axis_angle` builds a `Struct{axis, angle}` and +hard-casts axis and angle to `f32` internally. `to_scalars` with a single field +emits a plain scalar per row (not a 1-element list); with multiple fields it +emits one scalar series per field at the same entity. + +Apply lenses with `.stream().lenses([lens], content="/A", output_mode="drop_unmatched")`: + +- `content` is a pre-filter on the *source* entity path — it scopes which + chunks the lens may touch. Out-of-scope chunks pass through unchanged. Set it + to the reader's grouped entity (e.g. `"/A"`). +- `output_mode` decides the fate of in-scope-but-unmatched chunks: + `"drop_unmatched"` (default, keep only lens output), `"forward_unmatched"` + (output replaces matched, other originals survive), or `"forward_all"` + (output plus all originals). +- The lens's own `output_entity=` sets the *destination* entity — independent + of `content`, which gates the input side. + +End-to-end Transform3D example. The reader groups `A_*` columns into a `data` +struct at `/A`; the lens reads the prefix-stripped field names (`pos_x`, +`quat_w`), packs and casts them, and writes a full `Transform3D` to `/pose`: + +```python +from rerun.experimental import DeriveLens, ParquetReader + +lens = ( + DeriveLens("data", output_entity="/pose") + .to_translation("pos_x", "pos_y", "pos_z") + .to_quaternion("quat_x", "quat_y", "quat_z", "quat_w") +) + +chunks = ( + ParquetReader(table_path, index_columns=[("frame_index", "sequence")]) + .stream() + .lenses([lens], content="/A", output_mode="drop_unmatched") + .to_chunks() +) +``` + +Chaining several `.to_*` on **one lens with a shared `output_entity`** +accumulates multiple component columns into the same archetype at that entity — +above, both `Transform3D:translation` and `Transform3D:quaternion` land on +`/pose`, forming a complete `Transform3D`. For a generic fixed-size-list +component, pass the descriptor to `to_packed_component`: + +```python +import rerun as rr +from rerun.experimental import DeriveLens, ParquetReader + +lens = DeriveLens("data", output_entity="/points").to_packed_component( + rr.Points3D.descriptor_positions(), "x", "y", "z" +) +``` + +## Selectors + +Lens field paths use `Selector`, a jq-like grammar over Arrow columns +(`.field` to access a struct field, `[]` to iterate a list, `[N]` to index, `?` +to suppress errors on absent fields, `!` to assert non-null, `|` to pipe, and +`pack(.x, .y, .z)` to zip paths into a fixed-size list). The `to_*` helpers +build these selectors for you; reach for `to_component(component, Selector(".x"))` +when you need a custom field path. Field paths reference the +**prefix-stripped** struct field names — the lens sees `pos_x`, not `A_pos_x`. + +## Gotchas + +1. No `index_columns` → synthetic `row_index` timeline only. Queries that + expect a timestamp timeline find nothing. +2. The `unit` is the raw column's unit, not a desired output unit; a + microsecond column declared `"ns"` lands 1000x in the past. +3. `static_columns` raises if a listed column actually varies; that error is a + data-quality signal, not a reason to drop the static declaration. It is + raised lazily when the stream runs, not at construction. +4. A grouped prefix's struct component is named **`data`** — that is the + `input_component` string a `DeriveLens` matches against. A lone or + `"individual"` column is instead a raw component named after the column. +5. Selector field paths reference the **prefix-stripped** struct field names + (`pos_x`, not `gripper_pos_x`). +6. Drop the `/__properties` metadata chunk the reader emits from parquet schema + metadata: `.stream().drop(content="/__properties/**")`. +7. Quaternion column order is x, y, z, w in `to_quaternion`; check the source's + convention before wiring fields. +8. `to_packed_component` (and the transform helpers built on it) auto-casts + `f64`→`f32` to match component types; this is usually what you want for + parquet's double columns. +9. Anything the reader cannot express (per-row entity routing, derived values, + unit conversion) belongs in lenses downstream, not in pre-pandas munging; + keep the pipeline columnar (`rerun-chunk-processing`). + +## References + +- Lens builder source with full docstrings: `rerun/experimental/_lens.py` in + the installed `rerun-sdk` package (`to_translation`, `to_quaternion`, + `to_scale`, `to_rotation_axis_angle`, `to_scalars`, `to_packed_component`, + `to_component`, `to_timeline`). +- Reader source: `rerun/experimental/_parquet_reader.py`, or + `python -c "from rerun.experimental import ParquetReader; help(ParquetReader)"` +- Canonical worked examples: the integration tests + `rerun_py/tests/integration/test_parquet_reader.py` (grouping, index/static + columns, and the Transform3D / Points3D / Scalars lens flows) and + `rerun_py/tests/integration/test_lazy_chunk_stream.py` (lens application, + `content`/`output_mode`, selectors). +- `rerun-lerobot` — LeRobot datasets store episodes as parquet; that skill + covers the built-in importer route vs reading the parquet directly with + this reader. +- `rerun-data-model` (mapping decisions), `rerun-chunk-processing` (stream + mechanics after `.stream()`) diff --git a/skills/rerun-urdf/SKILL.md b/skills/rerun-urdf/SKILL.md new file mode 100644 index 000000000000..06c4216e4fac --- /dev/null +++ b/skills/rerun-urdf/SKILL.md @@ -0,0 +1,264 @@ +--- +name: rerun-urdf +description: Drive the Rerun URDF API (rerun.urdf.UrdfTree) to ingest a URDF as a Transform3D layer on a robot recording. Read when logging a robot model, running forward kinematics from joint states, composing a fixed chain for sensor extrinsics, or when the transform tree will not connect from the data alone. Builds on rerun-chunk-processing (stream/lens mechanics) and rerun-data-model (entity paths, timeline, base-vs-layer). +user_invocable: true +allowed-tools: Read, Grep, Bash, WebFetch +--- + +# Rerun URDF ingestion + +A URDF gives you a robot's geometry and its kinematic tree. +Ingesting it means two API calls on one `rerun.urdf.UrdfTree`: stream the static model, then drive it with forward kinematics from joint states. The transforms it produces are derived, so they are a **layer**, never base (the `URDF + joints (computed)` row of the `rerun-data-model` table). + +This skill is the `UrdfTree` API and the two judgment calls it cannot make for +you: how your joint values map to URDF joints, and how the disconnected frames +in the scene connect to one root. The stream/lens plumbing (`LazyChunkStream`, +`DeriveLens`, `Selector`, writing and optimizing RRDs) is in +`rerun-chunk-processing`; reach for it, do not re-derive it. Nothing below is +tied to a data format: where your joint names, joint values, and calibration +come from is yours to wire in. + +## The API + +```python +from rerun.urdf import UrdfTree + +urdf = UrdfTree.from_file_path( + urdf_path, + entity_path_prefix="robot", # links log under /robot/ + frame_prefix="", # prepended to every frame name + static_transform_entity_path="robot/tf_static", +) +``` + +- `entity_path_prefix` namespaces the entity tree. One per robot instance. +- `frame_prefix` namespaces the **frame names** (`base_link` -> `arm_base_link`). + Two robots in one recording need different prefixes or their roots collide and + transforms cross-wire. Leave it empty for a single robot. +- `static_transform_entity_path` is where the URDF's fixed-joint transforms log + (defaults to `/tf_static`). + +The tree is also introspectable (full surface in `help(UrdfTree)`) +For one-off, non-stream use there are `joint.compute_transform(value)`, `joint.compute_transform_columns(values)` (feeds `rr.send_columns`), and `urdf.log_urdf_to_recording()` to log the whole model through the classic logging API (the `animated_urdf` example, `https://github.com/rerun-io/rerun/tree/main/examples/python/animated_urdf`, is that style). + +For pipelines, a `UrdfTree` does two things. + +**1. Stream the static model.** Emits the visual meshes (`Asset3D`) and the +fixed-joint transforms as chunks. This is the whole "log the URDF" step: + +```python +model = ( + urdf.stream(include_joint_transforms=True).drop( # rest-pose joint transforms too + content="/robot/**/collision_geometries/**" + ) # unless you need collision meshes +) +``` + +Recolor a robot's meshes with a `MutateLens` on `Asset3D:albedo_factor` (see `rerun-chunk-processing`). + +**2. Solve forward kinematics.** Given joint names and the matching joint values, it returns one `rerun.urdf.JointTransformBatch` per input row. Each batch is a list of per-joint entries with `parent_frame`, `child_frame`, `translation`, and +`quaternion`: + +```python +batches = urdf.compute_joint_transform_batches(names, values, clamp=False) +# names, values: pyarrow arrays, one list per timestamp (names aligned to values) +# clamp=True clamps out-of-limit values and warns, useful while debugging units +``` + +You will almost always run this inside a stream so it stays columnar and lazy, +via the two-lens pattern: derive the batch, then `scatter=True` it into +`Transform3D.descriptor_translation/quaternion/parent_frame/child_frame`. The +full shape is the "Minimal shape" section below; the `robot_data_preprocessing` +example (References) is a complete working instance. The only URDF-specific +part is the `compute_joint_transform_batches` call inside the first lens; +everything else is generic stream mechanics (`rerun-chunk-processing`). + +Merge the model stream and the FK stream, `collect(optimize=OBJECT_STORE)`, and +`write_rrd(..., recording_id=)`. The `recording_id` must equal the +base segment id or the layer never attaches; `application_id` is discarded on +registration. + +## Mapping joint state to URDF joints (you supply this; the data will not) + +`compute_joint_transform_batches` is only as right as the `names`/`values` you +hand it, and the mapping is not in the URDF. Three things go wrong silently: + +- **Order.** Build an explicit `names` array aligned to the `values` you read. + Never assume your message's field order matches the URDF's `` order. +- **Count.** The URDF's non-`fixed` joint count rarely equals your reported value + count. A gripper sent as one value is often two prismatic joints in the URDF; + a mimic joint may be omitted from the message. Reconcile explicitly, and use + the API to do it: iterate `urdf.joints()`, partition by `joint_type` and + `mimic`. A joint with `mimic` set derives its value from the driver joint as + `driver * multiplier + offset`; feed it that, not a message field. Confirm + the count against `urdf.joints()`, not the message length. +- **Units.** URDF joints are radians (revolute) and meters (prismatic). Convert + if your source differs. + +Get any of these wrong and FK runs and writes a confident, wrong pose. + +## Make the joint states readable first + +Where the joint values come from is not this skill's problem, but a dead joint +source produces an empty FK layer **with no error**: a source path that +matched nothing, a decoder that yielded zero rows, or a reader that dropped the +message silently. Whatever the source, confirm the joint-state stream yields +rows before debugging FK. The importer skill for your source format covers its +own empty-stream failure modes. + +## How transforms compose (reason about this before logging anything) + +Rerun resolves a pose by chaining transforms from a frame up to a root. There +are two ways an edge in that chain gets defined, and a URDF ingest mixes both: + +- **By entity-path hierarchy.** A `Transform3D` on `/a/b` with no frame names is + the transform of `/a/b` relative to its parent path `/a`. Composition follows + the entity tree. +- **By explicit frame graph.** A `Transform3D` that carries `parent_frame` and + `child_frame` defines an edge between two **named frames**, independent of + where in the entity tree it is logged. URDF FK uses this: every joint + transform names its parent and child link frames. + +So a URDF ingest is a graph of named frames. An edge exists only if some +`Transform3D` names that exact `parent_frame -> child_frame` pair. A frame with +no incoming edge is a root. The viewer renders every root at the world origin, +which is why two unconnected robots silently overlap instead of erroring. + +## Resolving the transform forest (the part the data cannot always give you) + +A URDF is **one tree rooted at its base link**. FK and fixed joints supply every +edge inside that tree. A real scene is a **forest** of roots the URDF never +connects: a world or scene frame, each robot's base, every camera or sensor +frame. The edges that join those roots come from calibration (extrinsics in a +sidecar, a TF static publisher, a hand-eye result), not from the URDF, and some +are simply absent. A single connected tree is not always solvable. Resolve it +deliberately: + +1. **Enumerate every frame.** Iterate `urdf.joints()` and collect the + `parent_link`/`child_link` pairs (the in-tree edges, frame-prefixed); + `urdf.root_link()` is that URDF's root. Add every sensor/world frame the + scene needs (from the `rerun-data-model` table). Decide the one intended + root. +2. **Classify each edge by source.** In-URDF edges (FK joints, `fixed` joints) + come from the URDF plus joint values. Inter-root edges (root to each robot + base, root to each fixed sensor, an arm link to a wrist-mounted camera) come + from calibration and you must log them yourself. +3. **Compose fixed chains from the URDF** when you need the transform between two + links joined only by `fixed` joints (a camera bracket, a tool mount): walk + parent links across `fixed` joints via `urdf.joints()`, turning each joint's + `origin_xyz`/`origin_rpy` into a homogeneous matrix and multiplying along + the chain. If the walk cannot reach + the target link, **stop and say so** ("no fixed chain from A to B; stuck at + C"). A broken chain is a wrong pose, not a missing one. +4. **Build the edge set and find the roots.** Collect every `parent_frame -> +child_frame` pair you will log (URDF + calibration). Walk parents from each + frame; any frame that does not reach the intended root is an unconnected root + and names a **missing edge**. This is a pure graph check you can run before + writing the RRD. +5. **Resolve every missing edge, or fail loudly.** For each one: + - If calibration supplies the transform, log it (next section). + - If the data does not, the tree is unsolvable. Do **not** leave the frame + disconnected (it collapses onto the origin and reads as one merged scene). + Either abort and name the missing edge, or log identity and emit a loud + warning naming the assumed edge. State which you did. +6. **Match frame names across sources.** FK-derived `parent_frame`/`child_frame` + must equal the names `urdf.stream()` emits for the static geometry, and your + calibration edges must use those same names, or links float off the mesh. The + `frame_prefix` is what keeps them identical; reuse it everywhere. + +A correct ingest has exactly one root, and a path from every frame to it. + +## Logging a connection correctly + +A connecting edge is a **static** `Transform3D` carrying the bridging frame +names. Static (no time index) so it holds for the whole recording; the frame +names, not the entity path, are what create the graph edge. This +`Chunk.from_columns` is the rare **sidecar exception** — a calibration transform +no reader or FK lens can produce; do not generalize it to transforms a reader +emits (a `frame_transforms` topic → `Transform3D`) or that FK derives: + +```python +import rerun as rr +from rerun.experimental import Chunk, LazyChunkStream + +# world -> this robot's base, from your calibration (translation + xyzw quaternion) +edge = Chunk.from_columns( + "/world/robot_base", # any sensible bridging entity path + indexes=[], # no index == static + columns=rr.Transform3D.columns( + translation=[translation], + quaternion=[quaternion_xyzw], + parent_frame=["world"], # must match the root frame name + child_frame=["arm_base_link"], # must match the URDF root frame (prefixed) + ), +) +edges = LazyChunkStream.from_iter([edge]) # merge alongside model + FK streams +``` + +Log a fixed-chain result (step 3) the same way, with `parent_frame` and +`child_frame` set to the two link frames the chain spans. Merge all edge chunks +into the same recording as the model and FK streams so they share the graph. + +## Minimal shape (one robot, generic joint source) + +```python +import rerun as rr +from rerun.experimental import DeriveLens, LazyChunkStream, OptimizationProfile, Selector +from rerun.urdf import UrdfTree + +urdf = UrdfTree.from_file_path(urdf_path, entity_path_prefix="robot", static_transform_entity_path="robot/tf_static") +model = urdf.stream(include_joint_transforms=True).drop(content="/robot/**/collision_geometries/**") + +joints = source_joint_state_stream() # your reader; one message column of names+values +fk = ( + joints + .lenses( + DeriveLens(JOINT_MSG_COMPONENT, output_entity="/tmp/batches").to_component( + "rerun.urdf.JointTransformBatch", + Selector(".").pipe(lambda msgs: urdf.compute_joint_transform_batches(read_names(msgs), read_values(msgs))), + ), + content=JOINT_SOURCE_PATH, + output_mode="forward_all", + ) + .lenses( + DeriveLens("rerun.urdf.JointTransformBatch", output_entity="/robot/transforms", scatter=True) + .to_component(rr.Transform3D.descriptor_translation(), Selector("[].translation")) + .to_component(rr.Transform3D.descriptor_quaternion(), Selector("[].quaternion")) + .to_component(rr.Transform3D.descriptor_parent_frame(), Selector("[].parent_frame")) + .to_component(rr.Transform3D.descriptor_child_frame(), Selector("[].child_frame")), + content="/tmp/batches", + output_mode="drop_unmatched", + ) + .filter(content="/robot/transforms") +) + +LazyChunkStream.merge(model, fk).collect(optimize=OptimizationProfile.OBJECT_STORE).write_rrd( + out_path, + application_id="urdf", + recording_id=segment_id, +) +``` + +`read_names`, `read_values`, `JOINT_MSG_COMPONENT`, and `JOINT_SOURCE_PATH` are +the only data-specific pieces, and the mapping section above is what makes them +correct. + +## Gotchas that cause real failures + +1. Empty layer, no error: dead joint-state source (decoded to zero rows; see the importer skill for your format) or wrong `JOINT_SOURCE_PATH`/component name. +2. Confident wrong pose: joint count, order, or units wrong. +3. Layer writes but never attaches: `recording_id != segment_id`. +4. Frames collide: two robots sharing a `frame_prefix`. +5. Scene looks merged at the origin: unconnected roots logged as identity without a calibration edge. +6. Catalog ingest rejects or misorders chunks: `OBJECT_STORE` optimization skipped. + +## References + +- `https://github.com/rerun-io/rerun/tree/main/examples/python/robot_data_preprocessing` + (FK two-lens pattern, two robots + scene URDFs, prefixes, recoloring, + calibration offsets) +- `https://github.com/rerun-io/rerun/tree/main/examples/python/animated_urdf` + (classic logging API: `log_urdf_to_recording`, per-joint `compute_transform`) +- `rerun-data-model` (the mapping table this skill consumes) +- the importer skill for your joint-state source format (making the source readable) +- `rerun-chunk-processing` (lens/stream, write/optimize mechanics) diff --git a/tests/assets/mcap/trossen_transfer_cube.mcap b/tests/assets/mcap/trossen_transfer_cube.mcap index 124cbd4f934b..4693ebe60e00 100644 --- a/tests/assets/mcap/trossen_transfer_cube.mcap +++ b/tests/assets/mcap/trossen_transfer_cube.mcap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:518f0d6702fdfa4ca76d5d5e0c4673da645d8110271dc3469e55a3eeebf0c275 -size 16613133 +oid sha256:3dac2fb92794975f6788302ca3dd47c5c9b82552d3f5272e42d91649dc1d9d77 +size 16336096 diff --git a/tests/assets/rrd/examples/arkit_scenes.rrd b/tests/assets/rrd/examples/arkit_scenes.rrd index f7c652398e94..febb3626452b 100644 --- a/tests/assets/rrd/examples/arkit_scenes.rrd +++ b/tests/assets/rrd/examples/arkit_scenes.rrd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a6e5cd5bf6f906a6de3790594b051fff91c2b7f71bfe5e59c01c20a27c11cf0 -size 41032192 +oid sha256:d8292c5df12ba56d6f59759adb28668addbec4e9191e6aa4711184f4f94fb8f7 +size 41407791 diff --git a/tests/assets/rrd/generate-compatibility-rrds.sh b/tests/assets/rrd/generate-compatibility-rrds.sh index 97b2da0a96b6..723e0896edd8 100755 --- a/tests/assets/rrd/generate-compatibility-rrds.sh +++ b/tests/assets/rrd/generate-compatibility-rrds.sh @@ -11,7 +11,7 @@ DEST_DIR="tests/assets/rrd" # TODO(emilk): only update missing files echo "Generating example .rrd files…" -pixi run build-examples rrd --install --channel main ${DEST_DIR}/examples +pixi run build-examples rrd --install --channel main "${DEST_DIR}/examples" echo "Generating snippet .rrd files…" pixi run uvpy docs/snippets/compare_snippet_output.py --no-py --no-cpp --write-missing-backward-assets diff --git a/tests/assets/rrd/snippets/archetypes/ellipses2d_batch.rrd b/tests/assets/rrd/snippets/archetypes/ellipses2d_batch.rrd new file mode 100644 index 000000000000..6b9280559460 --- /dev/null +++ b/tests/assets/rrd/snippets/archetypes/ellipses2d_batch.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f817825f42777034fd11d307766322ed334d251b33dde8084b5cf1f10313965 +size 11653 diff --git a/tests/assets/rrd/snippets/archetypes/ellipses2d_simple.rrd b/tests/assets/rrd/snippets/archetypes/ellipses2d_simple.rrd new file mode 100644 index 000000000000..c22a180182c2 --- /dev/null +++ b/tests/assets/rrd/snippets/archetypes/ellipses2d_simple.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:613f4258ca9b70fbb027286ebb054af3a8b6c0f3866df06ccee3084c0ac8e928 +size 7869 diff --git a/tests/assets/rrd/snippets/archetypes/grid_map_pose.rrd b/tests/assets/rrd/snippets/archetypes/grid_map_pose.rrd new file mode 100644 index 000000000000..b059fc62e235 --- /dev/null +++ b/tests/assets/rrd/snippets/archetypes/grid_map_pose.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75dbdfe35f179fc5a36688d117a30691020f288d7a8204aec949090ce5c563ec +size 90909 diff --git a/tests/assets/rrd/snippets/archetypes/line_strips3d_time_window.rrd b/tests/assets/rrd/snippets/archetypes/line_strips3d_time_window.rrd new file mode 100644 index 000000000000..b757a4dbbfd0 --- /dev/null +++ b/tests/assets/rrd/snippets/archetypes/line_strips3d_time_window.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:608fd343ad6adb207dd1adcb2bcb01a40f81a640a29f5626a56d9724e30dab39 +size 1192863 diff --git a/tests/assets/rrd/snippets/archetypes/points3d_partial_updates.rrd b/tests/assets/rrd/snippets/archetypes/points3d_partial_updates.rrd index 5cf97bd74da1..db8de53cb8d4 100644 --- a/tests/assets/rrd/snippets/archetypes/points3d_partial_updates.rrd +++ b/tests/assets/rrd/snippets/archetypes/points3d_partial_updates.rrd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41310da7c9a699976ffecc2a8eb8fb5f4ec59211f4857f43474eb3960f2ca853 -size 36788 +oid sha256:2d0bbd205535ce3fd949d630a1334a7966d83ff136a354577381ddc5783bd7e7 +size 32572 diff --git a/tests/assets/rrd/snippets/archetypes/state_change.rrd b/tests/assets/rrd/snippets/archetypes/state_change.rrd new file mode 100644 index 000000000000..12dc4507e3d0 --- /dev/null +++ b/tests/assets/rrd/snippets/archetypes/state_change.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79f2eb6ca04c1878e7923a75d8473964596a991eb67c22560b867ba606984f68 +size 9824 diff --git a/tests/assets/rrd/snippets/archetypes/state_configuration.rrd b/tests/assets/rrd/snippets/archetypes/state_configuration.rrd new file mode 100644 index 000000000000..687fad69cfb4 --- /dev/null +++ b/tests/assets/rrd/snippets/archetypes/state_configuration.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d6b45f43fa84410915305cc10e45aa940c3dc3e5bb66f2ea8711c9f6e21182c +size 12999 diff --git a/tests/assets/rrd/snippets/archetypes/status.rrd b/tests/assets/rrd/snippets/archetypes/status.rrd deleted file mode 100644 index 3e2f2897a1ca..000000000000 --- a/tests/assets/rrd/snippets/archetypes/status.rrd +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:46f1548170e813367cc223bab7ba837b29b34e9685f95b39471a61210af8c8f7 -size 9711 diff --git a/tests/assets/rrd/snippets/archetypes/voxel_grid_map_simple.rrd b/tests/assets/rrd/snippets/archetypes/voxel_grid_map_simple.rrd new file mode 100644 index 000000000000..4eb19714912e --- /dev/null +++ b/tests/assets/rrd/snippets/archetypes/voxel_grid_map_simple.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59dbabff414513d77a011c4e3f32ac115ba78f6f983003b7d67b74e26fe7ebeb +size 11201 diff --git a/tests/assets/rrd/snippets/concepts/lenses.rrd b/tests/assets/rrd/snippets/concepts/lenses.rrd index 379758fcf56f..53469b31da2a 100644 --- a/tests/assets/rrd/snippets/concepts/lenses.rrd +++ b/tests/assets/rrd/snippets/concepts/lenses.rrd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2738f0ecb7f7bfff7360f6fe4ae33b551279b123b5cba6188765d6c01b63ce43 -size 11168 +oid sha256:50ca5ee61c621352ebc16564b4efaf0c828d4529f143fea88e32a39014f11e7e +size 11473 diff --git a/tests/assets/rrd/snippets/howto/load_mcap.rrd b/tests/assets/rrd/snippets/howto/load_mcap.rrd index df1b81719de2..59dc5464962a 100644 --- a/tests/assets/rrd/snippets/howto/load_mcap.rrd +++ b/tests/assets/rrd/snippets/howto/load_mcap.rrd @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:294d738ced2aec35f4b119bc9f4bdc9da65d1cbe650cfaed9d12d599a9c6d8ca -size 19398128 +oid sha256:05ec0a3db69c2d844258a0bf968569aae778c1dca2787c0bec1798eba56f38d3 +size 19386366 diff --git a/tests/assets/rrd/snippets/howto/state_remapping.rrd b/tests/assets/rrd/snippets/howto/state_remapping.rrd new file mode 100644 index 000000000000..2432f923c019 --- /dev/null +++ b/tests/assets/rrd/snippets/howto/state_remapping.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:318bcdf1f60c36bb0cebd41e0b4b08c317adae29e7ec8c299f70b1de959f95dc +size 35968 diff --git a/tests/assets/rrd/snippets/howto/state_timeline.rrd b/tests/assets/rrd/snippets/howto/state_timeline.rrd new file mode 100644 index 000000000000..562d71ac33e1 --- /dev/null +++ b/tests/assets/rrd/snippets/howto/state_timeline.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:793fb56e0eb49ed3719ff8ab9b192692473adb1eadd407bc5a49fea4ed70643b +size 16241 diff --git a/tests/assets/rrd/snippets/tutorials/getting_started_log.rrd b/tests/assets/rrd/snippets/tutorials/getting_started_log.rrd new file mode 100644 index 000000000000..41ee272f1720 --- /dev/null +++ b/tests/assets/rrd/snippets/tutorials/getting_started_log.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bb8f7a14ec81567c729c511d709c703e8c521243d4330c755c4ab41447e977d +size 31557 diff --git a/tests/assets/rrd/snippets/views/state_timeline.rrd b/tests/assets/rrd/snippets/views/state_timeline.rrd new file mode 100644 index 000000000000..f667a29f800d --- /dev/null +++ b/tests/assets/rrd/snippets/views/state_timeline.rrd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7927b8acccdedd40350855c7d35e9243bd416bb504a6ebf42fab5a754b577a16 +size 37531 diff --git a/tests/assets/rrd/snippets/views/status.rrd b/tests/assets/rrd/snippets/views/status.rrd deleted file mode 100644 index 87fb67ad44d4..000000000000 --- a/tests/assets/rrd/snippets/views/status.rrd +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:980d8c3a08f65ecd2757217fe7d93c114030d5d5225e2772d6c313890369f04a -size 37298 diff --git a/tests/assets/video/Big_Buck_Bunny_1080_1s_vp8.mp4 b/tests/assets/video/Big_Buck_Bunny_1080_1s_vp8.mp4 new file mode 100644 index 000000000000..614138aac3bd --- /dev/null +++ b/tests/assets/video/Big_Buck_Bunny_1080_1s_vp8.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eda7d14a055c11e673c45a590c363221a75ebbff9900f0b8258f77fce110d19e +size 408596 diff --git a/tests/assets/video/mpeg4_part2.mp4 b/tests/assets/video/mpeg4_part2.mp4 new file mode 100644 index 000000000000..c0a47abef4e3 --- /dev/null +++ b/tests/assets/video/mpeg4_part2.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c222f2e2ddf56af05896f59f6fa64f66ef5aac6b54ae26152546863f785db5ac +size 2811 diff --git a/tests/python/gc_stress/many_large_many_rows_recordings.py b/tests/python/gc_stress/many_large_many_rows_recordings.py index 2fedc30b382e..d6615a8d885b 100644 --- a/tests/python/gc_stress/many_large_many_rows_recordings.py +++ b/tests/python/gc_stress/many_large_many_rows_recordings.py @@ -6,7 +6,7 @@ Usage: - Start a Rerun Viewer in release mode with 2GiB of memory limit: `cargo r -p rerun-cli --release --no-default-features --features native_viewer -- --memory-limit 2GiB` -- Open the memory panel to see what's going on. +- Open the dev panel to see what's going on. - Run this script. - You should see recordings coming in and going out in a ringbuffer-like rolling fashion. """ diff --git a/tests/python/gc_stress/many_large_single_row_recordings.py b/tests/python/gc_stress/many_large_single_row_recordings.py index ddc9fb47013d..6b2dacc5f8c7 100644 --- a/tests/python/gc_stress/many_large_single_row_recordings.py +++ b/tests/python/gc_stress/many_large_single_row_recordings.py @@ -6,7 +6,7 @@ Usage: - Start a Rerun Viewer in release mode with 2GiB of memory limit: `cargo r -p rerun-cli --release --no-default-features --features native_viewer -- --memory-limit 2GiB` -- Open the memory panel to see what's going on. +- Open the dev panel to see what's going on. - Run this script. - You should see recordings coming in and going out in a ringbuffer-like rolling fashion. """ diff --git a/tests/python/gc_stress/many_medium_sized_many_rows_recordings.py b/tests/python/gc_stress/many_medium_sized_many_rows_recordings.py index 7df05c12f801..b0d75fcfd208 100644 --- a/tests/python/gc_stress/many_medium_sized_many_rows_recordings.py +++ b/tests/python/gc_stress/many_medium_sized_many_rows_recordings.py @@ -6,7 +6,7 @@ Usage: - Start a Rerun Viewer in release mode with 500MiB of memory limit: `cargo r -p rerun-cli --release --no-default-features --features native_viewer -- --memory-limit 500MiB` -- Open the memory panel to see what's going on. +- Open the dev panel to see what's going on. - Run this script. - You should see recordings coming in and going out in a ringbuffer-like rolling fashion. """ diff --git a/tests/python/gc_stress/many_medium_sized_single_row_recordings.py b/tests/python/gc_stress/many_medium_sized_single_row_recordings.py index 192d6ce101f4..2bd44749b36a 100644 --- a/tests/python/gc_stress/many_medium_sized_single_row_recordings.py +++ b/tests/python/gc_stress/many_medium_sized_single_row_recordings.py @@ -6,7 +6,7 @@ Usage: - Start a Rerun Viewer in release mode with 200MiB of memory limit: `cargo r -p rerun-cli --release --no-default-features --features native_viewer -- --memory-limit 200MiB` -- Open the memory panel to see what's going on. +- Open the dev panel to see what's going on. - Run this script. - You should see recordings coming in and going out in a ringbuffer-like rolling fashion. """ diff --git a/tests/python/release_checklist/README.md b/tests/python/release_checklist/README.md index cb26269e5843..31301c65cd6a 100644 --- a/tests/python/release_checklist/README.md +++ b/tests/python/release_checklist/README.md @@ -1,4 +1,4 @@ -![Rerun.io](https://user-images.githubusercontent.com/1148717/218142418-1d320929-6b7a-486e-8277-fbeef2432529.png) +![Rerun.io](https://static.rerun.io/d0f5443d4803cac65c73fcc064936c09f5e7f208_rerun_banner.png) # Interactive release checklist Welcome to the release checklist. diff --git a/tests/python/release_checklist/check_blueprint_bw_compat.py b/tests/python/release_checklist/check_blueprint_bw_compat.py deleted file mode 100644 index 329938677812..000000000000 --- a/tests/python/release_checklist/check_blueprint_bw_compat.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import os -from argparse import Namespace -from uuid import uuid4 - -import rerun as rr -import rerun.blueprint as rrb - -README = """ -# Blueprint backwards compatibility - -Updating your Rerun version should never result in indecipherable Blueprint errors. - -Even in the case of irrecoverable ABI changes, we should make sure that the end-user has -a pleasant experience (i.e., in the worst case, the blueprint is ignored, with a warning). - -## Checks - -#### Step 1: instantiate blueprints from previous release - -* Install the latest official Rerun release in a virtual env: `pip install --force rerun-sdk`. -* Clear all your blueprint data: `rerun reset`. -* Start the old `rerun` (check Menu > About that you are indeed running the old version) -* Open all demos available in the welcome screen. -* Play around long enough for the blueprints to be saved to disk (a few seconds). - * You can check whether that's the case by listing the contents of: - - Linux: `/home/UserName/.local/share/rerun` - - macOS: `/Users/UserName/Library/Application Support/rerun` - - Windows: `C:\\Users\\UserName\\AppData\\Roaming\\rerun` - - Web: local storage - -#### Step 2: load blueprints from previous release into new one - -* Install the about-to-released Rerun version in a virtual env: `pip install --force rerun-sdk=whatever`. -* Start the new Rerun (check in Menu > About that you are indeed running the new version) -* Open all demos available in the welcome screen. - -#### Step 3: does it look okay? - -There are two acceptable outcomes here: -- The blueprints worked as-is. 👍 -- The blueprints completely or partially failed to load, but a clear warning explaining what's - going on was shown to the user. - -Anything else is a failure (e.g. deserialization failure spam). -""" - - -def log_readme() -> None: - rr.log("readme", rr.TextDocument(README, media_type=rr.MediaType.MARKDOWN), static=True) - - -def run(args: Namespace) -> None: - rr.script_setup(args, f"{os.path.basename(__file__)}", recording_id=uuid4()) - - rr.send_blueprint(rrb.Grid(rrb.TextDocumentView(origin="readme")), make_active=True, make_default=True) - - log_readme() - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="Interactive release checklist") - rr.script_add_args(parser) - args = parser.parse_args() - run(args) diff --git a/tests/rust/log_benchmark/Cargo.toml b/tests/rust/log_benchmark/Cargo.toml index f9009e8ddefa..2b674bb2db7c 100644 --- a/tests/rust/log_benchmark/Cargo.toml +++ b/tests/rust/log_benchmark/Cargo.toml @@ -17,4 +17,5 @@ anyhow.workspace = true clap = { workspace = true, features = ["derive"] } emath.workspace = true glam.workspace = true +itertools.workspace = true rand.workspace = true diff --git a/tests/rust/log_benchmark/src/main.rs b/tests/rust/log_benchmark/src/main.rs index 9fa6c828b2bd..c3c5fc027aca 100644 --- a/tests/rust/log_benchmark/src/main.rs +++ b/tests/rust/log_benchmark/src/main.rs @@ -142,22 +142,23 @@ fn main() -> anyhow::Result<()> { // Being able to log fast isn't particularly useful if the data happens to be corrupt at the // other end, so make sure we can encode/decode everything that was logged. if check && let Some(storage) = storage { + use itertools::Itertools as _; use rerun::external::re_log_encoding; use rerun::external::re_log_encoding::ToTransport as _; - let msgs: anyhow::Result> = storage + let msgs: Vec<_> = storage .take() .into_iter() - .map(|msg| Ok(msg.to_transport(re_log_encoding::rrd::Compression::LZ4)?)) - .collect(); + .map(|msg| anyhow::Ok(msg.to_transport(re_log_encoding::rrd::Compression::LZ4)?)) + .try_collect()?; use rerun::external::re_log_encoding::ToApplication as _; let mut app_id_injector = re_log_encoding::DummyApplicationIdInjector::new("dummy"); - let msgs: anyhow::Result> = msgs? + let msgs: Vec<_> = msgs .into_iter() - .map(|msg| Ok(msg.to_application((&mut app_id_injector, None))?)) - .collect(); + .map(|msg| anyhow::Ok(msg.to_application((&mut app_id_injector, None))?)) + .try_collect()?; - let _ = msgs?; + let _ = msgs; } Ok(()) diff --git a/tests/rust/re_integration_test/Cargo.toml b/tests/rust/re_integration_test/Cargo.toml index b4012aef9c59..a3f44aaa7ef0 100644 --- a/tests/rust/re_integration_test/Cargo.toml +++ b/tests/rust/re_integration_test/Cargo.toml @@ -23,7 +23,6 @@ re_viewer = { workspace = true, features = ["testing", "map_view"] } re_viewer_context.workspace = true re_viewport_blueprint.workspace = true -arrow.workspace = true egui_kittest.workspace = true egui_tiles.workspace = true egui.workspace = true @@ -34,15 +33,24 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [dev-dependencies] re_dataframe_ui.workspace = true +re_datafusion.workspace = true +re_log_types.workspace = true re_test_context.workspace = true re_view_bar_chart.workspace = true re_view_tensor.workspace = true re_view_text_document.workspace = true +re_view_state_timeline.workspace = true re_view_text_log.workspace = true re_view_time_series.workspace = true +arrow.workspace = true +datafusion.workspace = true +directories.workspace = true insta.workspace = true ndarray.workspace = true +reqwest = { workspace = true, features = ["json"] } +serde.workspace = true +uuid.workspace = true [lints] workspace = true diff --git a/tests/rust/re_integration_test/src/kittest_harness_ext.rs b/tests/rust/re_integration_test/src/kittest_harness_ext.rs index bff55b4fa750..d436eead539e 100644 --- a/tests/rust/re_integration_test/src/kittest_harness_ext.rs +++ b/tests/rust/re_integration_test/src/kittest_harness_ext.rs @@ -15,7 +15,7 @@ use re_sdk::{ use re_viewer::external::re_chunk::{ChunkBuilder, LatestAtQuery}; use re_viewer::external::re_entity_db::EntityDb; use re_viewer::external::re_sdk_types; -use re_viewer::external::re_viewer_context::{self, ViewerContext, blueprint_timeline}; +use re_viewer::external::re_viewer_context::{self, AppContext, ViewerContext, blueprint_timeline}; use re_viewer::viewer_test_utils::AppTestingExt as _; use re_viewer::{SystemCommand, SystemCommandSender as _}; use re_viewer_context::{ContainerId, Route}; @@ -25,6 +25,37 @@ use re_viewport_blueprint::ViewportBlueprint; // use crate::GetSection as _; use crate::ViewerSection; +/// Returns true if `s` contains a `YYYY-MM-DD` or `HH:MM:SS` substring. +/// +/// Both date and time use jiff's zero-padded `%Y-%m-%d` / `%H:%M:%S` +/// format, so fixed 10/8-byte sliding windows are sufficient. +fn contains_date_or_time_pattern(s: &str) -> bool { + let bytes = s.as_bytes(); + let has_date = bytes.windows(10).any(|w| { + w[0].is_ascii_digit() + && w[1].is_ascii_digit() + && w[2].is_ascii_digit() + && w[3].is_ascii_digit() + && w[4] == b'-' + && w[5].is_ascii_digit() + && w[6].is_ascii_digit() + && w[7] == b'-' + && w[8].is_ascii_digit() + && w[9].is_ascii_digit() + }); + let has_time = bytes.windows(8).any(|w| { + w[0].is_ascii_digit() + && w[1].is_ascii_digit() + && w[2] == b':' + && w[3].is_ascii_digit() + && w[4].is_ascii_digit() + && w[5] == b':' + && w[6].is_ascii_digit() + && w[7].is_ascii_digit() + }); + has_date || has_time +} + // Kittest harness utilities specific to the Rerun app. pub trait HarnessExt<'h> { // Initializes the chunk store with a new, empty recording and blueprint. @@ -41,6 +72,13 @@ pub trait HarnessExt<'h> { func: impl FnOnce(&ViewerContext<'_>) -> R + 'static, ) -> R; + // Runs a function with the `AppContext` generated by the actual Rerun application. + // Prefer this over `run_with_viewer_context` unless you genuinely need the `ViewerContext`. + fn run_with_app_context( + &mut self, + func: impl FnOnce(&AppContext<'_>) -> R + 'static, + ) -> R; + // Removes all views and containers from the current blueprint. fn clear_current_blueprint(&mut self); @@ -81,6 +119,15 @@ pub trait HarnessExt<'h> { // Takes a snapshot of the current app state with good-enough snapshot options. fn snapshot_app(&mut self, snapshot_name: &str); + /// Mask every accessibility node whose label or value contains a + /// `YYYY-MM-DD` or `HH:MM:SS` substring. + /// + /// Useful before snapshotting UIs that display recording timestamps: + /// the rendered text drifts as the calendar day rolls over and as the + /// test machine's timezone changes, which would silently break + /// snapshots over time. + fn mask_dates(&mut self); + // Prints the current viewer state. fn debug_viewer_state(&mut self); @@ -127,6 +174,11 @@ pub trait HarnessExt<'h> { self.section("_streams_tree") } + // The viewer section whose root node is the recording panel (the "Sources" panel). + fn recording_panel<'a>(&'a mut self) -> ViewerSection<'a, 'h> { + self.section("_recording_panel") + } + // The viewer section whose root node is the selection panel. fn selection_panel<'a>(&'a mut self) -> ViewerSection<'a, 'h> { self.section("_selection_panel") @@ -204,7 +256,7 @@ impl<'h> HarnessExt<'h> for egui_kittest::Harness<'h, re_viewer::App> { let result = Arc::new(Mutex::new(None)); let result_clone = Arc::clone(&result); self.state_mut() - .testonly_set_test_hook(Box::new(move |viewer_context| { + .testonly_set_recording_test_hook(Box::new(move |viewer_context| { *result_clone.lock() = Some(func(viewer_context)); })); self.run_ok(); @@ -214,6 +266,23 @@ impl<'h> HarnessExt<'h> for egui_kittest::Harness<'h, re_viewer::App> { .expect("test hook should have been called") } + fn run_with_app_context( + &mut self, + func: impl FnOnce(&AppContext<'_>) -> R + 'static, + ) -> R { + let result = Arc::new(Mutex::new(None)); + let result_clone = Arc::clone(&result); + self.state_mut() + .testonly_set_app_test_hook(Box::new(move |app_context| { + *result_clone.lock() = Some(func(app_context)); + })); + self.run_ok(); + result + .lock() + .take() + .expect("app test hook should have been called") + } + fn log_entity( &mut self, entity_path: impl Into, @@ -230,7 +299,6 @@ impl<'h> HarnessExt<'h> for egui_kittest::Harness<'h, re_viewer::App> { store_hub .add_chunk_for_tests(&recording_id, &chunk) .expect("chunk should be successfully added"); - self.run_ok(); } fn init_recording(&mut self) { @@ -391,9 +459,7 @@ impl<'h> HarnessExt<'h> for egui_kittest::Harness<'h, re_viewer::App> { } fn cursor_icon(&mut self) -> egui::CursorIcon { - self.run_with_viewer_context(|viewer_context| { - viewer_context.egui_ctx().output(|o| o.cursor_icon) - }) + self.run_with_app_context(|app_context| app_context.egui_ctx.output(|o| o.cursor_icon)) } fn debug_viewer_state(&mut self) { @@ -425,6 +491,24 @@ impl<'h> HarnessExt<'h> for egui_kittest::Harness<'h, re_viewer::App> { self.snapshot(snapshot_name); } + fn mask_dates(&mut self) { + let rects: Vec = self + .query_all_by(|node| { + node.role() != Role::TextRun // Don't mask labels twice + && (node + .label() + .is_some_and(|l| contains_date_or_time_pattern(&l)) + || node + .value() + .is_some_and(|l| contains_date_or_time_pattern(&l))) + }) + .map(|node| node.rect()) + .collect(); + for rect in rects { + self.mask(rect); + } + } + fn add_blueprint_container( &mut self, kind: egui_tiles::ContainerKind, diff --git a/tests/rust/re_integration_test/src/lib.rs b/tests/rust/re_integration_test/src/lib.rs index 8bc0b7eca6a4..da74e45091e8 100644 --- a/tests/rust/re_integration_test/src/lib.rs +++ b/tests/rust/re_integration_test/src/lib.rs @@ -7,10 +7,11 @@ mod viewer_section; use std::net::TcpListener; pub use kittest_harness_ext::HarnessExt; -use re_protos::common::v1alpha1::SegmentId; use re_redap_client::{ApiResult, ConnectionClient, ConnectionRegistry}; +use re_sdk_types::SegmentId; use re_server::ServerHandle; use re_uri::external::url::Host; +pub use test_data::register_table_blueprint; // pub use viewer_section::GetSection; pub use viewer_section::ViewerSection; @@ -69,6 +70,30 @@ impl TestServer { (self, segment_id) } + /// Register `count` recordings with time-invariant data in a fresh dataset, suitable for + /// stable segment-preview snapshots. Returns the segment ids in registration order. + pub async fn with_static_preview_data( + self, + dataset_name: &str, + dataset_id: &str, + recording_id_prefix: &str, + count: usize, + ) -> (Self, Vec) { + let segment_ids = { + let mut client = self.client().await.expect("Failed to connect"); + test_data::load_static_preview_data( + &mut client, + dataset_name, + dataset_id, + recording_id_prefix, + count, + ) + .await + .expect("Failed to load static preview data") + }; + (self, segment_ids) + } + pub fn port(&self) -> u16 { self.port } diff --git a/tests/rust/re_integration_test/src/test_data.rs b/tests/rust/re_integration_test/src/test_data.rs index 76c95740099b..28a8baa68030 100644 --- a/tests/rust/re_integration_test/src/test_data.rs +++ b/tests/rust/re_integration_test/src/test_data.rs @@ -4,16 +4,18 @@ use std::time::Duration; use futures::StreamExt as _; -use re_protos::cloud::v1alpha1::QueryTasksResponse; -use re_protos::cloud::v1alpha1::ext::{DataSource, QueryTasksOnCompletionResponse}; +use re_protos::cloud::v1alpha1::ext as cloud_ext; +use re_protos::cloud::v1alpha1::ext::{ + DataSource, QueryTasksOnCompletionResponse, TableDetails, TableEntry, +}; use re_protos::cloud::v1alpha1::{EntryFilter, EntryKind}; -use re_protos::common::v1alpha1::SegmentId; use re_protos::common::v1alpha1::ext::IfDuplicateBehavior; use re_redap_client::ConnectionClient; use re_sdk::external::re_tuid; use re_sdk::time::TimeType; use re_sdk::{RecordingStreamBuilder, TimeCell}; -use re_viewer::external::re_sdk_types::archetypes; +use re_sdk_types::SegmentId; +use re_viewer::external::re_sdk_types::{archetypes, components::Color}; pub async fn load_test_data(mut client: ConnectionClient) -> Result> { load_test_data_with_name( @@ -31,12 +33,7 @@ pub async fn load_test_data_with_name( dataset_id_str: &str, recording_id: &str, ) -> Result> { - let path = { - let path = tempfile::NamedTempFile::new()?; - let stream = RecordingStreamBuilder::new("rerun_example_integration_test") - .recording_id(recording_id) - .save(path.path())?; - + let path = recording_rrd(recording_id, |stream| { for x in 0..20 { stream.set_time("test_time", TimeCell::new(TimeType::Sequence, x)); stream @@ -46,79 +43,215 @@ pub async fn load_test_data_with_name( ) .expect("Failed to log points 3D"); } - - stream.flush_with_timeout(Duration::from_secs(60))?; - - path - }; + })?; // Make sure that we have an entries table. let entries_table = client - .find_entries(EntryFilter::default().with_entry_kind(EntryKind::Table)) + .find_entries(EntryFilter::default().with_entry_kinds([EntryKind::Table])) .await?; assert_eq!(entries_table.len(), 1); assert_eq!(entries_table[0].name, re_protos::EntryName::entries_table()); assert_eq!(entries_table[0].kind, EntryKind::Table); + let segment_ids = register_rrds(client, dataset_name, dataset_id_str, &[path.path()]).await?; + Ok(segment_ids + .into_iter() + .next() + .expect("We registered exactly one recording")) +} + +/// Logs `count` recordings with static `Points3D` and registers them in a fresh dataset, one +/// segment per recording. Returns the segment ids in registration order. +/// +/// Each recording uses a different point color so the segment previews look distinct. The data +/// is time-invariant, so a preview renders identically at every point on its looping preview +/// timeline. That keeps preview snapshots stable. +pub async fn load_static_preview_data( + client: &mut ConnectionClient, + dataset_name: &str, + dataset_id_str: &str, + recording_id_prefix: &str, + count: usize, +) -> Result, Box> { + let mut paths = Vec::with_capacity(count); + for i in 0..count { + let color = preview_segment_color(i); + let path = recording_rrd(&format!("{recording_id_prefix}_{i}"), |stream| { + stream + .log_static( + "test_entity", + &archetypes::Points3D::new([ + (0.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + (0.0, 0.0, 1.0), + ]) + .with_radii([0.3]) + .with_colors([color]), + ) + .expect("Failed to log static points 3D"); + })?; + paths.push(path); + } + + let path_refs: Vec<&std::path::Path> = paths.iter().map(|p| p.path()).collect(); + register_rrds(client, dataset_name, dataset_id_str, &path_refs).await +} + +/// A distinct color for the segment at `index`, cycling through a small fixed palette. +fn preview_segment_color(index: usize) -> Color { + const PALETTE: [(u8, u8, u8); 6] = [ + (230, 80, 80), + (80, 200, 120), + (80, 140, 230), + (230, 200, 80), + (190, 100, 220), + (90, 210, 210), + ]; + let (r, g, b) = PALETTE[index % PALETTE.len()]; + Color::from_rgb(r, g, b) +} + +/// Build an `.rrd` file from a recording, running `log_data` to populate it. +fn recording_rrd( + recording_id: &str, + log_data: impl FnOnce(&re_sdk::RecordingStream), +) -> Result> { + let path = tempfile::NamedTempFile::new()?; + let stream = RecordingStreamBuilder::new("rerun_example_integration_test") + .recording_id(recording_id) + .save(path.path())?; + + log_data(&stream); + + stream.flush_with_timeout(Duration::from_mins(1))?; + + Ok(path) +} + +/// Create a dataset entry and register the `.rrd`s at `paths`, waiting for registration to finish. +/// +/// Returns the segment ids in the same order as `paths`. +async fn register_rrds( + client: &mut ConnectionClient, + dataset_name: &str, + dataset_id_str: &str, + paths: &[&std::path::Path], +) -> Result, Box> { let dataset_id = re_tuid::Tuid::from_str(dataset_id_str).expect("Failed to parse TUID"); let entry = client .create_dataset_entry(dataset_name.to_owned(), Some(dataset_id.into())) .await?; - let item = client + let mut data_sources = Vec::with_capacity(paths.len()); + for path in paths { + data_sources.push(DataSource::new_rrd(format!( + "file://{}", + path.to_str() + .ok_or_else(|| "Failed to convert path to str".to_owned())? + ))?); + } + + let items = client + .register_with_dataset(entry.details.id, data_sources, IfDuplicateBehavior::Error) + .await? + .1; + + let mut segment_ids = Vec::with_capacity(items.len()); + let mut task_ids = Vec::with_capacity(items.len()); + for item in items { + let cloud_ext::RegisterWithDatasetTaskDescriptor { + layer_name: _, + segment_id, + segment_type: _, + storage_url: _, + task_id, + } = item; + segment_ids.push(segment_id); + task_ids.push(task_id); + } + + wait_for_tasks(client, task_ids).await?; + + Ok(segment_ids) +} + +/// Register a `.rbl` blueprint file with `table`'s implicit blueprint dataset and set it as the +/// table's default blueprint, mirroring `TableEntry.register_blueprint` in the Python SDK. +/// +/// The viewer fetches this registered blueprint when the table entry is opened, which is what +/// turns the preview column into inline 3D previews. +pub async fn register_table_blueprint( + client: &mut ConnectionClient, + table: &TableEntry, + blueprint_rbl: &std::path::Path, +) -> Result> { + let blueprint_dataset = table + .table_details + .blueprint_dataset + .ok_or("table is missing its implicit blueprint dataset")?; + + let data_source = DataSource::new_rrd(format!( + "file://{}", + blueprint_rbl + .to_str() + .ok_or_else(|| "Failed to convert blueprint path to str".to_owned())? + ))?; + + let items = client .register_with_dataset( - entry.details.id, - vec![DataSource::new_rrd(format!( - "file://{}", - path.path() - .to_str() - .ok_or_else(|| "Failed to convert path to str".to_owned())? - ))?], - IfDuplicateBehavior::Error, + blueprint_dataset, + vec![data_source], + IfDuplicateBehavior::Overwrite, ) .await? - .into_iter() - .next() - .expect("We created this with one segment"); + .1; - let re_protos::cloud::v1alpha1::ext::RegisterWithDatasetTaskDescriptor { - segment_id, - segment_type: _, - storage_url: _, - task_id, - } = item; + let mut segment_id = None; + let mut task_ids = Vec::with_capacity(items.len()); + for item in items { + segment_id = Some(item.segment_id); + task_ids.push(item.task_id); + } + let segment_id = segment_id.ok_or("Blueprint registration returned no segment")?; - // Wait for the registration task to complete: - let timeout = Duration::from_secs(10); - let mut response_stream = client - .query_tasks_on_completion(vec![task_id], timeout) + wait_for_tasks(client, task_ids).await?; + + client + .update_table_entry( + table.details.id, + TableDetails { + blueprint_dataset: Some(blueprint_dataset), + default_blueprint_segment: Some(segment_id.clone()), + }, + ) .await?; + Ok(segment_id) +} + +/// Wait for the given registration tasks to complete, returning an error if any task failed. +async fn wait_for_tasks( + client: &mut ConnectionClient, + task_ids: Vec, +) -> Result<(), Box> { + let timeout = Duration::from_secs(10); + let mut response_stream = client.query_tasks_on_completion(task_ids, timeout).await?; + while let Some(response) = response_stream.next().await { let response: QueryTasksOnCompletionResponse = response?.try_into()?; let batch = response.data; - let status_col = batch - .column_by_name(QueryTasksResponse::FIELD_EXEC_STATUS) - .ok_or("missing exec_status column")? - .as_any() - .downcast_ref::() - .ok_or("exec_status should be a string array")?; - let msgs_col = batch - .column_by_name(QueryTasksResponse::FIELD_MSGS) - .ok_or("missing msgs column")? - .as_any() - .downcast_ref::() - .ok_or("msgs should be a string array")?; - - for i in 0..batch.num_rows() { - let status = status_col.value(i); + let statuses = cloud_ext::QueryTasksDataframe::COLUMN_EXEC_STATUS.extract(&batch)?; + let msgs = cloud_ext::QueryTasksDataframe::COLUMN_MSGS.extract(&batch)?; + + for (status, msg) in std::iter::zip(&statuses, &msgs) { if status != "success" { - let msg = msgs_col.value(i); + let msg = msg.unwrap_or_default(); return Err(format!("Registration task failed with status {status}: {msg}").into()); } } } - Ok(segment_id.into()) + Ok(()) } diff --git a/tests/rust/re_integration_test/tests/dataset_folders.rs b/tests/rust/re_integration_test/tests/dataset_folders.rs index 2771ea794edd..53a82973b4c8 100644 --- a/tests/rust/re_integration_test/tests/dataset_folders.rs +++ b/tests/rust/re_integration_test/tests/dataset_folders.rs @@ -8,6 +8,7 @@ use egui::accesskit::Role; use egui_kittest::kittest::Queryable as _; use egui_kittest::{Harness, SnapshotResults}; use re_integration_test::{HarnessExt as _, TestServer}; +use re_protos::cloud::v1alpha1::ext; use re_protos::cloud::v1alpha1::ext::TableInsertMode; use re_sdk::external::re_log_types::EntryId; use re_viewer::App; @@ -19,7 +20,7 @@ fn assert_route_and_selection(harness: &mut Harness<'static, App>, expected_rout assert_eq!(&actual_route, expected_route, "unexpected route"); let actual_selection = - harness.run_with_viewer_context(|ctx| ctx.selection().single_item().cloned()); + harness.run_with_app_context(|ctx| ctx.selection().single_item().cloned()); assert_eq!( actual_selection, expected_route.item(), @@ -228,7 +229,7 @@ async fn create_table( name: &str, schema: &Arc, row_name: &str, -) -> re_protos::cloud::v1alpha1::ext::TableEntry { +) -> ext::TableEntry { let batch = RecordBatch::try_new_with_options( schema.clone(), vec![ diff --git a/tests/rust/re_integration_test/tests/datasets.rs b/tests/rust/re_integration_test/tests/datasets.rs index 32f88c16faf5..76171572ee6b 100644 --- a/tests/rust/re_integration_test/tests/datasets.rs +++ b/tests/rust/re_integration_test/tests/datasets.rs @@ -5,7 +5,10 @@ use egui_kittest::kittest::Queryable as _; use re_integration_test::{HarnessExt as _, TestServer}; use re_sdk::{ TimeCell, Timeline, - external::{re_log_types::AbsoluteTimeRange, re_tuid}, + external::{ + re_log_types::{AbsoluteTimeRange, EntityPath}, + re_tuid, + }, }; use re_viewer::{ external::{ @@ -110,18 +113,18 @@ pub async fn start_with_segment_fragment_url() { let dataset_id = re_tuid::Tuid::from_str("187b552b95a5c2f73f37894708825ba5").expect("Failed to parse TUID"); - let url = ViewerOpenUrl::RedapDatasetSegment(re_uri::DatasetSegmentUri { + let segment_uri = re_uri::DatasetSegmentUri { origin: re_uri::Origin { scheme: re_uri::Scheme::RerunHttp, host: re_uri::external::url::Host::Domain("localhost".to_owned()), port: server.port(), }, dataset_id, - segment_id: segment_id.id().to_owned(), + segment_id, fragment: re_uri::Fragment { selection: None, when: Some(( - TimelineName::new("test_time"), + TimelineName::from("test_time"), TimeCell::new(re_sdk::time::TimeType::Sequence, 10), )), time_selection: Some(re_uri::TimeSelection { @@ -129,7 +132,9 @@ pub async fn start_with_segment_fragment_url() { range: AbsoluteTimeRange::new(2, 8), }), }, - }); + }; + let recording_uri = segment_uri.clone().without_fragment(); + let url = ViewerOpenUrl::RedapDatasetSegment(segment_uri); let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { startup_url: Some(url.sharable_url(None).expect("Should be a sharable url")), @@ -139,12 +144,30 @@ pub async fn start_with_segment_fragment_url() { ..Default::default() }); + let preview_entity = EntityPath::from("test_entity"); + let timeline = TimelineName::from("test_time"); viewer_test_utils::step_until( - "Redap recording id appears", + "Recording opened, source tree populated, and point data arrived", &mut harness, |harness| { + let uri = recording_uri.clone(); + let entity = preview_entity.clone(); harness.query_by_label_contains("Streams").is_some() && harness.query_by_label("Loading entries…").is_none() + && harness.query_by_label_contains("my_dataset").is_some() + && harness.query_all_by_label("new_recording_id").count() == 2 + && harness.run_with_app_context(move |app_context| { + app_context + .storage_context + .hub + .find_recording_by_uri(&uri) + .is_some_and(|db| { + // Not only needs the recording be loaded, we also need the data to arrive. + db.storage_engine() + .store() + .entity_has_physical_temporal_data_on_timeline(&entity, &timeline) + }) + }) }, Duration::from_millis(100), Duration::from_secs(5), @@ -152,17 +175,5 @@ pub async fn start_with_segment_fragment_url() { harness.set_selection_panel_opened(false); - // Redact the loading bar, since it is racy - harness.mask(egui::Rect::from_x_y_ranges( - egui::Rangef::new(190.0, 1024.0), - egui::Rangef::new(604.0, 606.0), - )); - - // Redact timeline data because we have no way to consistently wait for it to arrive - harness.mask(egui::Rect::from_x_y_ranges( - egui::Rangef::new(190.0, 1024.0), - egui::Rangef::new(650.0, 690.0), - )); - harness.snapshot("start_with_segment_fragment_url"); } diff --git a/tests/rust/re_integration_test/tests/deleted_table_refresh.rs b/tests/rust/re_integration_test/tests/deleted_table_refresh.rs deleted file mode 100644 index 1e0af5164bac..000000000000 --- a/tests/rust/re_integration_test/tests/deleted_table_refresh.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Reproduces the scenario where: -//! 1. Two tables are created on a server. -//! 2. The viewer is opened directly at one of the tables (so its content is on screen). -//! 3. That table is deleted server-side while the viewer is still viewing it. -//! 4. The user triggers a table refresh from the bottom panel. -//! -//! Expected: the user sees an error and there is no infinite loop - -use std::sync::Arc; -use std::time::Duration; - -use arrow::array::{Int64Array, RecordBatch, StringArray}; -use arrow::datatypes::{DataType, Field, Schema}; -use egui_kittest::kittest::Queryable as _; -use re_integration_test::TestServer; -use re_protos::cloud::v1alpha1::ext::TableInsertMode; -use re_sdk::external::re_log_types; -use re_viewer::viewer_test_utils::{self, HarnessOptions}; - -const DELETED_TABLE: &str = "rr4424_to_delete"; -const PERSISTENT_TABLE: &str = "rr4424_keeps_around"; - -#[tokio::test(flavor = "multi_thread")] -pub async fn deleted_table_refresh() { - let server = TestServer::spawn().await; - let mut client = server.client().await.expect("Failed to connect to server"); - - let schema = Arc::new(Schema::new_with_metadata( - vec![ - Field::new("id", DataType::Int64, false), - Field::new("name", DataType::Utf8, false), - ], - Default::default(), - )); - - // Create two tables so we have a stable reference point to verify the - // refresh actually reloaded the list (vs. just rendering empty transiently). - let to_delete = create_table(&mut client, DELETED_TABLE, &schema).await; - let _persistent = create_table(&mut client, PERSISTENT_TABLE, &schema).await; - - // Open the viewer *directly at the table* that we're going to delete, so - // it is the currently-viewed entry when the delete + refresh happens. - let table_url = format!( - "rerun+http://localhost:{}/entry/{}", - server.port(), - to_delete.details.id - ); - let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { - startup_url: Some(table_url), - ..Default::default() - }); - - // Wait for the table's data to load in the main view — rows in the table - // contain the string "alpha" (from the first data batch). - viewer_test_utils::step_until( - "table data is rendered in main view", - &mut harness, - |harness| harness.query_by_label_contains("alpha").is_some(), - Duration::from_millis(100), - Duration::from_secs(10), - ); - - // Delete the currently-viewed table server-side. - client - .delete_entry(to_delete.details.id) - .await - .expect("Failed to delete table"); - - // Sanity check: confirm the server really dropped it. - let remaining = client - .find_entries(re_protos::cloud::v1alpha1::EntryFilter { - id: None, - name: Some(DELETED_TABLE.to_owned()), - entry_kind: None, - }) - .await - .expect("find_entries failed"); - assert!( - remaining.is_empty(), - "table still exists server-side after delete: {remaining:?}" - ); - - // Trigger a refresh via the bottom-right "Refresh table" button on the - // currently-viewed (now deleted) table widget. - harness.get_by_label("Refresh table").click(); - - // After refreshing the (now deleted) table itself, we expect the widget to - // surface a "Could not load table" error rather than hang or spam logs. - // Also wait for the error toasts (which contain the same non-deterministic - // entry id + trace-id) to auto-expire so only the inline error widget - // needs masking. - viewer_test_utils::step_until( - "deleted table shows an error and toasts have expired", - &mut harness, - |harness| { - harness - .query_by_label_contains("Could not load table") - .is_some() - && harness - .query_by_label_contains("DataFusion query error") - .is_none() - }, - Duration::from_millis(100), - Duration::from_secs(15), - ); - - // The inline error widget's text embeds a freshly-generated entry id and - // gRPC trace-id, so mask the whole widget before snapshotting. - let rects_to_mask: Vec = harness - .query_all_by_label_contains("Could not load table") - .map(|node| node.rect()) - .collect(); - for rect in rects_to_mask { - harness.mask(rect); - } - - harness.snapshot("deleted_table_refresh_should_show_error"); -} - -async fn create_table( - client: &mut re_redap_client::ConnectionClient, - name: &str, - schema: &Arc, -) -> re_protos::cloud::v1alpha1::ext::TableEntry { - let batch = RecordBatch::try_new_with_options( - schema.clone(), - vec![ - Arc::new(Int64Array::from(vec![1, 2, 3])), - Arc::new(StringArray::from(vec!["alpha", "beta", "gamma"])), - ], - &Default::default(), - ) - .expect("Failed to create record batch"); - let table = client - .create_table_entry( - re_log_types::EntryName::new(name).expect("Failed to create entry name"), - None, - schema.clone(), - ) - .await - .expect("Failed to create table"); - client - .write_table( - futures::stream::once(async { batch }), - table.details.id, - TableInsertMode::Append, - ) - .await - .expect("Failed to write initial data"); - table -} diff --git a/tests/rust/re_integration_test/tests/drop_component_to_state_timeline_view.rs b/tests/rust/re_integration_test/tests/drop_component_to_state_timeline_view.rs new file mode 100644 index 000000000000..3072325e10df --- /dev/null +++ b/tests/rust/re_integration_test/tests/drop_component_to_state_timeline_view.rs @@ -0,0 +1,88 @@ +//! Tests dragging a component from the streams tree onto a State Timeline view, which +//! should add a `StateVisualizer` instruction that remaps `StateChange.state` from +//! the dropped component. + +use re_integration_test::HarnessExt as _; +use re_sdk::log::RowId; +use re_viewer::external::re_sdk_types; +use re_viewer::viewer_test_utils::{self, HarnessOptions}; +use re_viewport_blueprint::ViewBlueprint; + +fn make_harness<'a>() -> egui_kittest::Harness<'a, re_viewer::App> { + let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { + window_size: Some(egui::Vec2::new(1200.0, 800.0)), + ..Default::default() + }); + harness.init_recording(); + + let timeline = re_sdk::Timeline::new_sequence("tick"); + + // `/mode` carries a native `StateChange` archetype. The `StateVisualizer` picks + // it up automatically, so the view has one lane before any drop. + let states = ["Idle", "Moving", "Working", "Idle"]; + for (i, state) in states.iter().enumerate() { + let tick = i64::try_from(i).expect("test index fits in i64") * 10; + harness.log_entity("mode", |builder| { + builder.with_archetype( + RowId::new(), + [(timeline, tick)], + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + // `/level` carries `Scalars`. There is no auto-attached visualizer for it; + // it should only appear in the view after the scalar component is dropped + // onto it. + for i in 0_i64..4 { + harness.log_entity("level", |builder| { + builder.with_archetype( + RowId::new(), + [(timeline, i * 10)], + &re_sdk_types::archetypes::Scalars::single(i as f64), + ) + }); + } + + harness.clear_current_blueprint(); + harness.setup_viewport_blueprint(|_viewer_context, blueprint| { + let mut view = ViewBlueprint::new_with_root_wildcard("StateTimeline".into()); + view.display_name = Some("State Timeline view".into()); + blueprint.add_view_at_root(view); + }); + + harness +} + +#[tokio::test(flavor = "multi_thread")] +pub async fn test_drop_component_to_state_timeline_view() { + let mut harness = make_harness(); + + let drop_point = harness.get_panel_position("State Timeline view").center(); + + // Expand the streams tree so component-level rows become visible and + // therefore draggable. + harness.streams_tree().right_click_label("/"); + harness.click_label("Expand all"); + harness.snapshot_app("drop_component_to_state_timeline_view_1_initial"); + + // Drag the `scalars` component onto the State Timeline view. + harness.streams_tree().drag_label("scalars"); + harness.hover_at(drop_point); + harness.snapshot_app("drop_component_to_state_timeline_view_2_hover"); + assert_eq!(harness.cursor_icon(), egui::CursorIcon::Grabbing); + + harness.drop_at(drop_point); + harness.snapshot_app("drop_component_to_state_timeline_view_3_after_drop"); + assert_eq!(harness.cursor_icon(), egui::CursorIcon::Default); + + // Dragging the same component a second time should be a no-op: the + // visualizer instruction is already present, so the snapshot after the + // second drop should match the snapshot after the first. + harness.streams_tree().drag_label("scalars"); + harness.hover_at(drop_point); + assert_eq!(harness.cursor_icon(), egui::CursorIcon::NoDrop); + harness.drop_at(drop_point); + harness.snapshot_app("drop_component_to_state_timeline_view_4_after_redrop"); + assert_eq!(harness.cursor_icon(), egui::CursorIcon::Default); +} diff --git a/tests/rust/re_integration_test/tests/internal_catalog.rs b/tests/rust/re_integration_test/tests/internal_catalog.rs new file mode 100644 index 000000000000..17627b125e41 --- /dev/null +++ b/tests/rust/re_integration_test/tests/internal_catalog.rs @@ -0,0 +1,206 @@ +//! Testing the internal catalog. +//! +//! As long as we still have the old loading path, we contrast both +//! to highlight things that we still need to adapt. + +use std::path::PathBuf; +use std::time::Duration; + +use egui_kittest::SnapshotResults; +use egui_kittest::kittest::Queryable as _; +use re_integration_test::HarnessExt as _; +use re_log_types::{EntityPath, TimelineName}; +use re_sdk::RecordingStreamBuilder; +use re_sdk::blueprint::{Blueprint, Spatial2DView}; +use re_sdk_types::archetypes::Points2D; +use re_sdk_types::components::{Color, Radius}; +use re_viewer::viewer_test_utils; + +// TODO(RR-4929): We should properly show the application id, +// and maybe even the recording id. + +const RRD_RECORDING_ID: &str = "test_recording"; +const RRD_APP_ID: &str = "test_app"; +const RRD_FILE_NAME: &str = "internal_catalog_test.rrd"; + +fn test_rrd() -> (tempfile::TempDir, PathBuf) { + let dir = tempfile::tempdir().expect("failed to create .rrd temp dir"); + let path = dir.path().join(RRD_FILE_NAME); + + let rec = RecordingStreamBuilder::new(RRD_APP_ID) + .recording_id(RRD_RECORDING_ID) + .save(&path) + .expect("failed to create .rrd recording stream"); + rec.set_time_sequence("frame", 0); + rec.log( + "points", + &Points2D::new([(0.0, 0.0), (1.0, 1.0)]) + .with_colors([Color::from_rgb(255, 0, 0)]) + .with_radii([Radius::new_ui_points(24.0)]), + ) + .expect("failed to log points"); + + // TODO(RR-5030): We don't load the blueprint yet, which is why the snapshots differ. + Blueprint::new( + Spatial2DView::new("points") + .with_origin("/") + .with_contents(["/points"]) + .with_override( + "points", + &Points2D::update_fields().with_colors([Color::from_rgb(0, 255, 0)]), + ), + ) + .send(&rec, Default::default()) + .expect("failed to log blueprint"); + + rec.flush_with_timeout(Duration::from_mins(1)) + .expect("failed to flush .rrd"); + + (dir, path) +} + +#[tokio::test(flavor = "multi_thread")] +async fn internal_catalog_load_rrd() { + let mut snapshot_results = SnapshotResults::new(); + + fn run_with_catalog(snapshot_results: &mut SnapshotResults, use_internal_catalog: bool) { + let (dir, rrd_path) = test_rrd(); + let mut harness = viewer_test_utils::viewer_harness(&viewer_test_utils::HarnessOptions { + app_options_editor: Some(Box::new(move |app_options| { + app_options.experimental.use_internal_catalog = use_internal_catalog; + })), + ..Default::default() + }); + + harness + .state() + .open_url_or_file(&rrd_path.display().to_string()); + + let points = EntityPath::from("points"); + let frame = TimelineName::from("frame"); + viewer_test_utils::step_until( + "file loaded", + &mut harness, + move |harness| { + let Some(store_id) = harness.state().active_recording_id().cloned() else { + return false; + }; + if store_id.recording_id().as_str() != RRD_RECORDING_ID { + return false; + } + + let points = points.clone(); + harness.run_with_app_context(move |ctx| { + ctx.storage_context + .hub + .entity_db(&store_id) + .is_some_and(|db| { + db.data_source + .as_ref() + .is_some_and(|source| source.is_redap() == use_internal_catalog) + && db + .storage_engine() + .store() + .entity_has_physical_temporal_data_on_timeline(&points, &frame) + }) + }) + }, + Duration::from_millis(100), + Duration::from_secs(10), + ); + + let loading_rrd_toast = format!("Loading {rrd_path:?}…"); + viewer_test_utils::step_until( + "loading toast gone", + &mut harness, + |harness| { + harness + .query_by_label_contains(&loading_rrd_toast) + .is_none() + }, + Duration::from_millis(100), + Duration::from_secs(10), + ); + + harness.set_time_panel_opened(false); + + if use_internal_catalog { + // TODO(RR-4929): Remove this mask once the catalog app id matches recording app id. + let app_id = harness + .state() + .active_recording_id() + .map(|store_id| store_id.application_id().to_string()) + .unwrap_or_default(); + let app_id_rects = { + let selection_panel = harness.selection_panel(); + let selection_panel_root = selection_panel.root(); + let selection_panel_rect = selection_panel_root.rect(); + selection_panel_root + .query_all_by(|node| { + node.label().is_some_and(|label| label.contains(&app_id)) + || node.value().is_some_and(|value| value.contains(&app_id)) + }) + .map(|node| { + let rect = node.rect(); + egui::Rect::from_min_max( + egui::pos2(selection_panel_rect.left(), rect.top()), + egui::pos2(selection_panel_rect.right(), rect.bottom()), + ) + }) + .collect::>() + }; + for rect in app_id_rects { + harness.mask(rect); + } + } + + if !use_internal_catalog { + // Mask the unstable temp-dir path wherever it appears. + let temp_dir_path = dir.path().display().to_string(); + let unstable_path_rects: Vec = harness + .query_all_by(|node| { + node.label().is_some_and(|l| l.contains(&temp_dir_path)) + || node.value().is_some_and(|v| v.contains(&temp_dir_path)) + }) + .map(|node| node.rect()) + .collect(); + for rect in unstable_path_rects { + harness.mask(rect); + } + + let selection_panel_path_rects = { + let selection_panel = harness.selection_panel(); + let selection_panel_root = selection_panel.root(); + let selection_panel_rect = selection_panel_root.rect(); + selection_panel_root + .query_all_by(|node| { + node.label().is_some_and(|l| l.contains(&temp_dir_path)) + || node.value().is_some_and(|v| v.contains(&temp_dir_path)) + }) + .map(|node| { + let rect = node.rect(); + egui::Rect::from_min_max( + egui::pos2(selection_panel_rect.left(), rect.top()), + egui::pos2(selection_panel_rect.right(), rect.bottom()), + ) + }) + .collect::>() + }; + for rect in selection_panel_path_rects { + harness.mask(rect); + } + } + + let suffix = if use_internal_catalog { + "catalog" + } else { + "recording" + }; + + harness.snapshot(format!("internal_catalog_load_rrd_{suffix}")); + snapshot_results.extend_harness(&mut harness); + } + + run_with_catalog(&mut snapshot_results, true); + run_with_catalog(&mut snapshot_results, false); +} diff --git a/tests/rust/re_integration_test/tests/preview_table.rs b/tests/rust/re_integration_test/tests/preview_table.rs new file mode 100644 index 000000000000..b317c89cff15 --- /dev/null +++ b/tests/rust/re_integration_test/tests/preview_table.rs @@ -0,0 +1,288 @@ +//! End-to-end test for table segment previews. +//! +//! Builds a remote table whose rows carry a recording URI and an embedded table blueprint +//! that defines a `Spatial3DView`. The viewer loads the referenced recording on demand and +//! renders it inline in the table's sticky preview column. We then verify both that the +//! recording was actually loaded and that the rendered preview matches a snapshot. +//! +//! The referenced recording logs its `Points3D` statically, so the preview renders the same +//! at every point on its looping preview timeline and the snapshot stays stable. + +use std::str::FromStr as _; +use std::sync::Arc; +use std::time::Duration; + +use arrow::array::{Int64Array, RecordBatch, RecordBatchOptions, StringArray}; +use arrow::datatypes::{DataType, Field, Schema}; +use egui_kittest::kittest::Queryable as _; + +use re_integration_test::{HarnessExt as _, TestServer}; +use re_sdk::RecordingStreamBuilder; +use re_sdk::external::{re_log_types, re_tuid}; +use re_sdk_types::blueprint::archetypes::{ + ContainerBlueprint, TableBlueprint, ViewBlueprint, ViewContents, ViewportBlueprint, +}; +use re_sdk_types::blueprint::components::{ + ContainerKind, IncludedContent, QueryExpression, RootContainer, ViewClass, +}; +use re_viewer::viewer_test_utils::{self, HarnessOptions}; + +const DATASET_ID: &str = "187b552b95a5c2f73f37894708825ba5"; +const PREVIEW_COLUMN: &str = "recording_uri"; +const TITLE_COLUMN: &str = "name"; +const SEGMENT_COUNT: usize = 4; + +#[tokio::test(flavor = "multi_thread")] +pub async fn preview_table() { + let (server, segment_ids) = TestServer::spawn() + .await + .with_static_preview_data( + "preview_dataset", + DATASET_ID, + "preview_recording", + SEGMENT_COUNT, + ) + .await; + + // One row per segment, each pointing at its segment's recording URI. + let dataset_id = re_tuid::Tuid::from_str(DATASET_ID).expect("Failed to parse TUID"); + let segment_uris: Vec = segment_ids + .iter() + .map(|segment_id| re_uri::DatasetSegmentUri { + origin: re_uri::Origin { + scheme: re_uri::Scheme::RerunHttp, + host: re_uri::external::url::Host::Domain("localhost".to_owned()), + port: server.port(), + }, + dataset_id, + segment_id: segment_id.clone(), + fragment: Default::default(), + }) + .collect(); + + // Create a remote table with a recording-URI column. A registered blueprint (set up below) + // renders each recording in a 3D preview. The `name` column gives grid-view cards stable + // titles. + let schema = Arc::new(Schema::new_with_metadata( + vec![ + Field::new("id", DataType::Int64, false) + .with_metadata([("rerun:is_table_index".to_owned(), "true".to_owned())].into()), + Field::new(TITLE_COLUMN, DataType::Utf8, false), + Field::new(PREVIEW_COLUMN, DataType::Utf8, false), + ], + Default::default(), + )); + + let mut client = server.client().await.expect("Failed to connect to server"); + let table = client + .create_table_entry( + re_log_types::EntryName::new("preview_table").expect("valid entry name"), + None, + schema.clone(), + ) + .await + .expect("Failed to create table"); + + let names: Vec = (0..SEGMENT_COUNT).map(|i| format!("segment {i}")).collect(); + let batch = RecordBatch::try_new_with_options( + schema, + vec![ + Arc::new(Int64Array::from_iter_values( + (0..SEGMENT_COUNT).map(|i| i64::try_from(i).expect("segment index fits in i64")), + )), + Arc::new(StringArray::from(names)), + Arc::new(StringArray::from( + segment_uris + .iter() + .map(|uri| uri.to_string()) + .collect::>(), + )), + ], + &RecordBatchOptions::new().with_row_count(Some(SEGMENT_COUNT)), + ) + .expect("Failed to build table batch"); + client + .write_table( + futures::stream::once(async { batch }), + table.details.id, + re_protos::cloud::v1alpha1::ext::TableInsertMode::Append, + ) + .await + .expect("Failed to write table data"); + + // Register the table blueprint with the table's implicit blueprint dataset and set it as the default. + let blueprint_rbl = blueprint_rbl_file(PREVIEW_COLUMN, TITLE_COLUMN); + re_integration_test::register_table_blueprint(&mut client, &table, blueprint_rbl.path()) + .await + .expect("Failed to register table blueprint"); + + // Open the viewer directly at the table entry. Make the window tall enough that all rows + // are on screen at once, so every preview loads. + let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { + window_size: Some(egui::vec2(1024.0, 1000.0)), + startup_url: Some(format!( + "rerun+http://localhost:{}/entry/{}", + server.port(), + table.details.id + )), + ..Default::default() + }); + + // Step until every preview recording has actually streamed in its point data. Rendering the + // preview column is what triggers the background loads, so this also exercises the column. + let preview_uris: Vec = segment_uris + .iter() + .map(|uri| uri.clone().without_fragment()) + .collect(); + let preview_entity = re_log_types::EntityPath::from("test_entity"); + viewer_test_utils::step_until( + "All preview recordings loaded", + &mut harness, + |harness| { + let uris = preview_uris.clone(); + let entity = preview_entity.clone(); + harness.run_with_app_context(move |app_context| { + uris.iter().all(|uri| { + app_context + .storage_context + .hub + .find_recording_by_uri(uri) + .is_some_and(|db| { + // Not only needs the recording be loaded, we also need the data to arrive. + db.storage_engine() + .store() + .entity_has_physical_static_data(&entity) + }) + }) + }) + }, + Duration::from_millis(100), + Duration::from_secs(30), + ); + + // Let the 3D views' camera framing settle before snapshotting. + harness.run_ok(); + harness.snapshot("preview_table"); + + // Switch to grid view and snapshot the same previews as cards. + harness.get_by_label("Grid view").click(); + harness.run_ok(); + harness.snapshot("preview_table_grid"); + + // Clicking the first card opens its recording, navigating away from the table. + // + // We have to drive this click by hand rather than via `.click()` / `click_at`: + // - `click_at` calls `run()`, which never settles while the previews keep repainting. + // - `.click()` presses and releases in a single frame, which the card's click region + // doesn't register. + // The card registers its click area behind its content, so a click on the title label or + // the preview never reaches it. We click the empty space to the right of the title. + let title = harness.get_by_label("segment 0").rect(); + let click_pos = egui::pos2(title.right() + 150.0, title.center().y); + harness.event(egui::Event::PointerMoved(click_pos)); + harness.step(); + for pressed in [true, false] { + harness.event(egui::Event::PointerButton { + pos: click_pos, + button: egui::PointerButton::Primary, + pressed, + modifiers: egui::Modifiers::NONE, + }); + harness.step(); + } + + let opened_segment = preview_uris[0].clone(); + viewer_test_utils::step_until( + "Clicked card opens its recording", + &mut harness, + |harness| { + let uri = opened_segment.clone(); + harness.run_with_app_context(move |app_context| { + let expected = app_context + .storage_context + .hub + .find_recording_by_uri(&uri) + .map(|db| db.store_id().clone()); + expected.is_some() && app_context.route.recording_id().cloned() == expected + }) + }, + Duration::from_millis(100), + Duration::from_secs(15), + ); + + viewer_test_utils::step_until( + "Opened recording finished loading", + &mut harness, + |harness| { + harness.query_by_label_contains("Streams").is_some() + && harness.query_by_label("Loading entries…").is_none() + }, + Duration::from_millis(100), + Duration::from_secs(15), + ); + // Close the selection panel rather than masking it: it shows the recording URI, which + // embeds the server's random port. + harness.set_selection_panel_opened(false); + harness.mask_dates(); + harness.snapshot("preview_table_opened_recording"); +} + +/// Build a `.rbl` blueprint file holding a `Spatial3DView` over `/test_entity` plus a +/// `TableBlueprint` archetype pointing segment previews at `preview_column` and grid-view card +/// titles at `title_column`. +fn blueprint_rbl_file(preview_column: &str, title_column: &str) -> tempfile::NamedTempFile { + let file = tempfile::Builder::new() + .suffix(".rbl") + .tempfile() + .expect("Failed to create blueprint temp file"); + + let stream = RecordingStreamBuilder::new("rerun_example_table_blueprint") + .blueprint() + .save(file.path()) + .expect("Failed to create blueprint memory stream"); + stream.set_time_sequence("blueprint", 0); + + let view_id = uuid::Uuid::new_v4(); + let view_path = format!("view/{view_id}"); + stream + .log( + format!("{view_path}/ViewContents"), + &ViewContents::new([QueryExpression("/test_entity/**".into())]), + ) + .expect("Failed to log view contents"); + stream + .log( + view_path.clone(), + &ViewBlueprint::new(ViewClass("3D".into())).with_space_origin("/test_entity"), + ) + .expect("Failed to log view blueprint"); + + let container_id = uuid::Uuid::new_v4(); + stream + .log( + format!("container/{container_id}"), + &ContainerBlueprint::new(ContainerKind::Tabs) + .with_contents([IncludedContent(view_path.into())]), + ) + .expect("Failed to log container blueprint"); + + stream + .log( + "viewport", + &ViewportBlueprint::new().with_root_container(RootContainer(container_id.into())), + ) + .expect("Failed to log viewport blueprint"); + + stream + .log( + "table", + &TableBlueprint::new() + .with_segment_preview_column(preview_column) + .with_grid_view_card_title(title_column) + // Clicking a card opens the recording referenced by this column. + .with_url_column(preview_column), + ) + .expect("Failed to log table blueprint"); + + file +} diff --git a/tests/rust/re_integration_test/tests/redap_catalog_select.rs b/tests/rust/re_integration_test/tests/redap_catalog_select.rs new file mode 100644 index 000000000000..caf490244a7a --- /dev/null +++ b/tests/rust/re_integration_test/tests/redap_catalog_select.rs @@ -0,0 +1,185 @@ +//! Behavior tests for `RedapCatalogProviderList`: a real `TestServer`, real `ConnectionClient`, +//! real `SessionContext`. Verifies that SELECT (qualified and unqualified), `table_exist`, and +//! `register_table` collision detection still work end-to-end with a single lazy provider list. + +use std::sync::Arc; + +use arrow::array::{Int64Array, RecordBatch, StringArray}; +use arrow::datatypes::{DataType, Field, Schema}; +use datafusion::catalog::TableProvider; +use datafusion::datasource::MemTable; +use datafusion::logical_expr::TableType; +use datafusion::prelude::SessionContext; +use re_datafusion::RedapCatalogProviderList; +use re_integration_test::TestServer; +use re_protos::cloud::v1alpha1::ext::TableInsertMode; + +const FLAT_TABLE: &str = "flat_table"; +const QUALIFIED_TABLE: &str = "cat.schema.qualified_table"; + +#[tokio::test(flavor = "multi_thread")] +async fn select_via_redap_catalog_provider_list() { + let server = TestServer::spawn().await; + let client = server.client().await.expect("connect"); + + let schema = Arc::new(Schema::new_with_metadata( + vec![ + Field::new("id", DataType::Int64, false), + Field::new("name", DataType::Utf8, false), + ], + Default::default(), + )); + + create_and_populate(&client, FLAT_TABLE, &schema).await; + create_and_populate(&client, QUALIFIED_TABLE, &schema).await; + + let runtime = tokio::runtime::Handle::current(); + let ctx = Arc::new(SessionContext::new()); + ctx.register_catalog_list(Arc::new(RedapCatalogProviderList::new( + client, runtime, None, + ))); + + // SELECT planning resolves through the async `SchemaProvider::table` path, so it does not + // hit the sync `block_on` in `lookup_table_on_server`. Run directly on the async test. + let count = run_count(&ctx, &format!("SELECT COUNT(*) FROM {FLAT_TABLE}")).await; + assert_eq!(count, 3); + + let count = run_count(&ctx, &format!("SELECT COUNT(*) FROM {QUALIFIED_TABLE}")).await; + assert_eq!(count, 3); + + // `table_exist` and `register_table` are sync trait methods that call `block_on` internally. + // From inside a tokio task that would panic, so escape the async context via + // `spawn_blocking`. (Production callers — the Python SDK and DataFusion's DDL paths — invoke + // these from non-task threads.) + let ctx_blocking = Arc::clone(&ctx); + let mem_table_schema = Arc::clone(&schema); + tokio::task::spawn_blocking(move || { + let cat = ctx_blocking + .catalog("cat") + .expect("catalog `cat` registered"); + let schema_provider = cat + .schema("schema") + .expect("schema `schema` resolved lazily"); + + assert!(schema_provider.table_exist("qualified_table")); + assert!(!schema_provider.table_exist("definitely_not_a_table")); + + let mem_table: Arc = + Arc::new(MemTable::try_new(mem_table_schema, vec![vec![]]).expect("mem table")); + assert!( + schema_provider + .register_table("qualified_table".to_owned(), Arc::clone(&mem_table)) + .is_err(), + "register_table must reject names that already exist server-side" + ); + let result = + schema_provider.register_table("brand_new_in_memory_table".to_owned(), mem_table); + assert!( + result.is_ok(), + "register_table must accept fresh names; got {result:?}" + ); + }) + .await + .expect("spawn_blocking task panicked"); +} + +async fn create_and_populate( + client: &re_redap_client::ConnectionClient, + name: &str, + schema: &Arc, +) { + let mut client = client.clone(); + let entry_name = + re_log_types::EntryName::new(name).expect("test name must be a valid EntryName"); + + let table = client + .create_table_entry(entry_name, None, schema.clone()) + .await + .expect("create_table_entry"); + + let batch = RecordBatch::try_new_with_options( + schema.clone(), + vec![ + Arc::new(Int64Array::from(vec![1, 2, 3])), + Arc::new(StringArray::from(vec!["alpha", "beta", "gamma"])), + ], + &Default::default(), + ) + .expect("record batch"); + + client + .write_table( + futures::stream::once(async { batch }), + table.details.id, + TableInsertMode::Append, + ) + .await + .expect("write_table"); +} + +async fn run_count(ctx: &SessionContext, sql: &str) -> i64 { + let df = ctx.sql(sql).await.expect("sql plan"); + let batches = df.collect().await.expect("collect"); + let batch = batches + .into_iter() + .find(|b| b.num_rows() > 0) + .expect("at least one batch with rows"); + batch + .column(0) + .as_any() + .downcast_ref::() + .expect("count column is i64") + .value(0) +} + +// `table_type` is an `async` trait method that historically routed through +// `lookup_table_on_server`, which calls `self.runtime.block_on(...)`. When the +// future was polled on a worker of the same runtime stored in `self.runtime` — +// the production case via `FFI_CatalogProviderList` — `Handle::block_on` +// panicked, crashing any path that awaited `table_type` (notably +// `INFORMATION_SCHEMA.tables`). +// +// The fix routes `table_type` through `lookup_table_on_server_async` so the +// future can be polled on the same runtime without re-entering `block_on`. +// This test pins that contract: install `RedapCatalogProviderList` on the test +// runtime via `Handle::current()`, then await `table_type` directly. It must +// resolve to `Some(TableType::Base)` for an existing table and `None` for a +// missing one — never panic. +#[tokio::test(flavor = "multi_thread")] +async fn table_type_resolves_when_polled_on_provider_runtime() { + let server = TestServer::spawn().await; + let client = server.client().await.expect("connect"); + + let schema = Arc::new(Schema::new_with_metadata( + vec![ + Field::new("id", DataType::Int64, false), + Field::new("name", DataType::Utf8, false), + ], + Default::default(), + )); + + create_and_populate(&client, QUALIFIED_TABLE, &schema).await; + + let runtime = tokio::runtime::Handle::current(); + let ctx = Arc::new(SessionContext::new()); + ctx.register_catalog_list(Arc::new(RedapCatalogProviderList::new( + client, runtime, None, + ))); + + let cat = ctx.catalog("cat").expect("catalog `cat` registered"); + let schema_provider = cat + .schema("schema") + .expect("schema `schema` resolved lazily"); + + let existing = schema_provider + .table_type("qualified_table") + .await + .expect("table_type must not error for an existing entry"); + assert_eq!(existing, Some(TableType::Base)); + + let missing = schema_provider + .table_type("definitely_not_a_table") + .await + .expect("table_type must map server NotFound to Ok(None)"); + assert_eq!(missing, None); +} diff --git a/tests/rust/re_integration_test/tests/rrd_bw_compat_test.rs b/tests/rust/re_integration_test/tests/rrd_bw_compat_test.rs new file mode 100644 index 000000000000..a8cc760ce573 --- /dev/null +++ b/tests/rust/re_integration_test/tests/rrd_bw_compat_test.rs @@ -0,0 +1,383 @@ +//! Integration test: load example .rrd files from the previous release into the current viewer. +//! +//! This catches backward-compatibility regressions for both recording data and blueprints. +//! The previous release version is derived from the workspace `CARGO_PKG_VERSION`. + +use egui::accesskit::Role; +use egui_kittest::kittest::Queryable as _; +use egui_kittest::{SnapshotOptions, SnapshotResults}; +use futures::StreamExt as _; +use re_integration_test::HarnessExt as _; +use re_viewer::external::re_log_types::TimelineName; +use re_viewer::external::re_ui::notifications::NotificationLevel; +use re_viewer::viewer_test_utils::{self, AppTestingExt as _, HarnessOptions, step_until}; +use re_viewer::{SystemCommand, SystemCommandSender as _}; +use re_viewer_context::TimeControlCommand; +use std::io::Write as _; +use std::path::Path; +use std::time::Duration; + +/// Maximum number of concurrent downloads. +const DOWNLOAD_CONCURRENCY: usize = 8; + +/// Prefix used for in-flight download temp files in the cache directory. +const DOWNLOAD_TEMP_PREFIX: &str = ".rrd-download-"; + +/// Derive previous minor version from `CARGO_PKG_VERSION`. +/// +/// E.g., `"0.32.0-alpha.1+dev"` → `(0, 31)`. +fn previous_minor_version() -> (u32, u32) { + let version = env!("CARGO_PKG_VERSION"); // e.g. "0.32.0-alpha.1+dev" + let parts: Vec<&str> = version.split('.').collect(); + let major: u32 = parts[0].parse().expect("failed to parse major version"); + let minor: u32 = parts[1].parse().expect("failed to parse minor version"); + assert!( + minor > 0, + "Cannot derive previous version from minor=0 (version={version})" + ); + (major, minor - 1) +} + +/// Probe `app.rerun.io` to find the latest patch for a given `major.minor`. +/// +/// Tries `major.minor.0`, `major.minor.1`, … until a HEAD request returns 404. +async fn resolve_latest_patch(client: &reqwest::Client, major: u32, minor: u32) -> String { + let mut patch = 0u32; + loop { + let next = patch + 1; + let version = format!("{major}.{minor}.{next}"); + let url = format!("https://app.rerun.io/version/{version}/examples/plots.rrd"); + match client.head(&url).send().await { + Ok(resp) if resp.status().is_success() => patch = next, + _ => break, + } + } + format!("{major}.{minor}.{patch}") +} + +/// Notification messages that are expected to be triggered by at least one example. +/// +/// The test fails if any of these is never triggered, so that entries are removed +/// from this list once the underlying issue is fixed. +const EXPECTED_NOTIFICATIONS: &[&str] = &[]; + +/// Examples whose heuristic-generated blueprints art unstable in some way. +/// +/// For these, "Reset blueprint" will be called once after everything loads. +const UNSTABLE_BLUEPRINT_EXAMPLES: &[&str] = &[ + // `segmentation/rgb_scaled` vs `segmentation` have different image sizes; + // depending on arrival order the heuristic either splits them into two views + // or groups them into one. + "detect_and_track_objects", +]; + +/// Examples which are completely nondeterministic, snapshots will be skipped. +const NONDETERMINISTIC_EXAMPLES: &[&str] = &[ + // The graphs are physics based and vary every reload + "graphs", +]; + +/// Examples that contain a `MapView` whose OSM tiles can change as OSM updates. +/// +/// We mask the map view's pane so the snapshot stays stable. +const MAP_VIEW_EXAMPLES: &[&str] = &[ + // Uses `rrb.MapView(name="MapView", …)`. + "nuscenes_dataset", +]; + +/// Examples whose snapshots are unstable enough on macOS/Windows that we need to +/// bump `failed_pixel_count_threshold` on those platforms to avoid spurious CI failures. +const HIGH_THRESHOLD_TESTS: &[&str] = &[ + // Small but consistent rendering diff on macOS. + "rgbd", + // Photogrammetry mesh rendering diverges noticeably on macOS. + "open_photogrammetry_format", + // The transparent gripper is slightly flakey + "animated_urdf", +]; + +/// Height in points of the bottom strip we mask to hide the collapsed time-control bar. +/// +/// The bar renders a timeline track whose playhead and ticks are positioned by time value. +/// On the `log_time` timeline those values are wall-clock based, so they drift between runs. +/// The collapsed bar is 32 points tall. We mask a little extra to also cover the playhead +/// marker, which pokes slightly above the track. +const TIME_BAR_MASK_HEIGHT: f32 = 40.0; + +/// An entry from the examples manifest hosted at `app.rerun.io`. +#[derive(serde::Deserialize)] +struct ManifestEntry { + name: String, + rrd_url: String, +} + +/// Fetch the example manifest for a given version from `app.rerun.io`. +/// +/// This returns only the stable examples shown on `rerun.io/viewer`. +async fn fetch_example_manifest(client: &reqwest::Client, version: &str) -> Vec { + let url = format!("https://app.rerun.io/version/{version}/examples_manifest.json"); + let resp = client + .get(&url) + .send() + .await + .and_then(|r| r.error_for_status()) + .unwrap_or_else(|e| panic!("Failed to fetch example manifest at {url}: {e}")); + resp.json() + .await + .unwrap_or_else(|e| panic!("Failed to parse example manifest: {e}")) +} + +/// Download a URL to a local path, streaming chunks to disk. +/// +/// Writes go to a sibling temp file that is `fsync`'d and then renamed into place, +/// so a partial download from an aborted run can never be observed at `path`. +async fn download(client: &reqwest::Client, url: &str, path: &Path) { + let mut resp = client + .get(url) + .send() + .await + .and_then(|r| r.error_for_status()) + .unwrap_or_else(|e| panic!("Failed to download {url}: {e}")); + // NOTE: We currently store `gzip`-ed versions on GCS, so this will always be `None`. + // If we ever decide to store and serve them unzipped, we'd benefit from additional + // checks, so I think it is worth leaving in. + let expected_len = resp.content_length(); + let parent_dir = path + .parent() + .unwrap_or_else(|| panic!("Cannot determine parent of {path:?}")); + let mut tmp = tempfile::Builder::new() + .prefix(DOWNLOAD_TEMP_PREFIX) + .tempfile_in(parent_dir) + .unwrap_or_else(|e| panic!("Failed to create temp file in {parent_dir:?}: {e}")); + let mut written: u64 = 0; + while let Some(chunk) = resp + .chunk() + .await + .unwrap_or_else(|e| panic!("Failed to read from {url}: {e}")) + { + tmp.write_all(&chunk) + .unwrap_or_else(|e| panic!("Failed to write to temp file for {path:?}: {e}")); + written += chunk.len() as u64; + } + tmp.as_file() + .sync_all() + .unwrap_or_else(|e| panic!("Failed to sync temp file for {path:?}: {e}")); + if let Some(expected) = expected_len { + assert_eq!( + written, expected, + "Truncated download from {url}: got {written} bytes, expected {expected}" + ); + } + tmp.persist(path) + .unwrap_or_else(|e| panic!("Failed to persist download to {path:?}: {e}")); +} + +/// Ensure a single example is cached at `path`, downloading it if missing. +async fn ensure_rrd_cached( + client: &reqwest::Client, + entry: &ManifestEntry, + path: &Path, + version: &str, +) { + if path.exists() { + return; + } + eprintln!("Downloading {}.rrd ({version})…", entry.name); + download(client, &entry.rrd_url, path).await; +} + +/// Load example .rrd files from the previous release into the current viewer. +/// +/// Asserts: +/// - No panics during load + render +/// - At least one .rrd was downloaded and loaded +/// - A snapshot is saved for each example (for visual review) +#[tokio::test(flavor = "multi_thread")] +async fn test_old_rrds_in_current_viewer() { + let client = reqwest::Client::new(); + let (major, prev_minor) = previous_minor_version(); + let version = resolve_latest_patch(&client, major, prev_minor).await; + eprintln!("Testing backward compatibility with version {version}"); + + let cache_dir = directories::ProjectDirs::from("io", "rerun", "rerun-integration-tests") + .expect("could not resolve the OS user cache directory (HOME unset?)") + .cache_dir() + .join("rrd_bw_compat") + .join(&version); + std::fs::create_dir_all(&cache_dir).expect("failed to create cache directory"); + + // Clean up stale temp files left behind by aborted runs (e.g. SIGKILL), + // which `NamedTempFile::drop` cannot remove. + for entry in std::fs::read_dir(&cache_dir).expect("failed to read cache directory") { + let entry = entry.expect("failed to read cache entry"); + if entry + .file_name() + .to_string_lossy() + .starts_with(DOWNLOAD_TEMP_PREFIX) + { + let path = entry.path(); + std::fs::remove_file(&path) + .unwrap_or_else(|e| panic!("Failed to remove stale temp file {path:?}: {e}")); + } + } + + let manifest = fetch_example_manifest(&client, &version).await; + assert!( + !manifest.is_empty(), + "Should have at least one example in the manifest for version {version}" + ); + + // Buffer up to `DOWNLOAD_CONCURRENCY` downloads in flight; yield each path as + // soon as its download finishes so the next test can start immediately. + let mut downloads = futures::stream::iter(manifest.into_iter().map(|entry| { + let path = cache_dir.join(format!("{}.rrd", entry.name)); + let version = version.clone(); + let client = client.clone(); + async move { + ensure_rrd_cached(&client, &entry, &path, &version).await; + path + } + })) + .buffer_unordered(DOWNLOAD_CONCURRENCY); + + let mut results = SnapshotResults::new(); + let mut expected_triggered = vec![false; EXPECTED_NOTIFICATIONS.len()]; + + while let Some(rrd_path) = downloads.next().await { + let example_name = rrd_path.file_stem().unwrap().to_str().unwrap().to_owned(); + eprintln!("Loading {example_name}.rrd…"); + + // Open the .rrd via the viewer's normal file-open path (same as Cmd+O). + let file_path = rrd_path.canonicalize().unwrap().display().to_string(); + let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { + window_size: Some(egui::vec2(1024.0, 768.0)), + startup_url: Some(file_path), + max_steps: Some(200), + ..Default::default() + }); + + // Wait for the loading popup to disappear. + step_until( + "loading popup dismissed", + &mut harness, + |harness| { + !harness + .query_all_by_role(Role::Window) + .any(|window| window.query_by_label_contains("Loading").is_some()) + }, + Duration::from_millis(100), + Duration::from_secs(10), + ); + + assert!( + harness.state().active_recording_id().is_some(), + "{example_name}.rrd did not produce a recording." + ); + + // Pause playback and seek to the end of the recording so the snapshot + // is deterministic. + let on_log_time = harness.run_with_app_context(|ctx| { + ctx.send_time_commands_to_active_recording(vec![ + TimeControlCommand::Pause, + TimeControlCommand::MoveEnd, + ]); + ctx.active_time_ctrl() + .is_some_and(|time_ctrl| *time_ctrl.timeline_name() == TimelineName::log_time()) + }); + harness.run(); + + if UNSTABLE_BLUEPRINT_EXAMPLES.contains(&example_name.as_str()) { + harness.run_with_app_context(|ctx| { + ctx.command_sender() + .send_system(SystemCommand::ClearActiveBlueprintAndEnableHeuristics); + }); + harness.run(); + } + + // Close all panels so the snapshot only shows the viewport. + harness.set_blueprint_panel_opened(false); + harness.set_selection_panel_opened(false); + harness.set_time_panel_opened(false); + + // Mask OSM-tile-backed map views whose content may change as OSM updates. + if MAP_VIEW_EXAMPLES.contains(&example_name.as_str()) { + let map_rect = harness.get_by_role_and_label(Role::Pane, "MapView").rect(); + harness.mask(map_rect); + } + + // On `log_time` the timeline track's playhead and tick positions are wall-clock based, + // so they drift between runs and would break the snapshot when it's regenerated on a + // patch release. The track is painted, not text, so `mask_dates` can't reach it. Mask + // the whole collapsed time bar at the bottom instead. + if on_log_time { + let screen = harness.ctx.content_rect(); + let time_bar = egui::Rect::from_min_max( + egui::pos2(screen.left(), screen.bottom() - TIME_BAR_MASK_HEIGHT), + screen.max, + ); + harness.mask(time_bar); + } + + if !NONDETERMINISTIC_EXAMPLES.contains(&example_name.as_str()) { + // Mask any timestamp text so snapshots stay stable as the calendar + // day rolls over. + harness.mask_dates(); + + let snapshot_options = if HIGH_THRESHOLD_TESTS.contains(&example_name.as_str()) { + SnapshotOptions::new() + .threshold(2.0) + .failed_pixel_count_threshold(10_000) + } else { + SnapshotOptions::new() + .threshold(2.0) + .failed_pixel_count_threshold(50) + }; + harness.snapshot_options(format!("rrd_bw_compat_{example_name}"), &snapshot_options); + } + + // Assert no unexpected warnings or errors were shown to the user, and + // record which expected notifications were triggered. + let mut bad_notifications = vec![]; + for n in harness + .state() + .testonly_get_notifications() + .notifications() + .iter() + .filter(|n| { + matches!( + n.level(), + NotificationLevel::Warning | NotificationLevel::Error + ) + }) + { + let mut matched = false; + for (i, expected) in EXPECTED_NOTIFICATIONS.iter().enumerate() { + if n.text().contains(expected) { + expected_triggered[i] = true; + matched = true; + break; + } + } + if !matched { + bad_notifications.push(format!("[{:?}] {}", n.level(), n.text())); + } + } + assert!( + bad_notifications.is_empty(), + "{example_name}.rrd produced unexpected notifications:\n{}", + bad_notifications.join("\n") + ); + + results.extend_harness(&mut harness); + } + + let untriggered: Vec<&str> = std::iter::zip(EXPECTED_NOTIFICATIONS, &expected_triggered) + .filter(|(_, triggered)| !**triggered) + .map(|(msg, _)| *msg) + .collect(); + assert!( + untriggered.is_empty(), + "Expected notifications were not triggered by any example (remove them from EXPECTED_NOTIFICATIONS):\n{}", + untriggered.join("\n") + ); +} diff --git a/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_1.png b/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_1.png index 384b74edcdd3..adc923e68119 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:223c6c1649cb5ad5e54f0101b495ab172d67404c56269b74a9636ac9eae3e530 -size 205175 +oid sha256:0b0bcb8834a9da5d2d433ccef8b73b31a8e15ecd47ebc1c1ccdb94661a219f1f +size 203437 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_2.png b/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_2.png index 3ea2d28e2dab..901e55409b5d 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc3fe245e68b51cd54b47c1e5530c72e9910ccdfd0c1513b0dd98fc466480ab0 -size 228143 +oid sha256:9d38beb0597545d437255e04c8947ea961c5a04c97d76b5966f90726389c61fd +size 234444 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_3.png b/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_3.png index 254a4f02026c..f8ecbb122e19 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_container_from_blueprint_panel_menu_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd4af6a3a6984082c3b2e848a532358578d09070765a916e0f9af6c02dd0956c -size 178331 +oid sha256:f1d5a2c6bbca453a93653ef64d546b8db2eceabcf527b9686a6191fa0bab31d9 +size 176607 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_1.png b/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_1.png index db34eb78029a..4a010cc2b832 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74794faf28309ad0e66b810f36067f65d0949459e29e91344cb753ab89c532a5 -size 167512 +oid sha256:4ce728d08bfbd7dbb15d3b2afd0584779b3c01fb1b27a37c41b73b816ff85a4c +size 165648 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_2.png b/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_2.png index 69b109e7b4bb..1872028aedca 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d57830089b08efdb6ea02989608d7cb4eeee54cc1c2862f3bef7290826b9f544 -size 203520 +oid sha256:7f3c486080d7ac2bcead4c924dd38745302536bd6cc17ea09df18af75b1ed282 +size 209419 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_3.png b/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_3.png index 3a6067ea2cdc..9d407f928497 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_container_from_selection_panel_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f11c5082dd083d9385c98a5463b321e25613a6d6778817fb2baa386c3bd4f79 -size 164478 +oid sha256:ef25fec2b7e7968ce991746b0edac9a68d9fd12ce88074e89654fc097a7628ec +size 162783 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_1.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_1.png index ad02c51137f8..68cfb6c33057 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fba50e33ab490b8e5c4847ce9a912c8e203ed6ea8fc0d4a8bbf0c0ddaa75e5c4 -size 187388 +oid sha256:a434b9a2ffe941aad4e81844edcfdb495c9f6a735e8f99852dfc30b23ebfb75f +size 184531 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_2.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_2.png index a9caec95d216..5d368bdc8062 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80979dd43efebabac97f68e2c3536d8e9d31c2f147b971fa8e42263f330acf47 -size 202045 +oid sha256:137403480a7c8e0bf476d2a7af3ba9ca2b82e30f345df21a60b35d3efdc7985a +size 196908 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_3.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_3.png index f62f861ea503..deb4917ddd9c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4df24c2437d1409719c0eda424b8e71d34fc4f5a9c2ffef70fb045bee0d47934 -size 192645 +oid sha256:7712a18ce07f1274fa1f2ff0dac2c38ae51ea8c0bf57fcb6bd007a1c5a58eea7 +size 188958 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_4.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_4.png index abfab6fb90fd..1ecbb96a8ef5 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7182b9d54f90991630c35b3780618a210182531b262053ff20bb1dbab52b1deb -size 205086 +oid sha256:04fec9532f05358b14948e0ec853ad846dbfd03284b43529b8456b15745a818b +size 199801 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_5.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_5.png index 0ca96f97bed5..39018aa84490 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_bar_chart_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5152be3090d4911a24bdd5fa0e07492b8bfbd0bfe6e0eeb510f8833dd8e74ac -size 150957 +oid sha256:f1ca3fac23d4a68082537daaddf7ad7c511f99edc56a0c270ff8b91aa849b234 +size 149419 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_1.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_1.png index f95c024e879c..e8e77d6d7882 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4164b743721a8de39f02d385e7265f4df1442ef0c7c6ac280ee4466effa0991a -size 200307 +oid sha256:c17e494100646e39078f2b4eb74c0d577eeb300c42b739caa3ea5b1ef5007e32 +size 200153 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_2.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_2.png index 5e4ece241bc3..24cfd7bc8d42 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a177188488f23ee23d4f1cffd82787e34e84d4d49b29d65d091ea18e449b7a24 -size 208682 +oid sha256:77b42c3f29076f71e7405776c70b11444d9a2bd1c0f3bddf8d142dd6db274b3b +size 208608 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_3.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_3.png index ef19095e69c5..8ac0fc052d84 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2715387796d873125bf5d8fe86f2ae7fd657e9e78223d585b4403d045a79d257 -size 193685 +oid sha256:ba78229825bf0f409de092461f0efb5c02e60683cfee682cd91197f7d6a67005 +size 204308 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_4.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_4.png index 95ad64525596..202398d6f1b0 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15486d28614fe69b7cf569e5483a5e42367ef091ba8ae9bba66a3672bc98755e -size 206215 +oid sha256:9c000c5a114ebe04ade679b34e9bed00868b8337ff1cf5fff4aec031c5272f3d +size 215367 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_5.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_5.png index 0ca96f97bed5..39018aa84490 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes2d_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5152be3090d4911a24bdd5fa0e07492b8bfbd0bfe6e0eeb510f8833dd8e74ac -size 150957 +oid sha256:f1ca3fac23d4a68082537daaddf7ad7c511f99edc56a0c270ff8b91aa849b234 +size 149419 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_1.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_1.png index a01d03d664c2..8232eb183303 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5432e255e2ac4bbb6e039085cc1ab3928f2d782831931bec65e2b4a4bcff4fbc -size 211757 +oid sha256:882881abee400b5527d40ff723a3b7ed912a6b44ad462ca48d0fb0d5c0298de8 +size 211268 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_2.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_2.png index ce7bdccd5482..8180b29d4a6f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:585c5d7fd9b5a69f359a4280c939a7bf5aca553061a166dff6fb5db417c8a0d7 -size 221198 +oid sha256:237ce8c5109031ddaa3f4fc5462870c3a1ac09a859a4ff655ded48b156bbbdcf +size 219316 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_3.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_3.png index 4d758e9d5284..7ed7b0780cbc 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:112ef8400973b1471db9758ddeb3fa047b3fea87fe5b68c31fbe3701d1489800 -size 230800 +oid sha256:8b575266f3e53ee877e56eb609d11ae2587f8a90c1055157bbeb51245550465f +size 229130 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_4.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_4.png index 2e0d468ce4ee..92e40c009dea 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8d8a4a7a46db6f530f9d5b9239aa4f1c07a0ec353769978d58675d9f46178aa -size 243936 +oid sha256:60fc05847d1a1195a9c4ab6750fa277b02a73d05bbe25237599cc4e9083537c6 +size 239839 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_5.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_5.png index 0ca96f97bed5..39018aa84490 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_boxes3d_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5152be3090d4911a24bdd5fa0e07492b8bfbd0bfe6e0eeb510f8833dd8e74ac -size 150957 +oid sha256:f1ca3fac23d4a68082537daaddf7ad7c511f99edc56a0c270ff8b91aa849b234 +size 149419 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_1.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_1.png index 6e9a85af78a4..e3c1d5f46aa0 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a2892bce29cb532397d3e16390d891365aeb2bfa760acd7a64d5e24876a69bd -size 183144 +oid sha256:e9ecf922c55de35bc5bb651392c7f7435bd7b545460380aa144e2432525f1969 +size 181079 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_2.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_2.png index 7a32649fab11..bf889604002a 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f67295061657d30ecf6f51d087d1b777595922d869804f84ae8828064e93d5c -size 196307 +oid sha256:595ef4dfe2a30c4316f8be748c6c4c25b5c38f0abefcd337723c6df32994428b +size 192807 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_3.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_3.png index ddc04f7e7a3b..784bbff29a5d 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0594899efd1ce154ae7630b7ab0fc43e1d8afe5227847ebecbce189fbd22a360 -size 214856 +oid sha256:cdb6c9e213122da97ec0c0ee0eb9e612e0b385bbee0a20966944e2d8350bc1dc +size 210987 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_4.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_4.png index 07c69cd782d3..87121f9520b6 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0eaaa4c7479827600fa2521684ed59e4544e37ddf61699a1effd8b4c5c0db83d -size 227002 +oid sha256:17671faae5e4db61b9415b541bd26355f99f6763518671dce23f69aae82876b4 +size 221918 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_5.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_5.png index 0ca96f97bed5..39018aa84490 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_tensor_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5152be3090d4911a24bdd5fa0e07492b8bfbd0bfe6e0eeb510f8833dd8e74ac -size 150957 +oid sha256:f1ca3fac23d4a68082537daaddf7ad7c511f99edc56a0c270ff8b91aa849b234 +size 149419 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_1.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_1.png index 3a90e1d8653b..beb996f177e3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6eae89e4a6359122fe58c78067d13b398bd4262dedb7dd3b1b9f5efd10f54d4 -size 184748 +oid sha256:bbb3b3fa5e55c50b49fecef7caaff6fa1be8f1b777aea011aa63e9e9dfac7731 +size 181046 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_2.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_2.png index 5483af754869..afcfc8a71ec4 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0083d17d757b3e420401754408600cf44a0449b61c24f41315a2daec5c007a43 -size 196750 +oid sha256:7a12c252ab8809cccad7e96e1f33963d9ed29c941ff3681c771641aba4ef8614 +size 193306 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_3.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_3.png index c4f7e5f7e30e..172aa27e81ad 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9470cd7d9283391c225bb15ccc78310065bec1ee8a063a04c7e37ea1d830e3c1 -size 192576 +oid sha256:16dc336a61ac2c0a641904b5a886f7b562fe06a093bf4a0ff5764af3a1ffaa79 +size 196441 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_4.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_4.png index 718583449a55..f0901917ac0a 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d7975baad1526423b925109b365706175f32fd07ae1a5e26524b9d9adfcadb0 -size 210998 +oid sha256:11fa049a848e85bec19575bd86620e4dbe740600b7daa41236c4b8d52c627152 +size 207612 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_5.png b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_5.png index 0ca96f97bed5..39018aa84490 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_entity_to_view_text_log_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5152be3090d4911a24bdd5fa0e07492b8bfbd0bfe6e0eeb510f8833dd8e74ac -size 150957 +oid sha256:f1ca3fac23d4a68082537daaddf7ad7c511f99edc56a0c270ff8b91aa849b234 +size 149419 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_1.png b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_1.png index 60a700d35f67..09433b555141 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adc374bdda66b080b71b1e834af537bf9222b65819a8d6002c708dde0831c3be -size 155734 +oid sha256:fe255307d1ae451a363293fdee196f270524ea19407128da778bfdf8371c701b +size 157639 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_2.png b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_2.png index 6d9b7e53a3a5..7b8cae3fbd1e 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51645152a8e13d81503ac09dc1488f5677f95c1c94d55d10b22af0d24af318e3 -size 184665 +oid sha256:2ed1a49a28d0eb4bc8dc01c636ddbb0d58066285f560db1c517f491bd104d7fd +size 185050 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_3.png b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_3.png index ed3d9204d7f8..43830091b949 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c05553bbf24f51e047e304e9982e23b5746ec31e96443511eda37a2480a1d8f -size 180233 +oid sha256:54dc242bc355ecf6cda117ac3c4632cad10fc88c7db2f0f82ff5691aab9b22e0 +size 181390 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_4.png b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_4.png index adf473dab0f5..1308f4f78732 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be5a7fdfc979ec074dcfef45cf264c817799f73f386540420465c53e68e92c82 -size 188896 +oid sha256:d66510ace7cadc276f3a40322ed720f3dcbf7b3cc0a99aad5e3df349f5331169 +size 186959 diff --git a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_5.png b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_5.png index 962732b32497..d139c47cbb14 100644 --- a/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/add_visualizer_axes_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2073b1bbad1339dc05639dc3a6e60b25f02f7da45a51214bb5318365e01d190 -size 190074 +oid sha256:c4dc8ad883dd1854f11eb9197c6094fab4c9d44030cb4b5269bf45073fc8596c +size 189349 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_01.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_01.png index 457f6b11a953..765db75c8a74 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_01.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_01.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d8f404a2b065b65cdb417413441be44f820d99ef2396d6a93b5f48d3c609e56 -size 120262 +oid sha256:c55c50725bcb539cec97b905923d57cec8bb49dfd4796d8d7d3e2a631b463556 +size 120493 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_02.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_02.png index a2cce5fd629d..27a6ed29a5d3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_02.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_02.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4f50dc25ad22862771daf9b200f908c79457b139813c7290b0da328646f5c10 -size 136150 +oid sha256:8cab2755cbdaf42519831818eab699034d565044fdf6e62c4448e16cadb356c7 +size 136110 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_03.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_03.png index 94349d87ae49..16530dc28701 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_03.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_03.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7925f7674e8c1da4fe457853215bc5d0b86a0afeaa3f2753c9e04bf6c5adb75 -size 130967 +oid sha256:59b989ed657fd48080e142773cb442afce7f1d369655264906d40d4089f3b4c8 +size 130912 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_04.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_04.png index edc3b3d302c2..6c3e061e0af5 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_04.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_04.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f13810fc6e7a2cbeb03501e096abcf2a225ffb6bfc740fa1c65b137a6e40d61f -size 135280 +oid sha256:46d346cb42f6a6a0db00558c41efe9830bfec64dfd76c8e018dc8df179e1819b +size 134710 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_05.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_05.png index 73279c30e7e0..c02b7a3df152 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_05.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_05.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8d8f3383eddadac0def506961b42e6cafb81cf36d2f361eaf17f75fd540aef7 -size 128503 +oid sha256:948f0af49e8782e875158900d5be46954c15f8b27727a8cef795893c600b4fa0 +size 128495 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_06.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_06.png index 76ac83107d71..73044543cf6a 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_06.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_06.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e43187020e0453aabaf7fbe49111852d28c7ddf35fbef3342a849ccf895b664c -size 143319 +oid sha256:6331545dda4fdac78c85470f91a6cb5c7661996c0d255fa74b12af3129c8708d +size 142809 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_07.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_07.png index c772a240a4e9..c004b0b66da9 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_07.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_07.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d492ed8350f08e69e297a3bdf27774fed902e7a72795399bb14b35831ee62bc -size 129915 +oid sha256:4d034541382c05b5ceaaea28f088d4d0d71350db35c29f019dedfb1f9ab42d10 +size 130211 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_08.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_08.png index 89d9fb6d6c51..41327c98e617 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_08.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_08.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae9f4d069c9361e785677597d4436eedf73240036a6f60bed04cf03aa3172234 -size 141527 +oid sha256:b498c796387176e5d81fea70fa62aa4b624964285aaf1b2366e470c7690c854a +size 142990 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_09.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_09.png index a9ccdfd25e20..92aac1dcaf1f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_09.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_09.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbd129146b658a9afe591ea437b274456aa49b634974de9c5c5542ba899b645a -size 130320 +oid sha256:d9b9d49fe099620a4db40b190c7ce484c214fd677a32044c0a07b4c0a02f1275 +size 130629 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_10.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_10.png index e84eae1f24b7..f9b585c36168 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_10.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_tree_context_menu_10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c30f1bfea31eda3c965092bb91c8480f6a5650ce78dbcf1ccf7d8d702724f999 -size 145301 +oid sha256:e86421d098aab421a9fa5da29d9f903f759d0a4de8ff33b4e551cd938ee984c9 +size 146449 diff --git a/tests/rust/re_integration_test/tests/snapshots/blueprint_view_context.png b/tests/rust/re_integration_test/tests/snapshots/blueprint_view_context.png index a2292b73b944..92422b237210 100644 --- a/tests/rust/re_integration_test/tests/snapshots/blueprint_view_context.png +++ b/tests/rust/re_integration_test/tests/snapshots/blueprint_view_context.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b344a0f8109cbd1cb449ba438a3713056ae15ee2a1c35ff4a032cfecd3ea902 -size 67111 +oid sha256:c341a63ca07f584838cc2b8ecd82265e392379cedc2973ba8a3a2499c6d957d1 +size 64170 diff --git a/tests/rust/re_integration_test/tests/snapshots/change_container_type_1.png b/tests/rust/re_integration_test/tests/snapshots/change_container_type_1.png index 548595f6ead7..9d8a7bcbb04e 100644 --- a/tests/rust/re_integration_test/tests/snapshots/change_container_type_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/change_container_type_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec66d4974a1c866da5d85bc28cbc57c272a8f97c10bbb2565822b6703cba05a2 -size 164036 +oid sha256:b902e1720cb2f25c8e697561a9d1f26ef942ecee2805ab733a346ef30b84a60d +size 162411 diff --git a/tests/rust/re_integration_test/tests/snapshots/change_container_type_2.png b/tests/rust/re_integration_test/tests/snapshots/change_container_type_2.png index 70bc09f1db75..fb97e55b5ace 100644 --- a/tests/rust/re_integration_test/tests/snapshots/change_container_type_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/change_container_type_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:041e853369d11464d83ff90086ade5d41f34695f2b38b84314cf3d157bfc5395 -size 149360 +oid sha256:3fd095041eb5fcdcb32149fdaa998d1781e5577b9f95161abecf9707778767c3 +size 147485 diff --git a/tests/rust/re_integration_test/tests/snapshots/check_focus_1.png b/tests/rust/re_integration_test/tests/snapshots/check_focus_1.png index 33bc73e8fefb..a8210fe098ef 100644 --- a/tests/rust/re_integration_test/tests/snapshots/check_focus_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/check_focus_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:898f4e1975f792e97f35ff9c4e1f64d4ff4b2700de91877333f69d5f536e9b9c -size 137117 +oid sha256:645b071b53237eed2c6e3c74a467822b9d807f2f5e14fee8924cf2018e308635 +size 135622 diff --git a/tests/rust/re_integration_test/tests/snapshots/check_focus_2.png b/tests/rust/re_integration_test/tests/snapshots/check_focus_2.png index a79571e469bb..f0941e493e7d 100644 --- a/tests/rust/re_integration_test/tests/snapshots/check_focus_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/check_focus_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:017c8e71549b795a485ad64b1741ebba0286ab4b02741f2456008d9c068dc57b -size 130578 +oid sha256:2d4b61c5a413a88173cb033ee93c6512841a97decce2a458b2312850d60801b8 +size 128591 diff --git a/tests/rust/re_integration_test/tests/snapshots/check_focus_3.png b/tests/rust/re_integration_test/tests/snapshots/check_focus_3.png index 6a73a795784a..057747625c8e 100644 --- a/tests/rust/re_integration_test/tests/snapshots/check_focus_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/check_focus_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59a8bb0a50dad054fd7389817d55042debe0cbcca92c57d0b12f51467f18db63 -size 129539 +oid sha256:79cd899b11016518b4a0f2c6444e3c3f327670a96d0799c85f2835abc96dd429 +size 128554 diff --git a/tests/rust/re_integration_test/tests/snapshots/check_focus_4.png b/tests/rust/re_integration_test/tests/snapshots/check_focus_4.png index b3402728a433..686d447e9cb2 100644 --- a/tests/rust/re_integration_test/tests/snapshots/check_focus_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/check_focus_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9418448bd41148c5eeefdbfabe88ddd9ea0bcc6c455281c739d06aa8ac5a76a -size 131839 +oid sha256:66279a2106f107db12ade305db2d3c622b9447ff145402073f8214af1a5cc8d9 +size 130793 diff --git a/tests/rust/re_integration_test/tests/snapshots/check_focus_5.png b/tests/rust/re_integration_test/tests/snapshots/check_focus_5.png index 012338d0801c..8e12a0a8aa05 100644 --- a/tests/rust/re_integration_test/tests/snapshots/check_focus_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/check_focus_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ba6f29a6486526853541f7fad46b49a0350b91d9deba2c5e1373a50922b411e -size 128978 +oid sha256:8a8d6138b8323cb5560832faac14713932b18bfd28d94cbf7e1020ab132b7e63 +size 128091 diff --git a/tests/rust/re_integration_test/tests/snapshots/check_focus_6.png b/tests/rust/re_integration_test/tests/snapshots/check_focus_6.png index c9842a421c6c..e966467928e0 100644 --- a/tests/rust/re_integration_test/tests/snapshots/check_focus_6.png +++ b/tests/rust/re_integration_test/tests/snapshots/check_focus_6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0eb76b99ee76e925d526344df389fdb408c91f30afe560c8f6a5b2e02a3b7c31 -size 129453 +oid sha256:c516b6d13df48b6256869589a0fa17631b9f31f481418d2b37d5d9eb85d3cbb8 +size 128515 diff --git a/tests/rust/re_integration_test/tests/snapshots/check_focus_7.png b/tests/rust/re_integration_test/tests/snapshots/check_focus_7.png index 065d585e58db..8f9601c5df3a 100644 --- a/tests/rust/re_integration_test/tests/snapshots/check_focus_7.png +++ b/tests/rust/re_integration_test/tests/snapshots/check_focus_7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73a80faa1a02575642fda56f6b3aa486827b7df7d20e8a58a990a3286c21118c -size 130516 +oid sha256:7f80fd299e40234327846efafef9458c3a47539fbd034e4e5b3c1c921d2ccbf0 +size 129321 diff --git a/tests/rust/re_integration_test/tests/snapshots/check_focus_8.png b/tests/rust/re_integration_test/tests/snapshots/check_focus_8.png index 5547601763c8..1f396b55482c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/check_focus_8.png +++ b/tests/rust/re_integration_test/tests/snapshots/check_focus_8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b0b21e5082360f3d5dcd0af1adc8746c818959bbcad94099dfff3c97370508b -size 129203 +oid sha256:1b5b1ec1b912aca99860002537a6bb912b50b6903d28cae51567c3761b9be40d +size 128382 diff --git a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_entity_1.png b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_entity_1.png index e3abc8de8486..036c7704d065 100644 --- a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_entity_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_entity_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31f7413e8f0b3be3d197256bc5e309f8ff7229235653b7b55e46bebfb079e282 -size 60686 +oid sha256:f012eca3080da20c26b1556f5ef2367299ebed4b3b6d217fb8b1f77f8478b66a +size 58793 diff --git a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_entity_2.png b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_entity_2.png index 689c3720aebc..a49d55cea314 100644 --- a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_entity_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_entity_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13388bc65f856bad4b7ad4aec2c06a4c6f10c19ad35ca48442932e9be0e0f535 -size 49928 +oid sha256:3680a99b93735bf6c5a71d379a4b81540ae4ccaea620e138967758d606c56422 +size 48755 diff --git a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_1.png b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_1.png index b1f99039471f..1e6454bee0f2 100644 --- a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4aa42be4e24aa857bc308b6332a87944722e74d5fd650a14555904885ad603ef -size 50076 +oid sha256:9cc86a63cea3483676ebc70b7f85f767f0ba1061db0cdc777bec03b0ec8fc2c9 +size 48668 diff --git a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_2.png b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_2.png index af8bd80e4de9..14288973a548 100644 --- a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dba2fdaac2be979ab2017b4f1ab6d3a704591e29b436d8c9c3dca7e7c973d79 -size 56495 +oid sha256:fcd824ebdcf5e7e92f1072516c55ded13917bb12cf9d5cc3584dea4729206a85 +size 54540 diff --git a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_3.png b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_3.png index 8e392ad40190..0be6e7de932a 100644 --- a/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/collapse_stream_root_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c2e3dee5aecc33a41f3443d4272bd90beebf08d27e333bb9f331a809242ee33 -size 46602 +oid sha256:908603145ac584b64329bb83ef7726c82705f1062dc37a368ce3bccea73ccf78 +size 45536 diff --git a/tests/rust/re_integration_test/tests/snapshots/container_selection_context_menu_1.png b/tests/rust/re_integration_test/tests/snapshots/container_selection_context_menu_1.png index 27219a611f21..0fce1b015737 100644 --- a/tests/rust/re_integration_test/tests/snapshots/container_selection_context_menu_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/container_selection_context_menu_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8168b86026b69eef1902eba99e21267f169402d9724de113873f1e204540d22a -size 91391 +oid sha256:a1a76bd1c35175fd294017241f49c8ad70e9d13c3348a39d2981c62d02cafaa8 +size 88213 diff --git a/tests/rust/re_integration_test/tests/snapshots/container_selection_context_menu_2.png b/tests/rust/re_integration_test/tests/snapshots/container_selection_context_menu_2.png index 84d2ff3b8039..82f43a68ea62 100644 --- a/tests/rust/re_integration_test/tests/snapshots/container_selection_context_menu_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/container_selection_context_menu_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc614823afb2adbb8697c92352af51d2ee2db2169830b5184521ddbf8db7dbd -size 91924 +oid sha256:7f065f80fa06b6dc154f76963be7c6935b76ddc60d46886e5f31f245cb2f8420 +size 88994 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_01.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_01.png index 88f51082ebcf..2f1418391739 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_01.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_01.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a10fc36bee1ed14a1eb490d1c343b85b34028aa5b54d5503ac5d711779cd50e6 -size 87306 +oid sha256:852057bf8191b581db06a40c0ff933693ba700684e10fed9d8d29ea5bc3d85df +size 86971 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_02.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_02.png index fb90da049c1f..78ba380326e3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_02.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_02.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be65e6665bbad98fce855294fc2e5650ce2bdbc61e20534a174c2973c75fde90 -size 113227 +oid sha256:6f0194c8e87917de4aee8c982323c778c4a7ab1e0ccaba24e40d3014c082b9ba +size 112500 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_03.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_03.png index a0bc25e3599f..f99c3a60a69f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_03.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_03.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:807c873ff0da5e3c151ffe3c307432e8c4d246cb0a4f77d3e98acad2ffd31c02 -size 107610 +oid sha256:b84528276c682a43d416aac2f5f5fb7fa5b1170ecb2bd83d4f9895e55d02b191 +size 107054 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_04.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_04.png index b4ff594af1c0..a5be9fbd9aa6 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_04.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_04.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:801533b718e49c0b6f977367bae9b64f33c60f07c9a8438ecd7ebeed0757abf4 -size 109133 +oid sha256:8ea8fe150ca977df1e961646a99c2cc264bc01ee915b8c2784dcde2d3c25f984 +size 107885 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_05.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_05.png index 0f10b87e59d1..d5a38da63cbc 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_05.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_invalid_sub_container_05.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c4ddc96fba4a98a88fa6232080d8a9ba0d3bb62a04cdb989e403ff261f981b9 -size 111255 +oid sha256:8f405132f2ee541808ab83ece345d4eacfd0729fd9d7e05522697f8c2479004b +size 108633 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_01.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_01.png index 88f51082ebcf..2f1418391739 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_01.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_01.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a10fc36bee1ed14a1eb490d1c343b85b34028aa5b54d5503ac5d711779cd50e6 -size 87306 +oid sha256:852057bf8191b581db06a40c0ff933693ba700684e10fed9d8d29ea5bc3d85df +size 86971 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_02.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_02.png index 92159a6c7b7c..16f000479956 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_02.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_02.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d53c1b693365fec2be0c957c0b3a55d907d66ebfcaa6bd3a7744cebcb3b673e8 -size 95071 +oid sha256:e3968199573a012054c864a09aa1c2c670a7583eacd8dce4e6a555490ad73720 +size 94133 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_03.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_03.png index 78f32e59490c..2d4cf1efe4d5 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_03.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_03.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8574ab92ce1f17ca55608f4ee31a391a31ce11a0a407549c8726a6ea5f3181a1 -size 105053 +oid sha256:3e60dd236c9f1ce5b31255dbfc57ff6d23407d4467e403b5ff88fd51d544cbb8 +size 103519 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_04.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_04.png index 5ec4cb3b8e42..911406ef5936 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_04.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_04.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:888a007f62f6add3eec212511a1eacf78c73806db49c0d18aa06008ad013ac6d -size 102794 +oid sha256:cff568ce8a8e7b9f4c5ac390aae9ac6266ce2be0cbf5b68b573a4995697670d8 +size 100984 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_05.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_05.png index 546267b9848a..50086df3fc4d 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_05.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_05.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:185f9c3494f6810c37b60541f6397b8dd0160a644da01fa7740b77fa1db2043f -size 95182 +oid sha256:9490d00c1e409c731c8bd3c1780c12831ee0a48be97f0cdd72f6c3af45f9ae46 +size 93509 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_06.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_06.png index ad1c47839513..09ecd632f0ce 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_06.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_06.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ba535bd19552dd327f8719e3c1e9e5b9786c1a422670860f10349f58894ecb8 -size 102609 +oid sha256:61e3a2ff89a06f5ffccf6b3dfd056ccd763102b2d257f2299399facd0e8e3dc8 +size 102511 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_07.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_07.png index 4c82ed60391d..e77654fe5128 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_07.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_07.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c51ed87f67ddb2f57713cda916ff2bf921c5ed6b3d4e719790056fdd6ac7cd47 -size 108229 +oid sha256:3f8d90a2f013f53197b0ed126228ca6d0b7220095b85fc87bab33c1d8b389850 +size 107761 diff --git a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_08.png b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_08.png index 9c427accd649..7a83fb47a192 100644 --- a/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_08.png +++ b/tests/rust/re_integration_test/tests/snapshots/context_menu_multi_selection_08.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51b08902048d75b2dc6677061073fecd762dd849e6ab1dec37892716ca91f425 -size 105383 +oid sha256:258c9e0a3496bcc013ae0d0baefca0b4b9aac1ad18b318cdb8d7573b61875e07 +size 104655 diff --git a/tests/rust/re_integration_test/tests/snapshots/dataset_folders_01_perception.png b/tests/rust/re_integration_test/tests/snapshots/dataset_folders_01_perception.png index bd2a58f80654..093a018832a7 100644 --- a/tests/rust/re_integration_test/tests/snapshots/dataset_folders_01_perception.png +++ b/tests/rust/re_integration_test/tests/snapshots/dataset_folders_01_perception.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70f56ba9cb41c0449781576ab84b6a4878755f937fafa3df7f7307afeb42830a -size 41715 +oid sha256:690d8c031e1d53499eed2c84ce8e5cba6fd2018063add30843b129a71f9e241f +size 42143 diff --git a/tests/rust/re_integration_test/tests/snapshots/dataset_folders_02_perception_detection.png b/tests/rust/re_integration_test/tests/snapshots/dataset_folders_02_perception_detection.png index 9eb060a14500..0aa21a3eb313 100644 --- a/tests/rust/re_integration_test/tests/snapshots/dataset_folders_02_perception_detection.png +++ b/tests/rust/re_integration_test/tests/snapshots/dataset_folders_02_perception_detection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6031a56a05ccb49d072f2ee4c013e6d6bcd46506b6c842c63681e32e4a537bf6 -size 45529 +oid sha256:671afdd485454a5aebfa730e9dab071815bfa5732765b0e98537714a5c6fb925 +size 45789 diff --git a/tests/rust/re_integration_test/tests/snapshots/dataset_folders_03_perception_after_parent.png b/tests/rust/re_integration_test/tests/snapshots/dataset_folders_03_perception_after_parent.png index 0a2289567bdd..71c9f42d84bc 100644 --- a/tests/rust/re_integration_test/tests/snapshots/dataset_folders_03_perception_after_parent.png +++ b/tests/rust/re_integration_test/tests/snapshots/dataset_folders_03_perception_after_parent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:693c492669683d8a6177b2c46b18bccd76069c1c5ddea7c6dd7d0f8a8d0f35f2 -size 46666 +oid sha256:67195d76621dcbd13b1e03f71c766d7aa319775d6a960205838259b140fd8db1 +size 47126 diff --git a/tests/rust/re_integration_test/tests/snapshots/dataset_folders_04_summary_dataset.png b/tests/rust/re_integration_test/tests/snapshots/dataset_folders_04_summary_dataset.png index cb0464debf0e..c43e9d20eb11 100644 --- a/tests/rust/re_integration_test/tests/snapshots/dataset_folders_04_summary_dataset.png +++ b/tests/rust/re_integration_test/tests/snapshots/dataset_folders_04_summary_dataset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f947fe046f0a339a80d8f21ff5ac227992bc6ac7508bdfee984967ee4648e10b -size 54904 +oid sha256:8e6074768ee71b28a186573472533fbf3c4de8856ad14ccc7998241e03587d1b +size 52225 diff --git a/tests/rust/re_integration_test/tests/snapshots/dataset_ui_empty_form.png b/tests/rust/re_integration_test/tests/snapshots/dataset_ui_empty_form.png index e8c76525ebdc..ce8b28e2a067 100644 --- a/tests/rust/re_integration_test/tests/snapshots/dataset_ui_empty_form.png +++ b/tests/rust/re_integration_test/tests/snapshots/dataset_ui_empty_form.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0d28a466430f8310ce679f184fddddad1e0bef2fde3d38174785e984e7f1868 -size 66763 +oid sha256:7ab55bfc3ff52aaceebbc2881952052047f849f4a9af7e039e9201d41d6282f5 +size 66597 diff --git a/tests/rust/re_integration_test/tests/snapshots/dataset_ui_table.png b/tests/rust/re_integration_test/tests/snapshots/dataset_ui_table.png index 64936cc5de1e..86dcb92bc071 100644 --- a/tests/rust/re_integration_test/tests/snapshots/dataset_ui_table.png +++ b/tests/rust/re_integration_test/tests/snapshots/dataset_ui_table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:048e4812bbc5e7586baecf61467c0877e281bfcfe6144c3d41146d6ac5be42bf -size 44177 +oid sha256:6f82d362cf115ef72f9ffa06ef39516e3bf1910f83cfff38f860c01959e8dc73 +size 41388 diff --git a/tests/rust/re_integration_test/tests/snapshots/deleted_table_refresh_should_show_error.png b/tests/rust/re_integration_test/tests/snapshots/deleted_table_refresh_should_show_error.png deleted file mode 100644 index 760fbdba5ffb..000000000000 --- a/tests/rust/re_integration_test/tests/snapshots/deleted_table_refresh_should_show_error.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:833817ce4850db3035eb26982f3d62940ad641cd3ae0f19b16db73ee239e07bc -size 30136 diff --git a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_1.png b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_1.png index c74bd8d4c8cd..260270e52843 100644 --- a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28d19b1b041309b27feceeaae287fcbbd8da9ee7a2d7cf1e65fba96d38380442 -size 129217 +oid sha256:041512314d257eb4cc61761e1be51bf5030f335b91fc90910b6bd16167966f21 +size 128565 diff --git a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_2.png b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_2.png index 905df32eb827..99741cd7d0df 100644 --- a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21ad743222a0ad54aab01c6c96d652d91570ddfecbebd31e2bd579c3ddcc2f15 -size 172939 +oid sha256:aaf48998fd8a1625e0eb4bd0e88ee2933e2b5b2fed48facb35d5d0a4b5d239a7 +size 171523 diff --git a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_3.png b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_3.png index f6e88df8fc7a..0457487b8e2f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee7ae86cc532e627036e7be1829a125241c8831c923299abc75398f2774f8e52 -size 148179 +oid sha256:b0a12f83d9b83fc1710d3fa647f96faf113ec6cfdba93da4cae3597e3ab9a48f +size 148843 diff --git a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_4.png b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_4.png index b7891dc101fb..be3dabb924aa 100644 --- a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2844bd0ed6ce02f88b0a47143188c09cc4a90df9c670f3de4aff13992b84b06 -size 217842 +oid sha256:64154c99ccf34e58257a90429ac3c8cf3e79e51844e406c667fed84e18fc68fc +size 223423 diff --git a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_5.png b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_5.png index 5eeaa5533a92..06afe8005910 100644 --- a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43a81338a932b546dc4d699aa7174b0bdc273c8e3f5e0ea155b6db8646b51dbd -size 173077 +oid sha256:96b7523de02e2f05cb5dd4fe7b81f9573324f01148735a88f0b6eb6c948a57c2 +size 171911 diff --git a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_6.png b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_6.png index c3f7acf019f0..2f1ed3a48a99 100644 --- a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_6.png +++ b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55c754c1d2873b70ad87e3fee6901634de2c1547aa87e9aec5530f3260bc9138 -size 190048 +oid sha256:cae009612062b44841ae7c0931dca8ba17e650c424e309598e02969ea74f24e2 +size 188266 diff --git a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_7.png b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_7.png index 77bfe04c38c3..5b0e3401d6d3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_7.png +++ b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f001cd442819dc5cec1c6270e6d5762a71f73b012c43d78adb2f10e7bc5ca39 -size 172963 +oid sha256:9d352b8c5444e58d6fe61ed84c8bc9573dc0f6d85d9d3c1205b17152e4efe8d1 +size 171668 diff --git a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_8.png b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_8.png index 85722cef8bc5..eec776cff8c7 100644 --- a/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_8.png +++ b/tests/rust/re_integration_test/tests/snapshots/deselect_on_escape_8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4bc9394b9c17224bafd639778f14c1d98b6559c41fbe8afe958df8b761cb9bf -size 128322 +oid sha256:666c8f46acab0d4ed96a23a80214e7bd32e809c70f9f660634c8142aafe5b89b +size 127698 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_bottom_1.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_bottom_1.png index ebfb24505b40..c15c2f0cef3c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_bottom_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_bottom_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21b6a316fe4bc39b0b2925ba567a79f0f7caf8bb3981f03164de7a23cbcb81c4 -size 186973 +oid sha256:f885a9f3e01ddb67e2036dac93d6f155b60154a59b405e6afa3e66129fb649bd +size 185777 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_bottom_2.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_bottom_2.png index f6ec88dbcb51..1e450403502c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_bottom_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_bottom_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f1ecce5e18360ebea53cfcb1e4db861d72dbbb3d159c6c99c811d3772ea632f -size 188522 +oid sha256:a95d748064f31479559dff5f9d2b510a098af8d94b63be034490061590fa8cbc +size 187569 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_center_1.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_center_1.png index 2d877bd7bc80..e4d5e7326a80 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_center_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_center_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:216bb692adfc9256eb64a525d45c12bb7c0bb4cb0b633fcfc248ae5bbad20c6c -size 186914 +oid sha256:5eac48d27b2bb9a8ab5b667a88139f27089b411a0808e780c92a0720acb67f69 +size 185700 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_center_2.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_center_2.png index 18e7284a7764..e2ff5a68bbae 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_center_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_center_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1922a5bc7c146da0001149850c65fc00072508c1e698ab4705be63768bdbbdcb -size 197653 +oid sha256:3635a4eec30ed3aebba735c550e3c2e6c30555879752d0ceb1478e478ec20f4e +size 196924 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_left_1.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_left_1.png index f2647ef50f0c..b77e0b2ad3b3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_left_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_left_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ce7a70762e7ea0847efa143ec9eeca9bfeb90e1a31a24a9a0ffd3de71b8cbb8 -size 186950 +oid sha256:9e4faed8fa6ad4dc1285e1df4f0b097d031e8eef7040867d7614cd6a443ae281 +size 185734 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_left_2.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_left_2.png index fb19aaba6f7b..f61f6c1d412e 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_left_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_left_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76c4c68cf8a4529d7f3ec425162dbfe921dbd5a3275f608731c498412902095f -size 186725 +oid sha256:238978267ba41e4cf9847547a3d6044095cc90ec5aba74e12f7d49613b297d2e +size 185537 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_right_1.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_right_1.png index 557d27beac94..60691de68401 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_right_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_right_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04244f11c0e8b27259e39a16cb147017787d857723cce877a9d984253a9253fd -size 186979 +oid sha256:599915a38783d9af46a678fb88caecb7449ece96464f853b104943294a58a886 +size 185738 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_right_2.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_right_2.png index 4342e8c6a535..745b6ca72e6c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_right_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_right_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ed8fe3ea7e29acda93df07d009217ad6c1f359c482854711d90e68de152175b -size 186986 +oid sha256:430183f9385223e11afb4103f4a8353443edf4a68be9a7910b1e27889be6f40a +size 185618 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_top_1.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_top_1.png index d06d6d6d0e97..9869bd400dfd 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_top_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_top_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de043f1b8e9bf019e281221c4a199b192551fb7c5e25455bb1894b90552cbc8b -size 186846 +oid sha256:6aa1ea1ca995669855e57f448a70377802008522fa17b4f4f2e343dd10815698 +size 185624 diff --git a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_top_2.png b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_top_2.png index bbe50c790d50..09dda6e0a919 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_top_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/drag_view_to_other_view_top_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03f71b8901217ff21e987c30d63a88fbcfe2a6761cf076ca9a17cdab6e8550a7 -size 188807 +oid sha256:9330d2b03224f6d8348824f720bf0ebf3f6ca0ac56ebdc7eff6e92faf663697a +size 187623 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_1_initial.png b/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_1_initial.png new file mode 100644 index 000000000000..28a01c41a7d7 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_1_initial.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:106924cf90294afed4425f05737b2abd72e9991e35eec6836578fed2037842e7 +size 102440 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_2_hover.png b/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_2_hover.png new file mode 100644 index 000000000000..cd599b439843 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_2_hover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:306f61e47d8ed016226fca46ccac650f692dc43719f427d9bcacad81c754619c +size 104033 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_3_after_drop.png b/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_3_after_drop.png new file mode 100644 index 000000000000..9e8d5c74d378 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_3_after_drop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:841dfe5b1a5cf2d5146bd99540934eb7556c94f75e82497e38ee9f65bfdb2609 +size 105397 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_4_after_redrop.png b/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_4_after_redrop.png new file mode 100644 index 000000000000..9e8d5c74d378 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/drop_component_to_state_timeline_view_4_after_redrop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:841dfe5b1a5cf2d5146bd99540934eb7556c94f75e82497e38ee9f65bfdb2609 +size 105397 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_1.png b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_1.png index 91c2a4045ab8..d2d2905ef02c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b431761c11dc7d43885eb8043efd70c9d5b7336a7368620a320f4df50c53341e -size 124498 +oid sha256:5900e791ea73eb5d21158910e2dff266a8443c39a2fb4dc2697ffa84ef672557 +size 126188 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_2.png b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_2.png index 8191e0a97446..f3987e503a40 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c5f9ea5011c5a438dbc98af7ea4c43b6ee13c5c51ada0b106e6337ddaa397f2 -size 100971 +oid sha256:081922bfe4805bc89f09e84d5d8b7a685ff8c4313c24bc10f468d77ab3a4dcb0 +size 99223 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_3.png b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_3.png index 3d1ac251b273..eeb02781a54d 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:834d718ce3544d6ce35c9566f49023e3ce97197bf2eba8b42e5328875260a3da -size 133572 +oid sha256:4f30f89a3ab708d0332f691860273015dc3a7550845523b209f37c204dfc03ca +size 132146 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_4.png b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_4.png index 5059ba5cce86..6f8f4d2ab263 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac26b9c49c78824b38db7f9d87b713c16de187559154cdb4d4e32b55e17f5cf1 -size 106940 +oid sha256:de0ea974f1ae388a91aa9caf6f9956de37e595c18c6f5cd6a152f7808319f9c5 +size 105030 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_5.png b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_5.png index 5ec4adeb2c61..8c5367661dbd 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718bfdc588c6215aca27c8e63a82cc83bdc5d77f877de6583a45241d45de34b6 -size 106791 +oid sha256:8717c10c1efa081fe693da798d82a10fe608adde821dec2dca1d170cd04fabec +size 104845 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_6.png b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_6.png index 79776b2a3777..913bcd49f9ff 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_6.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_multiple_streams_to_view_6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:befb4b52cadae2ee6032caa5a7b18c23eb5fdba09a603172d5169d2eb9bbccbd -size 149136 +oid sha256:69a53ac6e4d5239041122b33ddda84f098e8799e06baf055afb7bbbd3e8e00a5 +size 147519 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_1.png b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_1.png index 9067377f38f5..2ef4ccc02aa3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2950abdc02275c6267add894aa7288157862e75008b098f2aeb830ea0e9ae1bb -size 101615 +oid sha256:f05893de108eed0025a05e7000cc48b7c79158a16dcef87be2cb43fecb7eb564 +size 98998 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_2.png b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_2.png index 91c2a4045ab8..d2d2905ef02c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b431761c11dc7d43885eb8043efd70c9d5b7336a7368620a320f4df50c53341e -size 124498 +oid sha256:5900e791ea73eb5d21158910e2dff266a8443c39a2fb4dc2697ffa84ef672557 +size 126188 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_3.png b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_3.png index a2dc26beb431..2dcb98e5d17c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f43610b7835df005e9f0dcf60b3008a291d05c2b40f29a48ceee13528535e5d7 -size 131225 +oid sha256:2e18dc8863944df76c45f4b0064d6e3924c6f241ee20936f82788fbae5b07636 +size 129968 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_4.png b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_4.png index 91c2a4045ab8..d2d2905ef02c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b431761c11dc7d43885eb8043efd70c9d5b7336a7368620a320f4df50c53341e -size 124498 +oid sha256:5900e791ea73eb5d21158910e2dff266a8443c39a2fb4dc2697ffa84ef672557 +size 126188 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_5.png b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_5.png index 23c94fdcda77..3efef5e41dde 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1068ca3b28c4ba7607455c2b64715431a36f9aa7327676e0c5c9ee91b8ff3111 -size 126041 +oid sha256:237381d007f4b4c2aeb35b91e33d3308cea29253d97eb585309ab7cf3f550782 +size 127618 diff --git a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_6.png b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_6.png index 3d1ac251b273..eeb02781a54d 100644 --- a/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_6.png +++ b/tests/rust/re_integration_test/tests/snapshots/drop_stream_to_view_6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:834d718ce3544d6ce35c9566f49023e3ce97197bf2eba8b42e5328875260a3da -size 133572 +oid sha256:4f30f89a3ab708d0332f691860273015dc3a7550845523b209f37c204dfc03ca +size 132146 diff --git a/tests/rust/re_integration_test/tests/snapshots/grid_view_flagging_after.png b/tests/rust/re_integration_test/tests/snapshots/grid_view_flagging_after.png index 43c399b2d97a..4883f82a99a9 100644 --- a/tests/rust/re_integration_test/tests/snapshots/grid_view_flagging_after.png +++ b/tests/rust/re_integration_test/tests/snapshots/grid_view_flagging_after.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3676cfd4f253a4c3c0b5784c45fa038d85917208999a207bfe45866dd7d32c9c -size 43167 +oid sha256:f56627256d999bb5a93a446a37c2aaab02b76d6e91803ac57a6b95ed56e2d34a +size 42754 diff --git a/tests/rust/re_integration_test/tests/snapshots/grid_view_flagging_before.png b/tests/rust/re_integration_test/tests/snapshots/grid_view_flagging_before.png index 78efd2d5e624..7818e7bd1f95 100644 --- a/tests/rust/re_integration_test/tests/snapshots/grid_view_flagging_before.png +++ b/tests/rust/re_integration_test/tests/snapshots/grid_view_flagging_before.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8e0b55b38431ce99b1e642611e4a740669f51a501c42d09a5ccf46bee445f1 -size 43111 +oid sha256:14353b52e0e2ed22c0ace1035ebdfdfd2b3db61b7fdf8c4bb56d2a34c84e6bc7 +size 42711 diff --git a/tests/rust/re_integration_test/tests/snapshots/heuristics_mixed_2d_and_3d.png b/tests/rust/re_integration_test/tests/snapshots/heuristics_mixed_2d_and_3d.png index c700f24c1833..f21716935426 100644 --- a/tests/rust/re_integration_test/tests/snapshots/heuristics_mixed_2d_and_3d.png +++ b/tests/rust/re_integration_test/tests/snapshots/heuristics_mixed_2d_and_3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51820b05b4e51e5bbdc48461eafe034df09a4f6e638cfa61e852234ad500f975 -size 92500 +oid sha256:af772b7d37d2b50ed2912ef3bcc4c440475823009450cc73448a1ac54f038ec5 +size 91529 diff --git a/tests/rust/re_integration_test/tests/snapshots/heuristics_mixed_all_root.png b/tests/rust/re_integration_test/tests/snapshots/heuristics_mixed_all_root.png index c35145ea5e72..527d54b8a6b3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/heuristics_mixed_all_root.png +++ b/tests/rust/re_integration_test/tests/snapshots/heuristics_mixed_all_root.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:285f8cd2d7a42e69e0fb938cbe36224323b8c43e241d073973153dcbe488e3b5 -size 125296 +oid sha256:f9aa24bbd638e03e131a600af44f536c5db947a30ef7b972b0c06496e580efc6 +size 124072 diff --git a/tests/rust/re_integration_test/tests/snapshots/internal_catalog_load_rrd_catalog.png b/tests/rust/re_integration_test/tests/snapshots/internal_catalog_load_rrd_catalog.png new file mode 100644 index 000000000000..c68021921b56 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/internal_catalog_load_rrd_catalog.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54dd5f529992d9d65b6899ff8d2063f6c6804ff2b837de9f1094e460f0578608 +size 81798 diff --git a/tests/rust/re_integration_test/tests/snapshots/internal_catalog_load_rrd_recording.png b/tests/rust/re_integration_test/tests/snapshots/internal_catalog_load_rrd_recording.png new file mode 100644 index 000000000000..980d1a41f811 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/internal_catalog_load_rrd_recording.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b30c739ea96dd36ffb69f2e3d0129c08b3c6c970c918b652685322c0425b29a2 +size 83558 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_deep_nested.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_deep_nested.png index fac5140b0ec9..6f3e060904eb 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_deep_nested.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_deep_nested.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f65a7d021a4d1b496295d1d9d17bbbcbbcc66f05aa3cdb9047986573da142290 -size 240850 +oid sha256:9a7d161204e576267fec994adf7ecb8eeeedd4c324cdfc1fb6a276dc88808648 +size 239611 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_1.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_1.png index cdbdf0001808..3514da01a3e7 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0869e5b70c55b4ff5b474f36adaf9f4a7541bf0200a735c42d17c9b8862ca57f -size 246609 +oid sha256:611fd4d725c1a9a4a3a91d504908f1f3f92e5f79564258be07369a5f1c97a676 +size 244473 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_2.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_2.png index bab90570801b..b5df061eefd2 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:662f43e4e9a2f59d8e28ce60b119b8792d0280e69fa9d3ecd81929d5ee0c8824 -size 247353 +oid sha256:973e0f84c8fda4b1981448c076507a383bf17fbda112ca05e4727d66ca2661ea +size 245201 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_3.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_3.png index 85844e7ba6ae..067eb10b1f1e 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:778145f3ad47a3dc9d70097204dfb659bb5ab109181a0d44416c2f3f3a804df7 -size 247936 +oid sha256:b082d12ec7f673a5dec967d5173411910a349d67a24c29d5eabd4080108a4ffd +size 245703 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_4.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_4.png index c1f8a155ef07..cfb1efcfb397 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbeb5ad4c8bc2b2c34ae26bafb942da67411d4999fe9d6cc8eae429e05710926 -size 246694 +oid sha256:7d836d808e9a7d4203ca7daa61ac0512d700cec4cb87f9c9dd759baf82d53fe1 +size 244612 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_5.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_5.png index 6003849d22bb..5f17172086e8 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c21ed624d2dd6e00d47b342c1c5ca8335d28e777c87bd27df577190bdf9fbdc -size 246766 +oid sha256:ea5e2823c6e2d51a870365efc1f4353c5ce0859b8cd0f8f85ce756d996773d76 +size 244616 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_6.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_6.png index 08fd0e07e7dd..4d3324b0c0dd 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_6.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_container_6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efb1f00a85385d123a1bc03cbc39a9a30527d3d0c2835d66b1e7d124e16bae20 -size 250707 +oid sha256:cf6d0ff1efaee8de6f455f009ae0c423d3caee7df3a38d93b3b07ad37a6de3d5 +size 249012 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_1.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_1.png index 40f8a28d1514..cbb0612237ed 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfb2afe3ed3bdbe24ab3e11c299b21bc0d02bb3544459981105186668ab64007 -size 248499 +oid sha256:013986870f54d43251648c515698c0c1f0bf24c905d034e9622ea3d41b3c1f56 +size 246771 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_2.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_2.png index 03ba44373e2c..a29f66c6c420 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a86aa87d123d5670dbd01ca52d3ed09bf3a4c1b210092ae8eed16af0ecdb79af -size 248407 +oid sha256:d08b628b459e16df2a712ba0f8d6f3cf35d89257c1317503eacf74da13cf5a7a +size 246670 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_3.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_3.png index 4ab24cec35fb..5ffbc46a802a 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_drag_single_view_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ae548b6e03a75c1f4e8653d87af718658a840f9d638bf19ba6698bf1c4ac9d7 -size 245845 +oid sha256:25258bc9c2ea7cbce6225f4aaa8fbacf665f94352f592b8a0545a30b7eb677df +size 243973 diff --git a/tests/rust/re_integration_test/tests/snapshots/multi_container_many_views.png b/tests/rust/re_integration_test/tests/snapshots/multi_container_many_views.png index a22d90bda2f9..293317fac94f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multi_container_many_views.png +++ b/tests/rust/re_integration_test/tests/snapshots/multi_container_many_views.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97ab2e6db6e2b3348a98b68967905bc28821cfe23d678d500ca0650ff5c3ddca -size 473598 +oid sha256:85a40b009bc764cb3f2f4c606f2f12099830367b51b09d5a0aeade24f56635cd +size 471771 diff --git a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_1_initial.png b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_1_initial.png index c92e92b1e303..b74628ed8ae3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_1_initial.png +++ b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_1_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e011d20dd0057ab7cd3cb8910910286df6ec78d009654bc834c65ca09dcf91fc -size 41140 +oid sha256:0307cae8c8764cd8351b2ffe68614cf4a3dcd4605407afdabed455cfc9133162 +size 41614 diff --git a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_2_after_drag_1.png b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_2_after_drag_1.png index e3ce8201c4c3..4d6ca8eb12e6 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_2_after_drag_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_2_after_drag_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3755edf1a85fbc22be071dbe6e61a03f64001ba5d311f8ffab38c3b17fd59a1e -size 46238 +oid sha256:a90a2c5f8d010e78d912a3acacc60692c2efd759991709d5e525e7bc841dff43 +size 46330 diff --git a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_3_after_drag_2.png b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_3_after_drag_2.png index 338170586867..693c37688b2b 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_3_after_drag_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_3_after_drag_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10ba87fb2f5ab33fb8823e1034192f0f7e70c044eef62912e444a31f5490a700 -size 63007 +oid sha256:7a6652d0821de380b84f2adead2b88e7123b80126f1f9593cb82d5f28625f789 +size 62775 diff --git a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_4_undo_once.png b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_4_undo_once.png index e3ce8201c4c3..4d6ca8eb12e6 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_4_undo_once.png +++ b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_4_undo_once.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3755edf1a85fbc22be071dbe6e61a03f64001ba5d311f8ffab38c3b17fd59a1e -size 46238 +oid sha256:a90a2c5f8d010e78d912a3acacc60692c2efd759991709d5e525e7bc841dff43 +size 46330 diff --git a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_5_undo_twice.png b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_5_undo_twice.png index c92e92b1e303..b74628ed8ae3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_5_undo_twice.png +++ b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_5_undo_twice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e011d20dd0057ab7cd3cb8910910286df6ec78d009654bc834c65ca09dcf91fc -size 41140 +oid sha256:0307cae8c8764cd8351b2ffe68614cf4a3dcd4605407afdabed455cfc9133162 +size 41614 diff --git a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_6_redo_once.png b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_6_redo_once.png index e3ce8201c4c3..4d6ca8eb12e6 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_6_redo_once.png +++ b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_6_redo_once.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3755edf1a85fbc22be071dbe6e61a03f64001ba5d311f8ffab38c3b17fd59a1e -size 46238 +oid sha256:a90a2c5f8d010e78d912a3acacc60692c2efd759991709d5e525e7bc841dff43 +size 46330 diff --git a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_7_redo_twice.png b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_7_redo_twice.png index 338170586867..693c37688b2b 100644 --- a/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_7_redo_twice.png +++ b/tests/rust/re_integration_test/tests/snapshots/multiple_undo_redo_7_redo_twice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10ba87fb2f5ab33fb8823e1034192f0f7e70c044eef62912e444a31f5490a700 -size 63007 +oid sha256:7a6652d0821de380b84f2adead2b88e7123b80126f1f9593cb82d5f28625f789 +size 62775 diff --git a/tests/rust/re_integration_test/tests/snapshots/no_blueprint_from_sdk.png b/tests/rust/re_integration_test/tests/snapshots/no_blueprint_from_sdk.png index b149685ef902..48b16cd37070 100644 --- a/tests/rust/re_integration_test/tests/snapshots/no_blueprint_from_sdk.png +++ b/tests/rust/re_integration_test/tests/snapshots/no_blueprint_from_sdk.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22919800eb1d2a8c6532d04fe970a86d394667077f79bba851b3e4c4af751915 -size 186578 +oid sha256:f591d343ede6208ffd5fa0f662de38fb64442c3733f4f375d0257f2d544cc9e8 +size 187410 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_camera_2d.png b/tests/rust/re_integration_test/tests/snapshots/origin_camera_2d.png index 1fc0035cb9c1..1efca3b2af9b 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_camera_2d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_camera_2d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd13d3d1e3ef2f00c6b5b75b3b185131b5b79aa4964070ca42b5d118a36d3d8c -size 147363 +oid sha256:3e6b174798c5a57fcbc38fa8adb9c48ac4a295f3bec7442e26ddb23a7f8360d7 +size 153633 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_camera_3d.png b/tests/rust/re_integration_test/tests/snapshots/origin_camera_3d.png index cd37b4a7af7e..c3a2596e1b70 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_camera_3d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_camera_3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8edea1c797d5d23885381c2a3635f2babd08b88cc3ce67c3e0025ade0354ae8 -size 192419 +oid sha256:2fefd16f305f1a45128c52d70357a816995d78c49fd7b1c858f42004624cf69e +size 192260 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_image_2d.png b/tests/rust/re_integration_test/tests/snapshots/origin_image_2d.png index 3b785ee9ad81..401bff4b80ba 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_image_2d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_image_2d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34c70aa15d0607d051d1483b0c1cff2d00bd3d76590422aec7f8306048ee359e -size 171609 +oid sha256:614c650353a6ddbb668b41a6af90933fbd5c90172ba0df33aec676780326f85d +size 180592 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_image_3d.png b/tests/rust/re_integration_test/tests/snapshots/origin_image_3d.png index 470ded9edcc0..317bfa919eed 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_image_3d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_image_3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6ff8cdd5a3aa158c6f9fd3ee7e963bc56d18abf60cca3df789228c00fef0914 -size 192436 +oid sha256:6fc1138ffba6112c3de0703d04834863f9e65a695a5bdbe1a9fc00d9abda565a +size 192334 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_keypoint_2d.png b/tests/rust/re_integration_test/tests/snapshots/origin_keypoint_2d.png index 326d613de916..8a5ee053bedb 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_keypoint_2d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_keypoint_2d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b3c8e7f578baa82bec73bafbf7aca6e3ca89094a859a47a2546087e3e8d791a -size 159445 +oid sha256:fd127e22276d6c9041893dc8f0e0ef0ce58610c22a67828baa8b71934ac1a06d +size 166303 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_keypoint_3d.png b/tests/rust/re_integration_test/tests/snapshots/origin_keypoint_3d.png index cda20bb29ea5..ebf5c2cc0dbc 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_keypoint_3d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_keypoint_3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30930d7b598d2d30fbe6ec7fa6372bc91913ba274d99eb264d1cb9614537507a -size 190639 +oid sha256:066d2f481416f0c96b285489a5838f758997ae1cdcdeb886e0820856b18f236f +size 190510 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_root_2d.png b/tests/rust/re_integration_test/tests/snapshots/origin_root_2d.png index 219e6a7c61e9..d430918d3e72 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_root_2d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_root_2d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71a53eac9aa28e0c7bb9ac693f698dee497679d5939aabfae2abdbd48263cc6c -size 145823 +oid sha256:04f80858eff44f6de50b349dc860aafd285f3700f6643728070aa0441621ab54 +size 152007 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_root_3d.png b/tests/rust/re_integration_test/tests/snapshots/origin_root_3d.png index 1a8be23e7737..697cf2a557da 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_root_3d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_root_3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a79e29e74d33e0b8f29ebe62a516bfbb83e4d83a39580a2f1f6304fccb044096 -size 196424 +oid sha256:2975bbaab97390231d37d0dba046ab38ae06b36387d20d22160383d961dacea4 +size 196728 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_world_2d.png b/tests/rust/re_integration_test/tests/snapshots/origin_world_2d.png index c53c882b37de..0049aa4d40b1 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_world_2d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_world_2d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4f7054546fc65406934ad5cf5b5fb3a082634d4d03da5683a2379e5ca974d32 -size 146386 +oid sha256:5c69bd205f6d1836802c14abcaadd89d2f9547d5077659fe1580b89c8b97709e +size 152588 diff --git a/tests/rust/re_integration_test/tests/snapshots/origin_world_3d.png b/tests/rust/re_integration_test/tests/snapshots/origin_world_3d.png index c5c9518cd56e..72bdbbbf6839 100644 --- a/tests/rust/re_integration_test/tests/snapshots/origin_world_3d.png +++ b/tests/rust/re_integration_test/tests/snapshots/origin_world_3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:120197784ec4861e2092cc5787f54ea0aa3ff04beaac6f4ce3f5fe758be6c044 -size 191495 +oid sha256:3cd5ea5f2301e857d8482141a85ca8e155465ec7e903d31ce9d5c0a6d597b17a +size 191189 diff --git a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_1_warnings_only.png b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_1_warnings_only.png index 428424c9f8a9..66f8601a787a 100644 --- a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_1_warnings_only.png +++ b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_1_warnings_only.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3421c7370f98dff35755a3c97e6a1afb9d86cd2484eb5c083e45718183f9009f -size 89483 +oid sha256:ff2be2624b9bc77b5f1957a8af706937701e037aa79bb3473110acb55bccafef +size 88792 diff --git a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_1b_warnings_only_menu.png b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_1b_warnings_only_menu.png index b30fd6aadc5d..9628debb1aa3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_1b_warnings_only_menu.png +++ b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_1b_warnings_only_menu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59916de9844f36313c07a17a928f7ca50eb074c9e74b14a9faf272059cfe992b -size 100001 +oid sha256:f6d7fea905c67053e63cf591c3f8d4a34beed6a32cbbbb400c40bf0ff68e9f36 +size 98815 diff --git a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_2_warnings_and_errors_menu.png b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_2_warnings_and_errors_menu.png index 141928d0e849..e279f43f496d 100644 --- a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_2_warnings_and_errors_menu.png +++ b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_2_warnings_and_errors_menu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4ad49c3bd9ca58f7fe19ec80fdcf6e280a471e91135f60b390494934ff54c42 +oid sha256:2cd4059c3d419cce7b500f7ce11cb79e73e89445693714a9232f2b5c15835c48 size 109448 diff --git a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_2b_warnings_and_errors_dataresult.png b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_2b_warnings_and_errors_dataresult.png index 0c020872e731..ecaeca858e97 100644 --- a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_2b_warnings_and_errors_dataresult.png +++ b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_2b_warnings_and_errors_dataresult.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5077abab34b4506f1920a30031270990493b6b7cbbdd379a14b93450d2abca4a -size 100491 +oid sha256:ccfa731941585c2ec31ec1c3bc233cd4b5e893cbc634697b9f124288b8d2d323 +size 99285 diff --git a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_3_errors_only.png b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_3_errors_only.png index 43213d2682e0..0aaaff8b6c47 100644 --- a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_3_errors_only.png +++ b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_3_errors_only.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4739985ca49ca85714d857c1291e25ea3d2d4abfd040fcc1fd32e8cbb9f6fa1c -size 97356 +oid sha256:1a809d57b73d96007df5b83b6f307e36ac5e8fdbed1e4a8ea00709159ff2cc1a +size 96150 diff --git a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_3b_errors_only_menu.png b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_3b_errors_only_menu.png index 0262b596db60..f575efa5d26c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_3b_errors_only_menu.png +++ b/tests/rust/re_integration_test/tests/snapshots/per_visualizer_instruction_errors_3b_errors_only_menu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ada4e498bea976edcd10f2acf73f71bda81ef0b8be76b9a21fb59fa1db530e5 -size 107931 +oid sha256:a3781484b396272506497f65155b597e6e0cc0c87ba305035f8566ea175b1148 +size 106050 diff --git a/tests/rust/re_integration_test/tests/snapshots/preview_table.png b/tests/rust/re_integration_test/tests/snapshots/preview_table.png new file mode 100644 index 000000000000..1774616a7d51 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/preview_table.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51ff69cfb73ae215c42cf4291dc5176844b94f2bbf1ae2e4cd6bff35f2079417 +size 176197 diff --git a/tests/rust/re_integration_test/tests/snapshots/preview_table_grid.png b/tests/rust/re_integration_test/tests/snapshots/preview_table_grid.png new file mode 100644 index 000000000000..4faee1570503 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/preview_table_grid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a16dd06d2cdacefc58ae300d426a0a78302d227004c1809952add4ca83bf5a8 +size 238835 diff --git a/tests/rust/re_integration_test/tests/snapshots/preview_table_opened_recording.png b/tests/rust/re_integration_test/tests/snapshots/preview_table_opened_recording.png new file mode 100644 index 000000000000..18dd308ed9a2 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/preview_table_opened_recording.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c1d6127cc537b48d91cd46a06d8b2c5c5f4254d4b3be79742df047aabafd611 +size 220928 diff --git a/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_1.png b/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_1.png index a640dbee0ebc..63760469fc4a 100644 --- a/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fe9d3c98cc43b98558899a8508075a021692461b9d8cc6b72cd60cb06987943 -size 193787 +oid sha256:e1a3314d3ab6cd0c5253f0ada73879e20cf5b6e99c397f3716b3a7e4e2a19608 +size 192769 diff --git a/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_2.png b/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_2.png index 41d2e7f361ec..4c8fbf0ca517 100644 --- a/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:728303ac9be5e23cc447b6d6a729ed7e832612f52d61aad6f76eb306431e2e63 -size 195127 +oid sha256:29dc624fc38fb072697190266b952ebdc541bd08c0f829f0d79750ee994007ab +size 193835 diff --git a/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_3.png b/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_3.png index ab369b7857e7..6727eb263c60 100644 --- a/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/resize_view_horizontal_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cf4d998f99a6d1794e139062e01a42aea4c40b8c184d1c9fddf83c4d4141449 -size 194580 +oid sha256:a48dbc9d9314cdbb9e244fa569fe4a6ce3151de39be0fb17b238fc7d541ad458 +size 193306 diff --git a/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_1.png b/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_1.png index 34f4a8818bce..f13b63a89193 100644 --- a/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:458a6d7ae33dc80a9f9400b00370e1b66f11cd954197bb12a3d8346c14d7d5e1 -size 193648 +oid sha256:9eb3d58ec3710fcfe2bd6109c185a84810052fc2c16172e3e3caf6077fc1c07f +size 192259 diff --git a/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_2.png b/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_2.png index 5935e9d01c40..f89ae45a792c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abcd95463d8d5b688bd868b2fec87f7d7e624d0b318135b2f4c5a78620fa4ab6 -size 192763 +oid sha256:13db5ad558f9bef2f312741b415b09f29ba5240e87be0891b90fdeec5b94c990 +size 191406 diff --git a/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_3.png b/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_3.png index a024eac35a88..f61db17ca8c9 100644 --- a/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/resize_view_vertical_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc7be04e9721aff50f4e9608e03286684f6ad7d93d367b28166639ed8dd58506 -size 192986 +oid sha256:6c3abe4d472b6cf63132c66d70b3f660e8a5bfdca1251cf2d04274d26fbd7125 +size 191635 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_animated_urdf.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_animated_urdf.png new file mode 100644 index 000000000000..fa8e4eb30d53 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_animated_urdf.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e181cb9b407a4ff305c01eff16b7f4cb0ee8489109929c739bb4dc320fce895 +size 172261 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_arkit_scenes.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_arkit_scenes.png new file mode 100644 index 000000000000..50c0a023fc4b --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_arkit_scenes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0cda8251f90b21f33bdbf12c1bc262c60b5d0529e2c98eea78efae9de1bcd30 +size 414797 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_detect_and_track_objects.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_detect_and_track_objects.png new file mode 100644 index 000000000000..33da58aea969 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_detect_and_track_objects.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1999ac0fa11dcd86a7d64045b3b93c437a38945fa6e511c880de05d2bb3003fd +size 459011 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_dicom_mri.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_dicom_mri.png new file mode 100644 index 000000000000..f68004ddbbf6 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_dicom_mri.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4db0d0806485591984c59354ca6e33847b7f92789dea04c8e21ac36a05a8f06d +size 207634 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_dna.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_dna.png new file mode 100644 index 000000000000..8abacbf286a4 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_dna.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:611026214753a03ab50c9508b02ab4e86d67aaf93e6ed71dc52c9cd0b9c3da06 +size 279150 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_graphs.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_graphs.png new file mode 100644 index 000000000000..9f7f4ec5a7a9 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_graphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e97f501737581cce9f45c04074a50dd29e18932d03d4a3e82164b7ef52f3294 +size 230852 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_imu_signals.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_imu_signals.png new file mode 100644 index 000000000000..9a84de19723c --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_imu_signals.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c96f0a1b983c1af163c7fd6b91bdf964eb66162b276f295f5dd4c51951e007ae +size 434276 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_nuscenes_dataset.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_nuscenes_dataset.png new file mode 100644 index 000000000000..4b1742396888 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_nuscenes_dataset.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:343f9767de68d11195043eeb55d8f98ea28de5bf40f625a4e8879c7b4c30f120 +size 491072 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_open_photogrammetry_format.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_open_photogrammetry_format.png new file mode 100644 index 000000000000..f76109a9c1ef --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_open_photogrammetry_format.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee6995fccfa43e1f4a51aeb0d6fbc6c1d4a28287c626170f209da3ff286d494f +size 1008671 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_plots.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_plots.png new file mode 100644 index 000000000000..b2a2b587e396 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_plots.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e21c9eb70e0d369ef2b402b3ec9412f36b22281d8e2dfcbd6ecda865ec1dd899 +size 168101 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_raw_mesh.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_raw_mesh.png new file mode 100644 index 000000000000..9c23af1789b0 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_raw_mesh.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:153711be7b8d7e903682155e2d6f2f2940f51be9e256886f3f38b229594732ec +size 485483 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_rgbd.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_rgbd.png new file mode 100644 index 000000000000..12f5916f52bd --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_rgbd.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7017247f1db6d4bb147c7f3798971d610a6ea2855419163c886d8b92dd25c7a7 +size 408769 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_rrt_star.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_rrt_star.png new file mode 100644 index 000000000000..e283ce9529ab --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_rrt_star.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:393c7b3a1c753ddf7fc7742416bbcd0c658b2af14b633cf265dc5d746711e5be +size 201552 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_segment_anything_model.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_segment_anything_model.png new file mode 100644 index 000000000000..5eb9150e790f --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_segment_anything_model.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b18d93c7ce2826e67f8a41925f833546d41a98e68420be9bf9cdfeb09dc50eec +size 611736 diff --git a/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_structure_from_motion.png b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_structure_from_motion.png new file mode 100644 index 000000000000..b159afe6ca16 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/rrd_bw_compat_structure_from_motion.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4870deb51b8636fb009c5849e127336940dbff706471c269ca2036b85e602220 +size 361361 diff --git a/tests/rust/re_integration_test/tests/snapshots/series_count_exceeds_max.png b/tests/rust/re_integration_test/tests/snapshots/series_count_exceeds_max.png index 9540a83e641b..36044618f252 100644 --- a/tests/rust/re_integration_test/tests/snapshots/series_count_exceeds_max.png +++ b/tests/rust/re_integration_test/tests/snapshots/series_count_exceeds_max.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffd3296ee022665a52f8b24c486967e1b7142d9d8fe4e92752f57148de1d3deb -size 1294098 +oid sha256:2ade64d91b8290133e5fe61b33948a3533586047cd4f0fe90641b31f37eee473 +size 1281484 diff --git a/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_1.png b/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_1.png index 80f64f461ccd..def230ecaa4f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf13defa338f50c63c53a85076fab0147949c15d6f2d702692af5451a86d608 -size 74008 +oid sha256:4e9c3d7a3d2c64b157f33863c5c9a20549efbacd46b78552470e8ffd7cee9bd2 +size 71491 diff --git a/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_2.png b/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_2.png index 0a54acff8774..baec98973894 100644 --- a/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0763ddfe448ad0ed9855ef439d2c159cd0bf36b07335134f9ee254c6552f0184 -size 105100 +oid sha256:66934d80423931623777c0038e7dfc6f0d7911fd806fa7a853be93d3109a25dc +size 103533 diff --git a/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_3.png b/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_3.png index e6fc340bea4b..fd02f4632c8c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/simplify_container_hierarchy_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f083a1ba9e662e93bd5f189614009f4ea9679b218fa9dfe1f94ca1acd8d8d83a -size 126271 +oid sha256:6a2cb4a3f67dd2f2eca46a5e876b4ba42faa9096068d14efaa7d7bb7355b3e90 +size 124468 diff --git a/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_1.png b/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_1.png index 80f64f461ccd..def230ecaa4f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf13defa338f50c63c53a85076fab0147949c15d6f2d702692af5451a86d608 -size 74008 +oid sha256:4e9c3d7a3d2c64b157f33863c5c9a20549efbacd46b78552470e8ffd7cee9bd2 +size 71491 diff --git a/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_2.png b/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_2.png index e783c7b0a5dd..e836272c41ce 100644 --- a/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d07f6bb3ee82b0347f2a287397161e0080c611e21f089b086ae78029e10e652b -size 105622 +oid sha256:d43f87f841b9d8407afef14a157844e44ab3291156f0da0e825a867d81ff5207 +size 103847 diff --git a/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_3.png b/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_3.png index 3d4d99a7030c..8adae07ebed8 100644 --- a/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/simplify_root_hierarchy_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ac903483a702e91731dab83e24369984b4fd5025246b528cc7c89c573cfa732 -size 127659 +oid sha256:a24dce82c0c5c8a82a6f3e5f7782c6290e14508792c6a3346d18d27b9d2fa4df +size 125606 diff --git a/tests/rust/re_integration_test/tests/snapshots/single_text_document_1.png b/tests/rust/re_integration_test/tests/snapshots/single_text_document_1.png index d21b227d6043..a75cc751a7ab 100644 --- a/tests/rust/re_integration_test/tests/snapshots/single_text_document_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/single_text_document_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee208396e715cf3bfa4ad8c7ff3fef2a885cb8e7163659510328e33551b19b72 -size 42547 +oid sha256:77652482c5c7fcedf1bcf83f12ef3f56265b67f318de2ccd3db8726f4a07c10c +size 41344 diff --git a/tests/rust/re_integration_test/tests/snapshots/single_text_document_2.png b/tests/rust/re_integration_test/tests/snapshots/single_text_document_2.png index cbe0621f68bb..92bc8ffb2230 100644 --- a/tests/rust/re_integration_test/tests/snapshots/single_text_document_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/single_text_document_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f4701dcc391271b7ef8ba0b04428f8d6efc1ad2a787842a38ae3f4a286422aa -size 47502 +oid sha256:7305b55a3df4ef2f22cc2155e30d9a683cd2f3bbaf972cdc6f05fc7f48696bcf +size 46070 diff --git a/tests/rust/re_integration_test/tests/snapshots/source_component_1.png b/tests/rust/re_integration_test/tests/snapshots/source_component_1.png index 2a4f6eb4ae02..0a659e17178f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/source_component_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/source_component_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc31ea84604d12aaf89f121ef710b072aca793eb586c8d73c566f345cdffc340 -size 180406 +oid sha256:db3e3f46ba58bfb081702b7b8490fe384bfc5f53d88c24bbd852ec86b643248f +size 182374 diff --git a/tests/rust/re_integration_test/tests/snapshots/source_component_2.png b/tests/rust/re_integration_test/tests/snapshots/source_component_2.png index e66e4e7ddd00..80e0079b35a1 100644 --- a/tests/rust/re_integration_test/tests/snapshots/source_component_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/source_component_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8deff520b8d386b2b9e50e8474b7aa72743cb9bb446a94afd4aaa7f6b219c9da -size 183337 +oid sha256:5b6312cd522bc7ae216b1fd5e7693d027241ad8b97e1bb7820ee9bbdb141f5e8 +size 184864 diff --git a/tests/rust/re_integration_test/tests/snapshots/source_component_3.png b/tests/rust/re_integration_test/tests/snapshots/source_component_3.png index dafff51a7d40..a17259af3a42 100644 --- a/tests/rust/re_integration_test/tests/snapshots/source_component_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/source_component_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cb9ded3a658a19dba692f8a89a56bd781c0590ab16aebf265ced64919319a80 -size 197562 +oid sha256:23b9eae23c09cafbc72920a0802ce13841841b93558e6f1bf5afba5431b46c56 +size 197606 diff --git a/tests/rust/re_integration_test/tests/snapshots/source_component_4.png b/tests/rust/re_integration_test/tests/snapshots/source_component_4.png index 04bcee8da315..4dab9c9f92d7 100644 --- a/tests/rust/re_integration_test/tests/snapshots/source_component_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/source_component_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ec1276d9344085da783d66d7ae71bcbc39382c7fca5a83d5a303b33977155fd -size 194686 +oid sha256:1fce47aecc36d23a505595fbeb3ab9f63ca92735bc4dca1bd7fc10fb9b1efc3b +size 196313 diff --git a/tests/rust/re_integration_test/tests/snapshots/source_component_5.png b/tests/rust/re_integration_test/tests/snapshots/source_component_5.png index abb94113d71c..3f67b71ef799 100644 --- a/tests/rust/re_integration_test/tests/snapshots/source_component_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/source_component_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e22dc061966e952e9bf4474413a0ecd5bd3ffa53dbe6f815a82de31d313eb6e6 -size 203222 +oid sha256:dc079ef1f3207809db3f1cc9da51097a175db59f851ab9e1b6202e1e59a6889e +size 203138 diff --git a/tests/rust/re_integration_test/tests/snapshots/source_component_6.png b/tests/rust/re_integration_test/tests/snapshots/source_component_6.png index 668f40235d35..f0f1eef145af 100644 --- a/tests/rust/re_integration_test/tests/snapshots/source_component_6.png +++ b/tests/rust/re_integration_test/tests/snapshots/source_component_6.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a1b5c8d0d1e3488e03fe3a3cc3a05d004772c2a27717c8f5b60f33ecef62263 -size 193805 +oid sha256:c81f31163e7d07fdd0aeed872f2ab5f90a2fa5dae8cb0280119a5d6a55c5534c +size 195464 diff --git a/tests/rust/re_integration_test/tests/snapshots/source_component_7.png b/tests/rust/re_integration_test/tests/snapshots/source_component_7.png index 1d3d17eb5f83..3d1ee5a02c73 100644 --- a/tests/rust/re_integration_test/tests/snapshots/source_component_7.png +++ b/tests/rust/re_integration_test/tests/snapshots/source_component_7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbc8b9e7e7402751cf8049ded3246c09b9ee6c008a00b3c15bd3c418291fd4b5 -size 201258 +oid sha256:d87d55fa5580efb0cd3c17b76e821727a3deadef170d469dc1cab6ac0aa97072 +size 203649 diff --git a/tests/rust/re_integration_test/tests/snapshots/source_component_8.png b/tests/rust/re_integration_test/tests/snapshots/source_component_8.png index ebdc54907eed..36c94e27ad4b 100644 --- a/tests/rust/re_integration_test/tests/snapshots/source_component_8.png +++ b/tests/rust/re_integration_test/tests/snapshots/source_component_8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2161eef1a514af008d640f76852539bc1308cc64b634147a779209d1bdc5647b -size 211555 +oid sha256:30399ca92d15dc60ae2b235263b4229b8baa15a685945a28a8ee2ddf32811590 +size 209887 diff --git a/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_entity_hierarchy_2d_to_3d.png b/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_entity_hierarchy_2d_to_3d.png new file mode 100644 index 000000000000..84ac183d8a79 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_entity_hierarchy_2d_to_3d.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c36653616c138b29bbe2cc7fdb10be1ab88a81eb3ee07c271932e407f55e6aa +size 111969 diff --git a/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_entity_hierarchy_3d_to_2d.png b/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_entity_hierarchy_3d_to_2d.png new file mode 100644 index 000000000000..1beba06e3539 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_entity_hierarchy_3d_to_2d.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cae32bd37b4a40e3b9942bdd6e31fef1bd5e02d98dce08b95eb9562ee35adb92 +size 110373 diff --git a/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_named_transforms_2d_to_3d.png b/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_named_transforms_2d_to_3d.png new file mode 100644 index 000000000000..75252ff87a3d --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_named_transforms_2d_to_3d.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c69033b9ade87b27aeba9c5610982587116ee42e8623f87fcc440d866560eeb +size 104635 diff --git a/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_named_transforms_3d_to_2d.png b/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_named_transforms_3d_to_2d.png new file mode 100644 index 000000000000..3faf4e778fee --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/spatial_cross_view_interaction_named_transforms_3d_to_2d.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a168f7fda7fc2782bbbc57dc71c0f887cae5d182f68e6069974d7c0042f66bd +size 106462 diff --git a/tests/rust/re_integration_test/tests/snapshots/start_with_dataset_url.png b/tests/rust/re_integration_test/tests/snapshots/start_with_dataset_url.png index 93e8486fc6bc..8944cc6439f1 100644 --- a/tests/rust/re_integration_test/tests/snapshots/start_with_dataset_url.png +++ b/tests/rust/re_integration_test/tests/snapshots/start_with_dataset_url.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c5673d8dcbea9f78f49406ef436e3b9b86569d39bb5f61579cce574b54b6f5b -size 43814 +oid sha256:020b68ca45f9600a7e1e35541a85ed91134306b41744a41445994b57d1da229c +size 40952 diff --git a/tests/rust/re_integration_test/tests/snapshots/start_with_segment_fragment_url.png b/tests/rust/re_integration_test/tests/snapshots/start_with_segment_fragment_url.png index d26a861ce77f..ccf56b04b2d9 100644 --- a/tests/rust/re_integration_test/tests/snapshots/start_with_segment_fragment_url.png +++ b/tests/rust/re_integration_test/tests/snapshots/start_with_segment_fragment_url.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36ecd199cedd16dd32c9ae8fd19708803944f67b49b5b2abfb5cdcd91758303c -size 103798 +oid sha256:1c12546af0e5f96f8cff4d88e53ec3cf2fe5735660883f1d0e5240d07b0470ca +size 106093 diff --git a/tests/rust/re_integration_test/tests/snapshots/state_timeline_hover_highlight_after.png b/tests/rust/re_integration_test/tests/snapshots/state_timeline_hover_highlight_after.png new file mode 100644 index 000000000000..79f51b35ba73 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/state_timeline_hover_highlight_after.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dff37b909b1bd96835f4a49e08f7c202c0c925cde00cc7206594730da6f7d3ec +size 105504 diff --git a/tests/rust/re_integration_test/tests/snapshots/state_timeline_hover_highlight_before.png b/tests/rust/re_integration_test/tests/snapshots/state_timeline_hover_highlight_before.png new file mode 100644 index 000000000000..1b8f1ecfe8fe --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/state_timeline_hover_highlight_before.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ac60a126031e83da09e5beb778cdc9e20cacc4588f040345529d5a09660c838 +size 98150 diff --git a/tests/rust/re_integration_test/tests/snapshots/states_source_1_initial.png b/tests/rust/re_integration_test/tests/snapshots/states_source_1_initial.png new file mode 100644 index 000000000000..d95c6baa008e --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/states_source_1_initial.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4344031fe4f1016bb94aa40c4ec1bc24fa49a0664fcb57595f68e6c14d817a0 +size 103349 diff --git a/tests/rust/re_integration_test/tests/snapshots/states_source_2_tree_expanded.png b/tests/rust/re_integration_test/tests/snapshots/states_source_2_tree_expanded.png new file mode 100644 index 000000000000..52b4cf4d1c52 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/states_source_2_tree_expanded.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ba2c95a3c5e64d2f39446665fa565ad4ccd1c379530331c64fefbe3c14d26f9 +size 85696 diff --git a/tests/rust/re_integration_test/tests/snapshots/states_source_3_entity_selected.png b/tests/rust/re_integration_test/tests/snapshots/states_source_3_entity_selected.png new file mode 100644 index 000000000000..067f40c12b2e --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/states_source_3_entity_selected.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d7098508a49d5c64df435c963a1cddb8bb68e95cbe1bcdee4e1c777d1c356f5 +size 103524 diff --git a/tests/rust/re_integration_test/tests/snapshots/states_source_4_dropdown_open.png b/tests/rust/re_integration_test/tests/snapshots/states_source_4_dropdown_open.png new file mode 100644 index 000000000000..4833a150e3ed --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/states_source_4_dropdown_open.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6566a03ff6f50269d7771580d3c18832bab518ea290ec136343cad336b9a2bb +size 109420 diff --git a/tests/rust/re_integration_test/tests/snapshots/states_source_5_source_changed.png b/tests/rust/re_integration_test/tests/snapshots/states_source_5_source_changed.png new file mode 100644 index 000000000000..40f4b59f0212 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/states_source_5_source_changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12d0b84a2c136e70bb5de76b5ebf36c10154bb8ad2a8f5d4d170956ad65ede7c +size 102745 diff --git a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_1.png b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_1.png index e3abc8de8486..036c7704d065 100644 --- a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_1.png +++ b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31f7413e8f0b3be3d197256bc5e309f8ff7229235653b7b55e46bebfb079e282 -size 60686 +oid sha256:f012eca3080da20c26b1556f5ef2367299ebed4b3b6d217fb8b1f77f8478b66a +size 58793 diff --git a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_2.png b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_2.png index e9066463c3e8..68051aeb8933 100644 --- a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_2.png +++ b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2af73f217ff8f9aec769f2e31690d6edb463cf93873a5dd67a8ea70ffa0a6b42 -size 52214 +oid sha256:541aa9c6034fb2b2efa11f4d2cd1cad3b5c19bd5aea4663de9d97a1aa1e22905 +size 50870 diff --git a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_3.png b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_3.png index 51b3c89f907b..d351b919a3ee 100644 --- a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_3.png +++ b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06803e74ec03f0aa0c6015b88ed130a57aed08602e8193b07ac48d2e74af6ef2 -size 63041 +oid sha256:8f0365b2bb93b124f7e568a74e5c2a13e5a73b6edac1cd59a7aa58cee2674e7e +size 61052 diff --git a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_4.png b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_4.png index 16776b6da807..8ba1c90783b9 100644 --- a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_4.png +++ b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00c104b950d64268c594ccf92cc89cbbef5d4b54251936568fd1ec311cc544aa -size 52135 +oid sha256:83dc892e79d3d63d8942e3e2d1fb09b580f744b7ae2eeb7f04d075fd42377d98 +size 50811 diff --git a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_5.png b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_5.png index 9a9bed529daa..db495a7714a9 100644 --- a/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_5.png +++ b/tests/rust/re_integration_test/tests/snapshots/streams_context_single_select_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c539f88bd9b8ec6577fc2f6fc7bc8f01527e746d7feb84ebfc30167ccfd01a20 -size 55261 +oid sha256:62521f79493c8b6c3da769779ad48ea97354b59758d17cd368fe2df51f2a36b4 +size 54375 diff --git a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_1_initial.png b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_1_initial.png index c92e92b1e303..b74628ed8ae3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_1_initial.png +++ b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_1_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e011d20dd0057ab7cd3cb8910910286df6ec78d009654bc834c65ca09dcf91fc -size 41140 +oid sha256:0307cae8c8764cd8351b2ffe68614cf4a3dcd4605407afdabed455cfc9133162 +size 41614 diff --git a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_2_after_first_drag.png b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_2_after_first_drag.png index 7f6b01f20709..13cb32cc1eff 100644 --- a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_2_after_first_drag.png +++ b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_2_after_first_drag.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:845b7e9b1354edf05df1f30b249400521c102c8b4db551c584471adc6fb854e1 -size 46202 +oid sha256:0d01fde20e240db07862fa2366e346c446fc6df8d1aec6740789c6b0e6f9b509 +size 46140 diff --git a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_3_after_undo.png b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_3_after_undo.png index c92e92b1e303..b74628ed8ae3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_3_after_undo.png +++ b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_3_after_undo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e011d20dd0057ab7cd3cb8910910286df6ec78d009654bc834c65ca09dcf91fc -size 41140 +oid sha256:0307cae8c8764cd8351b2ffe68614cf4a3dcd4605407afdabed455cfc9133162 +size 41614 diff --git a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_4_after_redo.png b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_4_after_redo.png index 7f6b01f20709..13cb32cc1eff 100644 --- a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_4_after_redo.png +++ b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_4_after_redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:845b7e9b1354edf05df1f30b249400521c102c8b4db551c584471adc6fb854e1 -size 46202 +oid sha256:0d01fde20e240db07862fa2366e346c446fc6df8d1aec6740789c6b0e6f9b509 +size 46140 diff --git a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_5_after_second_undo.png b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_5_after_second_undo.png index c92e92b1e303..b74628ed8ae3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_5_after_second_undo.png +++ b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_5_after_second_undo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e011d20dd0057ab7cd3cb8910910286df6ec78d009654bc834c65ca09dcf91fc -size 41140 +oid sha256:0307cae8c8764cd8351b2ffe68614cf4a3dcd4605407afdabed455cfc9133162 +size 41614 diff --git a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_6_after_new_drag.png b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_6_after_new_drag.png index 37aa9e2fac11..0a9fb537e5e1 100644 --- a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_6_after_new_drag.png +++ b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_6_after_new_drag.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:669691d753f0d7b4ca8256b1d44e23156774bf60ebe749af23cf44a0d0d683e8 -size 48755 +oid sha256:4dc0b7294035f544009757936a4024e2ee7d13f602093b2d42ee6d562cfbbfba +size 48570 diff --git a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_7_redo_after_new_action.png b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_7_redo_after_new_action.png index 37aa9e2fac11..0a9fb537e5e1 100644 --- a/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_7_redo_after_new_action.png +++ b/tests/rust/re_integration_test/tests/snapshots/undo_redo_3d_rotation_7_redo_after_new_action.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:669691d753f0d7b4ca8256b1d44e23156774bf60ebe749af23cf44a0d0d683e8 -size 48755 +oid sha256:4dc0b7294035f544009757936a4024e2ee7d13f602093b2d42ee6d562cfbbfba +size 48570 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_defaults.png b/tests/rust/re_integration_test/tests/snapshots/view_defaults.png index 863419776967..6b5894d511b0 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_defaults.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_defaults.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4d244af316ffe8eebc426a75cb90dd6b20c7ef7754c72987587057d43821083 -size 176240 +oid sha256:1f5da8313f0e893a3efac6b19f3e97ded1cd3e369cfa75cbfd9de6918f0601d6 +size 176800 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_1_initial.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_1_initial.png index 2f83a3e7e3b2..1e8eaea81da4 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_1_initial.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_1_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a0d9451cae64063887c707b03108ab434e14e56c013b9354134ce32bc47e42f -size 166699 +oid sha256:114a237e40a73a8d5e2f6cdcce873eb0c58df8f3a7a970804ea6f121bb6f98b3 +size 160362 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_2_plots_view_selected.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_2_plots_view_selected.png index f9d154f19e94..9620f4fa6a5c 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_2_plots_view_selected.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_2_plots_view_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac11179ce670cc84dc2dab6d3055a7d760934b201e1159845a945512f40f1787 -size 230347 +oid sha256:2427727356dbef93873b9645e441f9657e20822c19326f488f4f3161e4f6ee3d +size 222539 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_3_other_view_selected.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_3_other_view_selected.png index e0a77b94f613..fb0d94130dd4 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_3_other_view_selected.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_3_other_view_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e45c98a390c219a95c1646f2839ecde68f7cb82d5f0987e34c8e91696ad69ea6 -size 215180 +oid sha256:d1553449a61b993c755e74173077d519ef39434628a6c5bf31352d354a1386cb +size 209598 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_4_hover_sin_line.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_4_hover_sin_line.png index e37a2354074c..edf166084893 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_4_hover_sin_line.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_4_hover_sin_line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72bafab9204bba15c87630003a2c2459871115b792986ff2b69d49b24871aa8e -size 222689 +oid sha256:4a7201c7db4a11d621b9ac1054ff15e6fecb524467ee2f68d341144f9189d0b7 +size 223320 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_5_after_hide.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_5_after_hide.png index 19fe96aaa319..fdd36e9ad410 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_5_after_hide.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_5_after_hide.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1071aa0e921f787a3b23073d30c3ee1f3bd556a7f1f9deeba0e1073d58fc56f8 -size 225051 +oid sha256:3554cced66a085d30ce14dc8fef53e2ff81a6430cb02b491da211a4ac9e2b4f6 +size 217091 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_6_hover_different_after_hide.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_6_hover_different_after_hide.png index 3b572cac9b14..73768e9ba45f 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_6_hover_different_after_hide.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_6_hover_different_after_hide.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9f74407d5af1b998576e5e9b35c740ee806fc91c044766d9496e23d016361e8 -size 226741 +oid sha256:84401f12ee5965b22c7d9719ae8b819df70a1b3f594eb40d62c39dcc15576a62 +size 217585 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_1_view_selected.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_1_view_selected.png index 0f746420eb6f..5c3b79a0b140 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_1_view_selected.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_1_view_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d7bbec6d4becd355d2ad82394b5dca05513bb9192380966f0ca58ebc7f70046 -size 169438 +oid sha256:75999e87b730a1c092dc59796b0615b2efee174850ae139bc0d0de0b38bcf18f +size 168930 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_2_popup_open.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_2_popup_open.png index ff4168f13426..0caac51215c6 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_2_popup_open.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_2_popup_open.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8aaf485f3453b2153da59e4a9fae34e2e64bb1dafff76e96ef8d5405fa3465d1 -size 169129 +oid sha256:1a3b72f1925d5776332dbbc724ec6102113f05a69c8107896190d5a4ae4943ad +size 166849 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_1_starting_state.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_1_starting_state.png index a00a447bbcf6..c4d01d644e74 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_1_starting_state.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_1_starting_state.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45af9a4d21410789bd1c8110943be1116d6442414bfe72db5a33fd55beb46ae1 -size 171689 +oid sha256:a14ecddbadba507ea0b08afa0f8800462077dc7885b70b3c2d6720c5f452a162 +size 169722 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_2_selected_visualizer.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_2_selected_visualizer.png index bb8ca2a96d56..13c1543263a5 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_2_selected_visualizer.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_2_selected_visualizer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51699fee8dc2fc7648a0f992b8e610d5e12309350eca2313a59b86c1750b786d -size 174314 +oid sha256:850bd308165933fd0dec056696d094789e2b0b713413d8d5aea8d13ab5bd606b +size 173512 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_3_removed_visualizer.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_3_removed_visualizer.png index ba7a059df9b0..777de96a5daf 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_3_removed_visualizer.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_3_removed_visualizer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8784ed77ddded2c5bf33b25192ee08cf7c63ef0676a1a78d58b68a122ba23c73 -size 138375 +oid sha256:fba75ab6ebcef4d3e8844d886f2ed1c01840fa1c2ba246b499269e60fbaaf17a +size 137772 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_4_view_selected_again.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_4_view_selected_again.png index b4a4db849a24..0a2a6c495265 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_4_view_selected_again.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_4_view_selected_again.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d70785c35d5a28859f98e64b45a70cdad4597209fdbfcef50e274c5cc164fdae -size 181281 +oid sha256:ca5b2e5d0bfbf2fdb5121daf7b4cf525ea27d139fe99688ad5eef74f40defba4 +size 179472 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_5_popup_open.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_5_popup_open.png index 98c869835c7b..227efe339d33 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_5_popup_open.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_5_popup_open.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97f57782bb05623fe328ea338a63efdafccfdc64b057e42fe6e74cd193138f82 -size 206174 +oid sha256:1d8f2f87adbd92dbdaec479c4d1f312eb51db247f54da00dcaa8fe22f3e88c33 +size 202217 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_6_first_added.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_6_first_added.png index 3d021e83ba6f..9d80a34fde1b 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_6_first_added.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_6_first_added.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4f8906b0a22d22b6190ef425e7a95b7c90bdfe7428844cf8122a313ce76d520 -size 171069 +oid sha256:d531301c84ffc85dbfaf7f0a499b2794018d80fa27d2b5aa3339643ab2f62825 +size 169320 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_7_second_added.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_7_second_added.png index f83242094be6..76f26a917b74 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_7_second_added.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_add_multi_7_second_added.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8da18c585e9ef1a800aa0652c7c48d9ab8f3c018cb1a0b1d07e5bab72cd845df -size 171507 +oid sha256:86b1f189e23ecff3e6cc0c6420388854655e1a9384997a2490c12f0ec7d30129 +size 169416 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_1_open.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_1_open.png index 502c314a8289..d9883c945207 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_1_open.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_1_open.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d887a7bf09aa48326040a57de4ac28666fcef3cb6557d42fc363b359a8d8aff3 -size 176549 +oid sha256:25e7e30fbda07e7cba80adaefacd02765a0511f9508185edaf2dc6c59e8ad79e +size 174478 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_2_after_hide.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_2_after_hide.png index 259729e4710c..7b5568eaf5c3 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_2_after_hide.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_2_after_hide.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:701caf05364ee71e3ef5b1eaeeec2e480042f07f966dfb3ca6ac6a77695dad86 -size 158381 +oid sha256:6afaac1f65488e5c4b69b0f9da8c53e854b82434413ba84139323764da0ffaa4 +size 158015 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_3_show_option.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_3_show_option.png index 985e3a272dc9..a10c21adc581 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_3_show_option.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_3_show_option.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08d4bd371416a8c16fc11f480467e0767a8ace1798745374879cb0ef31deb5a1 -size 163456 +oid sha256:4b8c455a00a12b2d73dd4b05dadae6718933347df11352d82a0ab4425160c868 +size 161651 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_4_after_show.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_4_after_show.png index 87a9e0da0417..f53d96656bad 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_4_after_show.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_4_after_show.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:183b5e2e39494241ddfe76c2f365da8a57f0aaeb1e7ef57e8f1494e4c6f08433 -size 169355 +oid sha256:b59e1cfc1bbf84a9fd179740a59cc13068ef2738d279191404c7b58f57b9d6d4 +size 168836 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_5_after_remove.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_5_after_remove.png index ac32b2e7678f..aca1775d62fb 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_5_after_remove.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_ctx_menu_5_after_remove.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70514acd67dd6bf30257f1e4ac4e0dd1475e140b5fc7dbec72345d59eccdc60a -size 155262 +oid sha256:aed61f2f1e42b7c92d99a3fe4d855213bcbf4d93c764e94da98034dcd806ece4 +size 154719 diff --git a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_multi_scalar_view_selected.png b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_multi_scalar_view_selected.png index 9708ce10d2cb..aa36028e8785 100644 --- a/tests/rust/re_integration_test/tests/snapshots/view_visualizers_multi_scalar_view_selected.png +++ b/tests/rust/re_integration_test/tests/snapshots/view_visualizers_multi_scalar_view_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3270a921a3712b4cd83f542ec76594757b7c6ca4203d9af278ee247911fa59c -size 235234 +oid sha256:fa3bd733e17616565f5c5d69651d0e59e11f4d4c64abae3a4072ffd83b4c029a +size 232104 diff --git a/tests/rust/re_integration_test/tests/snapshots/watch_events_1_initial.png b/tests/rust/re_integration_test/tests/snapshots/watch_events_1_initial.png new file mode 100644 index 000000000000..5b625be95942 --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/watch_events_1_initial.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d276159f2c9c005f0908b225f89984bd975532b700811c968706ece10124489 +size 47248 diff --git a/tests/rust/re_integration_test/tests/snapshots/watch_events_2_entries_added.png b/tests/rust/re_integration_test/tests/snapshots/watch_events_2_entries_added.png new file mode 100644 index 000000000000..597d695424cc --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/watch_events_2_entries_added.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7cdc8aa74dafa8360792c4bd78d14de82d3c869e096b732c4bb31a8e16fcbba +size 51618 diff --git a/tests/rust/re_integration_test/tests/snapshots/watch_events_3_entries_removed.png b/tests/rust/re_integration_test/tests/snapshots/watch_events_3_entries_removed.png new file mode 100644 index 000000000000..2cf6fb4b198d --- /dev/null +++ b/tests/rust/re_integration_test/tests/snapshots/watch_events_3_entries_removed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:497148cc42abc55b22fd39d43583465cdabaf677eac10fc8e518338d5d322dc7 +size 30089 diff --git a/tests/rust/re_integration_test/tests/spatial_cross_view_interaction.rs b/tests/rust/re_integration_test/tests/spatial_cross_view_interaction.rs new file mode 100644 index 000000000000..519ce6f87b4b --- /dev/null +++ b/tests/rust/re_integration_test/tests/spatial_cross_view_interaction.rs @@ -0,0 +1,174 @@ +//! Tests cross-view spatial interaction between a pinhole image in 2D and 3D, +//! for both entity-path-derived and named transforms. + +use re_integration_test::HarnessExt as _; +use re_sdk::{EntityPathFilter, TimePoint}; +use re_viewer::external::re_sdk_types::{ + archetypes::{CoordinateFrame, Image, Pinhole, Points3D, Transform3D}, + blueprint::archetypes::EyeControls3D, + components::Position3D, + datatypes::ColorModel, +}; +use re_viewer::external::re_view_spatial; +use re_viewer::external::re_viewer_context::{RecommendedView, ViewClass as _}; +use re_viewer::viewer_test_utils::{self, HarnessOptions}; +use re_viewport_blueprint::{ViewBlueprint, ViewProperty}; + +#[tokio::test(flavor = "multi_thread")] +pub async fn test_spatial_cross_view_interaction_entity_hierarchy() { + run_test(false); +} + +#[tokio::test(flavor = "multi_thread")] +pub async fn test_spatial_cross_view_interaction_named_transforms() { + run_test(true); +} + +fn run_test(use_named_transforms: bool) { + let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { + window_size: Some(egui::vec2(1000.0, 600.0)), + ..Default::default() + }); + harness.init_recording(); + harness.set_blueprint_panel_opened(false); + harness.set_selection_panel_opened(false); + harness.set_time_panel_opened(false); + + setup_scene(&mut harness, use_named_transforms); + + let (variant, image_origin) = if use_named_transforms { + ("named_transforms", "image") + } else { + ("entity_hierarchy", "world/camera") + }; + + let root_container = + harness.add_blueprint_container(egui_tiles::ContainerKind::Horizontal, None); + + harness.setup_viewport_blueprint(move |viewer_context, blueprint| { + let mut view_2d = ViewBlueprint::new( + re_view_spatial::SpatialView2D::identifier(), + RecommendedView { + origin: image_origin.into(), + query_filter: EntityPathFilter::all(), + }, + ); + view_2d.display_name = Some("Image in 2D".into()); + + let mut view_3d = + ViewBlueprint::new_with_root_wildcard(re_view_spatial::SpatialView3D::identifier()); + view_3d.display_name = Some("Image in 3D".into()); + let view_3d_id = view_3d.id; + + blueprint.add_views([view_2d, view_3d].into_iter(), Some(root_container), None); + + let eye_property = + ViewProperty::from_archetype_for_view::(viewer_context, view_3d_id); + eye_property.save_blueprint_component( + viewer_context, + &EyeControls3D::descriptor_position(), + &Position3D::new(1.5, -1.0, 3.0), + ); + eye_property.save_blueprint_component( + viewer_context, + &EyeControls3D::descriptor_look_target(), + &Position3D::new(0.0, 0.0, 0.5), + ); + }); + + // Hovering the image in 2D should draw the corresponding ray in the 3D view. + let image_2d_rect = harness.get_panel_position("Image in 2D"); + hover_and_render(&mut harness, image_2d_rect.max * 0.75); + harness.snapshot_app(&format!( + "spatial_cross_view_interaction_{variant}_2d_to_3d" + )); + + // Hovering the image plane in 3D should draw the corresponding position in the 2D view. + let image_3d_rect = harness.get_panel_position("Image in 3D"); + let image_plane_position = egui::pos2( + image_3d_rect.left() + image_3d_rect.width() * 0.7, + image_3d_rect.top() + image_3d_rect.height() * 0.2, + ); + hover_and_render(&mut harness, image_plane_position); + harness.snapshot_app(&format!( + "spatial_cross_view_interaction_{variant}_3d_to_2d" + )); +} + +fn setup_scene( + harness: &mut egui_kittest::Harness<'_, re_viewer::App>, + use_named_transforms: bool, +) { + // Don't make the image too small since the region-of-interest 2D visualization gets a bit odd for very small images. + const IMAGE_WIDTH: usize = 128; + const IMAGE_HEIGHT: usize = 96; + + // Simple colored matrix + let image = ndarray::Array3::from_shape_fn((IMAGE_HEIGHT, IMAGE_WIDTH, 3), |(y, x, c)| { + let color = match (x < IMAGE_WIDTH / 2, y < IMAGE_HEIGHT / 2) { + (true, true) => [255, 80, 80], + (false, true) => [80, 255, 80], + (true, false) => [80, 80, 255], + (false, false) => [255, 255, 80], + }; + color[c] + }); + let points = Points3D::new([ + [-1.0, -1.0, 0.0], + [1.0, -1.0, 0.0], + [-1.0, 1.0, 0.0], + [1.0, 1.0, 0.0], + ]); + let camera_transform = Transform3D::from_translation([0.0, 0.0, 1.0]); + let pinhole = Pinhole::from_focal_length_and_resolution( + [100.0, 100.0], + [IMAGE_WIDTH as f32, IMAGE_HEIGHT as f32], + ) + .with_image_plane_distance(0.75); + let image = Image::from_color_model_and_tensor(ColorModel::RGB, image) + .expect("test image should be valid"); + + if use_named_transforms { + harness.log_entity("points", |builder| { + builder + .with_archetype_auto_row(TimePoint::STATIC, &points) + .with_archetype_auto_row(TimePoint::STATIC, &CoordinateFrame::new("world")) + }); + harness.log_entity("camera", |builder| { + builder + .with_archetype_auto_row( + TimePoint::STATIC, + &camera_transform + .with_parent_frame("world") + .with_child_frame("camera"), + ) + .with_archetype_auto_row( + TimePoint::STATIC, + &pinhole + .with_parent_frame("camera") + .with_child_frame("image"), + ) + }); + harness.log_entity("image", |builder| { + builder + .with_archetype_auto_row(TimePoint::STATIC, &image) + .with_archetype_auto_row(TimePoint::STATIC, &CoordinateFrame::new("image")) + }); + } else { + harness.log_entity("world/points", |builder| { + builder.with_archetype_auto_row(TimePoint::STATIC, &points) + }); + harness.log_entity("world/camera", |builder| { + builder + .with_archetype_auto_row(TimePoint::STATIC, &camera_transform) + .with_archetype_auto_row(TimePoint::STATIC, &pinhole) + .with_archetype_auto_row(TimePoint::STATIC, &image) + }); + } +} + +fn hover_and_render(harness: &mut egui_kittest::Harness<'_, re_viewer::App>, position: egui::Pos2) { + harness.hover_at(position); + harness.run(); + harness.render().expect("app should render"); +} diff --git a/tests/rust/re_integration_test/tests/state_timeline_hover_highlight.rs b/tests/rust/re_integration_test/tests/state_timeline_hover_highlight.rs new file mode 100644 index 000000000000..7de1f666f565 --- /dev/null +++ b/tests/rust/re_integration_test/tests/state_timeline_hover_highlight.rs @@ -0,0 +1,125 @@ +//! Cross-view time-range highlighting: hovering over a state phase in the state +//! timeline view should publish a `TimeRangeHighlight` that the time series view +//! (and any other view on the same timeline) picks up and paints as a background +//! band. + +use re_integration_test::HarnessExt as _; +use re_sdk::Timeline; +use re_sdk::log::RowId; +use re_view_state_timeline::StateTimelineView; +use re_view_time_series::TimeSeriesView; +use re_viewer::external::re_sdk_types; +use re_viewer::external::re_viewer_context::{RecommendedView, TimeControlCommand, ViewClass as _}; +use re_viewer::viewer_test_utils::{self, HarnessOptions}; +use re_viewport_blueprint::ViewBlueprint; + +#[tokio::test(flavor = "multi_thread")] +pub async fn test_state_phase_hover_propagates_to_time_series() { + let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions::default()); + harness.init_recording(); + harness.set_blueprint_panel_opened(false); + harness.set_selection_panel_opened(false); + harness.set_time_panel_opened(true); + + let timeline = Timeline::new_sequence("frame"); + let timeline_name = *timeline.name(); + + // State phases on the "frame" timeline. + let state_phases: [(i64, &str); 3] = [(0, "Idle"), (40, "Moving"), (80, "Done")]; + for (t, state) in &state_phases { + harness.log_entity("state/robot", |builder| { + builder.with_archetype( + RowId::new(), + [(timeline, *t)], + &re_sdk_types::archetypes::StateChange::single(*state), + ) + }); + } + + // Scalars on the same timeline so the time series view has data to plot. + for t in 0..120i64 { + harness.log_entity("scalars/value", |builder| { + builder.with_archetype( + RowId::new(), + [(timeline, t)], + &re_sdk_types::archetypes::Scalars::single((t as f64 / 20.0).sin()), + ) + }); + } + + // Make the active timeline match the data we logged so both views query "frame". + harness.run_with_app_context(move |app_context| { + app_context.send_time_commands_to_active_recording([ + TimeControlCommand::SetActiveTimeline(timeline_name), + ]); + }); + harness.run(); + + // Two named views, one above the other. + harness.clear_current_blueprint(); + let mut state_view = ViewBlueprint::new( + StateTimelineView::identifier(), + RecommendedView::new_single_entity("state/robot"), + ); + state_view.display_name = Some("State view".into()); + + let mut ts_view = ViewBlueprint::new( + TimeSeriesView::identifier(), + RecommendedView::new_single_entity("scalars/value"), + ); + ts_view.display_name = Some("Time series view".into()); + + harness.setup_viewport_blueprint(move |_viewer_context, blueprint| { + blueprint.add_views([state_view, ts_view].into_iter(), None, None); + }); + harness.run(); + + // Baseline: no hover, no highlight band on either view. + harness.snapshot_app("state_timeline_hover_highlight_before"); + + // Position over the first lane band, inside the middle phase ("Moving", 40..80). + // The view auto-fits to the data plus a trailing overhang, so the middle phase + // sits a bit left of center. + let state_rect = harness.get_panel_position("State view"); + let hover_pos = egui::pos2( + state_rect.left() + state_rect.width() * 0.45, + state_rect.top() + 20.0 + 4.0 + 14.0 + 11.0, + ); + harness.hover_at(hover_pos); + harness.run(); + + let highlight = harness.run_with_app_context(move |app_context| { + app_context + .active_time_ctrl() + .expect("active recording route should have a time control") + .highlighted_range() + .filter(|h| { + h.timeline == timeline_name + && h.kind + == re_viewer::external::re_viewer_context::TimeRangeHighlightKind::StateTimeline + }) + .cloned() + }); + + let highlight = highlight.expect( + "hovering over a state phase should publish a StateTimeline TimeRangeHighlight via \ + TimeControl, so the time panel and other time-based views can render it", + ); + // Hovered the middle phase: 40..80. + assert_eq!( + highlight.range.min.as_i64(), + 40, + "highlight start should match the hovered phase's start tick (40 = 'Moving')", + ); + assert_eq!( + highlight.range.max.as_i64(), + 80, + "highlight end should match the next phase's start tick (80 = 'Done')", + ); + assert!( + highlight.color.is_some(), + "Data highlights from the state timeline view must carry a fill color", + ); + + harness.snapshot_app("state_timeline_hover_highlight_after"); +} diff --git a/tests/rust/re_integration_test/tests/undo_redo_test.rs b/tests/rust/re_integration_test/tests/undo_redo_test.rs index 9c121701018b..d427cc25be70 100644 --- a/tests/rust/re_integration_test/tests/undo_redo_test.rs +++ b/tests/rust/re_integration_test/tests/undo_redo_test.rs @@ -9,7 +9,9 @@ use re_integration_test::HarnessExt as _; use re_sdk::TimePoint; use re_sdk::log::RowId; -use re_viewer::external::re_ui::{UICommand, UICommandSender as _}; +use re_viewer::external::re_ui::{ + RecordingCommand, RecordingCommandKind, RecordingCommandSender as _, +}; use re_viewer::external::re_viewer_context::ViewClass as _; use re_viewer::external::{re_sdk_types, re_view_spatial}; use re_viewer::viewer_test_utils::{self, HarnessOptions}; @@ -80,13 +82,25 @@ fn drag_rotate_view( /// Send undo command (Cmd/Ctrl+Z). fn send_undo(harness: &mut egui_kittest::Harness<'_, re_viewer::App>) { - harness.state().command_sender.send_ui(UICommand::Undo); - harness.run(); + send_recording_command(harness, RecordingCommandKind::Undo); } /// Send redo command (Cmd/Ctrl+Shift+Z). fn send_redo(harness: &mut egui_kittest::Harness<'_, re_viewer::App>) { - harness.state().command_sender.send_ui(UICommand::Redo); + send_recording_command(harness, RecordingCommandKind::Redo); +} + +fn send_recording_command( + harness: &mut egui_kittest::Harness<'_, re_viewer::App>, + kind: RecordingCommandKind, +) { + let app = harness.state(); + let recording_id = app + .active_recording_id() + .expect("expected an active recording") + .clone(); + app.command_sender + .send_recording_command(RecordingCommand { recording_id, kind }); harness.run(); } diff --git a/tests/rust/re_integration_test/tests/view_defaults_test.rs b/tests/rust/re_integration_test/tests/view_defaults_test.rs index 68936983ad91..1386100011f8 100644 --- a/tests/rust/re_integration_test/tests/view_defaults_test.rs +++ b/tests/rust/re_integration_test/tests/view_defaults_test.rs @@ -69,8 +69,10 @@ pub async fn test_view_defaults_stroke_width() { harness.snapshot_app("view_defaults"); // Verify the default is queryable from the blueprint store on the blueprint timeline. - let stroke_width = harness.run_with_viewer_context(move |viewer_context| { - let blueprint_db = viewer_context.store_context.blueprint; + let stroke_width = harness.run_with_app_context(move |app_context| { + let blueprint_db = app_context + .active_blueprint() + .expect("active recording route should have a blueprint"); let query = LatestAtQuery::latest(blueprint_timeline()); let results = blueprint_db.latest_at(&query, &defaults_path, [component_id]); results.component_mono::(component_id) diff --git a/tests/rust/re_integration_test/tests/viewer_events_test.rs b/tests/rust/re_integration_test/tests/viewer_events_test.rs index 211d147e52ec..168f9ef6f92c 100644 --- a/tests/rust/re_integration_test/tests/viewer_events_test.rs +++ b/tests/rust/re_integration_test/tests/viewer_events_test.rs @@ -11,7 +11,7 @@ use re_viewer::App; use re_viewer::event::{ViewerEvent, ViewerEventDispatcher, ViewerEventKind}; use re_viewer::external::re_sdk_types::archetypes::TextLog; use re_viewer::external::re_sdk_types::blueprint::components::PlayState; -use re_viewer::external::re_viewer_context::TimeControlCommand; +use re_viewer::external::re_viewer_context::{AppContext, TimeControl, TimeControlCommand}; use re_viewer::viewer_test_utils::{self, HarnessOptions}; /// A simple event collector that records viewer events for later inspection. @@ -60,18 +60,23 @@ fn send_time_commands( commands: impl IntoIterator, ) { let commands: Vec<_> = commands.into_iter().collect(); - harness.run_with_viewer_context(move |ctx| { - ctx.send_time_commands(commands); + harness.run_with_app_context(move |ctx| { + ctx.send_time_commands_to_active_recording(commands); }); harness.run_ok(); } fn assert_play_state(harness: &mut Harness<'_, App>, expected: PlayState) { - let actual = harness.run_with_viewer_context(|ctx| ctx.time_ctrl.play_state()); + let actual = harness.run_with_app_context(|ctx| active_time_ctrl(ctx).play_state()); assert_eq!(actual, expected, "play state mismatch"); } +fn active_time_ctrl<'a>(ctx: &'a AppContext<'_>) -> &'a TimeControl { + ctx.active_time_ctrl() + .expect("active recording route should have a time control") +} + /// Verifies that switching timelines emits the appropriate viewer event. #[tokio::test] async fn time_control_emits_timeline_switch_event() { @@ -98,7 +103,7 @@ async fn time_control_emits_timeline_switch_event() { } let initial_timeline = - harness.run_with_viewer_context(move |ctx| *ctx.time_ctrl.timeline_name()); + harness.run_with_app_context(move |ctx| *active_time_ctrl(ctx).timeline_name()); let alternate_timeline = if initial_timeline == *timeline_a.name() { timeline_b } else { @@ -115,7 +120,7 @@ async fn time_control_emits_timeline_switch_event() { )], ); let timeline_after_switch = - harness.run_with_viewer_context(|ctx| *ctx.time_ctrl.timeline_name()); + harness.run_with_app_context(|ctx| *active_time_ctrl(ctx).timeline_name()); assert_eq!(timeline_after_switch, *alternate_timeline.name()); assert!( @@ -163,7 +168,7 @@ async fn time_control_emits_expected_viewer_events() { // Seek to a specific time let specific_time = TimeReal::from(4_i64); send_time_commands(&mut harness, [TimeControlCommand::SetTime(specific_time)]); - let time_after_seek = harness.run_with_viewer_context(|ctx| ctx.time_ctrl.time().unwrap()); + let time_after_seek = harness.run_with_app_context(|ctx| active_time_ctrl(ctx).time().unwrap()); assert_eq!(time_after_seek, specific_time); assert!( events.received_event( @@ -239,7 +244,7 @@ async fn test_time_control_update_emits_time_update_events() { ); assert_eq!( - harness.run_with_viewer_context(|ctx| ctx.time_ctrl.time().unwrap()), + harness.run_with_app_context(|ctx| active_time_ctrl(ctx).time().unwrap()), TimeReal::from(0_i64) ); diff --git a/tests/rust/re_integration_test/tests/max_views_spawned_test.rs b/tests/rust/re_integration_test/tests/views_spawned_test.rs similarity index 70% rename from tests/rust/re_integration_test/tests/max_views_spawned_test.rs rename to tests/rust/re_integration_test/tests/views_spawned_test.rs index 34cd0d6d3f9b..d165204c2c15 100644 --- a/tests/rust/re_integration_test/tests/max_views_spawned_test.rs +++ b/tests/rust/re_integration_test/tests/views_spawned_test.rs @@ -5,10 +5,10 @@ use std::sync::Arc; use re_integration_test::HarnessExt as _; -use re_sdk::Timeline; use re_sdk::external::arrow::array::Float64Array; +use re_sdk::{Component as _, Timeline}; use re_view_time_series::TimeSeriesView; -use re_viewer::external::re_sdk_types; +use re_viewer::external::re_sdk_types::{self, components, datatypes}; use re_viewer::external::re_viewer_context::ViewClass as _; use re_viewer::viewer_test_utils; @@ -86,3 +86,41 @@ pub async fn test_time_series_max_views_spawned() { origin: /native_5, filter: ResolvedEntityPathFilter("+ $origin\n- /__properties/**") "#); } + +#[tokio::test(flavor = "multi_thread")] +pub async fn test_time_series_does_not_spawn_for_non_scalar_physical_type() { + let mut harness = viewer_test_utils::viewer_harness(&Default::default()); + harness.init_recording(); + + let timeline = Timeline::new_sequence("frame"); + + for frame in 0..10 { + harness.log_entity("string_scalar", |builder| { + builder.with_archetype_auto_row( + [(timeline, frame)], + &re_sdk_types::DynamicArchetype::new("custom") + .with_component_override::( + "text_field", + components::Scalar::name(), // We attach a semantic value. + ["hello"], + ), + ) + }); + } + + harness.setup_viewport_blueprint(|ctx, blueprint| { + blueprint.set_auto_layout(true, ctx); + blueprint.set_auto_views(true, ctx); + }); + + let num_time_series_views = harness.setup_viewport_blueprint(|_ctx, blueprint| { + blueprint + .views + .values() + .filter(|view| view.class_identifier() == TimeSeriesView::identifier()) + .count() + }); + + // But naturally don't spawn a view, because we can't visualize the `Utf8`. + assert_eq!(num_time_series_views, 0); +} diff --git a/tests/rust/re_integration_test/tests/watch_events.rs b/tests/rust/re_integration_test/tests/watch_events.rs new file mode 100644 index 000000000000..4bd96403d345 --- /dev/null +++ b/tests/rust/re_integration_test/tests/watch_events.rs @@ -0,0 +1,211 @@ +use std::sync::Arc; +use std::time::Duration; + +use arrow::array::{Int64Array, RecordBatch, StringArray}; +use arrow::datatypes::{DataType, Field, Schema}; +use egui_kittest::SnapshotResults; +use egui_kittest::kittest::Queryable as _; +use re_integration_test::{HarnessExt as _, TestServer}; +use re_protos::cloud::v1alpha1::EntryFilter; +use re_protos::cloud::v1alpha1::ext::{self, TableInsertMode}; +use re_sdk::external::re_log_types; +use re_viewer::viewer_test_utils::{self, HarnessOptions}; + +/// The viewer should auto-refresh its catalog when entries are added/removed server-side, +/// driven by the `WatchEvents` stream (no manual refresh). +#[tokio::test(flavor = "multi_thread")] +pub async fn watch_events_auto_refresh_test() { + let server = TestServer::spawn().await; + let mut client = server.client().await.expect("Failed to connect to server"); + + let transient_dataset = "my_dataset"; + let persistent_dataset = "persistent_dataset"; + let transient_table = "my_table"; + let persistent_table = "persistent_table"; + + // Create a persistent dataset and table up front as stable reference points that stay + // around across the create/delete below. + let persistent = client + .create_dataset_entry(persistent_dataset.to_owned(), None) + .await + .expect("Failed to create persistent dataset"); + create_table(&mut client, persistent_table).await; + + // Open the viewer *directly at* the persistent dataset. This connects to the server, + // which spawns the `WatchEvents` listener for this origin. + let dataset_url = format!( + "rerun+http://localhost:{}/entry/{}", + server.port(), + persistent.details.id + ); + let mut harness = viewer_test_utils::viewer_harness(&HarnessOptions { + startup_url: Some(dataset_url), + ..Default::default() + }); + let mut snapshot_results = SnapshotResults::new(); + + harness.set_blueprint_panel_opened(true); + harness.set_selection_panel_opened(false); + harness.set_time_panel_opened(false); + + // Wait for the persistent dataset and table to appear in the panel. + viewer_test_utils::step_until( + "Persistent entries appear", + &mut harness, + |harness| { + let panel = harness.recording_panel(); + let root = panel.root(); + root.query_by_label_contains(persistent_dataset).is_some() + && root.query_by_label_contains(persistent_table).is_some() + }, + Duration::from_millis(100), + Duration::from_secs(5), + ); + + // Select the persistent table so its data is shown, avoiding the transient "Loading…" state + // of the persistent dataset in the snapshot. + harness + .get_all_by_label(persistent_table) + .next() + .expect("persistent table label should be present") + .click(); + harness.run_ok(); + viewer_test_utils::step_until( + "Persistent table data is rendered in main view", + &mut harness, + |harness| harness.query_by_label_contains("alpha").is_some(), + Duration::from_millis(100), + Duration::from_secs(10), + ); + snapshot_results.add(harness.try_snapshot("watch_events_1_initial")); + + // When creating entries, the server emits `EntryCreated` and the viewer's watch loop + // auto-refreshes the catalog without a manual refresh. + let dataset = client + .create_dataset_entry(transient_dataset.to_owned(), None) + .await + .expect("Failed to create dataset"); + let table = create_table(&mut client, transient_table).await; + + viewer_test_utils::step_until( + "Transient entries auto-appear", + &mut harness, + |harness| { + let panel = harness.recording_panel(); + let root = panel.root(); + root.query_by_label_contains(transient_dataset).is_some() + && root.query_by_label_contains(transient_table).is_some() + }, + Duration::from_millis(100), + Duration::from_secs(5), + ); + viewer_test_utils::step_until( + "Persistent table data is rendered again after refresh", + &mut harness, + |harness| harness.query_by_label_contains("alpha").is_some(), + Duration::from_millis(100), + Duration::from_secs(10), + ); + snapshot_results.add(harness.try_snapshot("watch_events_2_entries_added")); + + // Open the transient table so its data is the currently-viewed content when it gets deleted + // below. Rows contain the string "alpha" (from the first data batch). Pick the first match, + // which is the entry in the left panel. + harness + .get_all_by_label(transient_table) + .next() + .expect("transient table label should be present") + .click(); + harness.run_ok(); + viewer_test_utils::step_until( + "Transient table data is rendered in main view", + &mut harness, + |harness| harness.query_by_label_contains("alpha").is_some(), + Duration::from_millis(100), + Duration::from_secs(10), + ); + + // When deleting entries, the server emits `EntryDeleted` and the viewer auto-refreshes again. + client + .delete_entry(dataset.details.id) + .await + .expect("Failed to delete dataset"); + client + .delete_entry(table.details.id) + .await + .expect("Failed to delete table"); + + // Sanity check: confirm the server really dropped them. + for name in [transient_dataset, transient_table] { + let remaining = client + .find_entries(EntryFilter { + id: None, + name: Some(name.to_owned()), + entry_kind: None, + entry_kinds: vec![], + }) + .await + .expect("find_entries failed"); + assert!( + remaining.is_empty(), + "entry still exists server-side after delete: {name} {remaining:?}" + ); + } + + // The deleted entries auto-disappear on their own while the persistent ones stay around. + viewer_test_utils::step_until( + "Transient entries auto-disappear", + &mut harness, + |harness| { + let panel = harness.recording_panel(); + let root = panel.root(); + root.query_by_label_contains(transient_dataset).is_none() + && root.query_by_label_contains(transient_table).is_none() + && root.query_by_label_contains(persistent_dataset).is_some() + && root.query_by_label_contains(persistent_table).is_some() + }, + Duration::from_millis(100), + Duration::from_secs(5), + ); + snapshot_results.add(harness.try_snapshot("watch_events_3_entries_removed")); +} + +/// Creates a table entry and writes an initial data batch (rows `alpha`/`beta`/`gamma`). +async fn create_table( + client: &mut re_redap_client::ConnectionClient, + name: &str, +) -> ext::TableEntry { + let schema = Arc::new(Schema::new_with_metadata( + vec![ + Field::new("id", DataType::Int64, false), + Field::new("name", DataType::Utf8, false), + ], + Default::default(), + )); + let batch = RecordBatch::try_new_with_options( + schema.clone(), + vec![ + Arc::new(Int64Array::from(vec![1, 2, 3])), + Arc::new(StringArray::from(vec!["alpha", "beta", "gamma"])), + ], + &Default::default(), + ) + .expect("Failed to create record batch"); + let table = client + .create_table_entry( + re_log_types::EntryName::new(name).expect("Failed to create entry name"), + None, + schema.clone(), + ) + .await + .expect("Failed to create table"); + client + .write_table( + futures::stream::once(async { batch }), + table.details.id, + TableInsertMode::Append, + ) + .await + .expect("Failed to write initial data"); + table +} diff --git a/tests/rust/test_data_density_graph/src/main.rs b/tests/rust/test_data_density_graph/src/main.rs index cd03358e9ac9..b2d0236f3471 100644 --- a/tests/rust/test_data_density_graph/src/main.rs +++ b/tests/rust/test_data_density_graph/src/main.rs @@ -119,7 +119,7 @@ fn log( }); let mut chunk = rerun::log::Chunk::builder(entity_path.clone()); - for (time, component) in log_times.iter().zip(components) { + for (time, component) in std::iter::zip(&log_times, components) { chunk = chunk.with_archetype( rerun::log::RowId::new(), [( diff --git a/uv.lock b/uv.lock index 0d24f8236c68..37e00435b478 100644 --- a/uv.lock +++ b/uv.lock @@ -52,16 +52,21 @@ members = [ "rerun-sdk", "rerun-workspace", "rgbd", + "robot-data-preprocessing", "rrt-star", "segment-anything-model", "server-tables", "shared-recording", + "state-timeline", "stdio", "structure-from-motion", + "table-blueprints", + "table-grid-with-flags", "table-zoo", "template", "using-index-values", ] +constraints = [{ name = "pyarrow", specifier = ">=23.0.1,<24" }] [[package]] name = "accelerate" @@ -773,7 +778,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "accelerate" }, - { name = "diffusers", specifier = "==0.27.2" }, + { name = "diffusers", specifier = "<0.39" }, { name = "numpy" }, { name = "opencv-python" }, { name = "pillow" }, @@ -828,6 +833,71 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + [[package]] name = "curl-cffi" version = "0.13.0" @@ -884,19 +954,19 @@ requires-dist = [{ name = "rerun-sdk", editable = "rerun_py" }] [[package]] name = "datafusion" -version = "52.3.0" +version = "53.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyarrow" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/d4/a5ad7b665a80008901892fde61dc667318db0652a955d706ddca3a224b5a/datafusion-52.3.0.tar.gz", hash = "sha256:2e8b02ad142b1a0d673f035d96a0944a640ac78275003d7e453cee4afe4a20a4", size = 205026, upload-time = "2026-03-16T10:54:07.739Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/2b/0f96f12b70839c93930c4e17d767fc32b6c77d548c78784128049e944701/datafusion-53.0.0.tar.gz", hash = "sha256:ba9a5ec06b5453fbd8710d6aeeb515a8bcac4b6c140e254409bb53a5f322ef22", size = 224267, upload-time = "2026-04-13T00:45:02.686Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/63/1bb0737988cefa77274b459d64fa4b57ba4cf755639a39733e9581b5d599/datafusion-52.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a73f02406b2985b9145dd97f8221a929c9ef3289a8ba64c6b52043e240938528", size = 31503230, upload-time = "2026-03-16T10:53:50.312Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/ea3b79239953c3044d19d8e9581015da025b6640796db03799e435b17910/datafusion-52.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:118a1f0add6a3f91fcbc90c71819fe08750e2981637d5e7b346e099e94a20b8b", size = 28159497, upload-time = "2026-03-16T10:53:54.032Z" }, - { url = "https://files.pythonhosted.org/packages/24/c8/7d325feb4b7509ae03857fd7e164e95ec72e8c9f3dfd3178ec7f80d53977/datafusion-52.3.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:253ce7aee5fe84bd6ee290c20608114114bdb5115852617f97d3855d36ad9341", size = 30769154, upload-time = "2026-03-16T10:53:57.835Z" }, - { url = "https://files.pythonhosted.org/packages/37/ee/478689c69b3cb1ccabb2d52feac0c181f6cdf20b51a81df35344b1dab9a6/datafusion-52.3.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2af3469d2f06959bec88579ab107a72f965de18b32e607069bbdd0b859ed8dbb", size = 33060335, upload-time = "2026-03-16T10:54:01.715Z" }, - { url = "https://files.pythonhosted.org/packages/1c/48/01906ab5c1a70373c6874ac5192d03646fa7b94d9ff06e3f676cb6b0f43f/datafusion-52.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:9fb35738cf4dbff672dbcfffc7332813024cb0ad2ab8cda1fb90b9054277ab0c", size = 33765807, upload-time = "2026-03-16T10:54:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/4c/60e052813d81f1ffe3123ead013dbdd2cf961daa576cb9056cbb80228e6b/datafusion-53.0.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a0bd1a98d736571321416dc4ed361a9d1225da1ec9f6c5fad818d75f547697a7", size = 35774913, upload-time = "2026-04-13T00:44:46.235Z" }, + { url = "https://files.pythonhosted.org/packages/6e/59/beabe5301df3338d8206446cd624079e43bdad46e20377a6336017fb6ccf/datafusion-53.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ce186a8d2405afd67e11e2fb75715019f16b00d070b8d0da89d8aa61cc74c8b5", size = 32667118, upload-time = "2026-04-13T00:44:50.269Z" }, + { url = "https://files.pythonhosted.org/packages/ae/94/636ab61ade98395daea6e733e225e9c7beef111c7c5b575ac851513e203c/datafusion-53.0.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:288a00a7ef03e2807a4667683f7560efd80d60ed1d41696ac15ca9ded14c8251", size = 35585824, upload-time = "2026-04-13T00:44:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/b9f4889209af02f8d14bccb0e6f0519c329b072bc4d2595025a1303f144c/datafusion-53.0.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8fef0004f0161fcfc556c025a7201f9cc3169aa3adb97a86419ebb34182d9efb", size = 38083690, upload-time = "2026-04-13T00:44:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1a/ea4831fc6aeefedbcf186c9f6a273d507b1787c03cbb905bded7e1149a6a/datafusion-53.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:4c8410f5f659b926677be6c7d443bbc05d825c078c970b7d8cf977ebcf948314", size = 38120687, upload-time = "2026-04-13T00:45:00.633Z" }, ] [[package]] @@ -1038,10 +1108,11 @@ wheels = [ [[package]] name = "diffusers" -version = "0.27.2" +version = "0.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, + { name = "httpx" }, { name = "huggingface-hub" }, { name = "importlib-metadata" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1051,9 +1122,9 @@ dependencies = [ { name = "requests" }, { name = "safetensors" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/f9/9821366fddd7cdc3fe03cf39b7755665f09a8e645dee4a0d4248478b37ae/diffusers-0.27.2.tar.gz", hash = "sha256:6cefd7770d7fc1d139614233aa17cdcd639c138d0c3517b8d8bbc8cf573050a0", size = 1565845, upload-time = "2024-03-20T01:54:25.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ed/255d3dfd4a2271dffc8f1895f9d2720b3bf1beaecf02148bb5604439e594/diffusers-0.38.0.tar.gz", hash = "sha256:1e094ec5c16f18c42fb89d37f07a94cf9aab3ebbe527ab059c609597b8857626", size = 4328401, upload-time = "2026-05-01T05:42:15.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/c5/3b84fd731dd93c549a0c25657e4ce5a957aeccd32d60dba2958cd3cdac23/diffusers-0.27.2-py3-none-any.whl", hash = "sha256:85da5cd1098ab428535d592136973ec0c3f12f78148c94b379cb9f02d2414e75", size = 2025682, upload-time = "2024-03-20T01:54:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/42/c0/3237566ea6e3a542f3c0669a253d62fe75f27b84b3d7bd4fb3b5ee89d73c/diffusers-0.38.0-py3-none-any.whl", hash = "sha256:18e53f9e539096320470f62c6360a6fd5727ff28cffda566265316e13fcdb612", size = 5245919, upload-time = "2026-05-01T05:42:12.779Z" }, ] [[package]] @@ -1138,11 +1209,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.0" +version = "3.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] [[package]] @@ -1274,14 +1345,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.45" +version = "3.1.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] [[package]] @@ -1437,14 +1508,26 @@ requires-dist = [{ name = "rerun-sdk", editable = "rerun_py" }] [[package]] name = "griffe" -version = "1.4.1" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/65/f708fd15b91182e8928f5bf335255028c22f5d8a181e8819f3fa8f0230f4/griffe-1.4.1.tar.gz", hash = "sha256:911a201b01dc92e08c0e84c38a301e9da5ec067f00e7d9f2e39bc24dbfa3c176", size = 381239, upload-time = "2024-10-11T22:34:08.404Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/fc/570a1e503e19be24c5642ea8b93f23e3eef1dfa930e761cab72dedc2c2db/griffe-1.4.1-py3-none-any.whl", hash = "sha256:84295ee0b27743bd880aea75632830ef02ded65d16124025e4c263bb826ab645", size = 126956, upload-time = "2024-10-11T22:34:04.669Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "griffe-public-redundant-aliases" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/b8/a6515669cb0c000e0d05768c92e1f1e673e7ebea66ba1ddaf70cc87714f6/griffe_public_redundant_aliases-0.3.0.tar.gz", hash = "sha256:45cbcfbe7f28408d043b446f699f4b92ee2477b4933f97d344f3edb2970325ec", size = 25325, upload-time = "2025-11-08T17:48:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/bb/7c27ce87737b0a170052de0356023a8fafb77d01a76ed455e6517cd6090d/griffe_public_redundant_aliases-0.3.0-py3-none-any.whl", hash = "sha256:091af6f2d3fbcf3baba21ae3a8cca284812da4cddf3c564d6cc1013ffb005901", size = 5510, upload-time = "2025-11-08T17:48:29.142Z" }, ] [[package]] @@ -1686,11 +1769,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, ] [[package]] @@ -2125,7 +2208,7 @@ wheels = [ [[package]] name = "jupyter-server" -version = "2.17.0" +version = "2.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2148,9 +2231,9 @@ dependencies = [ { name = "traitlets" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/15/1eacb0fcb79ef86e8a0a79a708e6ad7435f6f223097dd29a4ce861fabc44/jupyter_server-2.18.2.tar.gz", hash = "sha256:06b4f40d8a7a00bb39d5216859c81374a0e7cfefe6d8a5a7facc5a5c37c679a7", size = 753177, upload-time = "2026-05-06T07:04:36.274Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, + { url = "https://files.pythonhosted.org/packages/e2/50/ecf4f70d65bdb7519b28a33d1b2fee8a4b4ba1ae1a92f15d97e877c5de21/jupyter_server-2.18.2-py3-none-any.whl", hash = "sha256:fa5e46539ded65791838035a2b6001f13e54d5f64b8b3752eb1e91fdd641a5b8", size = 391907, upload-time = "2026-05-06T07:04:34.014Z" }, ] [[package]] @@ -2181,7 +2264,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.5.0" +version = "4.5.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -2199,9 +2282,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/e5/4fa382a796a6d8e2cd867816b64f1ff27f906e43a7a83ad9eb389e448cd8/jupyterlab-4.5.0.tar.gz", hash = "sha256:aec33d6d8f1225b495ee2cf20f0514f45e6df8e360bdd7ac9bace0b7ac5177ea", size = 23989880, upload-time = "2025-11-18T13:19:00.365Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/22/8440ec827762146e7cdecf04335bd348795899d29dc6ae82238707353a2c/jupyterlab-4.5.7.tar.gz", hash = "sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0", size = 23992763, upload-time = "2026-04-29T16:43:51.328Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/1e/5a4d5498eba382fee667ed797cf64ae5d1b13b04356df62f067f48bb0f61/jupyterlab-4.5.0-py3-none-any.whl", hash = "sha256:88e157c75c1afff64c7dc4b801ec471450b922a4eae4305211ddd40da8201c8a", size = 12380641, upload-time = "2025-11-18T13:18:56.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/537b8f7d80e799af19af35fb3ddfc970b951088a13c57dd9387dcfbb7f61/jupyterlab-4.5.7-py3-none-any.whl", hash = "sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d", size = 12450123, upload-time = "2026-04-29T16:43:46.639Z" }, ] [[package]] @@ -2504,30 +2587,6 @@ dependencies = [ [package.metadata] requires-dist = [{ name = "rerun-sdk", editable = "rerun_py" }] -[[package]] -name = "lz4" -version = "4.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/45/2466d73d79e3940cad4b26761f356f19fd33f4409c96f100e01a5c566909/lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d", size = 207396, upload-time = "2025-11-03T13:01:24.965Z" }, - { url = "https://files.pythonhosted.org/packages/72/12/7da96077a7e8918a5a57a25f1254edaf76aefb457666fcc1066deeecd609/lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f", size = 207154, upload-time = "2025-11-03T13:01:26.922Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0e/0fb54f84fd1890d4af5bc0a3c1fa69678451c1a6bd40de26ec0561bb4ec5/lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3", size = 1291053, upload-time = "2025-11-03T13:01:28.396Z" }, - { url = "https://files.pythonhosted.org/packages/15/45/8ce01cc2715a19c9e72b0e423262072c17d581a8da56e0bd4550f3d76a79/lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758", size = 1278586, upload-time = "2025-11-03T13:01:29.906Z" }, - { url = "https://files.pythonhosted.org/packages/6d/34/7be9b09015e18510a09b8d76c304d505a7cbc66b775ec0b8f61442316818/lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1", size = 1367315, upload-time = "2025-11-03T13:01:31.054Z" }, - { url = "https://files.pythonhosted.org/packages/2a/94/52cc3ec0d41e8d68c985ec3b2d33631f281d8b748fb44955bc0384c2627b/lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc", size = 88173, upload-time = "2025-11-03T13:01:32.643Z" }, - { url = "https://files.pythonhosted.org/packages/ca/35/c3c0bdc409f551404355aeeabc8da343577d0e53592368062e371a3620e1/lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd", size = 99492, upload-time = "2025-11-03T13:01:33.813Z" }, - { url = "https://files.pythonhosted.org/packages/1d/02/4d88de2f1e97f9d05fd3d278fe412b08969bc94ff34942f5a3f09318144a/lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397", size = 91280, upload-time = "2025-11-03T13:01:35.081Z" }, - { url = "https://files.pythonhosted.org/packages/93/5b/6edcd23319d9e28b1bedf32768c3d1fd56eed8223960a2c47dacd2cec2af/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4", size = 207391, upload-time = "2025-11-03T13:01:36.644Z" }, - { url = "https://files.pythonhosted.org/packages/34/36/5f9b772e85b3d5769367a79973b8030afad0d6b724444083bad09becd66f/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43", size = 207146, upload-time = "2025-11-03T13:01:37.928Z" }, - { url = "https://files.pythonhosted.org/packages/04/f4/f66da5647c0d72592081a37c8775feacc3d14d2625bbdaabd6307c274565/lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7", size = 1292623, upload-time = "2025-11-03T13:01:39.341Z" }, - { url = "https://files.pythonhosted.org/packages/85/fc/5df0f17467cdda0cad464a9197a447027879197761b55faad7ca29c29a04/lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb", size = 1279982, upload-time = "2025-11-03T13:01:40.816Z" }, - { url = "https://files.pythonhosted.org/packages/25/3b/b55cb577aa148ed4e383e9700c36f70b651cd434e1c07568f0a86c9d5fbb/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989", size = 1368674, upload-time = "2025-11-03T13:01:42.118Z" }, - { url = "https://files.pythonhosted.org/packages/fb/31/e97e8c74c59ea479598e5c55cbe0b1334f03ee74ca97726e872944ed42df/lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d", size = 88168, upload-time = "2025-11-03T13:01:43.282Z" }, - { url = "https://files.pythonhosted.org/packages/18/47/715865a6c7071f417bef9b57c8644f29cb7a55b77742bd5d93a609274e7e/lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004", size = 99491, upload-time = "2025-11-03T13:01:44.167Z" }, - { url = "https://files.pythonhosted.org/packages/14/e7/ac120c2ca8caec5c945e6356ada2aa5cfabd83a01e3170f264a5c42c8231/lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b", size = 91271, upload-time = "2025-11-03T13:01:45.016Z" }, -] - [[package]] name = "markdown" version = "3.10" @@ -2581,14 +2640,14 @@ wheels = [ [[package]] name = "marshmallow" -version = "3.26.1" +version = "3.26.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, ] [[package]] @@ -2645,52 +2704,26 @@ wheels = [ [[package]] name = "maturin" -version = "1.10.2" +version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/44/c593afce7d418ae6016b955c978055232359ad28c707a9ac6643fc60512d/maturin-1.10.2.tar.gz", hash = "sha256:259292563da89850bf8f7d37aa4ddba22905214c1e180b1c8f55505dfd8c0e81", size = 217835, upload-time = "2025-11-19T11:53:17.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/74/7f7e93019bb71aa072a7cdf951cbe4c9a8d5870dd86c66ec67002153487f/maturin-1.10.2-py3-none-linux_armv6l.whl", hash = "sha256:11c73815f21a755d2129c410e6cb19dbfacbc0155bfc46c706b69930c2eb794b", size = 8763201, upload-time = "2025-11-19T11:52:42.98Z" }, - { url = "https://files.pythonhosted.org/packages/4a/85/1d1b64dbb6518ee633bfde8787e251ae59428818fea7a6bdacb8008a09bd/maturin-1.10.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7fbd997c5347649ee7987bd05a92bd5b8b07efa4ac3f8bcbf6196e07eb573d89", size = 17072583, upload-time = "2025-11-19T11:52:45.636Z" }, - { url = "https://files.pythonhosted.org/packages/7c/45/2418f0d6e1cbdf890205d1dc73ebea6778bb9ce80f92e866576c701ded72/maturin-1.10.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3ce9b2ad4fb9c341f450a6d32dc3edb409a2d582a81bc46ba55f6e3b6196b22", size = 8827021, upload-time = "2025-11-19T11:52:48.143Z" }, - { url = "https://files.pythonhosted.org/packages/7f/83/14c96ddc93b38745d8c3b85126f7d78a94f809a49dc9644bb22b0dc7b78c/maturin-1.10.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:f0d1b7b5f73c8d30a7e71cd2a2189a7f0126a3a3cd8b3d6843e7e1d4db50f759", size = 8751780, upload-time = "2025-11-19T11:52:51.613Z" }, - { url = "https://files.pythonhosted.org/packages/46/8d/753148c0d0472acd31a297f6d11c3263cd2668d38278ed29d523625f7290/maturin-1.10.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:efcd496a3202ffe0d0489df1f83d08b91399782fb2dd545d5a1e7bf6fd81af39", size = 9241884, upload-time = "2025-11-19T11:52:53.946Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f9/f5ca9fe8cad70cac6f3b6008598cc708f8a74dd619baced99784a6253f23/maturin-1.10.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a41ec70d99e27c05377be90f8e3c3def2a7bae4d0d9d5ea874aaf2d1da625d5c", size = 8671736, upload-time = "2025-11-19T11:52:57.133Z" }, - { url = "https://files.pythonhosted.org/packages/0a/76/f59cbcfcabef0259c3971f8b5754c85276a272028d8363386b03ec4e9947/maturin-1.10.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:07a82864352feeaf2167247c8206937ef6c6ae9533025d416b7004ade0ea601d", size = 8633475, upload-time = "2025-11-19T11:53:00.389Z" }, - { url = "https://files.pythonhosted.org/packages/53/40/96cd959ad1dda6c12301860a74afece200a3209d84b393beedd5d7d915c0/maturin-1.10.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:04df81ee295dcda37828bd025a4ac688ea856e3946e4cb300a8f44a448de0069", size = 11177118, upload-time = "2025-11-19T11:53:03.014Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b6/144f180f36314be183f5237011528f0e39fe5fd2e74e65c3b44a5795971e/maturin-1.10.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96e1d391e4c1fa87edf2a37e4d53d5f2e5f39dd880b9d8306ac9f8eb212d23f8", size = 9320218, upload-time = "2025-11-19T11:53:05.39Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2d/2c483c1b3118e2e10fd8219d5291843f5f7c12284113251bf506144a3ac1/maturin-1.10.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a217aa7c42aa332fb8e8377eb07314e1f02cf0fe036f614aca4575121952addd", size = 8985266, upload-time = "2025-11-19T11:53:07.618Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/1d0222521e112cd058b56e8d96c72cf9615f799e3b557adb4b16004f42aa/maturin-1.10.2-py3-none-win32.whl", hash = "sha256:da031771d9fb6ddb1d373638ec2556feee29e4507365cd5749a2d354bcadd818", size = 7667897, upload-time = "2025-11-19T11:53:10.14Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ec/c6c973b1def0d04533620b439d5d7aebb257657ba66710885394514c8045/maturin-1.10.2-py3-none-win_amd64.whl", hash = "sha256:da777766fd584440dc9fecd30059a94f85e4983f58b09e438ae38ee4b494024c", size = 8908416, upload-time = "2025-11-19T11:53:12.862Z" }, - { url = "https://files.pythonhosted.org/packages/1b/01/7da60c9f7d5dc92dfa5e8888239fd0fb2613ee19e44e6db5c2ed5595fab3/maturin-1.10.2-py3-none-win_arm64.whl", hash = "sha256:a4c29a770ea2c76082e0afc6d4efd8ee94405588bfae00d10828f72e206c739b", size = 7506680, upload-time = "2025-11-19T11:53:15.403Z" }, -] - -[[package]] -name = "mcap" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "lz4" }, - { name = "zstandard" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/38/8bd73953b9c37dd7a2c590e72ab4fa4682a43864fe1d55f7209f2536fa64/mcap-1.3.1.tar.gz", hash = "sha256:2878879a786021aa7f7f36319276396a778717ccd013b2191fe94d37572d7551", size = 21676, upload-time = "2025-12-24T21:31:35.476Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/22/dad47e86047344f110a9d589d90de49d9c54db7a7ea4f0ef91fb3e8e9f3f/mcap-1.3.1-py3-none-any.whl", hash = "sha256:9098685d67288a8087166504cf4adf617cfa8639bb60e936af113f62c11c293f", size = 20678, upload-time = "2025-12-24T21:31:33.959Z" }, -] - -[[package]] -name = "mcap-protobuf-support" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mcap" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/8e/98af824adce25082f1c7fb8e8aab48b1678a370f97633a55bfb06c40e3fd/mcap_protobuf_support-0.5.4.tar.gz", hash = "sha256:3af2de2c2dbda9d1dbb1a526d2ad0cf3604ab44947dd1c99a52a2f990b1eb308", size = 7059, upload-time = "2025-12-24T21:22:57.716Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/ee/cb4beace2bbaf3877848dbc555366c5a72ec1dabba5d620bf746143d91c4/mcap_protobuf_support-0.5.4-py3-none-any.whl", hash = "sha256:072f83b7b147b5faa25df21c175cbf6e53bf959f8a6aea534167055bc4526649", size = 7286, upload-time = "2025-12-24T21:22:56.393Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, + { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, ] [[package]] @@ -2745,14 +2778,14 @@ requires-dist = [ [[package]] name = "mistune" -version = "3.1.4" +version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, ] [[package]] @@ -2864,43 +2897,47 @@ wheels = [ [[package]] name = "mkdocs-redirects" -version = "1.3.1" -source = { git = "https://github.com/rerun-io/mkdocs-redirects.git?rev=fb6b074554975ba7729d68d04957ce7c7dfd5003#fb6b074554975ba7729d68d04957ce7c7dfd5003" } +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162, upload-time = "2024-11-07T14:57:21.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142, upload-time = "2024-11-07T14:57:19.143Z" }, +] [[package]] name = "mkdocstrings" -version = "0.26.2" +version = "0.28.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, - { name = "platformdirs" }, + { name = "mkdocs-get-deps" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/76/0475d10d27f3384df3a6ddfdf4a4fdfef83766f77cd4e327d905dc956c15/mkdocstrings-0.26.2.tar.gz", hash = "sha256:34a8b50f1e6cfd29546c6c09fbe02154adfb0b361bb758834bf56aa284ba876e", size = 92512, upload-time = "2024-10-12T16:56:52.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/48/d134ffefd61349ac96161078d836c7e0c15062e01104327c9a5b23398a0f/mkdocstrings-0.28.3.tar.gz", hash = "sha256:c753516b1b6cee12d00bf9c28255e22c0d71f34c721ca668971fce885d846e0f", size = 104109, upload-time = "2025-03-08T21:43:21.088Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/b6/4ee320d7c313da3774eff225875eb278f7e6bb26a9cd8e680b8dbc38fdea/mkdocstrings-0.26.2-py3-none-any.whl", hash = "sha256:1248f3228464f3b8d1a15bd91249ce1701fe3104ac517a5f167a0e01ca850ba5", size = 29716, upload-time = "2024-10-12T16:56:49.746Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/205e4991fad1fbfe78b0d1fcfcf85f55556bcc93a5d6d94c7935e8463b87/mkdocstrings-0.28.3-py3-none-any.whl", hash = "sha256:df5351ffd10477aa3c2ff5cdf17544b936477195436923660274d084a5c1359c", size = 35177, upload-time = "2025-03-08T21:43:19.355Z" }, ] [[package]] name = "mkdocstrings-python" -version = "1.12.1" +version = "1.16.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/19/7b186a49a957611270d6c4fc156face8748cf98680a40c00b5b0b7008fe1/mkdocstrings_python-1.12.1.tar.gz", hash = "sha256:60d6a5ca912c9af4ad431db6d0111ce9f79c6c48d33377dde6a05a8f5f48d792", size = 168014, upload-time = "2024-10-14T11:34:47.004Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/e8/3cf3467fb8e31f68bfc8a2bfd5f4891c1eaa584b0c62b76c783d24d1901d/mkdocstrings_python-1.12.1-py3-none-any.whl", hash = "sha256:205244488199c9aa2a39787ad6a0c862d39b74078ea9aa2be817bc972399563f", size = 111657, upload-time = "2024-10-14T11:34:44.477Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, ] [[package]] @@ -3151,7 +3188,7 @@ wheels = [ [[package]] name = "notebook" -version = "7.5.0" +version = "7.5.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, @@ -3160,9 +3197,9 @@ dependencies = [ { name = "notebook-shim" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/ac/a97041621250a4fc5af379fb377942841eea2ca146aab166b8fcdfba96c2/notebook-7.5.0.tar.gz", hash = "sha256:3b27eaf9913033c28dde92d02139414c608992e1df4b969c843219acf2ff95e4", size = 14052074, upload-time = "2025-11-19T08:36:20.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/c2/cf59bd2e6f2c8b976b52477e3e53bf6f97bc714ed046a51821afb428eaee/notebook-7.5.6.tar.gz", hash = "sha256:621174aade80108f0020b0f00738000b215f75fa3cd90771ad7aa0f24536a4e1", size = 14170814, upload-time = "2026-04-30T11:46:26.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/96/00df2a4760f10f5af0f45c4955573cae6189931f9a30265a35865f8c1031/notebook-7.5.0-py3-none-any.whl", hash = "sha256:3300262d52905ca271bd50b22617681d95f08a8360d099e097726e6d2efb5811", size = 14460968, upload-time = "2025-11-19T08:36:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl", hash = "sha256:4dde3f8fb55fa8fb7946d58c6e869ce9baf46d00fc070664f62604569d0faca0", size = 14581730, upload-time = "2026-04-30T11:46:22.342Z" }, ] [[package]] @@ -3336,136 +3373,152 @@ requires-dist = [ ] [[package]] -name = "nvidia-cublas-cu12" -version = "12.6.4.1" +name = "nvidia-cublas" +version = "13.1.0.3" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb", size = 393138322, upload-time = "2024-11-20T17:40:25.65Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, ] [[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.6.80" +name = "nvidia-cuda-cupti" +version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132", size = 8917980, upload-time = "2024-11-20T17:36:04.019Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/120ee57b218d9952c379d1e026c4479c9ece9997a4fb46303611ee48f038/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73", size = 8917972, upload-time = "2024-10-01T16:58:06.036Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, ] [[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.6.77" +name = "nvidia-cuda-nvrtc" +version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/2e/46030320b5a80661e88039f59060d1790298b4718944a65a7f2aeda3d9e9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53", size = 23650380, upload-time = "2024-10-01T17:00:14.643Z" }, + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, ] [[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.6.77" +name = "nvidia-cuda-runtime" +version = "13.0.96" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/23/e717c5ac26d26cf39a27fbc076240fad2e3b817e5889d671b67f4f9f49c5/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7", size = 897690, upload-time = "2024-11-20T17:35:30.697Z" }, - { url = "https://files.pythonhosted.org/packages/f0/62/65c05e161eeddbafeca24dc461f47de550d9fa8a7e04eb213e32b55cfd99/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8", size = 897678, upload-time = "2024-10-01T16:57:33.821Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, ] [[package]] -name = "nvidia-cudnn-cu12" -version = "9.5.1.17" +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386, upload-time = "2024-10-25T19:54:26.39Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, ] [[package]] -name = "nvidia-cufft-cu12" -version = "11.3.0.4" +name = "nvidia-cufft" +version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632, upload-time = "2024-11-20T17:41:32.357Z" }, - { url = "https://files.pythonhosted.org/packages/60/de/99ec247a07ea40c969d904fc14f3a356b3e2a704121675b75c366b694ee1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca", size = 200221622, upload-time = "2024-10-01T17:03:58.79Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, ] [[package]] -name = "nvidia-cufile-cu12" -version = "1.11.1.6" +name = "nvidia-cufile" +version = "1.15.1.6" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/66/cc9876340ac68ae71b15c743ddb13f8b30d5244af344ec8322b449e35426/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159", size = 1142103, upload-time = "2024-11-20T17:42:11.83Z" }, + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, ] [[package]] -name = "nvidia-curand-cu12" -version = "10.3.7.77" +name = "nvidia-curand" +version = "10.4.0.35" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/1b/44a01c4e70933637c93e6e1a8063d1e998b50213a6b65ac5a9169c47e98e/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf", size = 56279010, upload-time = "2024-11-20T17:42:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/4a/aa/2c7ff0b5ee02eaef890c0ce7d4f74bc30901871c5e45dee1ae6d0083cd80/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117", size = 56279000, upload-time = "2024-10-01T17:04:45.274Z" }, + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, ] [[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.1.2" +name = "nvidia-cusolver" +version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790, upload-time = "2024-11-20T17:43:43.211Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/baba53585da791d043c10084cf9553e074548408e04ae884cfe9193bd484/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6", size = 158229780, upload-time = "2024-10-01T17:05:39.875Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, ] [[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.4.2" +name = "nvidia-cusparse" +version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367, upload-time = "2024-11-20T17:44:54.824Z" }, - { url = "https://files.pythonhosted.org/packages/43/ac/64c4316ba163e8217a99680c7605f779accffc6a4bcd0c778c12948d3707/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f", size = 216561357, upload-time = "2024-10-01T17:06:29.861Z" }, + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, ] [[package]] -name = "nvidia-cusparselt-cu12" -version = "0.6.3" +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/9a/72ef35b399b0e183bc2e8f6f558036922d453c4d8237dab26c666a04244b/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46", size = 156785796, upload-time = "2024-10-15T21:29:17.709Z" }, + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, ] [[package]] -name = "nvidia-nccl-cu12" -version = "2.26.2" +name = "nvidia-nvjitlink" +version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/ca/f42388aed0fddd64ade7493dbba36e1f534d4e6fdbdd355c6a90030ae028/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6", size = 201319755, upload-time = "2025-03-13T00:29:55.296Z" }, + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, ] [[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.6.85" +name = "nvidia-nvshmem-cu13" +version = "3.4.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971, upload-time = "2024-11-20T17:46:53.366Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, ] [[package]] -name = "nvidia-nvtx-cu12" -version = "12.6.77" +name = "nvidia-nvtx" +version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/9a/fff8376f8e3d084cd1530e1ef7b879bb7d6d265620c95c1b322725c694f4/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2", size = 89276, upload-time = "2024-11-20T17:38:27.621Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265, upload-time = "2024-10-01T17:00:38.172Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, ] [[package]] @@ -3954,24 +4007,24 @@ wheels = [ [[package]] name = "pyarrow" -version = "22.0.0" +version = "23.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" }, - { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" }, - { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" }, - { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" }, - { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" }, - { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, - { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, - { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, - { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a8/24e5dc6855f50a62936ceb004e6e9645e4219a8065f304145d7fb8a79d5d/pyarrow-23.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:3fab8f82571844eb3c460f90a75583801d14ca0cc32b1acc8c361650e006fd56", size = 34307390, upload-time = "2026-02-16T10:08:08.654Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8e/4be5617b4aaae0287f621ad31c6036e5f63118cfca0dc57d42121ff49b51/pyarrow-23.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:3f91c038b95f71ddfc865f11d5876c42f343b4495535bd262c7b321b0b94507c", size = 35853761, upload-time = "2026-02-16T10:08:17.811Z" }, + { url = "https://files.pythonhosted.org/packages/2e/08/3e56a18819462210432ae37d10f5c8eed3828be1d6c751b6e6a2e93c286a/pyarrow-23.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d0744403adabef53c985a7f8a082b502a368510c40d184df349a0a8754533258", size = 44493116, upload-time = "2026-02-16T10:08:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/f8/82/c40b68001dbec8a3faa4c08cd8c200798ac732d2854537c5449dc859f55a/pyarrow-23.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c33b5bf406284fd0bba436ed6f6c3ebe8e311722b441d89397c54f871c6863a2", size = 47564532, upload-time = "2026-02-16T10:08:34.27Z" }, + { url = "https://files.pythonhosted.org/packages/20/bc/73f611989116b6f53347581b02177f9f620efdf3cd3f405d0e83cdf53a83/pyarrow-23.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ddf743e82f69dcd6dbbcb63628895d7161e04e56794ef80550ac6f3315eeb1d5", size = 48183685, upload-time = "2026-02-16T10:08:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/6c6b3ecdae2a8c3aced99956187e8302fc954cc2cca2a37cf2111dad16ce/pyarrow-23.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e052a211c5ac9848ae15d5ec875ed0943c0221e2fcfe69eee80b604b4e703222", size = 50605582, upload-time = "2026-02-16T10:08:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/8d/94/d359e708672878d7638a04a0448edf7c707f9e5606cee11e15aaa5c7535a/pyarrow-23.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5abde149bb3ce524782d838eb67ac095cd3fd6090eba051130589793f1a7f76d", size = 27521148, upload-time = "2026-02-16T10:08:58.077Z" }, + { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, ] [[package]] @@ -4159,11 +4212,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] @@ -4185,40 +4238,38 @@ crypto = [ [[package]] name = "pymdown-extensions" -version = "10.19" +version = "10.21.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/4e/e73e88f4f2d0b26cbd2e100074107470984f0a6055869805fc181b847ac7/pymdown_extensions-10.19.tar.gz", hash = "sha256:01bb917ea231f9ce14456fa9092cdb95ac3e5bd32202a3ee61dbd5ad2dd9ef9b", size = 847701, upload-time = "2025-12-11T18:20:46.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/56/fa9edaceb3805e03ac9faf68ca1ddc660a75b49aee5accb493511005fef5/pymdown_extensions-10.19-py3-none-any.whl", hash = "sha256:dc5f249fc3a1b6d8a6de4634ba8336b88d0942cee75e92b18ac79eaf3503bf7c", size = 266670, upload-time = "2025-12-11T18:20:44.736Z" }, + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, ] [[package]] name = "pynacl" -version = "1.6.1" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/46/aeca065d227e2265125aea590c9c47fbf5786128c9400ee0eb7c88931f06/pynacl-1.6.1.tar.gz", hash = "sha256:8d361dac0309f2b6ad33b349a56cd163c98430d409fa503b10b70b3ad66eaa1d", size = 3506616, upload-time = "2025-11-10T16:02:13.195Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/41/3cfb3b4f3519f6ff62bf71bf1722547644bcfb1b05b8fdbdc300249ba113/pynacl-1.6.1-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:a6f9fd6d6639b1e81115c7f8ff16b8dedba1e8098d2756275d63d208b0e32021", size = 387591, upload-time = "2025-11-10T16:01:49.1Z" }, - { url = "https://files.pythonhosted.org/packages/18/21/b8a6563637799f617a3960f659513eccb3fcc655d5fc2be6e9dc6416826f/pynacl-1.6.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e49a3f3d0da9f79c1bec2aa013261ab9fa651c7da045d376bd306cf7c1792993", size = 798866, upload-time = "2025-11-10T16:01:55.688Z" }, - { url = "https://files.pythonhosted.org/packages/e8/6c/dc38033bc3ea461e05ae8f15a81e0e67ab9a01861d352ae971c99de23e7c/pynacl-1.6.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7713f8977b5d25f54a811ec9efa2738ac592e846dd6e8a4d3f7578346a841078", size = 1398001, upload-time = "2025-11-10T16:01:57.101Z" }, - { url = "https://files.pythonhosted.org/packages/9f/05/3ec0796a9917100a62c5073b20c4bce7bf0fea49e99b7906d1699cc7b61b/pynacl-1.6.1-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a3becafc1ee2e5ea7f9abc642f56b82dcf5be69b961e782a96ea52b55d8a9fc", size = 834024, upload-time = "2025-11-10T16:01:50.228Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b7/ae9982be0f344f58d9c64a1c25d1f0125c79201634efe3c87305ac7cb3e3/pynacl-1.6.1-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ce50d19f1566c391fedc8dc2f2f5be265ae214112ebe55315e41d1f36a7f0a9", size = 1436766, upload-time = "2025-11-10T16:01:51.886Z" }, - { url = "https://files.pythonhosted.org/packages/b4/51/b2ccbf89cf3025a02e044dd68a365cad593ebf70f532299f2c047d2b7714/pynacl-1.6.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:543f869140f67d42b9b8d47f922552d7a967e6c116aad028c9bfc5f3f3b3a7b7", size = 817275, upload-time = "2025-11-10T16:01:53.351Z" }, - { url = "https://files.pythonhosted.org/packages/a8/6c/dd9ee8214edf63ac563b08a9b30f98d116942b621d39a751ac3256694536/pynacl-1.6.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a2bb472458c7ca959aeeff8401b8efef329b0fc44a89d3775cffe8fad3398ad8", size = 1401891, upload-time = "2025-11-10T16:01:54.587Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c1/97d3e1c83772d78ee1db3053fd674bc6c524afbace2bfe8d419fd55d7ed1/pynacl-1.6.1-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3206fa98737fdc66d59b8782cecc3d37d30aeec4593d1c8c145825a345bba0f0", size = 772291, upload-time = "2025-11-10T16:01:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4d/ca/691ff2fe12f3bb3e43e8e8df4b806f6384593d427f635104d337b8e00291/pynacl-1.6.1-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:53543b4f3d8acb344f75fd4d49f75e6572fce139f4bfb4815a9282296ff9f4c0", size = 1370839, upload-time = "2025-11-10T16:01:59.252Z" }, - { url = "https://files.pythonhosted.org/packages/30/27/06fe5389d30391fce006442246062cc35773c84fbcad0209fbbf5e173734/pynacl-1.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:319de653ef84c4f04e045eb250e6101d23132372b0a61a7acf91bac0fda8e58c", size = 791371, upload-time = "2025-11-10T16:02:01.075Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7a/e2bde8c9d39074a5aa046c7d7953401608d1f16f71e237f4bef3fb9d7e49/pynacl-1.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:262a8de6bba4aee8a66f5edf62c214b06647461c9b6b641f8cd0cb1e3b3196fe", size = 1363031, upload-time = "2025-11-10T16:02:02.656Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b6/63fd77264dae1087770a1bb414bc604470f58fbc21d83822fc9c76248076/pynacl-1.6.1-cp38-abi3-win32.whl", hash = "sha256:9fd1a4eb03caf8a2fe27b515a998d26923adb9ddb68db78e35ca2875a3830dde", size = 226585, upload-time = "2025-11-10T16:02:07.116Z" }, - { url = "https://files.pythonhosted.org/packages/12/c8/b419180f3fdb72ab4d45e1d88580761c267c7ca6eda9a20dcbcba254efe6/pynacl-1.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:a569a4069a7855f963940040f35e87d8bc084cb2d6347428d5ad20550a0a1a21", size = 238923, upload-time = "2025-11-10T16:02:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/35/76/c34426d532e4dce7ff36e4d92cb20f4cbbd94b619964b93d24e8f5b5510f/pynacl-1.6.1-cp38-abi3-win_arm64.whl", hash = "sha256:5953e8b8cfadb10889a6e7bd0f53041a745d1b3d30111386a1bb37af171e6daf", size = 183970, upload-time = "2025-11-10T16:02:05.786Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, ] [[package]] @@ -4376,7 +4427,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -4387,9 +4438,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -4417,6 +4468,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" }, +] + [[package]] name = "python-json-logger" version = "4.0.0" @@ -4661,7 +4725,7 @@ requires-dist = [{ name = "sitecustomize-entrypoints" }] [[package]] name = "rerun-notebook" -version = "0.32.0a1" +version = "0.35.0" source = { editable = "rerun_notebook" } dependencies = [ { name = "anywidget" }, @@ -4683,7 +4747,7 @@ test = [ requires-dist = [ { name = "anywidget" }, { name = "hatch", marker = "extra == 'dev'" }, - { name = "ipykernel", specifier = "<7.0.0" }, + { name = "ipykernel", specifier = "!=7.0.*" }, { name = "jupyter-ui-poll" }, { name = "jupyterlab", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0" }, @@ -4699,6 +4763,7 @@ dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, + { name = "psutil" }, { name = "pyarrow" }, { name = "typing-extensions" }, ] @@ -4709,6 +4774,10 @@ all = [ { name = "pandas" }, { name = "rerun-notebook" }, ] +catalog = [ + { name = "datafusion" }, + { name = "pandas" }, +] datafusion = [ { name = "datafusion" }, { name = "pandas" }, @@ -4751,10 +4820,11 @@ requires-dist = [ { name = "attrs", specifier = ">=23.1.0" }, { name = "av", marker = "extra == 'dataloader'" }, { name = "av", marker = "extra == 'tests'", specifier = ">=14.2.0" }, - { name = "datafusion", marker = "extra == 'all'", specifier = "==52.3.0" }, - { name = "datafusion", marker = "extra == 'datafusion'", specifier = "==52.3.0" }, - { name = "datafusion", marker = "extra == 'dataplatform'", specifier = "==52.3.0" }, - { name = "datafusion", marker = "extra == 'tests'", specifier = "==52.3.0" }, + { name = "datafusion", marker = "extra == 'all'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'catalog'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'datafusion'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'dataplatform'", specifier = "==53.0.0" }, + { name = "datafusion", marker = "extra == 'tests'", specifier = "==53.0.0" }, { name = "inline-snapshot", marker = "extra == 'tests'", specifier = "==0.31.1" }, { name = "numpy", specifier = ">=2" }, { name = "opencv-python", marker = "extra == 'tests'", specifier = ">4.6" }, @@ -4762,14 +4832,16 @@ requires-dist = [ { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "extra == 'tracing'" }, { name = "opentelemetry-sdk", marker = "extra == 'tracing'" }, { name = "pandas", marker = "extra == 'all'", specifier = ">=2" }, + { name = "pandas", marker = "extra == 'catalog'", specifier = ">=2" }, { name = "pandas", marker = "extra == 'datafusion'", specifier = ">=2" }, { name = "pandas", marker = "extra == 'dataplatform'", specifier = ">=2" }, { name = "pandas", marker = "extra == 'tests'", specifier = ">=2" }, { name = "pillow", specifier = ">=8.0.0" }, { name = "pillow", marker = "extra == 'dataloader'", specifier = ">=8.0.0" }, { name = "polars", marker = "extra == 'tests'", specifier = "==1.36.1" }, + { name = "psutil", specifier = ">=7.0" }, { name = "pyarrow", specifier = ">=18.0.0" }, - { name = "pytest", marker = "extra == 'tests'", specifier = "==8.4.2" }, + { name = "pytest", marker = "extra == 'tests'", specifier = "==9.0.3" }, { name = "rerun-notebook", marker = "extra == 'all'", editable = "rerun_notebook" }, { name = "rerun-notebook", marker = "extra == 'notebook'", editable = "rerun_notebook" }, { name = "semver", marker = "extra == 'tests'", specifier = ">=3.0,<3.1" }, @@ -4781,11 +4853,11 @@ requires-dist = [ { name = "torchvision", marker = "extra == 'tests'" }, { name = "typing-extensions", specifier = ">=4.5" }, ] -provides-extras = ["all", "datafusion", "dataloader", "dataplatform", "notebook", "tests", "tracing"] +provides-extras = ["all", "catalog", "datafusion", "dataloader", "dataplatform", "notebook", "tests", "tracing"] [[package]] name = "rerun-workspace" -version = "0.30.0a4" +version = "0.28.0a1+dev" source = { virtual = "." } [package.dev-dependencies] @@ -4827,7 +4899,7 @@ dev = [ { name = "tqdm" }, { name = "ty" }, { name = "types-colorama" }, - { name = "types-protobuf" }, + { name = "types-psutil" }, { name = "types-pyyaml" }, { name = "types-requests" }, { name = "types-tabulate" }, @@ -4836,6 +4908,7 @@ dev = [ ] docs = [ { name = "griffe" }, + { name = "griffe-public-redundant-aliases" }, { name = "griffe-warnings-deprecated" }, { name = "mkdocs" }, { name = "mkdocs-gen-files" }, @@ -4845,6 +4918,7 @@ docs = [ { name = "mkdocs-redirects" }, { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, + { name = "pygments" }, { name = "setuptools" }, { name = "sphobjinv" }, ] @@ -4886,6 +4960,7 @@ examples = [ { name = "polars" }, { name = "raw-mesh" }, { name = "rgbd" }, + { name = "robot-data-preprocessing" }, { name = "rrt-star" }, { name = "segment-anything" }, { name = "segment-anything-model" }, @@ -4899,9 +4974,8 @@ examples = [ ] snippets = [ { name = "av" }, - { name = "mcap-protobuf-support" }, { name = "pandas" }, - { name = "rerun-sdk" }, + { name = "rerun-sdk", extra = ["dataloader"] }, ] [package.metadata] @@ -4911,7 +4985,7 @@ dev = [ { name = "anywidget", specifier = ">=0.9" }, { name = "attrs", specifier = ">=23.1.0" }, { name = "colorama", specifier = ">=0.4" }, - { name = "datafusion", specifier = ">=50.0" }, + { name = "datafusion", specifier = ">=53.0" }, { name = "gitpython", specifier = ">=3.1" }, { name = "google-cloud-compute", specifier = ">=1.20.0" }, { name = "google-cloud-storage", specifier = ">=2.9.0" }, @@ -4921,7 +4995,7 @@ dev = [ { name = "jinja2", specifier = ">=3.1" }, { name = "jupyter", specifier = ">=1.0" }, { name = "jupyter-ui-poll", specifier = ">=0.2" }, - { name = "maturin", specifier = ">=1.0.0" }, + { name = "maturin", specifier = ">=1.14.1" }, { name = "mypy", specifier = "==1.19.1" }, { name = "nbqa", specifier = "==1.9.1" }, { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = "==1.39.0" }, @@ -4945,7 +5019,7 @@ dev = [ { name = "tqdm", specifier = ">=4.60" }, { name = "ty", specifier = "==0.0.31" }, { name = "types-colorama", specifier = ">=0.4.15" }, - { name = "types-protobuf" }, + { name = "types-psutil", specifier = ">=7.0" }, { name = "types-pyyaml", specifier = ">=6.0" }, { name = "types-requests", specifier = "==2.32.4.20250913" }, { name = "types-tabulate", specifier = ">=0.9.0" }, @@ -4953,16 +5027,18 @@ dev = [ { name = "wheel", specifier = ">=0.46" }, ] docs = [ - { name = "griffe", specifier = "==1.4.1" }, + { name = "griffe", specifier = ">=1.14,<2" }, + { name = "griffe-public-redundant-aliases", specifier = "==0.3.0" }, { name = "griffe-warnings-deprecated", specifier = "==1.1.0" }, { name = "mkdocs", specifier = "==1.6.1" }, { name = "mkdocs-gen-files", specifier = "==0.5.0" }, { name = "mkdocs-literate-nav", specifier = "==0.6.1" }, { name = "mkdocs-material", specifier = "==9.4.7" }, { name = "mkdocs-material-extensions", specifier = "==1.3" }, - { name = "mkdocs-redirects", git = "https://github.com/rerun-io/mkdocs-redirects.git?rev=fb6b074554975ba7729d68d04957ce7c7dfd5003" }, - { name = "mkdocstrings", specifier = "==0.26.2" }, - { name = "mkdocstrings-python", specifier = "==1.12.1" }, + { name = "mkdocs-redirects", specifier = "==1.2.2" }, + { name = "mkdocstrings", specifier = ">=0.28.2,<0.29" }, + { name = "mkdocstrings-python", specifier = ">=1.16.2,<2" }, + { name = "pygments" }, { name = "setuptools", specifier = ">75" }, { name = "sphobjinv", specifier = "==2.3.1" }, ] @@ -5004,6 +5080,7 @@ examples = [ { name = "polars", specifier = ">=0.12.0" }, { name = "raw-mesh", editable = "examples/python/raw_mesh" }, { name = "rgbd", editable = "examples/python/rgbd" }, + { name = "robot-data-preprocessing", editable = "examples/python/robot_data_preprocessing" }, { name = "rrt-star", editable = "examples/python/rrt_star" }, { name = "segment-anything", git = "https://github.com/facebookresearch/segment-anything.git" }, { name = "segment-anything-model", editable = "examples/python/segment_anything_model" }, @@ -5017,9 +5094,8 @@ examples = [ ] snippets = [ { name = "av" }, - { name = "mcap-protobuf-support" }, { name = "pandas" }, - { name = "rerun-sdk", editable = "rerun_py" }, + { name = "rerun-sdk", extras = ["dataloader"], editable = "rerun_py" }, ] [[package]] @@ -5090,6 +5166,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] +[[package]] +name = "robot-data-preprocessing" +version = "0.1.0" +source = { editable = "examples/python/robot_data_preprocessing" } +dependencies = [ + { name = "pyarrow" }, + { name = "rerun-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyarrow" }, + { name = "rerun-sdk", editable = "rerun_py" }, +] + [[package]] name = "rpds-py" version = "0.30.0" @@ -5194,28 +5285,26 @@ wheels = [ [[package]] name = "safetensors" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, - { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, - { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, - { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, - { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, - { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, - { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, ] [[package]] @@ -5411,7 +5500,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "datafusion", specifier = "==52.3.0" }, + { name = "datafusion", specifier = "==53.0.0" }, { name = "rerun-sdk", editable = "rerun_py" }, ] @@ -5536,6 +5625,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "state-timeline" +version = "0.1.0" +source = { editable = "examples/python/state_timeline" } +dependencies = [ + { name = "rerun-sdk" }, +] + +[package.metadata] +requires-dist = [{ name = "rerun-sdk", editable = "rerun_py" }] + [[package]] name = "stdio" version = "0.1.0" @@ -5599,6 +5699,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/9a/6c68aad2ccfce6e2eeebbf5bb709d0240592eb51ff142ec4c8fbf3c2460a/syrupy-5.0.0-py3-none-any.whl", hash = "sha256:c848e1a980ca52a28715cd2d2b4d434db424699c05653bd1158fb31cf56e9546", size = 49087, upload-time = "2025-09-28T21:15:11.639Z" }, ] +[[package]] +name = "table-blueprints" +version = "0.1.0" +source = { editable = "examples/python/table_blueprints" } +dependencies = [ + { name = "pyarrow" }, + { name = "rerun-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyarrow" }, + { name = "rerun-sdk", editable = "rerun_py" }, +] + +[[package]] +name = "table-grid-with-flags" +version = "0.1.0" +source = { editable = "examples/python/table_grid_with_flags" } +dependencies = [ + { name = "pyarrow" }, + { name = "rerun-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyarrow" }, + { name = "rerun-sdk", editable = "rerun_py" }, +] + [[package]] name = "table-zoo" version = "0.1.0" @@ -5761,46 +5891,39 @@ wheels = [ [[package]] name = "torch" -version = "2.7.1" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, { name = "filelock" }, { name = "fsspec" }, { name = "jinja2" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/27/2e06cb52adf89fe6e020963529d17ed51532fc73c1e6d1b18420ef03338c/torch-2.7.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a103b5d782af5bd119b81dbcc7ffc6fa09904c423ff8db397a1e6ea8fd71508f", size = 99089441, upload-time = "2025-06-04T17:38:48.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/0a5b3aee977596459ec45be2220370fde8e017f651fecc40522fd478cb1e/torch-2.7.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:fe955951bdf32d182ee8ead6c3186ad54781492bf03d547d31771a01b3d6fb7d", size = 821154516, upload-time = "2025-06-04T17:36:28.556Z" }, - { url = "https://files.pythonhosted.org/packages/f9/91/3d709cfc5e15995fb3fe7a6b564ce42280d3a55676dad672205e94f34ac9/torch-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:885453d6fba67d9991132143bf7fa06b79b24352f4506fd4d10b309f53454162", size = 216093147, upload-time = "2025-06-04T17:39:38.132Z" }, - { url = "https://files.pythonhosted.org/packages/92/f6/5da3918414e07da9866ecb9330fe6ffdebe15cb9a4c5ada7d4b6e0a6654d/torch-2.7.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:d72acfdb86cee2a32c0ce0101606f3758f0d8bb5f8f31e7920dc2809e963aa7c", size = 68630914, upload-time = "2025-06-04T17:39:31.162Z" }, - { url = "https://files.pythonhosted.org/packages/11/56/2eae3494e3d375533034a8e8cf0ba163363e996d85f0629441fa9d9843fe/torch-2.7.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:236f501f2e383f1cb861337bdf057712182f910f10aeaf509065d54d339e49b2", size = 99093039, upload-time = "2025-06-04T17:39:06.963Z" }, - { url = "https://files.pythonhosted.org/packages/e5/94/34b80bd172d0072c9979708ccd279c2da2f55c3ef318eceec276ab9544a4/torch-2.7.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:06eea61f859436622e78dd0cdd51dbc8f8c6d76917a9cf0555a333f9eac31ec1", size = 821174704, upload-time = "2025-06-04T17:37:03.799Z" }, - { url = "https://files.pythonhosted.org/packages/50/9e/acf04ff375b0b49a45511c55d188bcea5c942da2aaf293096676110086d1/torch-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:8273145a2e0a3c6f9fd2ac36762d6ee89c26d430e612b95a99885df083b04e52", size = 216095937, upload-time = "2025-06-04T17:39:24.83Z" }, - { url = "https://files.pythonhosted.org/packages/5b/2b/d36d57c66ff031f93b4fa432e86802f84991477e522adcdffd314454326b/torch-2.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:aea4fc1bf433d12843eb2c6b2204861f43d8364597697074c8d38ae2507f8730", size = 68640034, upload-time = "2025-06-04T17:39:17.989Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f2/c1690994afe461aae2d0cac62251e6802a703dec0a6c549c02ecd0de92a9/torch-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e", size = 80526521, upload-time = "2026-03-23T18:12:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f0/98ae802fa8c09d3149b0c8690741f3f5753c90e779bd28c9613257295945/torch-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18", size = 419723025, upload-time = "2026-03-23T18:11:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/18a9b10b4bd34f12d4e561c52b0ae7158707b8193c6cfc0aad2b48167090/torch-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5", size = 530589207, upload-time = "2026-03-23T18:11:23.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/40/2d532e8c0e23705be9d1debce5bc37b68d59a39bda7584c26fe9668076fe/torch-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f", size = 114518313, upload-time = "2026-03-23T18:11:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, ] [[package]] name = "torchvision" -version = "0.22.1" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -5809,14 +5932,14 @@ dependencies = [ { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/15/2c/7b67117b14c6cc84ae3126ca6981abfa3af2ac54eb5252b80d9475fb40df/torchvision-0.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b47d8369ee568c067795c0da0b4078f39a9dfea6f3bc1f3ac87530dfda1dd56", size = 1947825, upload-time = "2025-06-04T17:43:15.523Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9f/c4dcf1d232b75e28bc37e21209ab2458d6d60235e16163544ed693de54cb/torchvision-0.22.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:990de4d657a41ed71680cd8be2e98ebcab55371f30993dc9bd2e676441f7180e", size = 2512611, upload-time = "2025-06-04T17:43:03.951Z" }, - { url = "https://files.pythonhosted.org/packages/e2/99/db71d62d12628111d59147095527a0ab492bdfecfba718d174c04ae6c505/torchvision-0.22.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3347f690c2eed6d02aa0edfb9b01d321e7f7cf1051992d96d8d196c39b881d49", size = 7485668, upload-time = "2025-06-04T17:43:09.453Z" }, - { url = "https://files.pythonhosted.org/packages/32/ff/4a93a4623c3e5f97e8552af0f9f81d289dcf7f2ac71f1493f1c93a6b973d/torchvision-0.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:86ad938f5a6ca645f0d5fb19484b1762492c2188c0ffb05c602e9e9945b7b371", size = 1707961, upload-time = "2025-06-04T17:43:13.038Z" }, - { url = "https://files.pythonhosted.org/packages/f6/00/bdab236ef19da050290abc2b5203ff9945c84a1f2c7aab73e8e9c8c85669/torchvision-0.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4addf626e2b57fc22fd6d329cf1346d474497672e6af8383b7b5b636fba94a53", size = 1947827, upload-time = "2025-06-04T17:43:10.84Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d0/18f951b2be3cfe48c0027b349dcc6fde950e3dc95dd83e037e86f284f6fd/torchvision-0.22.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:8b4a53a6067d63adba0c52f2b8dd2290db649d642021674ee43c0c922f0c6a69", size = 2514021, upload-time = "2025-06-04T17:43:07.608Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1a/63eb241598b36d37a0221e10af357da34bd33402ccf5c0765e389642218a/torchvision-0.22.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b7866a3b326413e67724ac46f1ee594996735e10521ba9e6cdbe0fa3cd98c2f2", size = 7487300, upload-time = "2025-06-04T17:42:58.349Z" }, - { url = "https://files.pythonhosted.org/packages/e5/73/1b009b42fe4a7774ba19c23c26bb0f020d68525c417a348b166f1c56044f/torchvision-0.22.1-cp311-cp311-win_amd64.whl", hash = "sha256:bb3f6df6f8fd415ce38ec4fd338376ad40c62e86052d7fc706a0dd51efac1718", size = 1707989, upload-time = "2025-06-04T17:43:14.332Z" }, + { url = "https://files.pythonhosted.org/packages/74/b4/cdfee31e0402ea035135462cb0ab496e974d56fab6b4e7a1f0cbccb8cd28/torchvision-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a06d4772a8e13e772906ed736cc53ec6639e5e60554f8e5fa6ca165aabebc464", size = 1863503, upload-time = "2026-03-23T18:13:01.384Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/11fee109841e80ad14e5ca2d80bff6b10eb11b7838ff06f35bfeaa9f7251/torchvision-0.26.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2adfbe438473236191ff077a4a9a0c767436879c89628aa97137e959b0c11a94", size = 7766423, upload-time = "2026-03-23T18:12:56.049Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/24d8c7845c3f270153fb81395a5135b2778e2538e81d14c6aea5106c689c/torchvision-0.26.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b6f9ad1ecc0eab52647298b379ee9426845f8903703e6127973f8f3d049a798b", size = 7518249, upload-time = "2026-03-23T18:12:51.743Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/e53cd7c0da7ae002e5e929c1796ebbe7ec0c700c29f7a0a6696497fb3d8b/torchvision-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:f13f12b3791a266de2d599cb8162925261622a037d87fc03132848343cf68f75", size = 3669784, upload-time = "2026-03-23T18:12:49.949Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/d552a2521bade3295b2c6e7a4a0d1022261cab7ca7011f4e2a330dbb3caa/torchvision-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bd6ad4ae77be01ba67a410b05b51f53b0d0ee45f146eb6a0dfb9007e70ab3c", size = 1863499, upload-time = "2026-03-23T18:12:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/33/bf/21b899792b08cae7a298551c68398a79e333697479ed311b3b067aab4bdc/torchvision-0.26.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1c55dc8affbcc0eb2060fbabbe996ae9e5839b24bb6419777f17848945a411b1", size = 7767527, upload-time = "2026-03-23T18:12:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/57bbf9e216850d065e66dd31a50f57424b607f1d878ab8956e56a1f4e36b/torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd10b5f994c210f4f6d6761cf686f82d748554adf486cb0979770c3252868c8f", size = 7519925, upload-time = "2026-03-23T18:12:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/10/58/ed8f7754299f3e91d6414b6dc09f62b3fa7c6e5d63dfe48d69ab81498a37/torchvision-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:de6424b12887ad884f39a0ee446994ae3cd3b6a00a9cafe1bead85a031132af0", size = 3983834, upload-time = "2026-03-23T18:13:00.224Z" }, ] [[package]] @@ -5859,7 +5982,7 @@ wheels = [ [[package]] name = "transformers" -version = "4.57.3" +version = "4.57.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -5874,9 +5997,9 @@ dependencies = [ { name = "tokenizers" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/70/d42a739e8dfde3d92bb2fff5819cbf331fe9657323221e79415cd5eb65ee/transformers-4.57.3.tar.gz", hash = "sha256:df4945029aaddd7c09eec5cad851f30662f8bd1746721b34cc031d70c65afebc", size = 10139680, upload-time = "2025-11-25T15:51:30.139Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/6b/2f416568b3c4c91c96e5a365d164f8a4a4a88030aa8ab4644181fdadce97/transformers-4.57.3-py3-none-any.whl", hash = "sha256:c77d353a4851b1880191603d36acb313411d3577f6e2897814f333841f7003f4", size = 11993463, upload-time = "2025-11-25T15:51:26.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, ] [[package]] @@ -5894,14 +6017,13 @@ wheels = [ [[package]] name = "triton" -version = "3.3.1" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, -] wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/a9/549e51e9b1b2c9b854fd761a1d23df0ba2fbc60bd0c13b489ffa518cfcb7/triton-3.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e", size = 155600257, upload-time = "2025-05-29T23:39:36.085Z" }, - { url = "https://files.pythonhosted.org/packages/21/2f/3e56ea7b58f80ff68899b1dbe810ff257c9d177d288c6b0f55bf2fe4eb50/triton-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b", size = 155689937, upload-time = "2025-05-29T23:39:44.182Z" }, + { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, ] [[package]] @@ -5947,12 +6069,12 @@ wheels = [ ] [[package]] -name = "types-protobuf" -version = "6.32.1.20251210" +name = "types-psutil" +version = "7.2.2.20260408" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/14/279fd5defebbd560ede04aecd38f7651cccee7336f2264d0889d8c9a9d43/types_psutil-7.2.2.20260408.tar.gz", hash = "sha256:e8053450685965b8cd52afb62569073d00ea9967ae78bb45dff5f606847f97f2", size = 26556, upload-time = "2026-04-08T04:27:44.349Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" }, + { url = "https://files.pythonhosted.org/packages/af/40/2fd92a4a1ee088c4dbcc44c977908d9869838d9cd2a2fa2e001352f56694/types_psutil-7.2.2.20260408-py3-none-any.whl", hash = "sha256:0c334f6f6bc9e9c24fca5c7d1f0b6971c961a0a2e3956dc5ce704722c01f9762", size = 32861, upload-time = "2026-04-08T04:27:42.929Z" }, ] [[package]] @@ -6080,11 +6202,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] @@ -6112,43 +6234,44 @@ requires-dist = [{ name = "rerun-sdk", editable = "rerun_py" }] [[package]] name = "uv" -version = "0.9.17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/1a/cb0c37ae8513b253bcbc13d42392feb7d95ea696eb398b37535a28df9040/uv-0.9.17.tar.gz", hash = "sha256:6d93ab9012673e82039cfa7f9f66f69b388bc3f910f9e8a2ebee211353f620aa", size = 3815957, upload-time = "2025-12-09T23:01:21.756Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/e2/b6e2d473bdc37f4d86307151b53c0776e9925de7376ce297e92eab2e8894/uv-0.9.17-py3-none-linux_armv6l.whl", hash = "sha256:c708e6560ae5bc3cda1ba93f0094148ce773b6764240ced433acf88879e57a67", size = 21254511, upload-time = "2025-12-09T23:00:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:233b3d90f104c59d602abf434898057876b87f64df67a37129877d6dab6e5e10", size = 20384366, upload-time = "2025-12-09T23:01:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4b8e5513d48a267bfa180ca7fefaf6f27b1267e191573b3dba059981143e88ef", size = 18924624, upload-time = "2025-12-09T23:01:10.291Z" }, - { url = "https://files.pythonhosted.org/packages/21/56/9daf8bbe4a9a36eb0b9257cf5e1e20f9433d0ce996778ccf1929cbe071a4/uv-0.9.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8f283488bbcf19754910cc1ae7349c567918d6367c596e5a75d4751e0080eee0", size = 20671687, upload-time = "2025-12-09T23:00:51.927Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c8/4050ff7dc692770092042fcef57223b8852662544f5981a7f6cac8fc488d/uv-0.9.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9cf8052ba669dc17bdba75dae655094d820f4044990ea95c01ec9688c182f1da", size = 20861866, upload-time = "2025-12-09T23:01:12.555Z" }, - { url = "https://files.pythonhosted.org/packages/84/d4/208e62b7db7a65cb3390a11604c59937e387d07ed9f8b63b54edb55e2292/uv-0.9.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06749461b11175a884be193120044e7f632a55e2624d9203398808907d346aad", size = 21858420, upload-time = "2025-12-09T23:01:00.009Z" }, - { url = "https://files.pythonhosted.org/packages/86/2c/91288cd5a04db37dfc1e0dad26ead84787db5832d9836b4cc8e0fa7f3c53/uv-0.9.17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:35eb1a519688209160e48e1bb8032d36d285948a13b4dd21afe7ec36dc2a9787", size = 23471658, upload-time = "2025-12-09T23:00:49.503Z" }, - { url = "https://files.pythonhosted.org/packages/44/ba/493eba650ffad1df9e04fd8eabfc2d0aebc23e8f378acaaee9d95ca43518/uv-0.9.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bfb60a533e82690ab17dfe619ff7f294d053415645800d38d13062170230714", size = 23062950, upload-time = "2025-12-09T23:00:39.055Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9e/f7f679503c06843ba59451e3193f35fb7c782ff0afc697020d4718a7de46/uv-0.9.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd0f3e380ff148aff3d769e95a9743cb29c7f040d7ef2896cafe8063279a6bc1", size = 22080299, upload-time = "2025-12-09T23:00:44.026Z" }, - { url = "https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2c3d25fbd8f91b30d0fac69a13b8e2c2cd8e606d7e6e924c1423e4ff84e616", size = 22087554, upload-time = "2025-12-09T23:00:41.715Z" }, - { url = "https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:330e7085857e4205c5196a417aca81cfbfa936a97dd2a0871f6560a88424ebf2", size = 20823225, upload-time = "2025-12-09T23:00:57.041Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/e0f816cacd802a1cb25e71de9d60e57fa1f6c659eb5599cef708668618cc/uv-0.9.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:45880faa9f6cf91e3cda4e5f947da6a1004238fdc0ed4ebc18783a12ce197312", size = 22004893, upload-time = "2025-12-09T23:01:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/15/6b/700f6256ee191136eb06e40d16970a4fc687efdccf5e67c553a258063019/uv-0.9.17-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8e775a1b94c6f248e22f0ce2f86ed37c24e10ae31fb98b7e1b9f9a3189d25991", size = 20853850, upload-time = "2025-12-09T23:01:02.694Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6a/13f02e2ed6510223c40f74804586b09e5151d9319f93aab1e49d91db13bb/uv-0.9.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8650c894401ec96488a6fd84a5b4675e09be102f5525c902a12ba1c8ef8ff230", size = 21322623, upload-time = "2025-12-09T23:00:46.806Z" }, - { url = "https://files.pythonhosted.org/packages/d0/18/2d19780cebfbec877ea645463410c17859f8070f79c1a34568b153d78e1d/uv-0.9.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:673066b72d8b6c86be0dae6d5f73926bcee8e4810f1690d7b8ce5429d919cde3", size = 22290123, upload-time = "2025-12-09T23:00:54.394Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/ab79bde3f7b6d2ac89f839ea40411a9cf3e67abede2278806305b6ba797e/uv-0.9.17-py3-none-win32.whl", hash = "sha256:7407d45afeae12399de048f7c8c2256546899c94bd7892dbddfae6766616f5a3", size = 20070709, upload-time = "2025-12-09T23:01:05.105Z" }, - { url = "https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl", hash = "sha256:22fcc26755abebdf366becc529b2872a831ce8bb14b36b6a80d443a1d7f84d3b", size = 22122852, upload-time = "2025-12-09T23:01:07.783Z" }, - { url = "https://files.pythonhosted.org/packages/37/ef/813cfedda3c8e49d8b59a41c14fcc652174facfd7a1caf9fee162b40ccbd/uv-0.9.17-py3-none-win_arm64.whl", hash = "sha256:6761076b27a763d0ede2f5e72455d2a46968ff334badf8312bb35988c5254831", size = 20435751, upload-time = "2025-12-09T23:01:19.732Z" }, +version = "0.11.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/8e/ec34c19d0f254fcbcc5c1ce8c7f06e47e0f69a7e1a0269c1d59cb0b0f279/uv-0.11.17.tar.gz", hash = "sha256:1d1be74deec997db1dda05a7e67541c904d65cbfd72e455d3c0a2a1e4bf2cddf", size = 4203607, upload-time = "2026-05-28T20:39:47.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/2e/e6d42f9d39009eee976f1e5dfd31d3d1943e6e593ad7b191cf11e9744a36/uv-0.11.17-py3-none-linux_armv6l.whl", hash = "sha256:8426bfe315564d414cbc5ba5467595dc6348965e19acec742914f47da3ff269f", size = 23551216, upload-time = "2026-05-28T20:39:05.395Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ee/d72bcc60f3585653a4b768425854d737d98d65c1765547d25c2999547ea9/uv-0.11.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6d1a033cc68cabb4141d6c1e3b66ffc6e970b98ba42e210f33270251e0bd8697", size = 22997377, upload-time = "2026-05-28T20:39:25.21Z" }, + { url = "https://files.pythonhosted.org/packages/58/34/1bc69798d9ae998fbc42c61b02883f2ba00d04bdd858e589604d01846287/uv-0.11.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58c07ffc272c847d29cd98ca5082fa4304a645f87c718ec900e3cca9026bd096", size = 21630197, upload-time = "2026-05-28T20:39:28.935Z" }, + { url = "https://files.pythonhosted.org/packages/6b/93/1be48ec6a8933d9a77d0ce5240ed63f68869f68517ccf5d62268ed03f3e8/uv-0.11.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:036d6e2940afe8b79637530b01b9241d8cfd174b07f1179a1ebbd42409c38ca3", size = 23414940, upload-time = "2026-05-28T20:39:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/00/31/b7488ff49d80090ea9d05d67a4d381a1b4479502e9853e654caa1c1c678e/uv-0.11.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:283186700c3e65a4644a73a917232da7d3e4a94d25ea0377a44f5b263fa49577", size = 23096330, upload-time = "2026-05-28T20:39:01.284Z" }, + { url = "https://files.pythonhosted.org/packages/fe/95/42b6137c5de06278d229c7eef2f314df2a738cd799795bbb44dace21bd6e/uv-0.11.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2e44dfbfc7778d0d90edc6738f237c91e5e37e4e3cfe94c8a312cec56a41485", size = 23101906, upload-time = "2026-05-28T20:39:17.149Z" }, + { url = "https://files.pythonhosted.org/packages/17/7c/0ca03b2d19965db6d5dfe0c8cf96a3d0b424503c8cbc3cd2ffdc5869a15d/uv-0.11.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a817eeb3026f27a53d3f4b7855a5105f6787dd192140e201eda4d2b9a11b72e", size = 24444409, upload-time = "2026-05-28T20:39:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fb/179f55a3b19d47c30ec1f41b9b964da74dfa7053ff310a70a9c4d8cb998d/uv-0.11.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf8f5ad959583dcd2c4ae445c754a97c05700246ff89259f3fd285c9c20f4c00", size = 25540153, upload-time = "2026-05-28T20:39:09.535Z" }, + { url = "https://files.pythonhosted.org/packages/f7/29/592f42012765c43ae45c112110e214bca7b0cfc08c4c1b52e1dfa47dedd5/uv-0.11.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce16892a45134d20165c1ceababe06f3e9ce6a58902db1eff812c8c93626823f", size = 24665906, upload-time = "2026-05-28T20:39:41.254Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/b75808766f895248553c6370968509cd4f726e6943e310a8f7a171036ad0/uv-0.11.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da839e5a491c9a701d7d327a199cafc76ac27a03ac84fd2a8d4bf32c3af2448", size = 24863325, upload-time = "2026-05-28T20:39:51.006Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6a/6f27ee69e97f480104bb8ec335f04c2a12add98edfcc4844a68e9538b6e2/uv-0.11.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ec004b3c9bf9cb7756067ad1bd0bf64eb843e6fa2edbfbb3135ee152c14cea91", size = 23521674, upload-time = "2026-05-28T20:38:55.869Z" }, + { url = "https://files.pythonhosted.org/packages/df/11/1344aca7c710f794750f74de0e552a54ab24193ecc01fa3b3ae22ff822a1/uv-0.11.17-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:659227cac719b618cc91e02be9e274ad5bd72d74fa278123e6373537e9f28216", size = 24224725, upload-time = "2026-05-28T20:39:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/ad/44/7b11550c1453ea13b81e549c83523e6ab6ed3231d09b2fd6b9eb19acceaf/uv-0.11.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e301d844eed9401f0f0351de12c55f1306ca05372acb0f28d35717c8ba663a22", size = 24301643, upload-time = "2026-05-28T20:39:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/1a/36/8f683bc60547b8f93d0e752a8574d13fad776999cb978482b360c053ca22/uv-0.11.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f0bf483c0d9fa14283992d56061b498b9d3d4adebd285af8744dc33f64dadfba", size = 23786049, upload-time = "2026-05-28T20:39:20.999Z" }, + { url = "https://files.pythonhosted.org/packages/10/dc/7a495db39c2970de4fa375c337dbd617b16780911f88f0511f8fe7f6747c/uv-0.11.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2ccd5487a4a192bc832ea04c867a26883757db8fdfe88bed85d8129c82f9e505", size = 25049786, upload-time = "2026-05-28T20:40:03.292Z" }, + { url = "https://files.pythonhosted.org/packages/37/dd/74eff72d749eaf7e19f489878e21a368a7fef58d26ea0c63ec044ecd78b1/uv-0.11.17-py3-none-win32.whl", hash = "sha256:12b701fa32c5be3691759a73956e4462f30fa7b0dfa52ec66cb305bbb6ea4129", size = 22479213, upload-time = "2026-05-28T20:39:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/8af4a92b99a8a4823297c26df727fe957267e03e1196e3caa803c3f6ccb2/uv-0.11.17-py3-none-win_amd64.whl", hash = "sha256:44ec1fe3af839f87370dcf0400c0cab917cc1ce697d563e860fc7d9ed72655e7", size = 25083161, upload-time = "2026-05-28T20:40:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/00/76/a689077832d585d29d87f9cd0d65eca1af58abd29a4eab004d0a8a858b9c/uv-0.11.17-py3-none-win_arm64.whl", hash = "sha256:37c915bfcf86f99c1c5be7c9ed21e0d80624067ba47bc8916a3cb0530bc94d27", size = 23544936, upload-time = "2026-05-28T20:39:37.137Z" }, ] [[package]] name = "virtualenv" -version = "20.35.4" +version = "21.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, + { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, ] [[package]] @@ -6380,44 +6503,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] - -[[package]] -name = "zstandard" -version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, - { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, - { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, - { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, - { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, - { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, - { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, - { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, - { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, - { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, - { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, - { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, - { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, - { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, - { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, -]